summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/lazy-humans-invent.md5
-rw-r--r--packages/markdown/remark/src/rehype-collect-headers.ts23
-rw-r--r--packages/markdown/remark/test/expressions.test.js33
3 files changed, 51 insertions, 10 deletions
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('<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>)}`,