lion/packages-node/remark-extend/src/remarkExtend.js
2021-07-28 17:17:19 +02:00

215 lines
6.8 KiB
JavaScript

// unified works my modifying the original passed node
/* eslint-disable no-param-reassign */
const visit = require('unist-util-visit');
const { select } = require('unist-util-select');
const unified = require('unified');
const markdown = require('remark-parse');
const gfm = require('remark-gfm');
const is = require('unist-util-is');
const fs = require('fs');
const path = require('path');
/**
* Allows to execute an actual node module code block.
* Supports imports (via require) within those code blocks.
*
* @example
* const virtualMod = requireFromString('module.export = { a: "a value", fn: () => {} }');
* console.log(virtualMod.a); // a value
* // execute function
* virtualMod.fn();
*
* @param {*} src
* @param {*} filename
*/
function requireFromString(src, filename = 'tmp.js') {
const srcWithPath = `const path = require('path');\n${src}`;
const m = new module.constructor();
m.paths = module.paths;
m._compile(srcWithPath, filename);
return m.exports;
}
let toInsertNodes = [];
function handleImportedFile({
startSelector,
endSelector,
userFunction,
filePath,
missingEndSelectorMeansUntilEndOfFile = false,
}) {
return tree => {
const start = select(startSelector, tree);
if (!start) {
const msg = `The start selector "${startSelector}" could not find a matching node in "${filePath}".`;
throw new Error(msg);
}
const startIsNode = { ...start };
delete startIsNode.children; // unified is comparison does not support children
let endIsNode;
if (endSelector !== ':end-of-file') {
const end = select(endSelector, tree);
if (!end) {
if (missingEndSelectorMeansUntilEndOfFile === false) {
const msg = `The end selector "${endSelector}" could not find a matching node in "${filePath}".`;
throw new Error(msg);
}
} else {
endIsNode = { ...end };
delete endIsNode.children; // unified is comparison does not support children
}
}
let insertIt = false;
visit(tree, (node, index, parent) => {
if (is(node, startIsNode)) {
insertIt = true;
}
if (endIsNode && is(node, endIsNode)) {
insertIt = false;
}
if (insertIt) {
if (userFunction) {
node = userFunction(node, { index, parent, tree });
}
}
if (insertIt && parent && parent.type === 'root') {
toInsertNodes.push(node);
}
});
};
}
// unified expect direct
// eslint-disable-next-line consistent-return
function remarkExtend({ rootDir = process.cwd(), page } = {}) {
return tree => {
visit(tree, (node, index, parent) => {
if (
node.type === 'code' &&
node.lang === 'js' &&
node.meta &&
node.meta.startsWith('::import')
) {
// eslint-disable-next-line prefer-const
let [fileImport, startSelector = ':root', endSelector = ':end-of-file'] = node.meta
.substring(node.meta.indexOf("('") + 2, node.meta.indexOf("')"))
.split("', '")
.map(paramPart => paramPart.trim());
let filePath;
let missingEndSelectorMeansUntilEndOfFile = false;
try {
filePath = require.resolve(fileImport);
} catch (err) {
filePath = path.resolve(path.join(rootDir, fileImport));
}
if (!fs.existsSync(filePath)) {
const inputPath = page ? page.inputPath : 'no page.inputPath given';
throw new Error(
`The import "${fileImport}" in "${inputPath}" does not exist. Resolved to "${filePath}".`,
);
}
const importFileContent = fs.readFileSync(filePath);
if (
node.meta.startsWith('::importBlock(') ||
node.meta.startsWith('::importBlockContent(') ||
node.meta.startsWith('::importSmallBlock(') ||
node.meta.startsWith('::importSmallBlockContent(')
) {
missingEndSelectorMeansUntilEndOfFile = true;
const [identifier] = node.meta.split('(');
if (!startSelector.startsWith('#')) {
throw new Error(
`${identifier} only works for headlines like "## My Headline" but "${startSelector}" was given`,
);
}
const [hashes, ...headline] = startSelector.split(' ');
switch (identifier) {
case '::importBlock':
startSelector = `heading[depth=${hashes.length}]:has([value=${headline.join(' ')}])`;
endSelector = `${startSelector} ~ heading[depth=${hashes.length}]`;
break;
case '::importBlockContent':
startSelector = `heading[depth=${hashes.length}]:has([value=${headline.join(
' ',
)}]) ~ *`;
endSelector = `${startSelector} ~ heading[depth=${hashes.length}]`;
break;
case '::importSmallBlock':
startSelector = `heading[depth=${hashes.length}]:has([value=${headline.join(' ')}])`;
endSelector = `${startSelector} ~ heading`;
break;
case '::importSmallBlockContent':
startSelector = `heading[depth=${hashes.length}]:has([value=${headline.join(
' ',
)}]) ~ *`;
endSelector = `${startSelector} ~ heading`;
/* no default */
}
}
let userFunction;
if (node.value !== '') {
const inputPath = page ? page.inputPath : 'no page.inputPath given';
const resolvedInputPath = path.resolve(inputPath);
if (!resolvedInputPath) {
throw new Error(
`The page.inputPath "${inputPath}" could not be resolved. Tried to resolve with "${resolvedInputPath}".`,
);
}
const virtualMod = requireFromString(node.value, resolvedInputPath);
const keys = Object.keys(virtualMod);
userFunction = virtualMod[keys[0]];
}
toInsertNodes = [];
const parser = unified()
.use(markdown)
.use(gfm)
.use(handleImportedFile, {
startSelector,
endSelector,
userFunction,
filePath,
fileImport,
missingEndSelectorMeansUntilEndOfFile,
})
.use(function plugin() {
this.Compiler = () => '';
});
parser.processSync(importFileContent.toString());
if (node.type === 'root') {
node.children.splice(0, 0, ...toInsertNodes);
} else {
parent.children[index].__remarkExtendRemove = true;
parent.children.splice(index + 1, 0, ...toInsertNodes);
}
}
});
// another pass to remove nodes
visit(
tree,
(node, index, parent) => {
if (node.__remarkExtendRemove) {
parent.children.splice(index, 1);
}
},
true,
);
return tree;
};
}
module.exports = {
remarkExtend,
};