215 lines
6.8 KiB
JavaScript
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,
|
|
};
|