@clarify-labs/cli 0.10.1 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,14 +27,20 @@ let _tailwindcss_vite = require("@tailwindcss/vite");
27
27
  _tailwindcss_vite = __toESM(_tailwindcss_vite, 1);
28
28
  let _vitejs_plugin_react = require("@vitejs/plugin-react");
29
29
  _vitejs_plugin_react = __toESM(_vitejs_plugin_react, 1);
30
+ let rehype_raw = require("rehype-raw");
31
+ rehype_raw = __toESM(rehype_raw, 1);
30
32
  let _mdx_js_mdx = require("@mdx-js/mdx");
31
- let github_slugger = require("github-slugger");
32
- github_slugger = __toESM(github_slugger, 1);
33
- let mdast_util_to_string = require("mdast-util-to-string");
33
+ let _shikijs_rehype = require("@shikijs/rehype");
34
+ _shikijs_rehype = __toESM(_shikijs_rehype, 1);
35
+ let rehype_slug = require("rehype-slug");
36
+ rehype_slug = __toESM(rehype_slug, 1);
34
37
  let shiki = require("shiki");
35
38
  let unist_util_visit = require("unist-util-visit");
36
39
  let _clarify_labs_renderer = require("@clarify-labs/renderer");
37
40
  let node_fs_promises = require("node:fs/promises");
41
+ let github_slugger = require("github-slugger");
42
+ github_slugger = __toESM(github_slugger, 1);
43
+ let mdast_util_to_string = require("mdast-util-to-string");
38
44
  let remark = require("remark");
39
45
  let gray_matter = require("gray-matter");
40
46
  gray_matter = __toESM(gray_matter, 1);
@@ -47,38 +53,29 @@ let _apidevtools_swagger_parser = require("@apidevtools/swagger-parser");
47
53
  _apidevtools_swagger_parser = __toESM(_apidevtools_swagger_parser, 1);
48
54
  let node_os = require("node:os");
49
55
  let node_module = require("node:module");
50
- //#region source/parsers/markdown/utils.ts
51
- function escapeHtml(text) {
52
- return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
56
+ //#region source/parsers/content/diagnostic.ts
57
+ function displayPath(filePath, projectRoot) {
58
+ if (!filePath) return void 0;
59
+ if (!projectRoot) return filePath.replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "");
60
+ const normalized = (0, node_path.relative)(projectRoot, filePath);
61
+ return normalized && !normalized.startsWith("..") && !(0, node_path.isAbsolute)(normalized) ? normalized : filePath.replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "");
62
+ }
63
+ function createContentDiagnostic(options) {
64
+ const { error } = options;
65
+ return {
66
+ kind: options.kind,
67
+ title: options.title,
68
+ message: options.message,
69
+ filePath: displayPath(options.filePath, options.projectRoot),
70
+ details: error instanceof Error ? options.includeStack ? error.stack ?? error.message : error.message : String(error)
71
+ };
53
72
  }
54
73
  //#endregion
55
74
  //#region source/parsers/markdown/mdx.ts
56
75
  function getLanguage(className) {
57
76
  return (Array.isArray(className) ? className : typeof className === "string" ? className.split(/\s+/) : []).find((item) => typeof item === "string" && item.startsWith("language-"))?.replace(/^language-/, "") || "txt";
58
77
  }
59
- function getTextContent(node) {
60
- if (!node) return "";
61
- if (typeof node.value === "string") return node.value;
62
- return node.children?.map(getTextContent).join("") ?? "";
63
- }
64
- function renderPlainCode(code) {
65
- return code.split("\n").map((line) => `<span>${escapeHtml(line)}</span>`).join("\n");
66
- }
67
- function isBundledLanguage(language) {
68
- return language in shiki.bundledLanguages;
69
- }
70
- var highlighterPromise;
71
78
  var cssVariablesTheme = (0, shiki.createCssVariablesTheme)();
72
- async function getHighlighter() {
73
- highlighterPromise = highlighterPromise ?? (0, shiki.createHighlighter)({
74
- themes: [cssVariablesTheme],
75
- langs: []
76
- });
77
- return highlighterPromise;
78
- }
79
- function renderHighlightedCode(html) {
80
- return (/<code[^>]*>([\s\S]*?)<\/code>/.exec(html)?.[1] ?? html).split("\n").map((line) => line.trimStart().startsWith("<span") ? line : `<span>${line}</span>`).join("\n");
81
- }
82
79
  function rehypeParseCodeBlocks() {
83
80
  return (tree) => {
84
81
  (0, unist_util_visit.visit)(tree, "element", (node, _nodeIndex, parentNode) => {
@@ -96,65 +93,31 @@ function rehypeParseCodeBlocks() {
96
93
  });
97
94
  };
98
95
  }
99
- function rehypeShiki() {
100
- return async (tree) => {
101
- const shikiHighlighter = await getHighlighter();
102
- const languages = /* @__PURE__ */ new Set();
103
- (0, unist_util_visit.visit)(tree, "element", (node) => {
104
- if (node.tagName !== "pre") return;
105
- const codeNode = node.children?.[0];
106
- if (codeNode?.tagName !== "code") return;
107
- const language = typeof node.properties?.language === "string" ? node.properties.language : getLanguage(codeNode.properties?.className);
108
- if (language && language !== "txt" && isBundledLanguage(language)) languages.add(language);
109
- });
110
- if (languages.size > 0) await shikiHighlighter.loadLanguage(...languages);
111
- (0, unist_util_visit.visit)(tree, "element", (node) => {
112
- if (node.tagName !== "pre") return;
113
- const codeNode = node.children?.[0];
114
- if (codeNode?.tagName !== "code") return;
115
- node.properties = node.properties ?? {};
116
- codeNode.properties = codeNode.properties ?? {};
117
- const code = getTextContent(codeNode);
118
- const language = typeof node.properties.language === "string" ? node.properties.language : getLanguage(codeNode.properties.className);
119
- node.properties.code = code;
120
- codeNode.properties.code = code;
121
- codeNode.properties.language = language;
122
- let highlighted = renderPlainCode(code);
123
- if (language && language !== "txt") try {
124
- highlighted = renderHighlightedCode(shikiHighlighter.codeToHtml(code, {
125
- lang: language,
126
- theme: cssVariablesTheme
127
- }));
128
- } catch {
129
- highlighted = renderPlainCode(code);
130
- }
131
- codeNode.children = [{
132
- type: "text",
133
- value: highlighted
134
- }];
135
- });
136
- };
137
- }
138
- function rehypeSlugSections() {
139
- return (tree) => {
140
- const slugger = new github_slugger.default();
141
- (0, unist_util_visit.visit)(tree, "element", (node) => {
142
- if (node.tagName !== "h2" && node.tagName !== "h3") return;
143
- node.properties = node.properties ?? {};
144
- if (node.properties.id) return;
145
- node.properties.id = slugger.slug((0, mdast_util_to_string.toString)(node));
146
- });
147
- };
148
- }
149
96
  var remarkPlugins = _clarify_labs_renderer.markdownRemarkPlugins;
150
97
  var rehypePlugins = [
151
- rehypeSlugSections,
98
+ rehype_slug.default,
152
99
  rehypeParseCodeBlocks,
153
- rehypeShiki
100
+ [_shikijs_rehype.default, {
101
+ theme: cssVariablesTheme,
102
+ langs: [],
103
+ lazy: true,
104
+ addLanguageClass: true,
105
+ fallbackLanguage: "text",
106
+ parseMetaString: _clarify_labs_renderer.parseCodeMeta
107
+ }]
154
108
  ];
155
- function formatMdxDiagnostic(error) {
156
- if (error instanceof Error) return error.message;
157
- return String(error);
109
+ function createContentCompileDiagnostic(options) {
110
+ const { format, phase, error, filePath, projectRoot } = options;
111
+ const label = format === "mdx" ? "MDX" : "Markdown";
112
+ return createContentDiagnostic({
113
+ kind: format,
114
+ title: `${label} ${phase} error`,
115
+ message: phase === "syntax" ? "This page could not be compiled. Fix the MDX syntax or component usage above, then reload the route." : `This page could not be compiled. Fix the ${label} syntax or plugin error below, then reload the route.`,
116
+ error,
117
+ filePath,
118
+ projectRoot,
119
+ includeStack: phase === "compilation"
120
+ });
158
121
  }
159
122
  /**
160
123
  * Validate MDX content for diagnostics WITHOUT running the build-time rehype
@@ -162,7 +125,8 @@ function formatMdxDiagnostic(error) {
162
125
  *
163
126
  * `rehypePlugins` includes `rehypeShiki`, which loads grammar/WASM assets and
164
127
  * is expensive. The real highlighting happens at Vite build time via the
165
- * `@mdx-js/rollup` plugin (see `createMdxPlugin` in `core/adapters.ts`), which
128
+ * `@mdx-js/rollup` plugins (see `createMarkdownPlugin` and `createMdxPlugin`
129
+ * in `core/adapters.ts`), which
166
130
  * runs the full remark + rehype pipeline. Re-running it here, during route
167
131
  * discovery (Phase 3), would compile every `.mdx` file twice and invoke Shiki
168
132
  * once per file per `engine.refresh()` — see ARCHITECTURE.md §2.2 which states
@@ -184,13 +148,13 @@ async function compileMdxContent(content, filePath, projectRoot) {
184
148
  } catch (error) {
185
149
  return {
186
150
  ok: false,
187
- diagnostic: {
188
- kind: "mdx",
189
- title: "MDX syntax error",
190
- message: "This page could not be compiled. Fix the MDX syntax or component usage above, then reload the route.",
191
- filePath: filePath && projectRoot ? filePath.replace(projectRoot + "/", "").replace(/^\/+/, "") : filePath ? filePath.replace(/^[A-Za-z]:\//, "").replace(/^\/+/, "") : void 0,
192
- details: formatMdxDiagnostic(error)
193
- }
151
+ diagnostic: createContentCompileDiagnostic({
152
+ format: "mdx",
153
+ phase: "syntax",
154
+ error,
155
+ filePath,
156
+ projectRoot
157
+ })
194
158
  };
195
159
  }
196
160
  }
@@ -311,6 +275,11 @@ function kebabToTitle(str) {
311
275
  function parseMdxTree(content) {
312
276
  return remark.remark.parse(content);
313
277
  }
278
+ async function compileRouteDiagnostic(content, filePath, baseDir) {
279
+ if ((0, node_path.extname)(filePath).toLowerCase() !== ".mdx") return void 0;
280
+ const result = await compileMdxContent(content, filePath, baseDir);
281
+ return result.ok ? void 0 : result.diagnostic;
282
+ }
314
283
  /** 从内容中提取第一个 H1 标题 */
315
284
  function extractH1(content) {
316
285
  const tree = parseMdxTree(content);
@@ -405,7 +374,7 @@ async function findContentRoutes(dir, base = dir, options = {}) {
405
374
  frontmatter,
406
375
  content
407
376
  };
408
- const mdxResult = await compileMdxContent(page.content, fullPath, base);
377
+ const diagnostic = await compileRouteDiagnostic(page.content, fullPath, base);
409
378
  let title = typeof page.frontmatter.title === "string" ? page.frontmatter.title : "";
410
379
  if (!title) {
411
380
  const lastPart = pathParts[pathParts.length - 1] ?? "";
@@ -427,7 +396,7 @@ async function findContentRoutes(dir, base = dir, options = {}) {
427
396
  frontmatter: page.frontmatter,
428
397
  content: page.content
429
398
  },
430
- diagnostic: mdxResult.ok ? void 0 : mdxResult.diagnostic
399
+ diagnostic
431
400
  });
432
401
  }
433
402
  }
@@ -541,6 +510,7 @@ function routeIntentFromPagesItem(item) {
541
510
  kind: "page",
542
511
  ref: item
543
512
  };
513
+ if ("group" in item) throw new Error("Navigation groups do not resolve to route intents");
544
514
  if ("openapi" in item) return {
545
515
  kind: "openapi",
546
516
  ref: item.openapi,
@@ -568,15 +538,19 @@ function firstNavigationPath(nodes) {
568
538
  }
569
539
  function collectExplicitPagePathIntents(tabs) {
570
540
  const intents = [];
571
- for (const tab of tabs ?? []) {
572
- if (!tab.pages || tab.pages === "FileTree") continue;
573
- for (const group of tab.pages) for (const item of group.pages) if (typeof item !== "string" && "page" in item && item.path) {
541
+ const collectItems = (items) => {
542
+ for (const item of items) if (typeof item !== "string" && "group" in item) collectItems(item.pages);
543
+ else if (typeof item !== "string" && "page" in item && item.path) {
574
544
  const intent = routeIntentFromPagesItem(item);
575
545
  if (intent.kind === "page" && intent.path) intents.push({
576
546
  ...intent,
577
547
  path: intent.path
578
548
  });
579
549
  }
550
+ };
551
+ for (const tab of tabs ?? []) {
552
+ if (!tab.pages || tab.pages === "FileTree") continue;
553
+ for (const group of tab.pages) collectItems(group.pages);
580
554
  }
581
555
  return intents;
582
556
  }
@@ -627,28 +601,38 @@ function buildNavigationFromTabsConfig(routes, tabs) {
627
601
  }
628
602
  function buildNavigationFromConfig(routes, config) {
629
603
  const routeMap = new Map(routes.map((r) => [r.path, r]));
630
- return config.map((group) => {
631
- const children = group.pages.map((item) => {
632
- const intent = routeIntentFromPagesItem(item);
633
- if (intent.kind === "openapi") {
634
- const path = openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path);
635
- const route = routeMap.get(path);
636
- return {
637
- path,
638
- title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
639
- icon: intent.icon,
640
- sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
641
- };
642
- }
643
- const path = routePathFromRef(intent.ref, intent.path);
604
+ const buildItem = (item) => {
605
+ if (typeof item !== "string" && "group" in item) {
606
+ const children = item.pages.map(buildItem);
607
+ return {
608
+ path: firstNavigationPath(children),
609
+ title: resolveLocalizedText(item.group, "", "") ?? "",
610
+ icon: item.icon,
611
+ children
612
+ };
613
+ }
614
+ const intent = routeIntentFromPagesItem(item);
615
+ if (intent.kind === "openapi") {
616
+ const path = openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path);
644
617
  const route = routeMap.get(path);
645
618
  return {
646
- path: intent.redirect ? normalizePath(intent.redirect) : path,
619
+ path,
647
620
  title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
648
621
  icon: intent.icon,
649
622
  sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
650
623
  };
651
- });
624
+ }
625
+ const path = routePathFromRef(intent.ref, intent.path);
626
+ const route = routeMap.get(path);
627
+ return {
628
+ path: intent.redirect ? normalizePath(intent.redirect) : path,
629
+ title: resolveLocalizedText(intent.title, "", "") ?? route?.meta.title ?? kebabToTitle(path.split("/").pop() ?? intent.ref),
630
+ icon: intent.icon,
631
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
632
+ };
633
+ };
634
+ return config.map((group) => {
635
+ const children = group.pages.map(buildItem);
652
636
  return {
653
637
  path: children[0]?.path ?? "/",
654
638
  title: resolveLocalizedText(group.group, "", "") ?? "",
@@ -670,19 +654,29 @@ function buildLocalizedNavigationForLocale(routes, config, locale, i18n) {
670
654
  path: route.basePath ?? route.path
671
655
  }))), locale, i18n);
672
656
  const routeMap = new Map(routes.map((route) => [route.basePath ?? route.path, route]));
673
- return config.map((group) => {
674
- const children = group.pages.map((item) => {
675
- const intent = routeIntentFromPagesItem(item);
676
- const basePath = intent.kind === "openapi" ? openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path) : routePathFromRef(intent.ref, intent.path);
677
- const route = routeMap.get(basePath);
678
- const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
657
+ const buildItem = (item) => {
658
+ if (typeof item !== "string" && "group" in item) {
659
+ const children = item.pages.map(buildItem);
679
660
  return {
680
- path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale, i18n) : void 0) ?? path,
681
- title: resolveLocalizedText(intent.title, locale, i18n.defaultLocale) ?? route?.meta.title ?? kebabToTitle(basePath.split("/").pop() ?? intent.ref),
682
- icon: intent.icon,
683
- sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
661
+ path: firstNavigationPath(children),
662
+ title: resolveLocalizedText(item.group, locale, i18n.defaultLocale) ?? "",
663
+ icon: item.icon,
664
+ children
684
665
  };
685
- });
666
+ }
667
+ const intent = routeIntentFromPagesItem(item);
668
+ const basePath = intent.kind === "openapi" ? openAPIPagePathFromRef(intent.ref, intent.tagFilter, intent.path) : routePathFromRef(intent.ref, intent.path);
669
+ const route = routeMap.get(basePath);
670
+ const path = route?.path ?? localizedRoutePath(basePath, locale, i18n);
671
+ return {
672
+ path: (intent.kind === "page" && intent.redirect ? localizedRoutePath(routePathFromRef(intent.redirect), locale, i18n) : void 0) ?? path,
673
+ title: resolveLocalizedText(intent.title, locale, i18n.defaultLocale) ?? route?.meta.title ?? kebabToTitle(basePath.split("/").pop() ?? intent.ref),
674
+ icon: intent.icon,
675
+ sections: route?.meta.sections ? navigationSections(route.meta.sections) : void 0
676
+ };
677
+ };
678
+ return config.map((group) => {
679
+ const children = group.pages.map(buildItem);
686
680
  return {
687
681
  path: children[0]?.path ?? localizedRoutePath("/", locale, i18n),
688
682
  title: resolveLocalizedText(group.group, locale, i18n.defaultLocale) ?? "",
@@ -927,8 +921,9 @@ var clarifySourceConfigSchema = zod.z.object({
927
921
  branch: zod.z.string().optional(),
928
922
  directory: zod.z.string().optional()
929
923
  });
930
- var clarifyPagesItemSchema = zod.z.union([
924
+ var clarifyPagesItemSchema = zod.z.lazy(() => zod.z.union([
931
925
  zod.z.string(),
926
+ clarifyPagesGroupSchema,
932
927
  zod.z.object({
933
928
  page: zod.z.string(),
934
929
  path: zod.z.string().optional(),
@@ -943,12 +938,12 @@ var clarifyPagesItemSchema = zod.z.union([
943
938
  title: clarifyLocalizedTextSchema.optional(),
944
939
  filter: zod.z.object({ tags: zod.z.array(zod.z.string()).optional() }).optional()
945
940
  })
946
- ]);
947
- var clarifyPagesGroupSchema = zod.z.object({
941
+ ]));
942
+ var clarifyPagesGroupSchema = zod.z.lazy(() => zod.z.object({
948
943
  group: clarifyLocalizedTextSchema,
949
944
  icon: zod.z.string().optional(),
950
945
  pages: zod.z.array(clarifyPagesItemSchema)
951
- });
946
+ }));
952
947
  var clarifyPagesConfigSchema = zod.z.union([zod.z.array(clarifyPagesGroupSchema), zod.z.literal("FileTree")]);
953
948
  var clarifyTabItemSchema = zod.z.object({
954
949
  tab: clarifyLocalizedTextSchema,
@@ -1296,16 +1291,10 @@ function createRuntimeSlotsModule(plugins = [], root = process.cwd()) {
1296
1291
  }
1297
1292
  return `export const runtimeSlots = {\n${[...grouped.entries()].map(([name, list]) => ` ${JSON.stringify(name)}: [\n ${list.join(",\n ")}\n ]`).join(",\n")}\n};\n`;
1298
1293
  }
1299
- function generateMdxErrorModule(diagnostic) {
1294
+ function generateContentDiagnosticModule(diagnostic) {
1300
1295
  return `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';
1301
1296
 
1302
- export const contentDiagnostic = ${JSON.stringify({
1303
- kind: diagnostic.kind ?? "mdx",
1304
- title: diagnostic.title,
1305
- message: diagnostic.message,
1306
- filePath: diagnostic.filePath,
1307
- details: diagnostic.details
1308
- })};
1297
+ export const contentDiagnostic = ${JSON.stringify(diagnostic)};
1309
1298
 
1310
1299
  export default createContentDiagnosticComponent(contentDiagnostic);
1311
1300
  `;
@@ -1323,7 +1312,7 @@ function buildVirtualModules(args) {
1323
1312
  modules.set(VIRTUAL_CLIENT_ENTRY, clientEntryModule);
1324
1313
  modules.set(RESOLVED_CLIENT_ENTRY, clientEntryModule);
1325
1314
  for (const route of args.routes) {
1326
- const moduleContent = route.diagnostic ? generateMdxErrorModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1315
+ const moduleContent = route.diagnostic ? generateContentDiagnosticModule(route.diagnostic) : `export { default } from ${moduleSpecifier(route.source.filePath)};`;
1327
1316
  modules.set(route.module.virtualModuleId, moduleContent);
1328
1317
  }
1329
1318
  return modules;
@@ -1358,9 +1347,9 @@ async function refreshDevServer(engine, server) {
1358
1347
  invalidateVirtualModules(engine, server);
1359
1348
  return hasDevStructureChanged(before, createDevStructureSnapshot(engine.routes, engine.navigation));
1360
1349
  }
1361
- function createNormalizedMdxContentPlugin(engine) {
1350
+ function createNormalizedContentPlugin(engine) {
1362
1351
  return {
1363
- name: "clarify:normalized-mdx-content",
1352
+ name: "clarify:normalized-content",
1364
1353
  enforce: "pre",
1365
1354
  transform(_code, id) {
1366
1355
  if (!/\.mdx?(?:\?|$)/.test(id)) return null;
@@ -1374,16 +1363,44 @@ function createNormalizedMdxContentPlugin(engine) {
1374
1363
  }
1375
1364
  };
1376
1365
  }
1377
- function createMdxPlugin() {
1378
- return (0, _mdx_js_rollup.default)({
1379
- include: ["**/*.{md,mdx}"],
1366
+ function createContentCompileTransform(transform, format, projectRoot, errorPolicy = () => "diagnostic") {
1367
+ return async function contentCompileTransform(code, id) {
1368
+ try {
1369
+ return await transform.call(this, code, id);
1370
+ } catch (error) {
1371
+ if (errorPolicy() === "throw") throw error;
1372
+ return {
1373
+ code: generateContentDiagnosticModule(createContentCompileDiagnostic({
1374
+ format,
1375
+ phase: "compilation",
1376
+ error,
1377
+ filePath: id.replace(/\?.*$/, ""),
1378
+ projectRoot
1379
+ })),
1380
+ map: null
1381
+ };
1382
+ }
1383
+ };
1384
+ }
1385
+ function createContentCompilerPlugin(format, projectRoot, errorPolicy) {
1386
+ const markdown = format === "markdown";
1387
+ const plugin = (0, _mdx_js_rollup.default)({
1388
+ include: [markdown ? "**/*.md" : "**/*.mdx"],
1389
+ format: markdown ? "md" : "mdx",
1380
1390
  jsxImportSource: "react",
1381
1391
  providerImportSource: "@clarify-labs/renderer",
1382
1392
  remarkPlugins,
1383
- rehypePlugins
1393
+ ...markdown ? { remarkRehypeOptions: { allowDangerousHtml: true } } : {},
1394
+ rehypePlugins: markdown ? [rehype_raw.default, ...rehypePlugins] : rehypePlugins
1384
1395
  });
1396
+ const transform = typeof plugin.transform === "function" ? plugin.transform : plugin.transform?.handler;
1397
+ if (!transform) return plugin;
1398
+ return {
1399
+ ...plugin,
1400
+ transform: createContentCompileTransform(transform, format, projectRoot, errorPolicy)
1401
+ };
1385
1402
  }
1386
- function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1403
+ function createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown, mdx) {
1387
1404
  let viteConfig;
1388
1405
  return {
1389
1406
  name: "clarify:core",
@@ -1485,7 +1502,8 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1485
1502
  outputDirectory: viteConfig.build.outDir,
1486
1503
  ssrPlugins: [
1487
1504
  engine.createSSGVirtualPlugin(),
1488
- normalizedMdxContentPlugin,
1505
+ normalizedContentPlugin,
1506
+ markdown,
1489
1507
  mdx
1490
1508
  ]
1491
1509
  });
@@ -1502,20 +1520,31 @@ function createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx) {
1502
1520
  * site itself - it only bridges Vite lifecycle hooks to the engine.
1503
1521
  */
1504
1522
  function createViteAdapter(engine) {
1505
- const normalizedMdxContentPlugin = createNormalizedMdxContentPlugin(engine);
1506
- const mdx = createMdxPlugin();
1523
+ let contentCompileErrorPolicy = "diagnostic";
1524
+ const errorPolicy = () => contentCompileErrorPolicy;
1525
+ const normalizedContentPlugin = createNormalizedContentPlugin(engine);
1526
+ const markdown = createContentCompilerPlugin("markdown", engine.root, errorPolicy);
1527
+ const mdx = createContentCompilerPlugin("mdx", engine.root, errorPolicy);
1528
+ const compilePolicyPlugin = {
1529
+ name: "clarify:content-compile-policy",
1530
+ configResolved(config) {
1531
+ contentCompileErrorPolicy = config.command === "build" && engine.generateOptions.ssg.failOnError ? "throw" : "diagnostic";
1532
+ }
1533
+ };
1507
1534
  const tailwind = (0, _tailwindcss_vite.default)();
1508
1535
  return [
1509
1536
  ...(0, _vitejs_plugin_react.default)(),
1510
1537
  ...Array.isArray(tailwind) ? tailwind : [tailwind],
1511
- normalizedMdxContentPlugin,
1512
- createClarifyViteCorePlugin(engine, normalizedMdxContentPlugin, mdx),
1538
+ compilePolicyPlugin,
1539
+ normalizedContentPlugin,
1540
+ createClarifyViteCorePlugin(engine, normalizedContentPlugin, markdown, mdx),
1541
+ markdown,
1513
1542
  mdx
1514
1543
  ];
1515
1544
  }
1516
1545
  var cliPackageVersion = {
1517
1546
  name: "@clarify-labs/cli",
1518
- version: "0.10.1",
1547
+ version: "0.10.3",
1519
1548
  license: "AGPL-3.0-only",
1520
1549
  type: "module",
1521
1550
  repository: {
@@ -1556,6 +1585,7 @@ var cliPackageVersion = {
1556
1585
  "@clarify-labs/templates": "workspace:*",
1557
1586
  "@mdx-js/mdx": "^3.1.1",
1558
1587
  "@mdx-js/rollup": "^3.1.1",
1588
+ "@shikijs/rehype": "^4.3.0",
1559
1589
  "@tailwindcss/vite": "^4.3.2",
1560
1590
  "@vitejs/plugin-react": "^6.0.3",
1561
1591
  "cac": "^7.0.0",
@@ -1570,6 +1600,8 @@ var cliPackageVersion = {
1570
1600
  "react-router": "^7.0.0",
1571
1601
  "react-router-dom": "^7.0.0",
1572
1602
  "react-zoom-pan-pinch": "^4.0.3",
1603
+ "rehype-raw": "^7.0.0",
1604
+ "rehype-slug": "^6.0.0",
1573
1605
  "remark": "^15.0.1",
1574
1606
  "remark-gfm": "^4.0.1",
1575
1607
  "shiki": "4.3.0",
@@ -1865,9 +1897,6 @@ var OPENAPI_HTTP_METHODS = [
1865
1897
  "trace"
1866
1898
  ];
1867
1899
  var CLARIFY_OPENAPI_SECTION_ID_EXTENSION = "x-clarify-section-id";
1868
- function errorMessage(error) {
1869
- return error instanceof Error ? error.message : String(error);
1870
- }
1871
1900
  function isSameOpenAPIFile(url, filePath) {
1872
1901
  try {
1873
1902
  return (0, node_path.resolve)((0, node_url.fileURLToPath)(url)) === (0, node_path.resolve)(filePath);
@@ -1982,7 +2011,7 @@ function extractOpenAPISections(spec, filterTags) {
1982
2011
  }
1983
2012
  return sections;
1984
2013
  }
1985
- async function readOpenAPISpec(filePath, contentProcessor) {
2014
+ async function readOpenAPISpec(filePath, contentProcessor, projectRoot) {
1986
2015
  try {
1987
2016
  if (!contentProcessor) return {
1988
2017
  ok: true,
@@ -2005,13 +2034,14 @@ async function readOpenAPISpec(filePath, contentProcessor) {
2005
2034
  } catch (error) {
2006
2035
  return {
2007
2036
  ok: false,
2008
- diagnostic: {
2037
+ diagnostic: createContentDiagnostic({
2009
2038
  kind: "openapi",
2010
2039
  title: "OpenAPI spec parse failed",
2011
- message: `Clarify could not parse ${filePath}.`,
2040
+ message: "Clarify could not parse this OpenAPI specification.",
2041
+ error,
2012
2042
  filePath,
2013
- details: errorMessage(error)
2014
- }
2043
+ projectRoot
2044
+ })
2015
2045
  };
2016
2046
  }
2017
2047
  }
@@ -2042,31 +2072,24 @@ export const routeData = {};
2042
2072
  export default createOpenApiRouteComponent({ ...routeData, spec });
2043
2073
  `;
2044
2074
  }
2045
- function generateOpenAPIErrorModule(diagnostic) {
2046
- return `import { createContentDiagnosticComponent } from '@clarify-labs/renderer';
2047
-
2048
- export const contentDiagnostic = ${JSON.stringify({
2049
- kind: diagnostic.kind ?? "openapi",
2050
- title: diagnostic.title,
2051
- message: diagnostic.message,
2052
- filePath: diagnostic.filePath,
2053
- details: diagnostic.details
2054
- })};
2055
-
2056
- export default createContentDiagnosticComponent(contentDiagnostic);
2057
- `;
2058
- }
2059
2075
  //#endregion
2060
2076
  //#region source/plugins/openapi/index.ts
2061
2077
  function collectOpenAPIPageIntents(config) {
2062
2078
  const intents = [];
2063
- function visitPages(pages) {
2064
- if (!pages || pages === "FileTree") return;
2065
- for (const group of pages) for (const item of group.pages) {
2079
+ function visitItems(items) {
2080
+ for (const item of items) {
2081
+ if (typeof item !== "string" && "group" in item) {
2082
+ visitItems(item.pages);
2083
+ continue;
2084
+ }
2066
2085
  const intent = routeIntentFromPagesItem(item);
2067
2086
  if (intent.kind === "openapi" && (intent.path || intent.tagFilter?.length)) intents.push(intent);
2068
2087
  }
2069
2088
  }
2089
+ function visitPages(pages) {
2090
+ if (!pages || pages === "FileTree") return;
2091
+ for (const group of pages) visitItems(group.pages);
2092
+ }
2070
2093
  for (const tab of config.tabs ?? []) visitPages(tab.pages);
2071
2094
  return intents;
2072
2095
  }
@@ -2212,7 +2235,7 @@ function createOpenAPIPlugin() {
2212
2235
  const result = specFromCache ? {
2213
2236
  ok: true,
2214
2237
  spec: specFromCache
2215
- } : await readOpenAPISpec(route.source.filePath, processor);
2238
+ } : await readOpenAPISpec(route.source.filePath, processor, ctx.generateOptions.projectRoot);
2216
2239
  if (!result.ok) {
2217
2240
  route.diagnostic = result.diagnostic;
2218
2241
  route.meta.title = route.meta.title || "OpenAPI parse error";
@@ -2259,7 +2282,7 @@ function createOpenAPIPlugin() {
2259
2282
  for (const [routeSpecModuleId, spec] of specModules) modules.set(specVirtualModuleId(routeSpecModuleId), generateOpenAPISpecModule(spec));
2260
2283
  for (const route of ctx.routes) {
2261
2284
  if (route.kind !== "openapi") continue;
2262
- if (route.diagnostic) modules.set(route.module.virtualModuleId, generateOpenAPIErrorModule(route.diagnostic));
2285
+ if (route.diagnostic) modules.set(route.module.virtualModuleId, generateContentDiagnosticModule(route.diagnostic));
2263
2286
  else if (route.openapi?.sourceSpecId) {
2264
2287
  const routeSpecModuleId = route.openapi.routeSpecModuleId ?? route.openapi.sourceSpecId;
2265
2288
  if (!specModules.has(routeSpecModuleId)) continue;
@@ -2714,6 +2737,11 @@ function writeClarifyEnvDts(projectRoot, plugins) {
2714
2737
  (0, node_fs.writeFileSync)(filePath, next, "utf-8");
2715
2738
  }
2716
2739
  //#endregion
2740
+ //#region source/parsers/markdown/utils.ts
2741
+ function escapeHtml(text) {
2742
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2743
+ }
2744
+ //#endregion
2717
2745
  //#region source/core/runtime/html-template.ts
2718
2746
  /**
2719
2747
  * HTML template manipulation helpers used during SSG.
@@ -3086,6 +3114,12 @@ function loadVirtualModule(id, modules) {
3086
3114
  const bareId = stripVirtualPrefix(id);
3087
3115
  return modules.get(bareId) ?? modules.get(id) ?? null;
3088
3116
  }
3117
+ function assertNoContentDiagnostics(routes) {
3118
+ const failures = routes.filter((route) => route.diagnostic);
3119
+ if (failures.length === 0) return;
3120
+ const summary = failures.map((route) => `- ${route.path}: ${route.diagnostic?.title ?? "Content error"}${route.diagnostic?.filePath ? ` (${route.diagnostic.filePath})` : ""}`).join("\n");
3121
+ throw new Error(`[clarify] Content diagnostics prevented the build:\n${summary}`);
3122
+ }
3089
3123
  var ClarifyEngine = class {
3090
3124
  options;
3091
3125
  root;
@@ -3310,6 +3344,7 @@ var ClarifyEngine = class {
3310
3344
  await runPhase(this.plugins, "ssg", this.ctx, async () => {
3311
3345
  if (process.env.SKIP_CLARIFY_SSG) return;
3312
3346
  if (!await runInterceptHooks(this.plugins, "ssg:shouldRun", this.ctx)) return;
3347
+ if (this.generateOptions.ssg.failOnError) assertNoContentDiagnostics(this.routes);
3313
3348
  const outputDir = this.runtime.outputDirectory ?? this.generateOptions.outputDirectory;
3314
3349
  if (!outputDir) throw new Error("[clarify] outputDirectory is required before SSG runs");
3315
3350
  const ssrOutputDir = (0, node_path.join)(outputDir, ".ssr");
@@ -3417,4 +3452,4 @@ Object.defineProperty(exports, "defineConfig", {
3417
3452
  }
3418
3453
  });
3419
3454
 
3420
- //# sourceMappingURL=engine-5bdNX2Oo.cjs.map
3455
+ //# sourceMappingURL=engine-BDdPszs2.cjs.map