Skip to content

Commit 604b642

Browse files
committed
🔀 Merge remote-tracking branch 'origin/main' into docs
2 parents 557e4c2 + dca0114 commit 604b642

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1609
-361
lines changed

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"svelte-eslint-parser": "^1.2.0",
1414
"typescript-eslint": "^8.31.0"
1515
},
16-
"packageManager": "pnpm@10.12.1+sha512.f0dda8580f0ee9481c5c79a1d927b9164f2c478e90992ad268bbb2465a736984391d6333d2c327913578b2804af33474ca554ba29c04a8b13060a717675ae3ac",
16+
"packageManager": "pnpm@10.26.1+sha512.664074abc367d2c9324fdc18037097ce0a8f126034160f709928e9e9f95d98714347044e5c3164d65bd5da6c59c6be362b107546292a8eecb7999196e5ce58fa",
1717
"scripts": {
1818
"build": "pnpm -r build",
1919
"format": "pnpm -r format",

‎packages/markdoc-svelte/package.json‎

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "markdoc-svelte",
3-
"version": "3.0.0",
3+
"version": "4.0.0",
44
"type": "module",
55
"description": "A preprocessor to render Markdoc in Svelte",
66
"sideEffects": false,
@@ -36,27 +36,36 @@
3636
],
3737
"license": "MIT",
3838
"dependencies": {
39-
"@markdoc/markdoc": "^0.5.2",
40-
"slugify": "^1.6.6",
41-
"yaml": "^2.7.1"
39+
"@markdoc/markdoc": "^0.5.4",
40+
"slug": "^11.0.1",
41+
"yaml": "^2.8.2"
4242
},
4343
"devDependencies": {
44-
"@rollup/plugin-commonjs": "^28.0.6",
45-
"@rollup/plugin-node-resolve": "^16.0.1",
44+
"@rollup/plugin-commonjs": "^29.0.0",
45+
"@rollup/plugin-node-resolve": "^16.0.3",
4646
"@rollup/plugin-terser": "^0.4.4",
47-
"@rollup/plugin-typescript": "^12.1.2",
48-
"@tsconfig/svelte": "^5.0.4",
49-
"@types/node": "^22.15.2",
50-
"@vitest/coverage-v8": "^3.1.4",
51-
"rollup": "^4.43.0",
52-
"rollup-plugin-dts": "^6.2.1",
47+
"@rollup/plugin-typescript": "^12.3.0",
48+
"@tsconfig/svelte": "^5.0.6",
49+
"@types/node": "^25.0.3",
50+
"@types/slug": "^5.0.9",
51+
"@vitest/coverage-v8": "4.0.16",
52+
"eslint": "^9.39.2",
53+
"eslint-config-prettier": "^10.1.8",
54+
"eslint-import-resolver-typescript": "^4.4.4",
55+
"eslint-plugin-import": "^2.32.0",
56+
"eslint-plugin-svelte": "^3.13.1",
57+
"prettier": "^3.7.4",
58+
"prettier-plugin-svelte": "^3.4.1",
59+
"rollup": "^4.54.0",
60+
"rollup-plugin-dts": "^6.3.0",
5361
"tslib": "^2.8.1",
54-
"typescript": "^5.7.3",
55-
"vitest": "^3.1.2"
62+
"typescript": "^5.9.3",
63+
"typescript-eslint": "^8.50.0",
64+
"vitest": "^4.0.16"
5665
},
5766
"peerDependencies": {
5867
"svelte": "4.x || 5.x",
59-
"vite": "^6.0.0"
68+
"vite": "^6.0.0 || ^7.0.0"
6069
},
6170
"keywords": [
6271
"svelte",

‎packages/markdoc-svelte/rollup.config.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default [
2727
typescript({
2828
sourceMap: true,
2929
inlineSources: true,
30+
exclude: ["**/*.test.ts"],
3031
}),
3132
nodeResolve(),
3233
terser(),
Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
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

46
export 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
*/
2447
export 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+
};
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1+
export { heading as headingNode } from "./headings.ts";
12
export { markdocPreprocess } from "./main.ts";
2-
export type { MarkdocModule } from "./types.ts";
3+
export type { MarkdocModule, MarkdocSvelteConfig as Config } from "./types.ts";
34

45
export { default as Markdoc } from "@markdoc/markdoc";
5-
export type { Config } from "@markdoc/markdoc";

‎packages/markdoc-svelte/src/main.ts‎

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { basename, extname } from "path";
22

33
import Markdoc from "@markdoc/markdoc";
4-
import type { Config, ParserArgs } from "@markdoc/markdoc";
4+
import type { ParserArgs } from "@markdoc/markdoc";
5+
import slug from "slug";
56
import type { PreprocessorGroup } from "svelte/compiler";
67
import YAML from "yaml";
78

@@ -11,27 +12,28 @@ import {
1112
} from "./components.ts";
1213
import { handleValidationErrors } from "./errors.ts";
1314
import { findFirstDirectory, makePathProjectRelative } from "./files.ts";
14-
import { collectHeadings } from "./headings.ts";
15+
import { collectHeadings, heading, type Heading } from "./headings.ts";
1516
import log from "./logs.ts";
1617
import loadPartials from "./partials.ts";
1718
import render from "./render.ts";
1819
import loadSchemas from "./schema.ts";
19-
import type { Options } from "./types.ts";
20+
import type { MarkdocSvelteConfig, Options } from "./types.ts";
2021

2122
const validOptionKeys: (keyof Options)[] = [
23+
"comments",
24+
"components",
2225
"extensions",
23-
"schema",
24-
"nodes",
25-
"tags",
26-
"variables",
2726
"functions",
28-
"partials",
29-
"components",
27+
"headingIds",
3028
"layout",
31-
"comments",
3229
"linkify",
30+
"nodes",
31+
"partials",
32+
"schema",
33+
"tags",
3334
"typographer",
3435
"validationLevel",
36+
"variables",
3537
];
3638

3739
/**
@@ -67,6 +69,10 @@ export const markdocPreprocess = (options: Options = {}): PreprocessorGroup => {
6769
const componentsPath = options.components || "$lib/components";
6870
const layoutPath = options.layout;
6971
const allowComments = options.comments ?? true;
72+
const processHeadings = options.headingIds ?? false;
73+
// Use custom slugger if provided, or fallback to default
74+
const headingSlugger =
75+
typeof processHeadings === "function" ? processHeadings : slug;
7076
const linkify = options.linkify ?? false;
7177
const typographer = options.typographer ?? false;
7278
const validationLevel = options.validationLevel || "error";
@@ -115,9 +121,9 @@ export const markdocPreprocess = (options: Options = {}): PreprocessorGroup => {
115121

116122
// Prepare to load schemas & partials
117123
const dependencies: string[] = [];
118-
let configFromSchema: Config = {};
119-
let partialsFromSchema: Config["partials"] = {};
120-
let partialsFromPartials: Config["partials"] = {};
124+
let configFromSchema: MarkdocSvelteConfig = {};
125+
let partialsFromSchema: MarkdocSvelteConfig["partials"] = {};
126+
let partialsFromPartials: MarkdocSvelteConfig["partials"] = {};
121127

122128
// Discover optional schema directory
123129
const schemaDir = findFirstDirectory(schemaPaths);
@@ -141,15 +147,22 @@ export const markdocPreprocess = (options: Options = {}): PreprocessorGroup => {
141147
}
142148

143149
// Assemble full config
144-
const fullConfig: Config = {
150+
const fullConfig: MarkdocSvelteConfig = {
145151
// Start with base config loaded from the schema directory
146152
// Explicitly set options overwrite the base config
147-
nodes: { ...configFromSchema.nodes, ...nodes },
153+
// For example, if processing headings,
154+
// This processor's heading comes first so it's overwritten
155+
nodes: {
156+
...(processHeadings ? { heading } : {}), // Only include if passed as option
157+
...configFromSchema.nodes,
158+
...nodes,
159+
},
148160
tags: { ...configFromSchema.tags, ...tags },
149161
functions: { ...configFromSchema.functions, ...functions },
150162
partials: { ...partialsFromSchema, ...partialsFromPartials },
151163
// Make $frontmatter available as variable
152164
variables: { ...configFromSchema.variables, ...variables, frontmatter },
165+
headingSlugger,
153166
};
154167

155168
// Validate Markdoc AST
@@ -164,8 +177,14 @@ export const markdocPreprocess = (options: Options = {}): PreprocessorGroup => {
164177
// eslint-disable-next-line @typescript-eslint/await-thenable
165178
const transformedContent = await Markdoc.transform(ast, fullConfig);
166179

167-
// --- Collect headings from transformed content ---
168-
const headings = collectHeadings(transformedContent);
180+
// Collect headings from transformed content
181+
const getHeadings = (): Heading[] => {
182+
if (processHeadings) {
183+
return collectHeadings(transformedContent, headingSlugger);
184+
}
185+
return [];
186+
};
187+
const headings = getHeadings();
169188

170189
// Render Markdoc AST to Svelte
171190
const svelteContent = render(transformedContent);

0 commit comments

Comments
 (0)