diff options
author | 2022-12-20 23:08:15 +0100 | |
---|---|---|
committer | 2022-12-20 23:08:15 +0100 | |
commit | 2c65b433bf840a1bb93b0a1947df5949e33512ff (patch) | |
tree | 02314c6ee09b86d08e40367da647458806c932db /packages/markdown/remark/src | |
parent | a467139e169ad2eb7931e03004f1d658f7362e59 (diff) | |
download | astro-2c65b433bf840a1bb93b0a1947df5949e33512ff.tar.gz astro-2c65b433bf840a1bb93b0a1947df5949e33512ff.tar.zst astro-2c65b433bf840a1bb93b0a1947df5949e33512ff.zip |
MD/MDX collect headings refactor (#5654)
Diffstat (limited to 'packages/markdown/remark/src')
-rw-r--r-- | packages/markdown/remark/src/index.ts | 13 | ||||
-rw-r--r-- | packages/markdown/remark/src/rehype-collect-headings.ts | 112 | ||||
-rw-r--r-- | packages/markdown/remark/src/types.ts | 6 |
3 files changed, 70 insertions, 61 deletions
diff --git a/packages/markdown/remark/src/index.ts b/packages/markdown/remark/src/index.ts index c64bdac0e..6d69bcd20 100644 --- a/packages/markdown/remark/src/index.ts +++ b/packages/markdown/remark/src/index.ts @@ -1,7 +1,7 @@ -import type { MarkdownRenderingOptions, MarkdownRenderingResult } from './types'; +import type { MarkdownRenderingOptions, MarkdownRenderingResult, MarkdownVFile } from './types'; import { loadPlugins } from './load-plugins.js'; -import createCollectHeadings from './rehype-collect-headings.js'; +import { rehypeHeadingIds } from './rehype-collect-headings.js'; import rehypeEscape from './rehype-escape.js'; import rehypeExpressions from './rehype-expressions.js'; import rehypeIslands from './rehype-islands.js'; @@ -22,6 +22,7 @@ import markdownToHtml from 'remark-rehype'; import { unified } from 'unified'; import { VFile } from 'vfile'; +export { rehypeHeadingIds } from './rehype-collect-headings.js'; export * from './types.js'; export const DEFAULT_REMARK_PLUGINS = ['remark-gfm', 'remark-smartypants']; @@ -44,7 +45,6 @@ export async function renderMarkdown( } = opts; const input = new VFile({ value: content, path: fileURL }); const scopedClassName = opts.$?.scopedClassName; - const { headings, rehypeCollectHeadings } = createCollectHeadings(); let parser = unified() .use(markdown) @@ -99,12 +99,12 @@ export async function renderMarkdown( parser .use( isAstroFlavoredMd - ? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeCollectHeadings] - : [rehypeCollectHeadings, rehypeRaw] + ? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeHeadingIds] + : [rehypeHeadingIds, rehypeRaw] ) .use(rehypeStringify, { allowDangerousHtml: true }); - let vfile: VFile; + let vfile: MarkdownVFile; try { vfile = await parser.process(input); } catch (err) { @@ -116,6 +116,7 @@ export async function renderMarkdown( throw err; } + const headings = vfile?.data.__astroHeadings || []; return { metadata: { headings, source: content, html: String(vfile.value) }, code: String(vfile.value), diff --git a/packages/markdown/remark/src/rehype-collect-headings.ts b/packages/markdown/remark/src/rehype-collect-headings.ts index b42ed9030..03a3c6a23 100644 --- a/packages/markdown/remark/src/rehype-collect-headings.ts +++ b/packages/markdown/remark/src/rehype-collect-headings.ts @@ -2,72 +2,74 @@ import Slugger from 'github-slugger'; import { toHtml } from 'hast-util-to-html'; import { visit } from 'unist-util-visit'; -import type { MarkdownHeading, RehypePlugin } from './types.js'; +import type { MarkdownHeading, MarkdownVFile, RehypePlugin } from './types.js'; -export default function createCollectHeadings() { - const headings: MarkdownHeading[] = []; - const slugger = new Slugger(); +const rawNodeTypes = new Set(['text', 'raw', 'mdxTextExpression']); +const codeTagNames = new Set(['code', 'pre']); - function rehypeCollectHeadings(): ReturnType<RehypePlugin> { - return function (tree) { - visit(tree, (node) => { - if (node.type !== 'element') return; - const { tagName } = node; - if (tagName[0] !== 'h') return; - const [_, level] = tagName.match(/h([0-6])/) ?? []; - if (!level) return; - const depth = Number.parseInt(level); +export function rehypeHeadingIds(): ReturnType<RehypePlugin> { + return function (tree, file: MarkdownVFile) { + const headings: MarkdownHeading[] = []; + const slugger = new Slugger(); + const isMDX = isMDXFile(file); + visit(tree, (node) => { + if (node.type !== 'element') return; + const { tagName } = node; + if (tagName[0] !== 'h') return; + const [_, level] = tagName.match(/h([0-6])/) ?? []; + if (!level) return; + const depth = Number.parseInt(level); - let text = ''; - let isJSX = false; - visit(node, (child, __, parent) => { - if (child.type === 'element' || parent == null) { + let text = ''; + let isJSX = false; + visit(node, (child, __, parent) => { + if (child.type === 'element' || parent == null) { + return; + } + if (child.type === 'raw') { + if (child.value.match(/^\n?<.*>\n?$/)) { return; } - if (child.type === 'raw') { - if (child.value.match(/^\n?<.*>\n?$/)) { - return; - } - } - if (child.type === 'text' || child.type === 'raw') { - if (new Set(['code', 'pre']).has(parent.tagName)) { - text += child.value; - } else { - text += child.value.replace(/\{/g, '${'); - isJSX = isJSX || child.value.includes('{'); - } + } + if (rawNodeTypes.has(child.type)) { + if (isMDX || codeTagNames.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}\`)`; - (node as any).type = 'raw'; - ( - node as any - ).value = `<${node.tagName} id={${node.properties.id}}>${raw}</${node.tagName}>`; - } else { - let slug = slugger.slug(text); + 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}\`)`; + (node as any).type = 'raw'; + ( + node as any + ).value = `<${node.tagName} id={${node.properties.id}}>${raw}</${node.tagName}>`; + } else { + let slug = slugger.slug(text); - if (slug.endsWith('-')) slug = slug.slice(0, -1); + if (slug.endsWith('-')) slug = slug.slice(0, -1); - node.properties.id = slug; - } + node.properties.id = slug; } + } - headings.push({ depth, slug: node.properties.id, text }); - }); - }; - } + headings.push({ depth, slug: node.properties.id, text }); + }); - return { - headings, - rehypeCollectHeadings, + file.data.__astroHeadings = headings; }; } + +function isMDXFile(file: MarkdownVFile) { + return Boolean(file.history[0]?.endsWith('.mdx')); +} diff --git a/packages/markdown/remark/src/types.ts b/packages/markdown/remark/src/types.ts index 5b40c9f9d..76dfe9b73 100644 --- a/packages/markdown/remark/src/types.ts +++ b/packages/markdown/remark/src/types.ts @@ -68,6 +68,12 @@ export interface MarkdownMetadata { html: string; } +export interface MarkdownVFile extends VFile { + data: { + __astroHeadings?: MarkdownHeading[]; + }; +} + export interface MarkdownRenderingResult { metadata: MarkdownMetadata; vfile: VFile; |