astro/packages/markdown/remark/src/rehype-collect-headings.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

130 lines
4 KiB
TypeScript
Raw Normal View History

import type { Expression, Super } from 'estree';
import Slugger from 'github-slugger';
import type { MdxTextExpression } from 'mdast-util-mdx-expression';
import type { Node } from 'unist';
import { visit } from 'unist-util-visit';
import { InvalidAstroDataError, safelyGetAstroData } from './frontmatter-injection.js';
import type { MarkdownAstroData, MarkdownHeading, MarkdownVFile, RehypePlugin } from './types.js';
Fix markdown issues (#208) * Init fix/markdown * Astro Markdown (#207) * Add Astro Markdown to VSCode Extension * Add Astro Markdown to Astro * refactor: update astro-markdown example * feat: remove embedded components from `.md` files * fix: resolve `.md.astro` files at runtime * chore: update markdown tests * feat: add <Markdown> component * chore: bump examples * chore: update example * fix: improve Markdown child handling * feat: harden markdown support, add code fence support, add automatic dedenting * chore: add weird markdown edge cases * chore: update remote-markdown examples * chore: add comment to Markdown.astro * feat: improve markdown support (codefences, nested inside HTML) * refactor: extract import specifier types to set * refactor: conditionally import markdown renderer * refactor: revert special-cased "astro/components" * refactor: revert special-cased "astro/components" * refactor: use astro/components/Markdown.astro * refactor: remove `.md.astro` support in favor of Markdown component * refactor: use regular .astro files * refactor: remove unused code * refactor: move Markdown inside Layout * wip: markdown scoped styles * feat: improve scoped styles in Markdown * feat: micromark => remark ecosystem * fix: markdown build * fix: markdown build * chore: add todo * fix: collect headers text * docs: add Markdown doc * chore: add changeset * docs: improve Markdown highlighting * refactor: prefer Set * refactor: exclude large unified deps * docs: update markdown docs Co-authored-by: Jonathan Neal <jonathantneal@hotmail.com> * chore: remove extra markdown deps * perf: optimize markdown * fix: unified/rehype deps * temp: fix markdown test * test: add TODO comment * fix: do not namespace frontmatter, just astro metadata * test: fix astro-markdown test * test: add realworld markdown example * fix: prism language bug * docs: update markdown docs * chore: bump dependencies * fix: escape codespan * fix: unterminated string literal * fix(vscode): inline dependencies * fix(vscode): dependencies * feat(vscode): embedded markdown * feat: add Markdown syntax highlighting * chore: improve markdown example * fix: markdown example * feat: highlighting improvements * chore: add changeset * fix: CodeBlock => CodeSpan * chore: get astro-markdown example running Co-authored-by: Jonathan Neal <jonathantneal@hotmail.com>
2021-05-17 14:29:16 +00:00
const rawNodeTypes = new Set(['text', 'raw', 'mdxTextExpression']);
const codeTagNames = new Set(['code', 'pre']);
Fix markdown issues (#208) * Init fix/markdown * Astro Markdown (#207) * Add Astro Markdown to VSCode Extension * Add Astro Markdown to Astro * refactor: update astro-markdown example * feat: remove embedded components from `.md` files * fix: resolve `.md.astro` files at runtime * chore: update markdown tests * feat: add <Markdown> component * chore: bump examples * chore: update example * fix: improve Markdown child handling * feat: harden markdown support, add code fence support, add automatic dedenting * chore: add weird markdown edge cases * chore: update remote-markdown examples * chore: add comment to Markdown.astro * feat: improve markdown support (codefences, nested inside HTML) * refactor: extract import specifier types to set * refactor: conditionally import markdown renderer * refactor: revert special-cased "astro/components" * refactor: revert special-cased "astro/components" * refactor: use astro/components/Markdown.astro * refactor: remove `.md.astro` support in favor of Markdown component * refactor: use regular .astro files * refactor: remove unused code * refactor: move Markdown inside Layout * wip: markdown scoped styles * feat: improve scoped styles in Markdown * feat: micromark => remark ecosystem * fix: markdown build * fix: markdown build * chore: add todo * fix: collect headers text * docs: add Markdown doc * chore: add changeset * docs: improve Markdown highlighting * refactor: prefer Set * refactor: exclude large unified deps * docs: update markdown docs Co-authored-by: Jonathan Neal <jonathantneal@hotmail.com> * chore: remove extra markdown deps * perf: optimize markdown * fix: unified/rehype deps * temp: fix markdown test * test: add TODO comment * fix: do not namespace frontmatter, just astro metadata * test: fix astro-markdown test * test: add realworld markdown example * fix: prism language bug * docs: update markdown docs * chore: bump dependencies * fix: escape codespan * fix: unterminated string literal * fix(vscode): inline dependencies * fix(vscode): dependencies * feat(vscode): embedded markdown * feat: add Markdown syntax highlighting * chore: improve markdown example * fix: markdown example * feat: highlighting improvements * chore: add changeset * fix: CodeBlock => CodeSpan * chore: get astro-markdown example running Co-authored-by: Jonathan Neal <jonathantneal@hotmail.com>
2021-05-17 14:29:16 +00:00
export function rehypeHeadingIds(): ReturnType<RehypePlugin> {
return function (tree, file: MarkdownVFile) {
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;
if (tagName[0] !== 'h') return;
const [, level] = tagName.match(/h([0-6])/) ?? [];
if (!level) return;
const depth = Number.parseInt(level);
let text = '';
visit(node, (child, __, parent) => {
if (child.type === 'element' || parent == null) {
return;
}
if (child.type === 'raw') {
if (child.value.match(/^\n?<.*>\n?$/)) {
return;
}
}
if (rawNodeTypes.has(child.type)) {
if (isMDX || codeTagNames.has(parent.tagName)) {
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, '${');
}
}
});
node.properties = node.properties || {};
if (typeof node.properties.id !== 'string') {
let slug = slugger.slug(text);
2022-07-19 05:23:28 +00:00
if (slug.endsWith('-')) slug = slug.slice(0, -1);
2022-07-19 05:23:28 +00:00
node.properties.id = slug;
}
headings.push({ depth, slug: node.properties.id, text });
});
file.data.__astroHeadings = headings;
2021-05-17 14:30:21 +00:00
};
Fix markdown issues (#208) * Init fix/markdown * Astro Markdown (#207) * Add Astro Markdown to VSCode Extension * Add Astro Markdown to Astro * refactor: update astro-markdown example * feat: remove embedded components from `.md` files * fix: resolve `.md.astro` files at runtime * chore: update markdown tests * feat: add <Markdown> component * chore: bump examples * chore: update example * fix: improve Markdown child handling * feat: harden markdown support, add code fence support, add automatic dedenting * chore: add weird markdown edge cases * chore: update remote-markdown examples * chore: add comment to Markdown.astro * feat: improve markdown support (codefences, nested inside HTML) * refactor: extract import specifier types to set * refactor: conditionally import markdown renderer * refactor: revert special-cased "astro/components" * refactor: revert special-cased "astro/components" * refactor: use astro/components/Markdown.astro * refactor: remove `.md.astro` support in favor of Markdown component * refactor: use regular .astro files * refactor: remove unused code * refactor: move Markdown inside Layout * wip: markdown scoped styles * feat: improve scoped styles in Markdown * feat: micromark => remark ecosystem * fix: markdown build * fix: markdown build * chore: add todo * fix: collect headers text * docs: add Markdown doc * chore: add changeset * docs: improve Markdown highlighting * refactor: prefer Set * refactor: exclude large unified deps * docs: update markdown docs Co-authored-by: Jonathan Neal <jonathantneal@hotmail.com> * chore: remove extra markdown deps * perf: optimize markdown * fix: unified/rehype deps * temp: fix markdown test * test: add TODO comment * fix: do not namespace frontmatter, just astro metadata * test: fix astro-markdown test * test: add realworld markdown example * fix: prism language bug * docs: update markdown docs * chore: bump dependencies * fix: escape codespan * fix: unterminated string literal * fix(vscode): inline dependencies * fix(vscode): dependencies * feat(vscode): embedded markdown * feat: add Markdown syntax highlighting * chore: improve markdown example * fix: markdown example * feat: highlighting improvements * chore: add changeset * fix: CodeBlock => CodeSpan * chore: get astro-markdown example running Co-authored-by: Jonathan Neal <jonathantneal@hotmail.com>
2021-05-17 14:29:16 +00:00
}
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']];