feat: as BREAKING CHANGE convert to import based approach
This commit is contained in:
parent
9371bfa857
commit
5d528fd795
9 changed files with 473 additions and 1241 deletions
|
|
@ -1,326 +1,3 @@
|
|||
# remark-extend
|
||||
# Remark Extend
|
||||
|
||||
A plugin for remark to extend one markdown file with another.
|
||||
|
||||
`remark-extend` is build to be integrated within the [unifiedjs](https://unifiedjs.com/) system.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm i -D remark-extend
|
||||
```
|
||||
|
||||
```js
|
||||
const unified = require('unified');
|
||||
const markdown = require('remark-parse');
|
||||
const mdStringify = require('remark-html');
|
||||
|
||||
const { remarkExtend } = require('remark-extend');
|
||||
|
||||
const sourceMd = '# Headline';
|
||||
const extendMd = 'extending instructions';
|
||||
|
||||
const parser = unified().use(markdown).use(remarkExtend, { extendMd }).use(mdStringify);
|
||||
const result = await parser.process(sourceMd);
|
||||
```
|
||||
|
||||
## Extending Instructions
|
||||
|
||||
### Selection
|
||||
|
||||
For modifications you will need to provide a starting node.
|
||||
In order to get this node css like selectors from [unist-util-select](https://github.com/syntax-tree/unist-util-select#support) are supported.
|
||||
|
||||
Some examples are:
|
||||
|
||||
- `:root` for the top of the markdown file
|
||||
- `:scope:last-child` for the end of the markdown file
|
||||
- `heading:has([value=Red])` first heading with a text value of Red (e.g. ### Red)
|
||||
- `heading[depth=2]` first second level heading (e.g. ## Something)
|
||||
- `heading[depth=2]:has([value=Red]) ~ heading[depth=2]` following h2 after h2 with "Red" (e.g. ## Red ... ## Something)
|
||||
|
||||
### Markdown AST
|
||||
|
||||
All adjustments to the markdown file happen via the markdown AST (Abstract Syntax Tree).
|
||||
|
||||
You can explore it via the [ASTExplorer](https://astexplorer.net/). (> Markdown > remark)
|
||||
|
||||
```md
|
||||
### Red
|
||||
|
||||
red is the fire
|
||||
```
|
||||
|
||||
Resulting AST.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "root",
|
||||
"children": [
|
||||
{
|
||||
"type": "heading",
|
||||
"depth": 3,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"value": "Red"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "paragraph",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"value": "red is the fire"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Replacement
|
||||
|
||||
Does adjustments from a start point downwards.
|
||||
The provided function does get called for every node from the starting point (including the starting point).
|
||||
|
||||
#### Replacement Input File
|
||||
|
||||
```md
|
||||
### Red
|
||||
|
||||
red is the fire
|
||||
```
|
||||
|
||||
#### Replacement Extending File
|
||||
|
||||
Goal is to replace all red with green.
|
||||
|
||||
````md
|
||||
```js ::replaceFrom(':root')
|
||||
module.exports.replaceSection = node => {
|
||||
if (node.value) {
|
||||
node.value = node.value.replace(/red/g, 'green').replace(/Red/g, 'Green');
|
||||
}
|
||||
return node;
|
||||
};
|
||||
```
|
||||
````
|
||||
|
||||
This function gets called with these nodes in order.
|
||||
|
||||
1. root
|
||||
2. heading
|
||||
3. text
|
||||
4. paragraph
|
||||
5. text
|
||||
|
||||
#### Replacement Result
|
||||
|
||||
```md
|
||||
### Green
|
||||
|
||||
green is the fire
|
||||
```
|
||||
|
||||
### Replacement Range
|
||||
|
||||
Whenever a section or part of the original markdown needs to be adjusted.
|
||||
The function does get every node from the starting point (including the starting point) till the end point (excluding the end point).
|
||||
|
||||
#### Replacement Range Input File
|
||||
|
||||
```md
|
||||
### Red <-- starting point (including)
|
||||
|
||||
red is the fire
|
||||
|
||||
### More Red <-- end point (excluding)
|
||||
|
||||
the sun can get red
|
||||
```
|
||||
|
||||
#### Replacement Range Extending File
|
||||
|
||||
````md
|
||||
```js ::replaceBetween('heading:has([value=Red])', 'heading:has([value=More Red])')
|
||||
module.exports.replaceSection = node => {
|
||||
if (node.value) {
|
||||
node.value = node.value.replace(/red/g, 'green').replace(/Red/g, 'Green');
|
||||
}
|
||||
return node;
|
||||
};
|
||||
```
|
||||
````
|
||||
|
||||
#### Replacement Range Result
|
||||
|
||||
```md
|
||||
### Green <-- starting point (including)
|
||||
|
||||
green is the fire
|
||||
|
||||
### More Red <-- end point (excluding)
|
||||
|
||||
the sun can get red
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Add More Markdown Content After
|
||||
|
||||
If additional markdown content should be inserted after at a specific location.
|
||||
|
||||
#### Add After Input File
|
||||
|
||||
```md
|
||||
### Red
|
||||
|
||||
red is the fire
|
||||
```
|
||||
|
||||
#### Add After Extending File
|
||||
|
||||
````md
|
||||
```
|
||||
::addMdAfter('heading:has([value=Red])')
|
||||
```
|
||||
|
||||
the ocean is blue
|
||||
````
|
||||
|
||||
#### Add After Result
|
||||
|
||||
```md
|
||||
### Red
|
||||
|
||||
the ocean is blue
|
||||
red is the fire
|
||||
```
|
||||
|
||||
More example use cases:
|
||||
|
||||
- Add more markdown at the top `::addMdAfter(':root')`
|
||||
- Add more markdown at the bottom `::addMdAfter(':scope:last-child')`
|
||||
|
||||
### Add More Markdown Content Before
|
||||
|
||||
If additional markdown content should be inserted before at a specific location.
|
||||
Useful for adding disclaimers above a headline.
|
||||
|
||||
#### Add Before Input File
|
||||
|
||||
```md
|
||||
### Red
|
||||
|
||||
red is the fire
|
||||
|
||||
### Green <-- staring point
|
||||
```
|
||||
|
||||
#### Add Before Extending File
|
||||
|
||||
````md
|
||||
```
|
||||
::addMdBefore('heading:has([value=Red])')
|
||||
```
|
||||
|
||||
the ocean is blue
|
||||
````
|
||||
|
||||
#### Add Before Result
|
||||
|
||||
```md
|
||||
### Red
|
||||
|
||||
red is the fire
|
||||
|
||||
the ocean is blue
|
||||
|
||||
### Green <-- staring point
|
||||
```
|
||||
|
||||
More example use cases:
|
||||
|
||||
- Add something at the end of a "section": `::addMdBefore('heading:has([value=Red]) ~ heading[depth=3]')`
|
||||
It works by selecting the headline of your section and add before the next sibling headline with the same depth.
|
||||
|
||||
---
|
||||
|
||||
### Removal
|
||||
|
||||
Does adjustments from a start point downwards.
|
||||
The provided function does get called for every node from the starting point (including the starting point).
|
||||
|
||||
#### Removal Input File
|
||||
|
||||
```md
|
||||
### Red
|
||||
|
||||
red is the fire
|
||||
|
||||
### More Red // <-- start
|
||||
|
||||
the sun can get red
|
||||
```
|
||||
|
||||
#### Removal Extending File
|
||||
|
||||
Goal is to remove everything after `### More Red`
|
||||
|
||||
````md
|
||||
```
|
||||
::removeFrom('heading:has([value=More Red])')
|
||||
```
|
||||
````
|
||||
|
||||
#### Removal Result
|
||||
|
||||
```md
|
||||
### Red
|
||||
|
||||
red is the fire
|
||||
```
|
||||
|
||||
### Removal Range
|
||||
|
||||
Whenever a section or part of the original markdown needs to be removed.
|
||||
|
||||
#### Removal Range Input File
|
||||
|
||||
```md
|
||||
### Red <-- starting point (including)
|
||||
|
||||
red is the fire
|
||||
|
||||
### More Red <-- end point (excluding)
|
||||
|
||||
the sun can get red
|
||||
```
|
||||
|
||||
#### Removal Range Extending File
|
||||
|
||||
Starting from `### Red` until the next headline with depth of 3.
|
||||
|
||||
````md
|
||||
```
|
||||
::removeBetween('heading:has([value=Red])', 'heading:has([value=Red]) ~ heading[depth=3]')
|
||||
```
|
||||
````
|
||||
|
||||
#### Removal Range Result
|
||||
|
||||
```md
|
||||
### More Red <-- end point (excluding)
|
||||
|
||||
the sun can get red
|
||||
```
|
||||
|
||||
```js script
|
||||
export default {
|
||||
title: 'Tools/Remark Extend',
|
||||
};
|
||||
```
|
||||
[=> See Source <=](../../docs/docs/node-tools/remark-extend/overview.md)
|
||||
|
|
|
|||
3
packages-node/remark-extend/docs/overview.md
Normal file
3
packages-node/remark-extend/docs/overview.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Remark Extend
|
||||
|
||||
[=> See Source <=](../../../docs/docs/node-tools/remark-extend/overview.md)
|
||||
|
|
@ -18,14 +18,15 @@
|
|||
"test"
|
||||
],
|
||||
"scripts": {
|
||||
"prepublishOnly": "../../scripts/npm-prepublish.js",
|
||||
"publish-docs": "node ../../packages-node/publish-docs/src/cli.js --github-url https://github.com/ing-bank/lion/ --git-root-dir ../../",
|
||||
"prepublishOnly": "npm run publish-docs",
|
||||
"test": "npm run test:node",
|
||||
"test:node": "mocha test-node",
|
||||
"test:watch": "mocha test-node --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"remark-parse": "^8.0.0",
|
||||
"unified": "^9.0.0",
|
||||
"remark-parse": "^9.0.0",
|
||||
"unified": "^9.2.0",
|
||||
"unist-util-is": "^4.0.2",
|
||||
"unist-util-select": "^3.0.1",
|
||||
"unist-util-visit": "^2.0.2"
|
||||
|
|
@ -36,5 +37,8 @@
|
|||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"exports": "./index.js"
|
||||
"exports": {
|
||||
".": "./index.js",
|
||||
"./docs/": "./docs/"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,143 +5,8 @@ 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 = [];
|
||||
}
|
||||
};
|
||||
}
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Allows to execute an actual node module code block.
|
||||
|
|
@ -157,170 +22,195 @@ function findExtendTasks() {
|
|||
* @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(src, filename);
|
||||
m._compile(srcWithPath, 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);
|
||||
let toInsertNodes = [];
|
||||
|
||||
function handleImportedFile({
|
||||
startSelector,
|
||||
endSelector,
|
||||
userFunction,
|
||||
filePath,
|
||||
fileImport,
|
||||
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 (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);
|
||||
if (insertIt && parent && parent.type === 'root') {
|
||||
toInsertNodes.push(node);
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
if (node.type === 'image') {
|
||||
node.url = path.join(path.dirname(fileImport), node.url);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// 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 = () => '';
|
||||
});
|
||||
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());
|
||||
|
||||
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);
|
||||
let filePath;
|
||||
let missingEndSelectorMeansUntilEndOfFile = false;
|
||||
try {
|
||||
filePath = require.resolve(fileImport);
|
||||
} catch (err) {
|
||||
filePath = path.resolve(path.join(rootDir, fileImport));
|
||||
}
|
||||
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);
|
||||
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 */
|
||||
}
|
||||
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 */
|
||||
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(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 = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
# import me headline
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# import me headline
|
||||
|
||||

|
||||
[link to](./import-me.md)
|
||||
1
packages-node/remark-extend/test-node/fixtures/my.svg
Normal file
1
packages-node/remark-extend/test-node/fixtures/my.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
# Red
|
||||
|
||||
red is the fire
|
||||
|
||||
## More Red
|
||||
|
||||
the sun can get red
|
||||
|
||||
## Additional Red
|
||||
|
||||
the red sea
|
||||
|
|
@ -6,7 +6,13 @@ const mdStringify = require('remark-html');
|
|||
|
||||
const { remarkExtend } = require('../src/remarkExtend.js');
|
||||
|
||||
async function expectThrowsAsync(method, errorMessage) {
|
||||
/**
|
||||
* @param {function} method
|
||||
* @param {object} options
|
||||
* @param {string} [options.errorMatch]
|
||||
* @param {string} [options.errorMessage]
|
||||
*/
|
||||
async function expectThrowsAsync(method, { errorMatch, errorMessage } = {}) {
|
||||
let error = null;
|
||||
try {
|
||||
await method();
|
||||
|
|
@ -14,682 +20,317 @@ async function expectThrowsAsync(method, errorMessage) {
|
|||
error = err;
|
||||
}
|
||||
expect(error).to.be.an('Error', 'No error was thrown');
|
||||
if (errorMatch) {
|
||||
expect(error.message).to.match(errorMatch);
|
||||
}
|
||||
if (errorMessage) {
|
||||
expect(error.message).to.equal(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
async function execute(input) {
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { rootDir: __dirname, page: { inputPath: 'test-file.md' } })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
return result.contents;
|
||||
}
|
||||
|
||||
describe('remarkExtend', () => {
|
||||
it('does no modifications if no action is found', async () => {
|
||||
const input = [
|
||||
'### Red',
|
||||
'',
|
||||
'red is the fire',
|
||||
'',
|
||||
'#### More Red',
|
||||
'',
|
||||
'the sun can get red',
|
||||
].join('\n');
|
||||
const extendMd = '';
|
||||
const output = [
|
||||
'<h3>Red</h3>',
|
||||
'<p>red is the fire</p>',
|
||||
'<h4>More Red</h4>',
|
||||
'<p>the sun can get red</p>',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
});
|
||||
|
||||
it('can do replacements on the full file', async () => {
|
||||
const input = [
|
||||
'### Red', // <-- start
|
||||
'',
|
||||
'red is the fire',
|
||||
'',
|
||||
'#### More Red',
|
||||
'',
|
||||
'the sun can get red',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
//
|
||||
"```js ::replaceFrom(':root')",
|
||||
'module.exports.replaceSection = (node) => {',
|
||||
' if (node.value) {',
|
||||
" node.value = node.value.replace(/red/g, 'green').replace(/Red/g, 'Green');",
|
||||
' }',
|
||||
' return node;',
|
||||
'};',
|
||||
'```',
|
||||
].join('\n');
|
||||
const output = [
|
||||
'<h3>Green</h3>', // <-- start
|
||||
'<p>green is the fire</p>',
|
||||
'<h4>More Green</h4>',
|
||||
'<p>the sun can get green</p>',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
});
|
||||
|
||||
it('can replace from a starting point downward', async () => {
|
||||
const input = [
|
||||
'### Red',
|
||||
'',
|
||||
'red is the fire',
|
||||
'',
|
||||
'### More Red', // <-- start
|
||||
'',
|
||||
'the sun can get red',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
"```js ::replaceFrom('heading[depth=3]:has([value=More Red])')",
|
||||
'module.exports.replaceSection = (node) => {',
|
||||
' if (node.value) {',
|
||||
" node.value = node.value.replace(/red/g, 'green').replace(/Red/g, 'Green');",
|
||||
' }',
|
||||
' return node;',
|
||||
'};',
|
||||
'```',
|
||||
].join('\n');
|
||||
const output = [
|
||||
'<h3>Red</h3>',
|
||||
'<p>red is the fire</p>',
|
||||
'<h3>More Green</h3>', // <-- start
|
||||
'<p>the sun can get green</p>',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
});
|
||||
|
||||
it('can replace within a range (start point included, endpoint not)', async () => {
|
||||
const input = [
|
||||
'### Red',
|
||||
'',
|
||||
'red is the fire',
|
||||
'',
|
||||
'red is the cross', // <-- start
|
||||
'',
|
||||
'red is the flag',
|
||||
'',
|
||||
'#### More Red',
|
||||
'',
|
||||
'the sun can get red',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
"```js ::replaceBetween('heading:has([value=Red]) ~ paragraph:nth-of-type(2)', 'heading:has([value=More Red])')",
|
||||
'module.exports.replaceSection = (node) => {',
|
||||
' if (node.value) {',
|
||||
" node.value = node.value.replace(/red/g, 'green').replace(/Red/g, 'Green');",
|
||||
' }',
|
||||
' return node',
|
||||
'};',
|
||||
].join('\n');
|
||||
const output = [
|
||||
'<h3>Red</h3>',
|
||||
'<p>red is the fire</p>',
|
||||
'<p>green is the cross</p>', // <-- start
|
||||
'<p>green is the flag</p>',
|
||||
'<h4>More Red</h4>', // <-- end
|
||||
'<p>the sun can get red</p>',
|
||||
'',
|
||||
].join('\n');
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
});
|
||||
|
||||
it('throws if a start selector is not found', async () => {
|
||||
const input = [
|
||||
//
|
||||
'### Red',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
"```js ::replaceFrom('heading:has([value=More Red])')",
|
||||
'module.exports.replaceSection = (node) => {}',
|
||||
'```',
|
||||
].join('\n');
|
||||
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
|
||||
await expectThrowsAsync(
|
||||
() => parser.process(input),
|
||||
'The start selector "heading:has([value=More Red])" could not find a matching node.',
|
||||
const result = await execute(
|
||||
['### Red', 'red is the fire', '#### More Red', 'the sun can get red'].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws with addition info (if provide as filePath, overrideFilePath) if a start selector is not found', async () => {
|
||||
const input = [
|
||||
//
|
||||
'### Red',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
"```js ::replaceFrom('heading:has([value=More Red])')",
|
||||
'module.exports.replaceSection = (node) => {}',
|
||||
'```',
|
||||
].join('\n');
|
||||
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, {
|
||||
extendMd,
|
||||
filePath: '/path/to/input.md',
|
||||
overrideFilePath: '/path/to/override.md',
|
||||
})
|
||||
.use(mdStringify);
|
||||
|
||||
await expectThrowsAsync(
|
||||
() => parser.process(input),
|
||||
expect(result).to.equal(
|
||||
[
|
||||
'The start selector "heading:has([value=More Red])" could not find a matching node.',
|
||||
'Markdown File: /path/to/input.md',
|
||||
'Override File: /path/to/override.md',
|
||||
'<h3>Red</h3>',
|
||||
'<p>red is the fire</p>',
|
||||
'<h4>More Red</h4>',
|
||||
'<p>the sun can get red</p>',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if a end selector is not found', async () => {
|
||||
const input = [
|
||||
//
|
||||
'### Red',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
"```js ::replaceBetween('heading:has([value=Red])', 'heading:has([value=Red2])')",
|
||||
'module.exports.replaceSection = (node) => {}',
|
||||
'```',
|
||||
].join('\n');
|
||||
|
||||
const parser = unified().use(markdown).use(remarkExtend, { extendMd }).use(mdStringify);
|
||||
|
||||
await expectThrowsAsync(
|
||||
() => parser.process(input),
|
||||
'The end selector "heading:has([value=Red2])" could not find a matching node.',
|
||||
it('can import another file', async () => {
|
||||
const result = await execute(
|
||||
[
|
||||
//
|
||||
'### Red',
|
||||
"```js ::import('./fixtures/import-me.md')",
|
||||
'```',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
expect(result).to.equal(['<h3>Red</h3>', '<h1>import me headline</h1>', ''].join('\n'));
|
||||
});
|
||||
|
||||
it('throws with addition info (if provide as filePath, overrideFilePath) if a end selector is not found', async () => {
|
||||
const input = [
|
||||
//
|
||||
'### Red',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
"```js ::replaceBetween('heading:has([value=Red])', 'heading:has([value=Red2])')",
|
||||
'module.exports.replaceSection = (node) => {}',
|
||||
'```',
|
||||
].join('\n');
|
||||
|
||||
const parser = unified()
|
||||
.use(markdown)
|
||||
.use(remarkExtend, {
|
||||
extendMd,
|
||||
filePath: '/path/to/input.md',
|
||||
overrideFilePath: '/path/to/override.md',
|
||||
})
|
||||
.use(mdStringify);
|
||||
|
||||
await expectThrowsAsync(
|
||||
() => parser.process(input),
|
||||
it('will rewrite image urls/paths but not links', async () => {
|
||||
const result = await execute(
|
||||
[
|
||||
'The end selector "heading:has([value=Red2])" could not find a matching node.',
|
||||
'Markdown File: /path/to/input.md',
|
||||
'Override File: /path/to/override.md',
|
||||
//
|
||||
'### Static Headline',
|
||||
"```js ::import('./fixtures/import-with-image.md')",
|
||||
'```',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
expect(result).to.equal(
|
||||
[
|
||||
'<h3>Static Headline</h3>',
|
||||
'<h1>import me headline</h1>',
|
||||
'<p><img src="fixtures/my.svg" alt="my image">',
|
||||
'<a href="./import-me.md">link to</a></p>',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('replaces a single node if replacing between the start and end of the same node', async () => {
|
||||
const input = [
|
||||
//
|
||||
'### Red',
|
||||
'red',
|
||||
'### Red',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
"```js ::replaceBetween('heading:has([value=Red]) > text', 'heading:has([value=Red]) > text')",
|
||||
'module.exports.replaceSection = (node) => {',
|
||||
' if (node.value) {',
|
||||
" node.value = node.value.replace(/red/g, 'green').replace(/Red/g, 'Green');",
|
||||
' }',
|
||||
' return node',
|
||||
'};',
|
||||
'```',
|
||||
].join('\n');
|
||||
const output = [
|
||||
'<h3>Green</h3>', // text node "Green" == start == end
|
||||
'<p>red</p>',
|
||||
'<h3>Red</h3>',
|
||||
'',
|
||||
].join('\n');
|
||||
it('can import another file from a start point', async () => {
|
||||
const result = await execute(
|
||||
[
|
||||
//
|
||||
'# Static Headline',
|
||||
"```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=More Red])')",
|
||||
'```',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
|
||||
const result = await parser.process(input);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
});
|
||||
|
||||
it('can put something after via "::addMdAfter"', async () => {
|
||||
const input = [
|
||||
//
|
||||
'### Red',
|
||||
'',
|
||||
'red is the fire',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
'```',
|
||||
"::addMdAfter('heading:has([value=Red])')",
|
||||
'```',
|
||||
'',
|
||||
'the ocean is blue',
|
||||
].join('\n');
|
||||
const output = [
|
||||
//
|
||||
'<h3>Red</h3>',
|
||||
'<p>the ocean is blue</p>',
|
||||
'<p>red is the fire</p>',
|
||||
'',
|
||||
].join('\n');
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
});
|
||||
|
||||
it('adding stops at the next ::[action]', async () => {
|
||||
const input = [
|
||||
//
|
||||
'### Red',
|
||||
'',
|
||||
'red is the fire',
|
||||
'',
|
||||
'### More Red',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
'```',
|
||||
"::addMdAfter('heading:has([value=Red])')",
|
||||
'```',
|
||||
'',
|
||||
'the ocean is blue',
|
||||
'',
|
||||
'```',
|
||||
"::addMdAfter('heading:has([value=More Red])')",
|
||||
'```',
|
||||
'',
|
||||
'as in the sun is the ultimate red',
|
||||
'',
|
||||
"```js ::replaceBetween('heading:has([value=Red])', 'heading:has([value=Red])')",
|
||||
'module.exports.replaceSection = (node) => {}',
|
||||
'```',
|
||||
'content not part of an add so it gets ignored',
|
||||
].join('\n');
|
||||
const output = [
|
||||
//
|
||||
'<h3>Red</h3>',
|
||||
'<p>the ocean is blue</p>',
|
||||
'<p>red is the fire</p>',
|
||||
'<h3>More Red</h3>',
|
||||
'<p>as in the sun is the ultimate red</p>',
|
||||
'',
|
||||
].join('\n');
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
});
|
||||
|
||||
it('will throw if trying to immediate run replacements of added content', async () => {
|
||||
const input = [
|
||||
//
|
||||
'### Red',
|
||||
'',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
'```',
|
||||
"::addMdAfter('heading:has([value=Red])')",
|
||||
'```',
|
||||
'',
|
||||
'## Blue',
|
||||
'',
|
||||
"```js ::replaceFrom('heading:has([value=Blue])')",
|
||||
'module.exports.replaceSection = (node) => {',
|
||||
' if (node.value) {',
|
||||
" node.value = node.value.replace(/Blue/g, 'Yellow');",
|
||||
' }',
|
||||
' return node',
|
||||
'}',
|
||||
'```',
|
||||
].join('\n');
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
|
||||
await expectThrowsAsync(
|
||||
() => parser.process(input),
|
||||
'The start selector "heading:has([value=Blue])" could not find a matching node.',
|
||||
expect(result).to.equal(
|
||||
[
|
||||
'<h1>Static Headline</h1>',
|
||||
'<h2>More Red</h2>',
|
||||
'<p>the sun can get red</p>',
|
||||
'<h2>Additional Red</h2>',
|
||||
'<p>the red sea</p>',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it(`can put something right at the top via "::addMdAfter(':root')"`, async () => {
|
||||
const input = [
|
||||
//
|
||||
'### Red',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
//
|
||||
'```',
|
||||
"::addMdAfter(':root')",
|
||||
'```',
|
||||
'',
|
||||
'# New Headline',
|
||||
].join('\n');
|
||||
const output = [
|
||||
//
|
||||
'<h1>New Headline</h1>',
|
||||
'<h3>Red</h3>',
|
||||
'',
|
||||
].join('\n');
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
it('can import another file from a start to an end point', async () => {
|
||||
const result = await execute(
|
||||
[
|
||||
//
|
||||
'# Red',
|
||||
"```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=More Red])', 'heading:has([value=More Red]) ~ heading[depth=2]')",
|
||||
'```',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
expect(result).to.equal(
|
||||
['<h1>Red</h1>', '<h2>More Red</h2>', '<p>the sun can get red</p>', ''].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it(`can put something right at the bottom via "::addMdAfter(':scope:last-child')"`, async () => {
|
||||
const input = [
|
||||
//
|
||||
'### Red',
|
||||
'',
|
||||
'red is the fire',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
//
|
||||
'```',
|
||||
"::addMdAfter(':scope:last-child')",
|
||||
'```',
|
||||
'',
|
||||
'extra text',
|
||||
].join('\n');
|
||||
const output = [
|
||||
//
|
||||
'<h3>Red</h3>',
|
||||
'<p>red is the fire</p>',
|
||||
'<p>extra text</p>',
|
||||
'',
|
||||
].join('\n');
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
it('can do replacements on imports', async () => {
|
||||
const result = await execute(
|
||||
[
|
||||
//
|
||||
'# Red',
|
||||
"```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=More Red])', 'heading:has([value=More Red]) ~ heading[depth=2]')",
|
||||
'module.exports.replaceSection = (node) => {',
|
||||
' if (node.value) {',
|
||||
" node.value = node.value.replace(/red/g, 'green').replace(/Red/g, 'Green');",
|
||||
' }',
|
||||
' return node;',
|
||||
'};',
|
||||
'```',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
expect(result).to.equal(
|
||||
['<h1>Red</h1>', '<h2>More Green</h2>', '<p>the sun can get green</p>', ''].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('can put something before via "::addMdBefore"', async () => {
|
||||
const input = [
|
||||
//
|
||||
'### Red',
|
||||
'',
|
||||
'red is the fire',
|
||||
'',
|
||||
'### Blue',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
'```',
|
||||
"::addMdBefore('heading:has([value=Blue])')",
|
||||
'```',
|
||||
'',
|
||||
'the ocean is blue',
|
||||
].join('\n');
|
||||
const output = [
|
||||
//
|
||||
'<h3>Red</h3>',
|
||||
'<p>red is the fire</p>',
|
||||
'<p>the ocean is blue</p>',
|
||||
'<h3>Blue</h3>',
|
||||
'',
|
||||
].join('\n');
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
it('throws if an import file does not exist', async () => {
|
||||
await expectThrowsAsync(() => execute("```js ::import('./fixtures/not-available.md')\n```"), {
|
||||
errorMatch: /The import "\.\/fixtures\/not-available.md" in "test-file.md" does not exist\. Resolved to ".*"\.$/,
|
||||
});
|
||||
});
|
||||
|
||||
it(`can put something at the end of a "section" via "::addMdBefore('heading:has([value=Red]) ~ heading[depth=3]')"`, async () => {
|
||||
const input = [
|
||||
//
|
||||
'### Red',
|
||||
'',
|
||||
'red is the fire',
|
||||
'',
|
||||
'### Blue',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
'```',
|
||||
"::addMdBefore('heading:has([value=Red]) ~ heading[depth=3]')",
|
||||
'```',
|
||||
'',
|
||||
'the sun will be red',
|
||||
].join('\n');
|
||||
const output = [
|
||||
//
|
||||
'<h3>Red</h3>',
|
||||
'<p>red is the fire</p>',
|
||||
'<p>the sun will be red</p>',
|
||||
'<h3>Blue</h3>',
|
||||
'',
|
||||
].join('\n');
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
it('throws if an start selector can not be found', async () => {
|
||||
const input =
|
||||
"```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=Does not exit])')\n```";
|
||||
await expectThrowsAsync(() => execute(input), {
|
||||
errorMatch: /The start selector "heading:has\(\[value=Does not exit\]\)" could not find a matching node in ".*"\.$/,
|
||||
});
|
||||
});
|
||||
|
||||
it(`can put something at the top of the file via '::addMdBefore(":root")'`, async () => {
|
||||
const input = [
|
||||
//
|
||||
'### Red',
|
||||
'',
|
||||
'red is the fire',
|
||||
'',
|
||||
'### Blue',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
//
|
||||
'```',
|
||||
"::addMdBefore(':root')",
|
||||
'```',
|
||||
'',
|
||||
'the ocean is blue',
|
||||
].join('\n');
|
||||
const output = [
|
||||
//
|
||||
'<p>the ocean is blue</p>',
|
||||
'<h3>Red</h3>',
|
||||
'<p>red is the fire</p>',
|
||||
'<h3>Blue</h3>',
|
||||
'',
|
||||
].join('\n');
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
it('throws if an end selector can not be found', async () => {
|
||||
const input =
|
||||
"```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=More Red])', 'heading:has([value=Does not exit])')\n```";
|
||||
await expectThrowsAsync(() => execute(input), {
|
||||
errorMatch: /The end selector "heading:has\(\[value=Does not exit\]\)" could not find a matching node in ".*"\./,
|
||||
});
|
||||
});
|
||||
|
||||
it('can remove from a starting point downward', async () => {
|
||||
const input = [
|
||||
'### Red',
|
||||
'',
|
||||
'red is the fire',
|
||||
'',
|
||||
'### More Red', // <-- start
|
||||
'',
|
||||
'the sun can get red',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
//
|
||||
'```',
|
||||
"::removeFrom('heading:has([value=More Red])')",
|
||||
'```',
|
||||
].join('\n');
|
||||
const output = [
|
||||
//
|
||||
'<h3>Red</h3>',
|
||||
'<p>red is the fire</p>',
|
||||
'',
|
||||
].join('\n');
|
||||
it('can import a block (from start headline to next headline same level)', async () => {
|
||||
const result = await execute(
|
||||
[
|
||||
//
|
||||
'### Static Headline',
|
||||
"```js ::importBlock('./fixtures/three-sections-red.md', '## More Red')",
|
||||
'```',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
expect(result).to.equal(
|
||||
[
|
||||
//
|
||||
'<h3>Static Headline</h3>',
|
||||
'<h2>More Red</h2>',
|
||||
'<p>the sun can get red</p>',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('can remove a range (start point included, endpoint not)', async () => {
|
||||
const input = [
|
||||
'### Red', // <-- start
|
||||
'',
|
||||
'red is the fire',
|
||||
'',
|
||||
'### More Red', // <-- end
|
||||
'',
|
||||
'the sun can get red',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
'```',
|
||||
`::removeBetween('heading:has([value=Red])', 'heading:has([value=Red]) ~ heading[depth=3]')`,
|
||||
'```',
|
||||
].join('\n');
|
||||
const output = [
|
||||
'<h3>More Red</h3>', // <-- end
|
||||
'<p>the sun can get red</p>',
|
||||
'',
|
||||
].join('\n');
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
it('can import the last block (from start headline to end of file)', async () => {
|
||||
const result = await execute(
|
||||
[
|
||||
//
|
||||
'### Static Headline',
|
||||
"```js ::importBlock('./fixtures/three-sections-red.md', '## Additional Red')",
|
||||
'```',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
expect(result).to.equal(
|
||||
[
|
||||
//
|
||||
'<h3>Static Headline</h3>',
|
||||
'<h2>Additional Red</h2>',
|
||||
'<p>the red sea</p>',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('does replacements in order of extendMd', async () => {
|
||||
const input = [
|
||||
'### Red', // <-- start
|
||||
'',
|
||||
'red is the fire',
|
||||
'### More',
|
||||
].join('\n');
|
||||
const extendMd = [
|
||||
'```',
|
||||
"::removeBetween('heading:has([value=Red]) + *', 'heading:has([value=Red]) ~ heading')",
|
||||
'```',
|
||||
"```js ::replaceFrom(':root')",
|
||||
'module.exports.replaceSection = (node) => {',
|
||||
' if (node.value) {',
|
||||
" node.value = node.value.replace(/red/g, 'green').replace(/Red/g, 'Green');",
|
||||
' }',
|
||||
' return node;',
|
||||
'};',
|
||||
'```',
|
||||
"```js ::replaceFrom(':root')",
|
||||
'module.exports.replaceSection = (node) => {',
|
||||
' if (node.value) {',
|
||||
" node.value = node.value.replace(/green/g, 'yellow').replace(/Green/g, 'Yellow');",
|
||||
' }',
|
||||
' return node;',
|
||||
'};',
|
||||
'```',
|
||||
'```',
|
||||
"::addMdAfter('heading:has([value=Yellow])')",
|
||||
'```',
|
||||
'This is added',
|
||||
].join('\n');
|
||||
const output = [
|
||||
'<h3>Yellow</h3>', // <-- start
|
||||
'<p>This is added</p>',
|
||||
'<h3>More</h3>',
|
||||
'',
|
||||
].join('\n');
|
||||
it('can import a block content (from start headline (excluding) to next headline same level)', async () => {
|
||||
const result = await execute(
|
||||
[
|
||||
//
|
||||
'### Static Headline',
|
||||
"```js ::importBlockContent('./fixtures/three-sections-red.md', '## More Red')",
|
||||
'```',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
const parser = unified()
|
||||
//
|
||||
.use(markdown)
|
||||
.use(remarkExtend, { extendMd })
|
||||
.use(mdStringify);
|
||||
const result = await parser.process(input);
|
||||
expect(result).to.equal(
|
||||
[
|
||||
//
|
||||
'<h3>Static Headline</h3>',
|
||||
'<p>the sun can get red</p>',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.contents).to.equal(output);
|
||||
it('can import a small block (from start headline to next headline of any level)', async () => {
|
||||
const result = await execute(
|
||||
[
|
||||
//
|
||||
'### Static Headline',
|
||||
"```js ::importSmallBlock('./fixtures/three-sections-red.md', '# Red')",
|
||||
'```',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
expect(result).to.equal(
|
||||
[
|
||||
//
|
||||
'<h3>Static Headline</h3>',
|
||||
'<h1>Red</h1>',
|
||||
'<p>red is the fire</p>',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('can import a small block content (from start headline (excluding) to next headline of any level)', async () => {
|
||||
const result = await execute(
|
||||
[
|
||||
//
|
||||
'### Static Headline',
|
||||
"```js ::importSmallBlockContent('./fixtures/three-sections-red.md', '# Red')",
|
||||
'```',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
expect(result).to.equal(
|
||||
[
|
||||
//
|
||||
'<h3>Static Headline</h3>',
|
||||
'<p>red is the fire</p>',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves imports via node resolution', async () => {
|
||||
// NOTE: this test can easily break as it reads content from a 3rd. party package
|
||||
// If that content changes the test should be adjusted
|
||||
const result = await execute(
|
||||
[
|
||||
//
|
||||
'### Static Headline',
|
||||
"```js ::importBlock('unified/readme.md', '## Install')",
|
||||
'```',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
expect(result).to.equal(
|
||||
[
|
||||
'<h3>Static Headline</h3>',
|
||||
'<h2>Install</h2>',
|
||||
'<p>[npm][]:</p>',
|
||||
'<pre><code class="language-sh">npm install unified',
|
||||
'</code></pre>',
|
||||
'<p>This package comes with types.',
|
||||
'If you’re using TypeScript, make sure to also install',
|
||||
'[<code>@types/unist</code>][ts-unist].</p>',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if importBlock is used not with headlines', async () => {
|
||||
const input =
|
||||
"```js ::importBlock('./fixtures/three-sections-red.md', 'heading:has([value=More Red])')\n```";
|
||||
await expectThrowsAsync(() => execute(input), {
|
||||
errorMessage:
|
||||
'::importBlock only works for headlines like "## My Headline" but "heading:has([value=More Red])" was given',
|
||||
});
|
||||
});
|
||||
|
||||
it('can import multiple files', async () => {
|
||||
const result = await execute(
|
||||
[
|
||||
//
|
||||
'### Static Headline',
|
||||
"```js ::importBlock('./fixtures/three-sections-red.md', '## More Red')",
|
||||
'```',
|
||||
'### Another Static Headline',
|
||||
"```js ::import('./fixtures/import-me.md')",
|
||||
'```',
|
||||
].join('\n'),
|
||||
);
|
||||
|
||||
expect(result).to.equal(
|
||||
[
|
||||
//
|
||||
'<h3>Static Headline</h3>',
|
||||
'<h2>More Red</h2>',
|
||||
'<p>the sun can get red</p>',
|
||||
'<h3>Another Static Headline</h3>',
|
||||
'<h1>import me headline</h1>',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue