Skip to content

Commit 72c9ffa

Browse files
committed
✨ Add IDs to headings when parsing
1 parent a0c5e8c commit 72c9ffa

File tree

2 files changed

+45
-8
lines changed

2 files changed

+45
-8
lines changed

src/headings.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Tag } from "@markdoc/markdoc";
2-
import type { RenderableTreeNode } from "@markdoc/markdoc";
2+
import type { RenderableTreeNode, Schema } from "@markdoc/markdoc";
33
import slugify from "slugify";
44

55
export interface Heading {
@@ -28,11 +28,14 @@ const getTextContent = (children: RenderableTreeNode[]): string => {
2828
}, "");
2929
};
3030

31-
const getSlug = (tag: Tag): string => {
32-
if (tag.attributes.id && typeof tag.attributes.id === "string") {
33-
return tag.attributes.id;
31+
const getSlug = (
32+
attributes: Record<string, any>, // eslint-disable-line @typescript-eslint/no-explicit-any
33+
children: RenderableTreeNode[],
34+
): string => {
35+
if (attributes.id && typeof attributes.id === "string") {
36+
return attributes.id;
3437
}
35-
return slugify(getTextContent(tag.children), {
38+
return slugify(getTextContent(children), {
3639
lower: true,
3740
strict: true,
3841
}) as string;
@@ -61,7 +64,7 @@ export function collectHeadings(
6164
sections.push({
6265
level: parseInt(tag.name[1]),
6366
title: getTextContent(tag.children),
64-
id: getSlug(tag),
67+
id: getSlug(tag.attributes, tag.children),
6568
});
6669
}
6770

@@ -75,3 +78,36 @@ export function collectHeadings(
7578

7679
return sections;
7780
}
81+
82+
export const heading: Schema = {
83+
children: ["inline"],
84+
attributes: {
85+
id: { type: String },
86+
level: { type: Number, required: true, default: 1 },
87+
},
88+
transform(node, config) {
89+
const { level, ...attributes } = node.transformAttributes(config);
90+
const children = node.transformChildren(config);
91+
92+
const slug = getSlug(node.attributes, children);
93+
94+
const render = config.nodes?.heading?.render ?? `h${level}`;
95+
96+
/**
97+
* When the tag has a component as its render option,
98+
* add an attribute to collect it as a header
99+
* and also the level as a prop, not an HTML attribute.
100+
*/
101+
const tagProps =
102+
typeof render === "string"
103+
? { ...attributes, id: slug }
104+
: {
105+
...attributes,
106+
id: slug,
107+
__collectHeading: true,
108+
level: level as number,
109+
};
110+
111+
return new Tag(render, tagProps, children);
112+
},
113+
};

src/main.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from "./components.ts";
1212
import { handleValidationErrors } from "./errors.ts";
1313
import { findFirstDirectory, makePathProjectRelative } from "./files.ts";
14-
import { collectHeadings } from "./headings.ts";
14+
import { collectHeadings, heading } from "./headings.ts";
1515
import log from "./logs.ts";
1616
import loadPartials from "./partials.ts";
1717
import render from "./render.ts";
@@ -144,7 +144,8 @@ export const markdocPreprocess = (options: Options = {}): PreprocessorGroup => {
144144
const fullConfig: Config = {
145145
// Start with base config loaded from the schema directory
146146
// Explicitly set options overwrite the base config
147-
nodes: { ...configFromSchema.nodes, ...nodes },
147+
// For example, this processor's heading comes first so it's overwritten
148+
nodes: { heading, ...configFromSchema.nodes, ...nodes },
148149
tags: { ...configFromSchema.tags, ...tags },
149150
functions: { ...configFromSchema.functions, ...functions },
150151
partials: { ...partialsFromSchema, ...partialsFromPartials },

0 commit comments

Comments
 (0)