1- import type { RenderableTreeNode , Tag } from "@markdoc/markdoc" ;
2- import slugify from "slugify" ;
1+ import { Tag } from "@markdoc/markdoc" ;
2+ import type { RenderableTreeNode , Schema } from "@markdoc/markdoc" ;
3+
4+ import type { MarkdocSvelteConfig , SluggerType } from "./types.ts" ;
35
46export interface Heading {
57 /**
@@ -16,46 +18,114 @@ export interface Heading {
1618 id ?: string ;
1719}
1820
21+ const getTextContent = ( children : RenderableTreeNode [ ] ) : string => {
22+ return children . reduce ( ( text : string , child ) : string => {
23+ if ( typeof child === "string" || typeof child === "number" ) {
24+ return text + child ;
25+ } else if ( typeof child === "object" && Tag . isTag ( child ) ) {
26+ return text + getTextContent ( child . children ) ;
27+ }
28+ return text ;
29+ } , "" ) ;
30+ } ;
31+
32+ const getSlug = (
33+ sluggifier : SluggerType ,
34+ attributes : Record < string , any > , // eslint-disable-line @typescript-eslint/no-explicit-any
35+ children : RenderableTreeNode [ ] ,
36+ ) : string => {
37+ if ( attributes . id && typeof attributes . id === "string" ) {
38+ return attributes . id ;
39+ }
40+ return sluggifier ( getTextContent ( children ) ) ;
41+ } ;
1942/**
2043 * Recursively collects all heading nodes from a Markdoc AST
2144 * @param node - The Markdoc AST node to process
2245 * @returns Array of heading objects with title, level, and other attributes
2346 */
2447export function collectHeadings (
2548 node : RenderableTreeNode | RenderableTreeNode [ ] ,
49+ sluggifier : SluggerType ,
2650 sections : Heading [ ] = [ ] ,
2751) : Heading [ ] {
2852 // Handle array of nodes
2953 if ( Array . isArray ( node ) ) {
3054 for ( const child of node ) {
31- sections . push ( ...collectHeadings ( child ) ) ;
55+ sections . push ( ...collectHeadings ( child , sluggifier ) ) ;
3256 }
3357 return sections ;
3458 }
3559
3660 // Handle single node
37- if ( typeof node === "object" && node !== null && "name" in node ) {
38- const tag = node as Tag ;
39- if ( tag . name . match ( / ^ h \d $ / ) ) {
40- const title = tag . children [ 0 ] ;
41- if ( typeof title === "string" ) {
61+ if ( typeof node === "object" && node !== null ) {
62+ // Handle headings passed as custom components
63+ if (
64+ node . attributes ?. __collectHeading === true &&
65+ typeof node . attributes ?. level === "number"
66+ ) {
67+ sections . push ( {
68+ level : node . attributes ?. level as number ,
69+ title : getTextContent ( node . children ) ,
70+ id : getSlug ( sluggifier , node . attributes , node . children ) ,
71+ } ) ;
72+ }
73+
74+ if ( "name" in node ) {
75+ const tag = node as Tag ;
76+
77+ // Handle basic headings
78+ if ( tag . name . match ( / ^ h \d $ / ) ) {
4279 sections . push ( {
4380 level : parseInt ( tag . name [ 1 ] ) ,
44- title,
45- id :
46- ( tag . attributes . id as string ) ||
47- ( slugify ( title , { lower : true , strict : true } ) as string ) ,
81+ title : getTextContent ( tag . children ) ,
82+ id : getSlug ( sluggifier , tag . attributes , tag . children ) ,
4883 } ) ;
4984 }
50- }
5185
52- // Handle node children
53- if ( tag . children ) {
54- for ( const child of tag . children ) {
55- collectHeadings ( child , sections ) ;
86+ // Handle node children
87+ if ( tag . children ) {
88+ for ( const child of tag . children ) {
89+ collectHeadings ( child , sluggifier , sections ) ;
90+ }
5691 }
5792 }
5893 }
5994
6095 return sections ;
6196}
97+
98+ export const heading : Schema = {
99+ children : [ "inline" ] ,
100+ attributes : {
101+ id : { type : String } ,
102+ level : { type : Number , required : true , default : 1 } ,
103+ } ,
104+ transform ( node , config : MarkdocSvelteConfig ) {
105+ const { level, ...attributes } = node . transformAttributes ( config ) ;
106+ const children = node . transformChildren ( config ) ;
107+
108+ const slug = config . headingSlugger
109+ ? getSlug ( config . headingSlugger , node . attributes , children )
110+ : getTextContent ( children ) ;
111+
112+ const render = config . nodes ?. heading ?. render ?? `h${ level } ` ;
113+
114+ /**
115+ * When the tag has a component as its render option,
116+ * add an attribute to collect it as a header
117+ * and also the level as a prop, not an HTML attribute.
118+ */
119+ const tagProps =
120+ typeof render === "string"
121+ ? { ...attributes , id : slug }
122+ : {
123+ ...attributes ,
124+ id : slug ,
125+ __collectHeading : true ,
126+ level : level as number ,
127+ } ;
128+
129+ return new Tag ( render , tagProps , children ) ;
130+ } ,
131+ } ;
0 commit comments