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
This commit is contained in:
nokazn 2022-06-03 21:26:39 +09:00 committed by GitHub
parent d2bfb754c0
commit 939fe15925
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 51 additions and 10 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/markdown-remark': minor
---
Fix cases for JSX-like expressions in code blocks of headings

View file

@ -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

View file

@ -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('<h1 id="frontmattertitle"><code is:raw>{frontmatter.title}</code></h1>');
});
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(
'<h1 id="-foo--is-a-shorthand-for--foo-foo-"><code is:raw>{ foo }</code> is a shorthand for <code is:raw>{ foo: foo }</code></h1>'
);
});
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<never, never>\` <small>(at TypeScript v{frontmatter.version})</small>`,
{}
);
chai
.expect(code)
.to.equal(
`<h6 id={$$slug(\`{} is equivalent to Record&lt;never, never&gt; (at TypeScript v\${frontmatter.version})\`)}><code is:raw>{}</code> is equivalent to <code is:raw>Record&lt;never, never&gt;</code> <small>(at TypeScript v{frontmatter.version})</small></h6>`
);
});
it('should be able to serialize function expression', async () => {
const { code } = await renderMarkdown(
`{frontmatter.list.map(item => <p id={item}>{item}</p>)}`,