From 939fe159255cecf1cab5c1b3da2670d30ac8e4a7 Mon Sep 17 00:00:00 2001 From: nokazn <41154684+nokazn@users.noreply.github.com> Date: Fri, 3 Jun 2022 21:26:39 +0900 Subject: [PATCH] Fix cases for JSX-like expressions in code blocks of headings (#3502) * chore: fix typo in remark tests * test: add test cases for markdown expressions in header * fix: avoid evaluating JSX-like expressions inside inline code in heading * fix: generate slug for id including values in backtick blocks --- .changeset/lazy-humans-invent.md | 5 +++ .../remark/src/rehype-collect-headers.ts | 23 ++++++++----- .../markdown/remark/test/expressions.test.js | 33 ++++++++++++++++++- 3 files changed, 51 insertions(+), 10 deletions(-) create mode 100644 .changeset/lazy-humans-invent.md diff --git a/.changeset/lazy-humans-invent.md b/.changeset/lazy-humans-invent.md new file mode 100644 index 000000000..1203d20fd --- /dev/null +++ b/.changeset/lazy-humans-invent.md @@ -0,0 +1,5 @@ +--- +'@astrojs/markdown-remark': minor +--- + +Fix cases for JSX-like expressions in code blocks of headings diff --git a/packages/markdown/remark/src/rehype-collect-headers.ts b/packages/markdown/remark/src/rehype-collect-headers.ts index 426b782a3..44881864b 100644 --- a/packages/markdown/remark/src/rehype-collect-headers.ts +++ b/packages/markdown/remark/src/rehype-collect-headers.ts @@ -1,4 +1,5 @@ import { visit } from 'unist-util-visit'; +import { toHtml } from 'hast-util-to-html'; import Slugger from 'github-slugger'; import type { MarkdownHeader, RehypePlugin } from './types.js'; @@ -17,32 +18,36 @@ export default function createCollectHeaders() { if (!level) return; const depth = Number.parseInt(level); - let raw = ''; let text = ''; let isJSX = false; - visit(node, (child) => { - if (child.type === 'element') { + visit(node, (child, _, parent) => { + if (child.type === 'element' || parent == null) { return; } if (child.type === 'raw') { - // HACK: serialized JSX from internal plugins, ignore these for slug if (child.value.startsWith('\n<') || child.value.endsWith('>\n')) { - raw += child.value.replace(/^\n|\n$/g, ''); return; } } if (child.type === 'text' || child.type === 'raw') { - raw += child.value; - text += child.value; - isJSX = isJSX || child.value.includes('{'); + if (new Set(['code', 'pre']).has(parent.tagName)) { + text += child.value; + } else { + text += child.value.replace(/\{/g, '${'); + isJSX = isJSX || child.value.includes('{'); + } } }); node.properties = node.properties || {}; if (typeof node.properties.id !== 'string') { if (isJSX) { + // HACK: serialized JSX from internal plugins, ignore these for slug + const raw = toHtml(node.children, { allowDangerousHtml: true }) + .replace(/\n(<)/g, '<') + .replace(/(>)\n/g, '>'); // HACK: for ids that have JSX content, use $$slug helper to generate slug at runtime - node.properties.id = `$$slug(\`${text.replace(/\{/g, '${')}\`)`; + node.properties.id = `$$slug(\`${text}\`)`; (node as any).type = 'raw'; ( node as any diff --git a/packages/markdown/remark/test/expressions.test.js b/packages/markdown/remark/test/expressions.test.js index c3c341acd..ba28d9c8e 100644 --- a/packages/markdown/remark/test/expressions.test.js +++ b/packages/markdown/remark/test/expressions.test.js @@ -2,7 +2,7 @@ import { renderMarkdown } from '../dist/index.js'; import chai from 'chai'; describe('expressions', () => { - it('should be able to serialize bare expession', async () => { + it('should be able to serialize bare expression', async () => { const { code } = await renderMarkdown(`{a}`, {}); chai.expect(code).to.equal(`{a}`); @@ -40,6 +40,37 @@ describe('expressions', () => { ); }); + it('should be able to avoid evaluating JSX-like expressions in an inline code & generate a slug for id', async () => { + const { code } = await renderMarkdown(`# \`{frontmatter.title}\``, {}); + + chai + .expect(code) + .to.equal('

{frontmatter.title}

'); + }); + + it('should be able to avoid evaluating JSX-like expressions in inline codes', async () => { + const { code } = await renderMarkdown(`# \`{ foo }\` is a shorthand for \`{ foo: foo }\``, {}); + + chai + .expect(code) + .to.equal( + '

{ foo } is a shorthand for { foo: foo }

' + ); + }); + + it('should be able to avoid evaluating JSX-like expressions & escape HTML tag characters in inline codes', async () => { + const { code } = await renderMarkdown( + `###### \`{}\` is equivalent to \`Record\` (at TypeScript v{frontmatter.version})`, + {} + ); + + chai + .expect(code) + .to.equal( + `
{} is equivalent to Record<never, never> (at TypeScript v{frontmatter.version})
` + ); + }); + it('should be able to serialize function expression', async () => { const { code } = await renderMarkdown( `{frontmatter.list.map(item =>

{item}

)}`,