fix(remark-extend): support md files with frontmatter

This commit is contained in:
Thijs Louisse 2025-03-27 00:03:26 +01:00 committed by Thijs Louisse
parent 656b39e0e4
commit 7eb8588bb1
4 changed files with 105 additions and 11 deletions

View file

@ -0,0 +1,5 @@
---
'remark-extend': patch
---
support md files with frontmatter

View file

@ -19,17 +19,31 @@ const path = require('path');
* // execute function * // execute function
* virtualMod.fn(); * virtualMod.fn();
* *
* @param {*} src * @param {string} src
* @param {*} filename * @param {string} filename
* @returns {object}
*/ */
function requireFromString(src, filename = 'tmp.js') { function requireFromString(src, filename = 'tmp.js') {
const srcWithPath = `const path = require('path');\n${src}`; const srcWithPath = `const path = require('path');\n${src}`;
// @ts-expect-error
const m = new module.constructor(); const m = new module.constructor();
// @ts-expect-error
m.paths = module.paths; m.paths = module.paths;
m._compile(srcWithPath, filename); m._compile(srcWithPath, filename);
return m.exports; return m.exports;
} }
/**
* Frontmatter is problematic when traversing md files with
* unist-util-select: https://github.com/syntax-tree/unist-util-select/blob/main/index.js.
* So we remove it before parsing
* @param {string} mdFileString
* @returns {string}
*/
function stripFrontMatter(mdFileString) {
return mdFileString.replace(/^\s*---(.|\n)*?---/, '');
}
let toInsertNodes = []; let toInsertNodes = [];
function handleImportedFile({ function handleImportedFile({
@ -39,11 +53,12 @@ function handleImportedFile({
globalReplaceFunction, globalReplaceFunction,
filePath, filePath,
missingEndSelectorMeansUntilEndOfFile = false, missingEndSelectorMeansUntilEndOfFile = false,
currentFile,
}) { }) {
return tree => { return tree => {
const start = select(startSelector, tree); const start = select(startSelector, tree);
if (!start) { if (!start) {
const msg = `The start selector "${startSelector}" could not find a matching node in "${filePath}".`; const msg = `The start selector "${startSelector}", imported in "${currentFile}", could not find a matching node in "${filePath}".`;
throw new Error(msg); throw new Error(msg);
} }
const startIsNode = { ...start }; const startIsNode = { ...start };
@ -54,7 +69,7 @@ function handleImportedFile({
const end = select(endSelector, tree); const end = select(endSelector, tree);
if (!end) { if (!end) {
if (missingEndSelectorMeansUntilEndOfFile === false) { if (missingEndSelectorMeansUntilEndOfFile === false) {
const msg = `The end selector "${endSelector}" could not find a matching node in "${filePath}".`; const msg = `The end selector "${endSelector}", imported in "${currentFile}", could not find a matching node in "${filePath}".`;
throw new Error(msg); throw new Error(msg);
} }
} else { } else {
@ -92,6 +107,8 @@ function handleImportedFile({
// unified expect direct // unified expect direct
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
function remarkExtend({ rootDir = process.cwd(), page, globalReplaceFunction } = {}) { function remarkExtend({ rootDir = process.cwd(), page, globalReplaceFunction } = {}) {
const currentFile = path.resolve(rootDir, page.inputPath);
return tree => { return tree => {
visit(tree, (node, index, parent) => { visit(tree, (node, index, parent) => {
if ( if (
@ -186,11 +203,12 @@ function remarkExtend({ rootDir = process.cwd(), page, globalReplaceFunction } =
filePath, filePath,
fileImport, fileImport,
missingEndSelectorMeansUntilEndOfFile, missingEndSelectorMeansUntilEndOfFile,
currentFile,
}) })
.use(function plugin() { .use(function plugin() {
this.Compiler = () => ''; this.Compiler = () => '';
}); });
parser.processSync(importFileContent.toString()); parser.processSync(stripFrontMatter(importFileContent.toString()));
if (node.type === 'root') { if (node.type === 'root') {
node.children.splice(0, 0, ...toInsertNodes); node.children.splice(0, 0, ...toInsertNodes);

View file

@ -0,0 +1,24 @@
---
parts:
- API Table
- Form
- Systems
title: 'Form: API Table'
eleventyNavigation:
key: API Table >> Form >> Systems
title: API Table
order: 90
parent: Systems >> Form
---
# Red
red is the fire
## More Red
the sun can get red
## Additional Red
the red sea

View file

@ -4,6 +4,7 @@ import { expect } from 'chai';
import unified from 'unified'; import unified from 'unified';
import markdown from 'remark-parse'; import markdown from 'remark-parse';
import mdStringify from 'remark-html'; import mdStringify from 'remark-html';
import gfm from 'remark-gfm';
import { remarkExtend } from '../src/remarkExtend.js'; import { remarkExtend } from '../src/remarkExtend.js';
@ -22,6 +23,9 @@ async function expectThrowsAsync(method, { errorMatch, errorMessage } = {}) {
} catch (err) { } catch (err) {
error = err; error = err;
} }
console.debug(error);
expect(error).to.be.an('Error', 'No error was thrown'); expect(error).to.be.an('Error', 'No error was thrown');
if (errorMatch) { if (errorMatch) {
expect(error.message).to.match(errorMatch); expect(error.message).to.match(errorMatch);
@ -31,20 +35,42 @@ async function expectThrowsAsync(method, { errorMatch, errorMessage } = {}) {
} }
} }
async function execute(input, { globalReplaceFunction } = {}) { /**
*
* @param {string} input
* @param {{shouldStringify?: boolean; globalReplaceFunction?: Function;}} param1
*/
async function execute(input, { shouldStringify = true, globalReplaceFunction } = {}) {
const parser = unified() const parser = unified()
// //
.use(markdown) .use(markdown)
.use(gfm)
.use(remarkExtend, { .use(remarkExtend, {
rootDir: __dirname, rootDir: __dirname,
page: { inputPath: 'test-file.md' }, page: { inputPath: 'test-file.md' },
globalReplaceFunction, globalReplaceFunction,
}) });
.use(mdStringify);
if (shouldStringify) {
parser.use(mdStringify);
const result = await parser.process(input); const result = await parser.process(input);
return result.contents; return result.contents;
} }
let tree;
parser
.use(() => _tree => {
tree = _tree;
})
// @ts-expect-error
.use(function plugin() {
this.Compiler = () => '';
});
await parser.process(input);
return tree;
}
describe('remarkExtend', () => { describe('remarkExtend', () => {
it('does no modifications if no action is found', async () => { it('does no modifications if no action is found', async () => {
const result = await execute( const result = await execute(
@ -196,7 +222,7 @@ describe('remarkExtend', () => {
"```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=Does not exit])')\n```"; "```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=Does not exit])')\n```";
await expectThrowsAsync(() => execute(input), { await expectThrowsAsync(() => execute(input), {
errorMatch: errorMatch:
/The start selector "heading:has\(\[value=Does not exit\]\)" could not find a matching node in ".*"\.$/, /The start selector "heading:has\(\[value=Does not exit\]\)", imported in ".*", could not find a matching node in ".*"\.$/,
}); });
}); });
@ -205,7 +231,7 @@ describe('remarkExtend', () => {
"```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=More Red])', 'heading:has([value=Does not exit])')\n```"; "```js ::import('./fixtures/three-sections-red.md', 'heading:has([value=More Red])', 'heading:has([value=Does not exit])')\n```";
await expectThrowsAsync(() => execute(input), { await expectThrowsAsync(() => execute(input), {
errorMatch: errorMatch:
/The end selector "heading:has\(\[value=Does not exit\]\)" could not find a matching node in ".*"\./, /The end selector "heading:has\(\[value=Does not exit\]\)", imported in ".*", could not find a matching node in ".*"\./,
}); });
}); });
@ -404,4 +430,25 @@ describe('remarkExtend', () => {
].join('\n'), ].join('\n'),
); );
}); });
it('supports files with frontmatter', async () => {
const result = await execute(
[
//
'### Static Headline',
"```js ::importBlock('./fixtures/three-sections-red-with-frontmatter.md', '## More Red')",
'```',
].join('\n'),
);
expect(result).to.equal(
[
//
'<h3>Static Headline</h3>',
'<h2>More Red</h2>',
'<p>the sun can get red</p>',
'',
].join('\n'),
);
});
}); });