328 lines
9.3 KiB
JavaScript
328 lines
9.3 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 is = require('unist-util-is');
|
|
|
|
function addTask(file, newAction) {
|
|
if (!file.data.remarkExtend) {
|
|
file.data.remarkExtend = [];
|
|
}
|
|
file.data.remarkExtend.push(newAction);
|
|
}
|
|
|
|
function evaluateAsReplacementTask(node, file) {
|
|
if (
|
|
node.type === 'code' &&
|
|
node.lang === 'js' &&
|
|
node.meta &&
|
|
node.meta.startsWith('::replaceFrom')
|
|
) {
|
|
const startSelector = node.meta.substring(node.meta.indexOf("('") + 2, node.meta.indexOf("')"));
|
|
addTask(file, {
|
|
action: 'replaceFrom',
|
|
startSelector,
|
|
jsCode: node.value,
|
|
});
|
|
}
|
|
if (
|
|
node.type === 'code' &&
|
|
node.lang === 'js' &&
|
|
node.meta &&
|
|
node.meta.startsWith('::replaceBetween')
|
|
) {
|
|
const startSelector = node.meta.substring(node.meta.indexOf("('") + 2, node.meta.indexOf("',"));
|
|
const endSelector = node.meta
|
|
.substring(node.meta.indexOf("',") + 4, node.meta.indexOf("')"))
|
|
.trim();
|
|
addTask(file, {
|
|
action: 'replaceBetween',
|
|
startSelector,
|
|
endSelector,
|
|
jsCode: node.value,
|
|
});
|
|
}
|
|
}
|
|
|
|
function evaluateAsRemoveTask(node, file) {
|
|
if (node.type === 'code' && node.value && node.value.startsWith('::removeFrom')) {
|
|
const startSelector = node.value.substring(
|
|
node.value.indexOf("('") + 2,
|
|
node.value.indexOf("')"),
|
|
);
|
|
addTask(file, {
|
|
action: 'removeFrom',
|
|
startSelector,
|
|
});
|
|
}
|
|
if (node.type === 'code' && node.value && node.value.startsWith('::removeBetween')) {
|
|
const startSelector = node.value.substring(
|
|
node.value.indexOf("('") + 2,
|
|
node.value.indexOf("',"),
|
|
);
|
|
const endSelector = node.value
|
|
.substring(node.value.indexOf("',") + 4, node.value.indexOf("')"))
|
|
.trim();
|
|
addTask(file, {
|
|
action: 'removeBetween',
|
|
startSelector,
|
|
endSelector,
|
|
});
|
|
}
|
|
}
|
|
|
|
function shouldFinishGathering(node) {
|
|
if (node.type === 'code' && node.lang === 'js' && node.meta && node.meta.startsWith('::')) {
|
|
return true;
|
|
}
|
|
if (node.value && node.value.startsWith('::')) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
let mdAdditionAddNodes = [];
|
|
let mdAdditionGathering = false;
|
|
let mdAdditionStartSelector;
|
|
let mdAdditionAction = '';
|
|
function evaluateAsMdAdditionTask(node, file, parent) {
|
|
if (mdAdditionGathering === true && shouldFinishGathering(node)) {
|
|
mdAdditionGathering = false;
|
|
addTask(file, {
|
|
action: mdAdditionAction,
|
|
startSelector: mdAdditionStartSelector,
|
|
addNodes: mdAdditionAddNodes,
|
|
});
|
|
mdAdditionAddNodes = [];
|
|
}
|
|
|
|
if (mdAdditionGathering === true) {
|
|
if (parent.type === 'root') {
|
|
mdAdditionAddNodes.push(node);
|
|
}
|
|
}
|
|
|
|
if (node.type === 'code' && node.value && node.value.startsWith('::addMdAfter')) {
|
|
mdAdditionStartSelector = node.value.substring(
|
|
node.value.indexOf("('") + 2,
|
|
node.value.indexOf("')"),
|
|
);
|
|
mdAdditionGathering = true;
|
|
mdAdditionAction = 'addMdAfter';
|
|
}
|
|
if (node.type === 'code' && node.value && node.value.startsWith('::addMdBefore')) {
|
|
mdAdditionStartSelector = node.value.substring(
|
|
node.value.indexOf("('") + 2,
|
|
node.value.indexOf("')"),
|
|
);
|
|
mdAdditionGathering = true;
|
|
mdAdditionAction = 'addMdBefore';
|
|
}
|
|
}
|
|
|
|
function findExtendTasks() {
|
|
return (tree, file) => {
|
|
visit(tree, (node, index, parent) => {
|
|
evaluateAsReplacementTask(node, file);
|
|
evaluateAsMdAdditionTask(node, file, parent);
|
|
evaluateAsRemoveTask(node, file);
|
|
});
|
|
|
|
// for evaluateAsMdAdditionTask
|
|
if (mdAdditionGathering === true) {
|
|
mdAdditionGathering = false;
|
|
addTask(file, {
|
|
action: mdAdditionAction,
|
|
startSelector: mdAdditionStartSelector,
|
|
addNodes: mdAdditionAddNodes,
|
|
});
|
|
mdAdditionAddNodes = [];
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 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 m = new module.constructor();
|
|
m.paths = module.paths;
|
|
m._compile(src, filename);
|
|
return m.exports;
|
|
}
|
|
|
|
function handleAdditions(tree, action, startIsNode, addNodes) {
|
|
visit(tree, (node, index, parent) => {
|
|
if (is(node, startIsNode)) {
|
|
if (action === 'addMdAfter') {
|
|
if (node.type === 'root') {
|
|
node.children.splice(0, 0, ...addNodes);
|
|
} else {
|
|
parent.children.splice(index + 1, 0, ...addNodes);
|
|
}
|
|
}
|
|
if (action === 'addMdBefore') {
|
|
if (node.remarkExtendedProcessed === undefined) {
|
|
// preventing infinite loops as adding a node before means we visit the target node again and insert again
|
|
node.remarkExtendedProcessed = true;
|
|
if (node.type === 'root') {
|
|
node.children.splice(0, 0, ...addNodes);
|
|
} else {
|
|
parent.children.splice(index, 0, ...addNodes);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function handleReplacements(tree, action, startIsNode, endIsNode, jsCode) {
|
|
let doReplacements = false;
|
|
let resetAtEnd = false;
|
|
let userFunction;
|
|
if (action === 'replaceFrom' || action === 'replaceBetween') {
|
|
const virtualMod = requireFromString(jsCode);
|
|
const keys = Object.keys(virtualMod);
|
|
userFunction = virtualMod[keys[0]];
|
|
}
|
|
visit(tree, (node, index, parent) => {
|
|
if (is(node, startIsNode)) {
|
|
doReplacements = true;
|
|
}
|
|
if (action === 'replaceBetween' && is(node, endIsNode)) {
|
|
resetAtEnd = true;
|
|
}
|
|
if (doReplacements) {
|
|
node = userFunction(node, { index, parent, tree });
|
|
}
|
|
if (resetAtEnd === true) {
|
|
resetAtEnd = false;
|
|
doReplacements = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Needs 2 loops as
|
|
* - First to mark nodes for removal
|
|
* - Do actual removal in revers order (to not effect the index of the loop)
|
|
*
|
|
* @param {*} tree
|
|
* @param {*} action
|
|
* @param {*} startIsNode
|
|
* @param {*} endIsNode
|
|
*/
|
|
function handleRemovals(tree, action, startIsNode, endIsNode) {
|
|
let removeIt = false;
|
|
visit(tree, (node, index, parent) => {
|
|
if (is(node, startIsNode)) {
|
|
removeIt = true;
|
|
}
|
|
if (action === 'removeBetween' && is(node, endIsNode)) {
|
|
removeIt = false;
|
|
}
|
|
if (removeIt && parent.type === 'root') {
|
|
// only mark for removal
|
|
// removing directly messes with the index which prevents further removals down the line
|
|
parent.children[index].__remarkExtendRemove = true;
|
|
}
|
|
});
|
|
visit(
|
|
tree,
|
|
(node, index, parent) => {
|
|
if (node.__remarkExtendRemove) {
|
|
parent.children.splice(index, 1);
|
|
}
|
|
},
|
|
true,
|
|
);
|
|
}
|
|
|
|
// unified expect direct
|
|
// eslint-disable-next-line consistent-return
|
|
function remarkExtend(options) {
|
|
if (options.extendMd) {
|
|
const parser = unified()
|
|
.use(markdown)
|
|
.use(findExtendTasks)
|
|
.use(function plugin() {
|
|
this.Compiler = () => '';
|
|
});
|
|
|
|
const changes = parser.processSync(options.extendMd);
|
|
|
|
const extensionTasks = changes.data.remarkExtend;
|
|
|
|
if (!extensionTasks) {
|
|
return tree => tree;
|
|
}
|
|
|
|
return tree => {
|
|
for (const extensionTask of extensionTasks) {
|
|
const { action, startSelector, endSelector, jsCode, addNodes } = extensionTask;
|
|
const start = select(extensionTask.startSelector, tree);
|
|
if (!start) {
|
|
const msg = [
|
|
`The start selector "${startSelector}" could not find a matching node.`,
|
|
options.filePath ? `Markdown File: ${options.filePath}` : '',
|
|
options.overrideFilePath ? `Override File: ${options.overrideFilePath}` : '',
|
|
]
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
throw new Error(msg);
|
|
}
|
|
const startIsNode = { ...start };
|
|
delete startIsNode.children; // unified is comparison does not support children
|
|
|
|
let endIsNode;
|
|
if (action === 'replaceBetween' || action === 'removeBetween') {
|
|
const end = select(endSelector, tree);
|
|
if (!end) {
|
|
const msg = [
|
|
`The end selector "${endSelector}" could not find a matching node.`,
|
|
options.filePath ? `Markdown File: ${options.filePath}` : '',
|
|
options.overrideFilePath ? `Override File: ${options.overrideFilePath}` : '',
|
|
]
|
|
.filter(Boolean)
|
|
.join('\n');
|
|
throw new Error(msg);
|
|
}
|
|
endIsNode = { ...end };
|
|
delete endIsNode.children; // unified is comparison does not support children
|
|
}
|
|
|
|
switch (action) {
|
|
case 'addMdAfter':
|
|
case 'addMdBefore':
|
|
handleAdditions(tree, action, startIsNode, addNodes);
|
|
break;
|
|
case 'replaceFrom':
|
|
case 'replaceBetween':
|
|
handleReplacements(tree, action, startIsNode, endIsNode, jsCode);
|
|
break;
|
|
case 'removeFrom':
|
|
case 'removeBetween':
|
|
handleRemovals(tree, action, startIsNode, endIsNode);
|
|
break;
|
|
/* no default */
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
remarkExtend,
|
|
};
|