create-zudo-doc 0.1.0

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.
Files changed (212) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/bin/create-zudo-doc.js +2 -0
  4. package/dist/api.d.ts +20 -0
  5. package/dist/api.js +13 -0
  6. package/dist/claude-md-gen.d.ts +2 -0
  7. package/dist/claude-md-gen.js +113 -0
  8. package/dist/cli.d.ts +39 -0
  9. package/dist/cli.js +157 -0
  10. package/dist/compose.d.ts +95 -0
  11. package/dist/compose.js +206 -0
  12. package/dist/constants.d.ts +20 -0
  13. package/dist/constants.js +224 -0
  14. package/dist/features/body-foot-util.d.ts +10 -0
  15. package/dist/features/body-foot-util.js +12 -0
  16. package/dist/features/claude-resources.d.ts +2 -0
  17. package/dist/features/claude-resources.js +6 -0
  18. package/dist/features/design-token-panel.d.ts +14 -0
  19. package/dist/features/design-token-panel.js +27 -0
  20. package/dist/features/doc-history.d.ts +9 -0
  21. package/dist/features/doc-history.js +11 -0
  22. package/dist/features/doc-tags.d.ts +19 -0
  23. package/dist/features/doc-tags.js +33 -0
  24. package/dist/features/footer-taglist.d.ts +14 -0
  25. package/dist/features/footer-taglist.js +17 -0
  26. package/dist/features/footer.d.ts +8 -0
  27. package/dist/features/footer.js +10 -0
  28. package/dist/features/i18n.d.ts +22 -0
  29. package/dist/features/i18n.js +41 -0
  30. package/dist/features/image-enlarge.d.ts +11 -0
  31. package/dist/features/image-enlarge.js +13 -0
  32. package/dist/features/index.d.ts +15 -0
  33. package/dist/features/index.js +53 -0
  34. package/dist/features/llms-txt.d.ts +11 -0
  35. package/dist/features/llms-txt.js +13 -0
  36. package/dist/features/search.d.ts +9 -0
  37. package/dist/features/search.js +11 -0
  38. package/dist/features/sidebar-resizer.d.ts +14 -0
  39. package/dist/features/sidebar-resizer.js +16 -0
  40. package/dist/features/sidebar-toggle.d.ts +13 -0
  41. package/dist/features/sidebar-toggle.js +15 -0
  42. package/dist/features/tag-governance.d.ts +14 -0
  43. package/dist/features/tag-governance.js +16 -0
  44. package/dist/features/tauri-dev.d.ts +2 -0
  45. package/dist/features/tauri-dev.js +25 -0
  46. package/dist/features/tauri.d.ts +11 -0
  47. package/dist/features/tauri.js +52 -0
  48. package/dist/features/versioning.d.ts +27 -0
  49. package/dist/features/versioning.js +43 -0
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.js +150 -0
  52. package/dist/preset.d.ts +37 -0
  53. package/dist/preset.js +156 -0
  54. package/dist/prompts.d.ts +32 -0
  55. package/dist/prompts.js +248 -0
  56. package/dist/scaffold.d.ts +4 -0
  57. package/dist/scaffold.js +344 -0
  58. package/dist/settings-gen.d.ts +2 -0
  59. package/dist/settings-gen.js +237 -0
  60. package/dist/utils.d.ts +8 -0
  61. package/dist/utils.js +34 -0
  62. package/dist/zfb-config-gen.d.ts +19 -0
  63. package/dist/zfb-config-gen.js +222 -0
  64. package/package.json +65 -0
  65. package/templates/base/.htmlvalidate.json +5 -0
  66. package/templates/base/.zfb/doc-history-meta.json +1 -0
  67. package/templates/base/pages/404.tsx +55 -0
  68. package/templates/base/pages/_data.ts +179 -0
  69. package/templates/base/pages/_mdx-components.ts +249 -0
  70. package/templates/base/pages/docs/[...slug].tsx +448 -0
  71. package/templates/base/pages/index.tsx +158 -0
  72. package/templates/base/pages/lib/_body-end-islands.tsx +201 -0
  73. package/templates/base/pages/lib/_category-nav.tsx +148 -0
  74. package/templates/base/pages/lib/_category-tree-nav.tsx +104 -0
  75. package/templates/base/pages/lib/_compose-meta-title.ts +29 -0
  76. package/templates/base/pages/lib/_details.tsx +30 -0
  77. package/templates/base/pages/lib/_doc-history-area.tsx +178 -0
  78. package/templates/base/pages/lib/_doc-metainfo-area.tsx +100 -0
  79. package/templates/base/pages/lib/_doc-tags-area.tsx +89 -0
  80. package/templates/base/pages/lib/_extract-headings.ts +81 -0
  81. package/templates/base/pages/lib/_footer-with-defaults.tsx +234 -0
  82. package/templates/base/pages/lib/_frontmatter-preview-data.ts +53 -0
  83. package/templates/base/pages/lib/_head-with-defaults.tsx +113 -0
  84. package/templates/base/pages/lib/_header-with-defaults.tsx +386 -0
  85. package/templates/base/pages/lib/_inline-version-switcher.tsx +84 -0
  86. package/templates/base/pages/lib/_math-block.tsx +63 -0
  87. package/templates/base/pages/lib/_nav-source-docs.ts +68 -0
  88. package/templates/base/pages/lib/_preset-generator.tsx +81 -0
  89. package/templates/base/pages/lib/_search-widget-script.ts +388 -0
  90. package/templates/base/pages/lib/_search-widget.tsx +196 -0
  91. package/templates/base/pages/lib/_sidebar-with-defaults.tsx +176 -0
  92. package/templates/base/pages/lib/_site-tree-nav.tsx +128 -0
  93. package/templates/base/pages/lib/locale-merge.ts +58 -0
  94. package/templates/base/pages/lib/route-enumerators.ts +302 -0
  95. package/templates/base/pages/sitemap.xml.tsx +51 -0
  96. package/templates/base/plugins/connect-adapter.mjs +144 -0
  97. package/templates/base/plugins/copy-public-plugin.mjs +50 -0
  98. package/templates/base/plugins/search-index-plugin.mjs +54 -0
  99. package/templates/base/scripts/run-b4push.sh +102 -0
  100. package/templates/base/src/components/ai-chat-modal.tsx +15 -0
  101. package/templates/base/src/components/client-router-bootstrap.tsx +14 -0
  102. package/templates/base/src/components/content/component-map.ts +25 -0
  103. package/templates/base/src/components/content/content-blockquote.tsx +16 -0
  104. package/templates/base/src/components/content/content-code.tsx +117 -0
  105. package/templates/base/src/components/content/content-link.tsx +83 -0
  106. package/templates/base/src/components/content/content-ol.tsx +19 -0
  107. package/templates/base/src/components/content/content-paragraph.tsx +10 -0
  108. package/templates/base/src/components/content/content-strong.tsx +16 -0
  109. package/templates/base/src/components/content/content-table.tsx +18 -0
  110. package/templates/base/src/components/content/content-ul.tsx +18 -0
  111. package/templates/base/src/components/content/heading-h2.tsx +26 -0
  112. package/templates/base/src/components/content/heading-h3.tsx +26 -0
  113. package/templates/base/src/components/content/heading-h4.tsx +26 -0
  114. package/templates/base/src/components/design-token-panel-bootstrap.tsx +15 -0
  115. package/templates/base/src/components/desktop-sidebar-toggle.tsx +15 -0
  116. package/templates/base/src/components/doc-history.tsx +18 -0
  117. package/templates/base/src/components/html-preview/highlighted-code.tsx +74 -0
  118. package/templates/base/src/components/html-preview/html-preview.tsx +108 -0
  119. package/templates/base/src/components/html-preview/preflight.ts +112 -0
  120. package/templates/base/src/components/html-preview/preview-base.tsx +159 -0
  121. package/templates/base/src/components/image-enlarge.tsx +19 -0
  122. package/templates/base/src/components/mobile-toc.tsx +94 -0
  123. package/templates/base/src/components/preset-generator.tsx +14 -0
  124. package/templates/base/src/components/sidebar-toggle.tsx +98 -0
  125. package/templates/base/src/components/sidebar-tree.tsx +543 -0
  126. package/templates/base/src/components/site-tree-nav.tsx +233 -0
  127. package/templates/base/src/components/theme-toggle.tsx +93 -0
  128. package/templates/base/src/components/toc.tsx +63 -0
  129. package/templates/base/src/components/tree-nav-shared.tsx +71 -0
  130. package/templates/base/src/config/color-scheme-utils.ts +182 -0
  131. package/templates/base/src/config/color-schemes.ts +128 -0
  132. package/templates/base/src/config/frontmatter-preview-defaults.ts +24 -0
  133. package/templates/base/src/config/frontmatter-preview-renderers.tsx +46 -0
  134. package/templates/base/src/config/i18n.ts +225 -0
  135. package/templates/base/src/config/settings-types.ts +162 -0
  136. package/templates/base/src/config/sidebars.ts +66 -0
  137. package/templates/base/src/config/tag-vocabulary-types.ts +39 -0
  138. package/templates/base/src/config/tag-vocabulary.ts +20 -0
  139. package/templates/base/src/hooks/use-active-heading.ts +133 -0
  140. package/templates/base/src/plugins/docs-source-map.ts +103 -0
  141. package/templates/base/src/plugins/hast-utils.ts +10 -0
  142. package/templates/base/src/plugins/rehype-code-title.ts +50 -0
  143. package/templates/base/src/plugins/rehype-heading-links.ts +53 -0
  144. package/templates/base/src/plugins/rehype-image-enlarge.ts +113 -0
  145. package/templates/base/src/plugins/rehype-mermaid.ts +41 -0
  146. package/templates/base/src/plugins/rehype-strip-md-extension.ts +58 -0
  147. package/templates/base/src/plugins/remark-admonitions.ts +99 -0
  148. package/templates/base/src/plugins/remark-resolve-markdown-links.ts +127 -0
  149. package/templates/base/src/plugins/url-utils.ts +4 -0
  150. package/templates/base/src/styles/global.css +1066 -0
  151. package/templates/base/src/types/docs-entry.ts +39 -0
  152. package/templates/base/src/types/heading.ts +5 -0
  153. package/templates/base/src/types/locale.ts +10 -0
  154. package/templates/base/src/utils/base.ts +139 -0
  155. package/templates/base/src/utils/content-files.ts +106 -0
  156. package/templates/base/src/utils/dedent.ts +24 -0
  157. package/templates/base/src/utils/docs.ts +335 -0
  158. package/templates/base/src/utils/git-info.ts +70 -0
  159. package/templates/base/src/utils/github.ts +19 -0
  160. package/templates/base/src/utils/header-right-items.ts +38 -0
  161. package/templates/base/src/utils/nav-scope.ts +63 -0
  162. package/templates/base/src/utils/sidebar.ts +104 -0
  163. package/templates/base/src/utils/slug.ts +10 -0
  164. package/templates/base/src/utils/smart-break.tsx +126 -0
  165. package/templates/base/src/utils/tags.ts +126 -0
  166. package/templates/base/tsconfig.json +36 -0
  167. package/templates/features/bodyFootUtil/files/src/utils/github.ts +19 -0
  168. package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +137 -0
  169. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +34 -0
  170. package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +376 -0
  171. package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +93 -0
  172. package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +586 -0
  173. package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +15 -0
  174. package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +99 -0
  175. package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +177 -0
  176. package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +50 -0
  177. package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +99 -0
  178. package/templates/features/docHistory/files/src/components/doc-history.tsx +598 -0
  179. package/templates/features/docHistory/files/src/types/doc-history.ts +23 -0
  180. package/templates/features/docHistory/files/src/utils/doc-history.ts +180 -0
  181. package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +116 -0
  182. package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +99 -0
  183. package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +101 -0
  184. package/templates/features/docTags/files/pages/docs/tags/index.tsx +86 -0
  185. package/templates/features/i18n/files/pages/[locale]/docs/[...slug].tsx +467 -0
  186. package/templates/features/i18n/files/pages/[locale]/index.tsx +213 -0
  187. package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +248 -0
  188. package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +74 -0
  189. package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +185 -0
  190. package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +126 -0
  191. package/templates/features/tagGovernance/files/scripts/tags-audit.ts +576 -0
  192. package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +428 -0
  193. package/templates/features/tauri/files/src/components/find-bar.tsx +122 -0
  194. package/templates/features/tauri/files/src/components/find-in-page-init.tsx +53 -0
  195. package/templates/features/tauri/files/src/utils/find-in-page.ts +175 -0
  196. package/templates/features/tauri/files/src-tauri/Cargo.toml +14 -0
  197. package/templates/features/tauri/files/src-tauri/build.rs +3 -0
  198. package/templates/features/tauri/files/src-tauri/capabilities/default.json +11 -0
  199. package/templates/features/tauri/files/src-tauri/src/main.rs +250 -0
  200. package/templates/features/tauri/files/src-tauri/tauri.conf.json +25 -0
  201. package/templates/features/tauriDev/files/src-tauri-dev/Cargo.toml +15 -0
  202. package/templates/features/tauriDev/files/src-tauri-dev/build.rs +3 -0
  203. package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +7 -0
  204. package/templates/features/tauriDev/files/src-tauri-dev/frontend/index.html +187 -0
  205. package/templates/features/tauriDev/files/src-tauri-dev/icons/icon.png +0 -0
  206. package/templates/features/tauriDev/files/src-tauri-dev/src/main.rs +995 -0
  207. package/templates/features/tauriDev/files/src-tauri-dev/tauri.conf.json +22 -0
  208. package/templates/features/tauriDev/files/src-tauri-dev/test-launch.sh +65 -0
  209. package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +100 -0
  210. package/templates/features/versioning/files/pages/docs/versions.tsx +78 -0
  211. package/templates/features/versioning/files/pages/v/[version]/docs/[...slug].tsx +451 -0
  212. package/templates/features/versioning/files/pages/v/[version]/ja/docs/[...slug].tsx +490 -0
@@ -0,0 +1,70 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+
4
+ export interface GitInfo {
5
+ createdAt: string | null;
6
+ updatedAt: string | null;
7
+ author: string | null;
8
+ }
9
+
10
+ const gitInfoCache = new Map<string, GitInfo>();
11
+
12
+ /** Resolve file path, trying .mdx/.md extensions if the path has no extension */
13
+ function resolveFilePath(filePath: string): string {
14
+ if (existsSync(filePath)) return filePath;
15
+ for (const ext of [".mdx", ".md"]) {
16
+ const withExt = filePath + ext;
17
+ if (existsSync(withExt)) return withExt;
18
+ }
19
+ return filePath;
20
+ }
21
+
22
+ export function getGitInfo(filePath: string): GitInfo {
23
+ if (gitInfoCache.has(filePath)) {
24
+ return gitInfoCache.get(filePath)!;
25
+ }
26
+
27
+ const resolved = resolveFilePath(filePath);
28
+
29
+ try {
30
+ // Get all commit dates for this file (oldest last)
31
+ const allDates = execFileSync(
32
+ "git",
33
+ ["log", "--follow", "--format=%aI", "--", resolved],
34
+ { encoding: "utf-8" },
35
+ ).trim();
36
+
37
+ const dates = allDates ? allDates.split("\n") : [];
38
+ const createdAt = dates.length > 0 ? dates[dates.length - 1] : null;
39
+ const updatedAt = dates.length > 0 ? dates[0] : null;
40
+
41
+ const author = execFileSync(
42
+ "git",
43
+ ["log", "-1", "--format=%aN", "--", resolved],
44
+ { encoding: "utf-8" },
45
+ ).trim() || null;
46
+
47
+ const result = { createdAt, updatedAt, author };
48
+ gitInfoCache.set(filePath, result);
49
+ return result;
50
+ } catch {
51
+ const result = { createdAt: null, updatedAt: null, author: null };
52
+ gitInfoCache.set(filePath, result);
53
+ return result;
54
+ }
55
+ }
56
+
57
+ /** Format ISO date to human-readable, respecting locale */
58
+ export function formatDate(isoDate: string, locale = "en"): string {
59
+ const d = new Date(isoDate);
60
+ const localeMap: Record<string, string> = {
61
+ en: "en-US",
62
+ ja: "ja-JP",
63
+ de: "de-DE",
64
+ };
65
+ return d.toLocaleDateString(localeMap[locale] ?? "en-US", {
66
+ year: "numeric",
67
+ month: "short",
68
+ day: "numeric",
69
+ });
70
+ }
@@ -0,0 +1,19 @@
1
+ import { settings } from "@/config/settings";
2
+
3
+ function trimTrailingSlash(url: string): string {
4
+ return url.replace(/\/+$/, "");
5
+ }
6
+
7
+ export function buildGitHubRepoUrl(): string | null {
8
+ if (!settings.githubUrl) return null;
9
+ return trimTrailingSlash(settings.githubUrl as string);
10
+ }
11
+
12
+ export function buildGitHubSourceUrl(
13
+ contentDir: string,
14
+ entryId: string,
15
+ ): string | null {
16
+ const repoUrl = buildGitHubRepoUrl();
17
+ if (!repoUrl) return null;
18
+ return `${repoUrl}/blob/HEAD/${contentDir}/${entryId}`;
19
+ }
@@ -0,0 +1,38 @@
1
+ import { settings } from "@/config/settings";
2
+ import type { HeaderRightItem } from "@/config/settings-types";
3
+
4
+ function isDesignTokenPanelEnabled(): boolean {
5
+ return Boolean(settings.designTokenPanel || settings.colorTweakPanel);
6
+ }
7
+
8
+ export function filterHeaderRightItems(
9
+ items: HeaderRightItem[],
10
+ ): HeaderRightItem[] {
11
+ return items.filter((item) => {
12
+ if (item.type === "trigger") {
13
+ if (item.trigger === "design-token-panel") {
14
+ return isDesignTokenPanelEnabled();
15
+ }
16
+ if (item.trigger === "ai-chat") {
17
+ return Boolean(settings.aiAssistant);
18
+ }
19
+ }
20
+
21
+ if (item.type === "component") {
22
+ if (item.component === "theme-toggle") {
23
+ return Boolean(settings.colorMode);
24
+ }
25
+ if (item.component === "language-switcher") {
26
+ return Object.keys(settings.locales).length > 0;
27
+ }
28
+ if (item.component === "version-switcher") {
29
+ return Boolean(settings.versions);
30
+ }
31
+ if (item.component === "github-link") {
32
+ return Boolean(settings.githubUrl);
33
+ }
34
+ }
35
+
36
+ return true;
37
+ });
38
+ }
@@ -0,0 +1,63 @@
1
+ import { settings } from "@/config/settings";
2
+ import type { NavNode } from "@/utils/docs";
3
+ export type { HeaderNavItem } from "@/config/settings";
4
+
5
+ /** Collect all categoryMatch strings from headerNav, including children (ordered). */
6
+ export function getCategoryOrder(): string[] {
7
+ return settings.headerNav.flatMap((item) => {
8
+ const matches: string[] = [];
9
+ if (item.categoryMatch) matches.push(item.categoryMatch);
10
+ if (item.children) {
11
+ for (const child of item.children) {
12
+ if (child.categoryMatch) matches.push(child.categoryMatch);
13
+ }
14
+ }
15
+ return matches;
16
+ });
17
+ }
18
+
19
+ /**
20
+ * Given a doc's slug (e.g. "getting-started/introduction" or "claude-agents/doc-reviewer"),
21
+ * return the categoryMatch value of the headerNav item it belongs to.
22
+ */
23
+ export function getNavSectionForSlug(slug: string): string | undefined {
24
+ const topCategory = slug.split("/")[0] ?? "";
25
+ const all = getCategoryOrder();
26
+
27
+ // First pass: find explicit matchers (not "!")
28
+ const explicitMatches = all.filter(
29
+ (cm) => cm !== "!" && topCategory.startsWith(cm),
30
+ );
31
+ if (explicitMatches.length > 0) {
32
+ // Longest prefix match wins
33
+ return explicitMatches.sort((a, b) => b.length - a.length)[0];
34
+ }
35
+
36
+ // Second pass: return the default ("!") matcher
37
+ const defaultItem = settings.headerNav.find(
38
+ (item) => item.categoryMatch === "!",
39
+ );
40
+ return defaultItem?.categoryMatch;
41
+ }
42
+
43
+ /**
44
+ * Filter top-level NavNodes by a headerNav categoryMatch value.
45
+ * - "!" means everything NOT claimed by explicit matchers
46
+ * - "claude" means nodes whose slug starts with "claude"
47
+ * - undefined means all nodes
48
+ */
49
+ export function getNavSubtree(
50
+ tree: NavNode[],
51
+ categoryMatch?: string,
52
+ ): NavNode[] {
53
+ if (!categoryMatch) return tree;
54
+
55
+ if (categoryMatch === "!") {
56
+ const explicitPrefixes = getCategoryOrder().filter((cm) => cm !== "!");
57
+ return tree.filter(
58
+ (node) => !explicitPrefixes.some((prefix) => node.slug.startsWith(prefix)),
59
+ );
60
+ }
61
+
62
+ return tree.filter((node) => node.slug.startsWith(categoryMatch));
63
+ }
@@ -0,0 +1,104 @@
1
+ import sidebars from "@/config/sidebars";
2
+ import type { SidebarItem } from "@/config/sidebars";
3
+ import type { NavNode, CategoryMeta } from "@/utils/docs";
4
+ import { buildNavTree, findNode } from "@/utils/docs";
5
+ import { getNavSubtree } from "@/utils/nav-scope";
6
+ import type { Locale } from "@/config/i18n";
7
+ import type { DocsEntry } from "@/types/docs-entry";
8
+
9
+ /**
10
+ * Build sidebar nodes for a given nav section.
11
+ * If sidebar config exists for this section, use it.
12
+ * Otherwise fall back to auto-generated tree.
13
+ */
14
+ export function buildSidebarForSection(
15
+ docs: DocsEntry[],
16
+ lang: Locale,
17
+ categoryMatch?: string,
18
+ categoryMeta?: Map<string, CategoryMeta>,
19
+ ): NavNode[] {
20
+ const tree = buildNavTree(docs, lang, categoryMeta);
21
+
22
+ // Check if there's a sidebar config for this section
23
+ const sectionKey = categoryMatch ?? "default";
24
+ const config = sidebars[sectionKey];
25
+
26
+ if (!config || config.length === 0) {
27
+ // Fall back to auto-generated
28
+ return getNavSubtree(tree, categoryMatch);
29
+ }
30
+
31
+ // Resolve config to nodes
32
+ return resolveItems(config, tree, lang);
33
+ }
34
+
35
+ function resolveItems(
36
+ items: SidebarItem[],
37
+ tree: NavNode[],
38
+ lang: Locale,
39
+ ): NavNode[] {
40
+ return items.flatMap((item) => resolveItem(item, tree, lang));
41
+ }
42
+
43
+ function resolveItem(
44
+ item: SidebarItem,
45
+ tree: NavNode[],
46
+ lang: Locale,
47
+ ): NavNode[] {
48
+ if (typeof item === "string") {
49
+ // String shorthand = doc reference (always rendered as leaf, children stripped)
50
+ const node = findNode(tree, item);
51
+ return node ? [{ ...node, children: [] }] : [];
52
+ }
53
+
54
+ switch (item.type) {
55
+ case "doc": {
56
+ const node = findNode(tree, item.id);
57
+ if (!node) return [];
58
+ // Doc references always render as leaves — strip children so they
59
+ // don't duplicate structure that the explicit config already defines.
60
+ return [{ ...node, children: [], ...(item.label ? { label: item.label } : {}) }];
61
+ }
62
+ case "link": {
63
+ return [
64
+ {
65
+ slug: `__link__${item.href}`,
66
+ label: item.label,
67
+ href: item.href,
68
+ position: 0,
69
+ hasPage: false,
70
+ children: [],
71
+ },
72
+ ];
73
+ }
74
+ case "category": {
75
+ const children = resolveItems(item.items, tree, lang);
76
+ if (item.sortOrder === "desc") {
77
+ children.reverse();
78
+ }
79
+ return [
80
+ {
81
+ slug: `__category__${item.label}`,
82
+ label: item.label,
83
+ position: 0,
84
+ hasPage: false,
85
+ href: undefined,
86
+ children,
87
+ sortOrder: item.sortOrder,
88
+ collapsed: item.collapsed,
89
+ },
90
+ ];
91
+ }
92
+ case "autogenerated": {
93
+ if (item.dirName) {
94
+ const node = findNode(tree, item.dirName);
95
+ return node ? node.children : [];
96
+ }
97
+ return tree;
98
+ }
99
+ default: {
100
+ const _exhaustive: never = item;
101
+ throw new Error(`Unhandled sidebar item type: ${JSON.stringify(_exhaustive)}`);
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,10 @@
1
+ export function toRouteSlug(id: string): string {
2
+ return id.replace(/\/index$/, "");
3
+ }
4
+
5
+ export function toTitleCase(str: string): string {
6
+ return str
7
+ .split("-")
8
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
9
+ .join(" ");
10
+ }
@@ -0,0 +1,126 @@
1
+ /** @jsxRuntime automatic */
2
+ /** @jsxImportSource preact */
3
+
4
+ import type { VNode } from "preact";
5
+
6
+ /**
7
+ * Heuristic: does `text` look like a URL, filesystem path, or similar
8
+ * structure where inserting <wbr> after delimiters aids line-wrapping?
9
+ *
10
+ * Returns true for URLs (contain "://"), POSIX-style absolute/relative
11
+ * paths ("/", "./", "../"), Windows drive paths ("C:\\..." or "C:/..."),
12
+ * strings with 2+ slashes (or backslashes) between alphanumerics, and
13
+ * plausible domain-plus-slash strings.
14
+ *
15
+ * Returns false for empty input and prose-y hyphen/slash/dot combinations
16
+ * like "and/or", "well-known", "state-of-the-art", "1.2.3-beta.4", "UI/UX".
17
+ */
18
+ export function isPathLike(text: string): boolean {
19
+ if (!text) return false;
20
+ if (text.includes("://")) return true;
21
+ // starts with "/", "./", or "../"
22
+ if (/^\.{0,2}\//.test(text)) return true;
23
+ // Windows drive letter paths, either backslash or forward slash
24
+ if (/^[A-Za-z]:[\\/]/.test(text)) return true;
25
+ // 2+ forward slashes appearing between alphanumeric runs
26
+ const forwardMatches = text.match(/[A-Za-z0-9]\/[A-Za-z0-9]/g);
27
+ if (forwardMatches && forwardMatches.length >= 2) return true;
28
+ // 2+ backslashes appearing between alphanumeric runs
29
+ const backMatches = text.match(/[A-Za-z0-9]\\[A-Za-z0-9]/g);
30
+ if (backMatches && backMatches.length >= 2) return true;
31
+ // Plausible domain-ish: a "." between alphanumerics AND at least one slash
32
+ const hasDomainDot = /[A-Za-z0-9]\.[A-Za-z0-9]/.test(text);
33
+ const hasSlash = /[\\/]/.test(text);
34
+ if (hasDomainDot && hasSlash) return true;
35
+ return false;
36
+ }
37
+
38
+ // Delimiter set: / \ - _ . : ? # & =
39
+ // Capture in split so the delimiter is preserved at odd indices.
40
+ const DELIM_SPLIT = /([/\\\-_.:?#&=])/;
41
+
42
+ const HTML_ESCAPE_MAP: Record<string, string> = {
43
+ "&": "&amp;",
44
+ "<": "&lt;",
45
+ ">": "&gt;",
46
+ '"': "&quot;",
47
+ "'": "&#39;",
48
+ };
49
+
50
+ function htmlEscape(s: string): string {
51
+ return s.replace(/[&<>"']/g, (ch) => HTML_ESCAPE_MAP[ch]!);
52
+ }
53
+
54
+ /**
55
+ * If `text` is path-like, return a Preact fragment with <wbr/> inserted
56
+ * after each delimiter (/, \, -, _, ., :, ?, #, &, =). Otherwise return
57
+ * the input string unchanged so callers can trust non-path prose passes
58
+ * through untouched.
59
+ */
60
+ export function smartBreak(text: string): VNode | string {
61
+ if (!isPathLike(text)) return text;
62
+ const parts = text.split(DELIM_SPLIT);
63
+ const nodes: (string | VNode)[] = [];
64
+ for (let i = 0; i < parts.length; i++) {
65
+ const part = parts[i];
66
+ if (part === "") continue;
67
+ nodes.push(part);
68
+ // Captured delimiter groups always land at odd indices.
69
+ if (i % 2 === 1) nodes.push(<wbr key={`wbr-${i}`} />);
70
+ }
71
+ return <>{nodes}</>;
72
+ }
73
+
74
+ /**
75
+ * Preact function component wrapper — pure, server-renderable.
76
+ * Stringifies children and defers to smartBreak.
77
+ *
78
+ * Return type is `any` so the component can be mounted from both
79
+ * Preact-typed and React-typed .tsx files (preact/compat makes this safe
80
+ * at runtime, but TypeScript treats Preact's VNode and React's JSX.Element
81
+ * as distinct types).
82
+ */
83
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
84
+ export function SmartBreak({ children }: { children?: unknown }): any {
85
+ return <>{smartBreak(String(children ?? ""))}</>;
86
+ }
87
+
88
+ /**
89
+ * HTML-escape `text` and inject a literal "<wbr>" tag after every
90
+ * delimiter character. Unlike smartBreakToHtml, this does NOT check
91
+ * isPathLike — it unconditionally breaks on delimiters.
92
+ *
93
+ * Useful when a caller has already decided that a larger string is
94
+ * path-like and wants to apply the same wbr-injection rule to a
95
+ * substring (e.g. a segment produced by splitting on a search-query
96
+ * regex) without re-running the heuristic on fragments that are too
97
+ * short to be classified correctly on their own.
98
+ *
99
+ * Byte-identical to smartBreakToHtml for inputs where isPathLike is
100
+ * true, so the shared contract holds.
101
+ */
102
+ export function escapeAndInjectWbr(text: string): string {
103
+ const parts = text.split(DELIM_SPLIT);
104
+ let out = "";
105
+ for (let i = 0; i < parts.length; i++) {
106
+ const part = parts[i];
107
+ if (part === "") continue;
108
+ out += htmlEscape(part);
109
+ if (i % 2 === 1) out += "<wbr>";
110
+ }
111
+ return out;
112
+ }
113
+
114
+ /**
115
+ * HTML-string counterpart of smartBreak. Produces a safe HTML string
116
+ * with literal "<wbr>" tags injected after each delimiter in path-like
117
+ * input, and HTML-escaped text elsewhere. For non-path input, returns
118
+ * the HTML-escaped text unchanged (no wbr injection).
119
+ *
120
+ * Use this when the consumer cannot render Preact VNodes (e.g. building
121
+ * an HTML string for `set:html` / dangerouslySetInnerHTML).
122
+ */
123
+ export function smartBreakToHtml(text: string): string {
124
+ if (!isPathLike(text)) return htmlEscape(text);
125
+ return escapeAndInjectWbr(text);
126
+ }
@@ -0,0 +1,126 @@
1
+ import type { DocsEntry } from "@/types/docs-entry";
2
+ import { settings } from "@/config/settings";
3
+ import { tagVocabulary } from "@/config/tag-vocabulary";
4
+ import type { TagVocabularyEntry } from "@/config/tag-vocabulary-types";
5
+
6
+ export interface TagInfo {
7
+ tag: string;
8
+ count: number;
9
+ docs: { slug: string; title: string; description?: string }[];
10
+ }
11
+
12
+ export interface ResolvedTag {
13
+ /** Canonical id after alias/redirect rewrites. Equal to the raw input when unchanged. */
14
+ canonical: string;
15
+ /** True when the canonical id should be dropped from aggregation (deprecated without redirect). */
16
+ deprecated: boolean;
17
+ /** True when the raw input matched a vocabulary entry (as id or alias). */
18
+ known: boolean;
19
+ }
20
+
21
+ interface VocabularyIndex {
22
+ byId: Map<string, TagVocabularyEntry>;
23
+ byAlias: Map<string, TagVocabularyEntry>;
24
+ }
25
+
26
+ let cachedIndex: VocabularyIndex | null = null;
27
+ function getIndex(): VocabularyIndex {
28
+ if (cachedIndex) return cachedIndex;
29
+ const byId = new Map<string, TagVocabularyEntry>();
30
+ const byAlias = new Map<string, TagVocabularyEntry>();
31
+ for (const entry of tagVocabulary) {
32
+ byId.set(entry.id, entry);
33
+ for (const alias of entry.aliases ?? []) byAlias.set(alias, entry);
34
+ }
35
+ cachedIndex = { byId, byAlias };
36
+ return cachedIndex;
37
+ }
38
+
39
+ function vocabularyActive(): boolean {
40
+ return Boolean(settings.tagVocabulary) && settings.tagGovernance !== "off";
41
+ }
42
+
43
+ /**
44
+ * Resolve a raw tag string to its canonical form.
45
+ *
46
+ * When the vocabulary is inactive (`tagVocabulary: false` or
47
+ * `tagGovernance: "off"`), the raw value passes through unchanged with
48
+ * `known: false, deprecated: false`. Otherwise:
49
+ *
50
+ * - A direct id match returns that id.
51
+ * - An alias match returns the aliased entry's id.
52
+ * - `deprecated: { redirect: "<id>" }` rewrites to the redirect target.
53
+ * - `deprecated: true` (no redirect) returns `deprecated: true` so callers
54
+ * can drop the tag from aggregation.
55
+ * - An unknown value returns the raw string with `known: false`.
56
+ */
57
+ export function resolveTag(raw: string): ResolvedTag {
58
+ if (!vocabularyActive()) {
59
+ return { canonical: raw, deprecated: false, known: false };
60
+ }
61
+ const { byId, byAlias } = getIndex();
62
+ const entry = byId.get(raw) ?? byAlias.get(raw);
63
+ if (!entry) return { canonical: raw, deprecated: false, known: false };
64
+ const dep = entry.deprecated;
65
+ if (dep && typeof dep === "object" && dep.redirect) {
66
+ const target = byId.get(dep.redirect);
67
+ if (target) return { canonical: target.id, deprecated: false, known: true };
68
+ // Redirect points at a missing id — treat like plain deprecation.
69
+ return { canonical: entry.id, deprecated: true, known: true };
70
+ }
71
+ if (dep === true) {
72
+ return { canonical: entry.id, deprecated: true, known: true };
73
+ }
74
+ return { canonical: entry.id, deprecated: false, known: true };
75
+ }
76
+
77
+ /**
78
+ * Resolve a list of raw tag strings (e.g. from frontmatter) to canonical ids,
79
+ * dropping deprecated-without-redirect entries and preserving order. Duplicates
80
+ * produced by alias collapse are removed.
81
+ */
82
+ export function resolvePageTags(rawTags: readonly string[]): string[] {
83
+ const out: string[] = [];
84
+ const seen = new Set<string>();
85
+ for (const raw of rawTags) {
86
+ const { canonical, deprecated } = resolveTag(raw);
87
+ if (deprecated) continue;
88
+ if (seen.has(canonical)) continue;
89
+ seen.add(canonical);
90
+ out.push(canonical);
91
+ }
92
+ return out;
93
+ }
94
+
95
+ export function collectTags(
96
+ entries: DocsEntry[],
97
+ slugFn: (id: string, data: { slug?: string }) => string,
98
+ ): Map<string, TagInfo> {
99
+ const tagMap = new Map<string, TagInfo>();
100
+
101
+ for (const entry of entries) {
102
+ const rawTags = entry.data.tags ?? [];
103
+ const slug = slugFn(entry.id, entry.data);
104
+
105
+ const seen = new Set<string>();
106
+ for (const raw of rawTags) {
107
+ const resolved = resolveTag(raw);
108
+ if (resolved.deprecated) continue;
109
+ if (seen.has(resolved.canonical)) continue;
110
+ seen.add(resolved.canonical);
111
+
112
+ if (!tagMap.has(resolved.canonical)) {
113
+ tagMap.set(resolved.canonical, { tag: resolved.canonical, count: 0, docs: [] });
114
+ }
115
+ const info = tagMap.get(resolved.canonical)!;
116
+ info.count++;
117
+ info.docs.push({
118
+ slug,
119
+ title: entry.data.title,
120
+ description: entry.data.description,
121
+ });
122
+ }
123
+ }
124
+
125
+ return tagMap;
126
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "compilerOptions": {
4
+ "target": "ESNext",
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "allowImportingTsExtensions": true,
8
+ "resolveJsonModule": true,
9
+ "verbatimModuleSyntax": true,
10
+ "isolatedModules": true,
11
+ "noEmit": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "allowJs": true,
16
+ "jsx": "preserve",
17
+ "strict": true,
18
+ "noImplicitAny": true,
19
+ "strictNullChecks": true,
20
+ "strictFunctionTypes": true,
21
+ "strictBindCallApply": true,
22
+ "strictPropertyInitialization": true,
23
+ "noImplicitThis": true,
24
+ "useUnknownInCatchVariables": true,
25
+ "alwaysStrict": true,
26
+ "baseUrl": ".",
27
+ "paths": {
28
+ "@/*": ["src/*"],
29
+ "#doc-history-meta": [".zfb/doc-history-meta.json"],
30
+ "react": ["./node_modules/preact/compat/"],
31
+ "react/jsx-runtime": ["./node_modules/preact/jsx-runtime"],
32
+ "react-dom": ["./node_modules/preact/compat/"]
33
+ }
34
+ },
35
+ "exclude": ["dist", "e2e/fixtures", "packages", "pages", "worktrees", "vendor", "src/**/__tests__", "vitest.config.ts"]
36
+ }
@@ -0,0 +1,19 @@
1
+ import { settings } from "@/config/settings";
2
+
3
+ function trimTrailingSlash(url: string): string {
4
+ return url.replace(/\/+$/, "");
5
+ }
6
+
7
+ export function buildGitHubRepoUrl(): string | null {
8
+ if (!settings.githubUrl) return null;
9
+ return trimTrailingSlash(settings.githubUrl as string);
10
+ }
11
+
12
+ export function buildGitHubSourceUrl(
13
+ contentDir: string,
14
+ entryId: string,
15
+ ): string | null {
16
+ const repoUrl = buildGitHubRepoUrl();
17
+ if (!repoUrl) return null;
18
+ return `${repoUrl}/blob/HEAD/${contentDir}/${entryId}`;
19
+ }