diff options
Diffstat (limited to 'packages/markdown/remark/src')
-rw-r--r-- | packages/markdown/remark/src/rehype-collect-headings.ts | 75 |
1 files changed, 72 insertions, 3 deletions
diff --git a/packages/markdown/remark/src/rehype-collect-headings.ts b/packages/markdown/remark/src/rehype-collect-headings.ts index 97fe30401..a1083f609 100644 --- a/packages/markdown/remark/src/rehype-collect-headings.ts +++ b/packages/markdown/remark/src/rehype-collect-headings.ts @@ -1,7 +1,10 @@ +import { type Expression, type Super } from 'estree'; import Slugger from 'github-slugger'; -import { visit } from 'unist-util-visit'; +import { type MdxTextExpression } from 'mdast-util-mdx-expression'; +import { visit, type Node } from 'unist-util-visit'; -import type { MarkdownHeading, MarkdownVFile, RehypePlugin } from './types.js'; +import { InvalidAstroDataError, safelyGetAstroData } from './frontmatter-injection.js'; +import type { MarkdownAstroData, MarkdownHeading, MarkdownVFile, RehypePlugin } from './types.js'; const rawNodeTypes = new Set(['text', 'raw', 'mdxTextExpression']); const codeTagNames = new Set(['code', 'pre']); @@ -11,6 +14,7 @@ export function rehypeHeadingIds(): ReturnType<RehypePlugin> { const headings: MarkdownHeading[] = []; const slugger = new Slugger(); const isMDX = isMDXFile(file); + const astroData = safelyGetAstroData(file.data); visit(tree, (node) => { if (node.type !== 'element') return; const { tagName } = node; @@ -31,7 +35,17 @@ export function rehypeHeadingIds(): ReturnType<RehypePlugin> { } if (rawNodeTypes.has(child.type)) { if (isMDX || codeTagNames.has(parent.tagName)) { - text += child.value; + let value = child.value; + if (isMdxTextExpression(child) && !(astroData instanceof InvalidAstroDataError)) { + const frontmatterPath = getMdxFrontmatterVariablePath(child); + if (Array.isArray(frontmatterPath) && frontmatterPath.length > 0) { + const frontmatterValue = getMdxFrontmatterVariableValue(astroData, frontmatterPath); + if (typeof frontmatterValue === 'string') { + value = frontmatterValue; + } + } + } + text += value; } else { text += child.value.replace(/\{/g, '${'); } @@ -57,3 +71,58 @@ export function rehypeHeadingIds(): ReturnType<RehypePlugin> { function isMDXFile(file: MarkdownVFile) { return Boolean(file.history[0]?.endsWith('.mdx')); } + +/** + * Check if an ESTree entry is `frontmatter.*.VARIABLE`. + * If it is, return the variable path (i.e. `["*", ..., "VARIABLE"]`) minus the `frontmatter` prefix. + */ +function getMdxFrontmatterVariablePath(node: MdxTextExpression): string[] | Error { + if (!node.data?.estree || node.data.estree.body.length !== 1) return new Error(); + + const statement = node.data.estree.body[0]; + + // Check for "[ANYTHING].[ANYTHING]". + if (statement?.type !== 'ExpressionStatement' || statement.expression.type !== 'MemberExpression') + return new Error(); + + let expression: Expression | Super = statement.expression; + const expressionPath: string[] = []; + + // Traverse the expression, collecting the variable path. + while ( + expression.type === 'MemberExpression' && + expression.property.type === (expression.computed ? 'Literal' : 'Identifier') + ) { + expressionPath.push( + expression.property.type === 'Literal' + ? String(expression.property.value) + : expression.property.name + ); + + expression = expression.object; + } + + // Check for "frontmatter.[ANYTHING]". + if (expression.type !== 'Identifier' || expression.name !== 'frontmatter') return new Error(); + + return expressionPath.reverse(); +} + +function getMdxFrontmatterVariableValue(astroData: MarkdownAstroData, path: string[]) { + let value: MdxFrontmatterVariableValue = astroData.frontmatter; + + for (const key of path) { + if (!value[key]) return undefined; + + value = value[key]; + } + + return value; +} + +function isMdxTextExpression(node: Node): node is MdxTextExpression { + return node.type === 'mdxTextExpression'; +} + +type MdxFrontmatterVariableValue = + MarkdownAstroData['frontmatter'][keyof MarkdownAstroData['frontmatter']]; |