From 7eb8588bb1fc7e1b559840142597fedd90dc98dd Mon Sep 17 00:00:00 2001 From: Thijs Louisse Date: Thu, 27 Mar 2025 00:03:26 +0100 Subject: [PATCH] fix(remark-extend): support md files with frontmatter --- .changeset/purple-ties-complain.md | 5 ++ .../remark-extend/src/remarkExtend.js | 28 +++++++-- .../three-sections-red-with-frontmatter.md | 24 ++++++++ .../test-node/remark-extend.test.mjs | 59 +++++++++++++++++-- 4 files changed, 105 insertions(+), 11 deletions(-) create mode 100644 .changeset/purple-ties-complain.md create mode 100644 packages-node/remark-extend/test-node/fixtures/three-sections-red-with-frontmatter.md diff --git a/.changeset/purple-ties-complain.md b/.changeset/purple-ties-complain.md new file mode 100644 index 000000000..b8a4b5b24 --- /dev/null +++ b/.changeset/purple-ties-complain.md @@ -0,0 +1,5 @@ +--- +'remark-extend': patch +--- + +support md files with frontmatter diff --git a/packages-node/remark-extend/src/remarkExtend.js b/packages-node/remark-extend/src/remarkExtend.js index 5fbe657c7..246cf9dd0 100644 --- a/packages-node/remark-extend/src/remarkExtend.js +++ b/packages-node/remark-extend/src/remarkExtend.js @@ -19,17 +19,31 @@ const path = require('path'); * // execute function * virtualMod.fn(); * - * @param {*} src - * @param {*} filename + * @param {string} src + * @param {string} filename + * @returns {object} */ function requireFromString(src, filename = 'tmp.js') { const srcWithPath = `const path = require('path');\n${src}`; + // @ts-expect-error const m = new module.constructor(); + // @ts-expect-error m.paths = module.paths; m._compile(srcWithPath, filename); 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 = []; function handleImportedFile({ @@ -39,11 +53,12 @@ function handleImportedFile({ globalReplaceFunction, filePath, missingEndSelectorMeansUntilEndOfFile = false, + currentFile, }) { return tree => { const start = select(startSelector, tree); 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); } const startIsNode = { ...start }; @@ -54,7 +69,7 @@ function handleImportedFile({ const end = select(endSelector, tree); if (!end) { 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); } } else { @@ -92,6 +107,8 @@ function handleImportedFile({ // unified expect direct // eslint-disable-next-line consistent-return function remarkExtend({ rootDir = process.cwd(), page, globalReplaceFunction } = {}) { + const currentFile = path.resolve(rootDir, page.inputPath); + return tree => { visit(tree, (node, index, parent) => { if ( @@ -186,11 +203,12 @@ function remarkExtend({ rootDir = process.cwd(), page, globalReplaceFunction } = filePath, fileImport, missingEndSelectorMeansUntilEndOfFile, + currentFile, }) .use(function plugin() { this.Compiler = () => ''; }); - parser.processSync(importFileContent.toString()); + parser.processSync(stripFrontMatter(importFileContent.toString())); if (node.type === 'root') { node.children.splice(0, 0, ...toInsertNodes); diff --git a/packages-node/remark-extend/test-node/fixtures/three-sections-red-with-frontmatter.md b/packages-node/remark-extend/test-node/fixtures/three-sections-red-with-frontmatter.md new file mode 100644 index 000000000..50073f914 --- /dev/null +++ b/packages-node/remark-extend/test-node/fixtures/three-sections-red-with-frontmatter.md @@ -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 diff --git a/packages-node/remark-extend/test-node/remark-extend.test.mjs b/packages-node/remark-extend/test-node/remark-extend.test.mjs index 34750177e..bf2949d38 100644 --- a/packages-node/remark-extend/test-node/remark-extend.test.mjs +++ b/packages-node/remark-extend/test-node/remark-extend.test.mjs @@ -4,6 +4,7 @@ import { expect } from 'chai'; import unified from 'unified'; import markdown from 'remark-parse'; import mdStringify from 'remark-html'; +import gfm from 'remark-gfm'; import { remarkExtend } from '../src/remarkExtend.js'; @@ -22,6 +23,9 @@ async function expectThrowsAsync(method, { errorMatch, errorMessage } = {}) { } catch (err) { error = err; } + + console.debug(error); + expect(error).to.be.an('Error', 'No error was thrown'); if (errorMatch) { expect(error.message).to.match(errorMatch); @@ -31,18 +35,40 @@ 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() // .use(markdown) + .use(gfm) .use(remarkExtend, { rootDir: __dirname, page: { inputPath: 'test-file.md' }, globalReplaceFunction, + }); + + if (shouldStringify) { + parser.use(mdStringify); + const result = await parser.process(input); + return result.contents; + } + + let tree; + parser + .use(() => _tree => { + tree = _tree; }) - .use(mdStringify); - const result = await parser.process(input); - return result.contents; + // @ts-expect-error + .use(function plugin() { + this.Compiler = () => ''; + }); + + await parser.process(input); + return tree; } describe('remarkExtend', () => { @@ -196,7 +222,7 @@ describe('remarkExtend', () => { "```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 ".*"\.$/, + /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```"; await expectThrowsAsync(() => execute(input), { 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'), ); }); + + 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( + [ + // + '

Static Headline

', + '

More Red

', + '

the sun can get red

', + '', + ].join('\n'), + ); + }); });