aboutsummaryrefslogtreecommitdiff
path: root/packages/integrations/markdoc/src/heading-ids.ts
diff options
context:
space:
mode:
authorGravatar github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 2025-06-05 14:25:23 +0000
committerGravatar github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 2025-06-05 14:25:23 +0000
commite586d7d704d475afe3373a1de6ae20d504f79d6d (patch)
tree7e3fa24807cebd48a86bd40f866d792181191ee9 /packages/integrations/markdoc/src/heading-ids.ts
downloadastro-e586d7d704d475afe3373a1de6ae20d504f79d6d.tar.gz
astro-e586d7d704d475afe3373a1de6ae20d504f79d6d.tar.zst
astro-e586d7d704d475afe3373a1de6ae20d504f79d6d.zip
Sync from a8e1c0a7402940e0fc5beef669522b315052df1blatest
Diffstat (limited to 'packages/integrations/markdoc/src/heading-ids.ts')
-rw-r--r--packages/integrations/markdoc/src/heading-ids.ts86
1 files changed, 86 insertions, 0 deletions
diff --git a/packages/integrations/markdoc/src/heading-ids.ts b/packages/integrations/markdoc/src/heading-ids.ts
new file mode 100644
index 000000000..7242e0e16
--- /dev/null
+++ b/packages/integrations/markdoc/src/heading-ids.ts
@@ -0,0 +1,86 @@
+import Markdoc, {
+ type Config as MarkdocConfig,
+ type RenderableTreeNode,
+ type Schema,
+} from '@markdoc/markdoc';
+import Slugger from 'github-slugger';
+import { getTextContent } from './runtime.js';
+import { MarkdocError } from './utils.js';
+
+function getSlug(
+ attributes: Record<string, any>,
+ children: RenderableTreeNode[],
+ headingSlugger: Slugger,
+ experimentalHeadingIdCompat: boolean,
+): string {
+ if (attributes.id && typeof attributes.id === 'string') {
+ return attributes.id;
+ }
+ const textContent = attributes.content ?? getTextContent(children);
+ let slug = headingSlugger.slug(textContent);
+
+ if (!experimentalHeadingIdCompat) {
+ if (slug.endsWith('-')) slug = slug.slice(0, -1);
+ }
+ return slug;
+}
+
+type HeadingIdConfig = MarkdocConfig & {
+ ctx: { headingSlugger: Slugger; experimentalHeadingIdCompat: boolean };
+};
+
+/*
+ Expose standalone node for users to import in their config.
+ Allows users to apply a custom `render: AstroComponent`
+ and spread our default heading attributes.
+*/
+export const heading: Schema = {
+ children: ['inline'],
+ attributes: {
+ id: { type: String },
+ level: { type: Number, required: true, default: 1 },
+ },
+ transform(node, config: HeadingIdConfig) {
+ const { level, ...attributes } = node.transformAttributes(config);
+ const children = node.transformChildren(config);
+
+ if (!config.ctx?.headingSlugger) {
+ throw new MarkdocError({
+ message:
+ 'Unexpected problem adding heading IDs to Markdoc file. Did you modify the `ctx.headingSlugger` property in your Markdoc config?',
+ });
+ }
+ const slug = getSlug(
+ attributes,
+ children,
+ config.ctx.headingSlugger,
+ config.ctx.experimentalHeadingIdCompat,
+ );
+
+ const render = config.nodes?.heading?.render ?? `h${level}`;
+
+ const tagProps =
+ // For components, pass down `level` as a prop,
+ // alongside `__collectHeading` for our `headings` collector.
+ // Avoid accidentally rendering `level` as an HTML attribute otherwise!
+ typeof render === 'string'
+ ? { ...attributes, id: slug }
+ : { ...attributes, id: slug, __collectHeading: true, level };
+
+ return new Markdoc.Tag(render, tagProps, children);
+ },
+};
+
+// Called internally to ensure `ctx` is generated per-file, instead of per-build.
+export function setupHeadingConfig(experimentalHeadingIdCompat: boolean): HeadingIdConfig {
+ const headingSlugger = new Slugger();
+ return {
+ ctx: {
+ headingSlugger,
+ experimentalHeadingIdCompat,
+ },
+ nodes: {
+ heading,
+ },
+ };
+}