2023-01-26 17:52:50 +00:00
|
|
|
import { type Expression, type Super } from 'estree';
|
2022-04-30 00:07:09 +00:00
|
|
|
import Slugger from 'github-slugger';
|
2023-01-26 17:52:50 +00:00
|
|
|
import { type MdxTextExpression } from 'mdast-util-mdx-expression';
|
|
|
|
import { visit, type Node } from 'unist-util-visit';
|
2022-04-30 00:07:09 +00:00
|
|
|
|
2023-01-26 17:52:50 +00:00
|
|
|
import { InvalidAstroDataError, safelyGetAstroData } from './frontmatter-injection.js';
|
|
|
|
import type { MarkdownAstroData, MarkdownHeading, MarkdownVFile, RehypePlugin } from './types.js';
|
2021-05-17 14:29:16 +00:00
|
|
|
|
2022-12-20 22:08:15 +00:00
|
|
|
const rawNodeTypes = new Set(['text', 'raw', 'mdxTextExpression']);
|
|
|
|
const codeTagNames = new Set(['code', 'pre']);
|
2021-05-17 14:29:16 +00:00
|
|
|
|
2022-12-20 22:08:15 +00:00
|
|
|
export function rehypeHeadingIds(): ReturnType<RehypePlugin> {
|
|
|
|
return function (tree, file: MarkdownVFile) {
|
|
|
|
const headings: MarkdownHeading[] = [];
|
|
|
|
const slugger = new Slugger();
|
|
|
|
const isMDX = isMDXFile(file);
|
2023-01-26 17:52:50 +00:00
|
|
|
const astroData = safelyGetAstroData(file.data);
|
2022-12-20 22:08:15 +00:00
|
|
|
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);
|
2021-08-25 12:17:45 +00:00
|
|
|
|
2022-12-20 22:08:15 +00:00
|
|
|
let text = '';
|
|
|
|
visit(node, (child, __, parent) => {
|
|
|
|
if (child.type === 'element' || parent == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (child.type === 'raw') {
|
|
|
|
if (child.value.match(/^\n?<.*>\n?$/)) {
|
2022-05-24 22:02:11 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-12-20 22:08:15 +00:00
|
|
|
}
|
|
|
|
if (rawNodeTypes.has(child.type)) {
|
|
|
|
if (isMDX || codeTagNames.has(parent.tagName)) {
|
2023-01-26 17:52:50 +00:00
|
|
|
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;
|
2022-12-20 22:08:15 +00:00
|
|
|
} else {
|
|
|
|
text += child.value.replace(/\{/g, '${');
|
2022-05-24 22:02:11 +00:00
|
|
|
}
|
2022-12-20 22:08:15 +00:00
|
|
|
}
|
|
|
|
});
|
2021-08-25 12:17:45 +00:00
|
|
|
|
2022-12-20 22:08:15 +00:00
|
|
|
node.properties = node.properties || {};
|
|
|
|
if (typeof node.properties.id !== 'string') {
|
2023-01-09 09:23:21 +00:00
|
|
|
let slug = slugger.slug(text);
|
2022-07-19 05:23:28 +00:00
|
|
|
|
2023-01-09 09:23:21 +00:00
|
|
|
if (slug.endsWith('-')) slug = slug.slice(0, -1);
|
2022-07-19 05:23:28 +00:00
|
|
|
|
2023-01-09 09:23:21 +00:00
|
|
|
node.properties.id = slug;
|
2022-12-20 22:08:15 +00:00
|
|
|
}
|
2022-04-30 00:07:09 +00:00
|
|
|
|
2022-12-20 22:08:15 +00:00
|
|
|
headings.push({ depth, slug: node.properties.id, text });
|
|
|
|
});
|
2021-08-25 12:17:45 +00:00
|
|
|
|
2022-12-20 22:08:15 +00:00
|
|
|
file.data.__astroHeadings = headings;
|
2021-05-17 14:30:21 +00:00
|
|
|
};
|
2021-05-17 14:29:16 +00:00
|
|
|
}
|
2022-12-20 22:08:15 +00:00
|
|
|
|
|
|
|
function isMDXFile(file: MarkdownVFile) {
|
|
|
|
return Boolean(file.history[0]?.endsWith('.mdx'));
|
|
|
|
}
|
2023-01-26 17:52:50 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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']];
|