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.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/bin/create-zudo-doc.js +2 -0
- package/dist/api.d.ts +20 -0
- package/dist/api.js +13 -0
- package/dist/claude-md-gen.d.ts +2 -0
- package/dist/claude-md-gen.js +113 -0
- package/dist/cli.d.ts +39 -0
- package/dist/cli.js +157 -0
- package/dist/compose.d.ts +95 -0
- package/dist/compose.js +206 -0
- package/dist/constants.d.ts +20 -0
- package/dist/constants.js +224 -0
- package/dist/features/body-foot-util.d.ts +10 -0
- package/dist/features/body-foot-util.js +12 -0
- package/dist/features/claude-resources.d.ts +2 -0
- package/dist/features/claude-resources.js +6 -0
- package/dist/features/design-token-panel.d.ts +14 -0
- package/dist/features/design-token-panel.js +27 -0
- package/dist/features/doc-history.d.ts +9 -0
- package/dist/features/doc-history.js +11 -0
- package/dist/features/doc-tags.d.ts +19 -0
- package/dist/features/doc-tags.js +33 -0
- package/dist/features/footer-taglist.d.ts +14 -0
- package/dist/features/footer-taglist.js +17 -0
- package/dist/features/footer.d.ts +8 -0
- package/dist/features/footer.js +10 -0
- package/dist/features/i18n.d.ts +22 -0
- package/dist/features/i18n.js +41 -0
- package/dist/features/image-enlarge.d.ts +11 -0
- package/dist/features/image-enlarge.js +13 -0
- package/dist/features/index.d.ts +15 -0
- package/dist/features/index.js +53 -0
- package/dist/features/llms-txt.d.ts +11 -0
- package/dist/features/llms-txt.js +13 -0
- package/dist/features/search.d.ts +9 -0
- package/dist/features/search.js +11 -0
- package/dist/features/sidebar-resizer.d.ts +14 -0
- package/dist/features/sidebar-resizer.js +16 -0
- package/dist/features/sidebar-toggle.d.ts +13 -0
- package/dist/features/sidebar-toggle.js +15 -0
- package/dist/features/tag-governance.d.ts +14 -0
- package/dist/features/tag-governance.js +16 -0
- package/dist/features/tauri-dev.d.ts +2 -0
- package/dist/features/tauri-dev.js +25 -0
- package/dist/features/tauri.d.ts +11 -0
- package/dist/features/tauri.js +52 -0
- package/dist/features/versioning.d.ts +27 -0
- package/dist/features/versioning.js +43 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +150 -0
- package/dist/preset.d.ts +37 -0
- package/dist/preset.js +156 -0
- package/dist/prompts.d.ts +32 -0
- package/dist/prompts.js +248 -0
- package/dist/scaffold.d.ts +4 -0
- package/dist/scaffold.js +344 -0
- package/dist/settings-gen.d.ts +2 -0
- package/dist/settings-gen.js +237 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +34 -0
- package/dist/zfb-config-gen.d.ts +19 -0
- package/dist/zfb-config-gen.js +222 -0
- package/package.json +65 -0
- package/templates/base/.htmlvalidate.json +5 -0
- package/templates/base/.zfb/doc-history-meta.json +1 -0
- package/templates/base/pages/404.tsx +55 -0
- package/templates/base/pages/_data.ts +179 -0
- package/templates/base/pages/_mdx-components.ts +249 -0
- package/templates/base/pages/docs/[...slug].tsx +448 -0
- package/templates/base/pages/index.tsx +158 -0
- package/templates/base/pages/lib/_body-end-islands.tsx +201 -0
- package/templates/base/pages/lib/_category-nav.tsx +148 -0
- package/templates/base/pages/lib/_category-tree-nav.tsx +104 -0
- package/templates/base/pages/lib/_compose-meta-title.ts +29 -0
- package/templates/base/pages/lib/_details.tsx +30 -0
- package/templates/base/pages/lib/_doc-history-area.tsx +178 -0
- package/templates/base/pages/lib/_doc-metainfo-area.tsx +100 -0
- package/templates/base/pages/lib/_doc-tags-area.tsx +89 -0
- package/templates/base/pages/lib/_extract-headings.ts +81 -0
- package/templates/base/pages/lib/_footer-with-defaults.tsx +234 -0
- package/templates/base/pages/lib/_frontmatter-preview-data.ts +53 -0
- package/templates/base/pages/lib/_head-with-defaults.tsx +113 -0
- package/templates/base/pages/lib/_header-with-defaults.tsx +386 -0
- package/templates/base/pages/lib/_inline-version-switcher.tsx +84 -0
- package/templates/base/pages/lib/_math-block.tsx +63 -0
- package/templates/base/pages/lib/_nav-source-docs.ts +68 -0
- package/templates/base/pages/lib/_preset-generator.tsx +81 -0
- package/templates/base/pages/lib/_search-widget-script.ts +388 -0
- package/templates/base/pages/lib/_search-widget.tsx +196 -0
- package/templates/base/pages/lib/_sidebar-with-defaults.tsx +176 -0
- package/templates/base/pages/lib/_site-tree-nav.tsx +128 -0
- package/templates/base/pages/lib/locale-merge.ts +58 -0
- package/templates/base/pages/lib/route-enumerators.ts +302 -0
- package/templates/base/pages/sitemap.xml.tsx +51 -0
- package/templates/base/plugins/connect-adapter.mjs +144 -0
- package/templates/base/plugins/copy-public-plugin.mjs +50 -0
- package/templates/base/plugins/search-index-plugin.mjs +54 -0
- package/templates/base/scripts/run-b4push.sh +102 -0
- package/templates/base/src/components/ai-chat-modal.tsx +15 -0
- package/templates/base/src/components/client-router-bootstrap.tsx +14 -0
- package/templates/base/src/components/content/component-map.ts +25 -0
- package/templates/base/src/components/content/content-blockquote.tsx +16 -0
- package/templates/base/src/components/content/content-code.tsx +117 -0
- package/templates/base/src/components/content/content-link.tsx +83 -0
- package/templates/base/src/components/content/content-ol.tsx +19 -0
- package/templates/base/src/components/content/content-paragraph.tsx +10 -0
- package/templates/base/src/components/content/content-strong.tsx +16 -0
- package/templates/base/src/components/content/content-table.tsx +18 -0
- package/templates/base/src/components/content/content-ul.tsx +18 -0
- package/templates/base/src/components/content/heading-h2.tsx +26 -0
- package/templates/base/src/components/content/heading-h3.tsx +26 -0
- package/templates/base/src/components/content/heading-h4.tsx +26 -0
- package/templates/base/src/components/design-token-panel-bootstrap.tsx +15 -0
- package/templates/base/src/components/desktop-sidebar-toggle.tsx +15 -0
- package/templates/base/src/components/doc-history.tsx +18 -0
- package/templates/base/src/components/html-preview/highlighted-code.tsx +74 -0
- package/templates/base/src/components/html-preview/html-preview.tsx +108 -0
- package/templates/base/src/components/html-preview/preflight.ts +112 -0
- package/templates/base/src/components/html-preview/preview-base.tsx +159 -0
- package/templates/base/src/components/image-enlarge.tsx +19 -0
- package/templates/base/src/components/mobile-toc.tsx +94 -0
- package/templates/base/src/components/preset-generator.tsx +14 -0
- package/templates/base/src/components/sidebar-toggle.tsx +98 -0
- package/templates/base/src/components/sidebar-tree.tsx +543 -0
- package/templates/base/src/components/site-tree-nav.tsx +233 -0
- package/templates/base/src/components/theme-toggle.tsx +93 -0
- package/templates/base/src/components/toc.tsx +63 -0
- package/templates/base/src/components/tree-nav-shared.tsx +71 -0
- package/templates/base/src/config/color-scheme-utils.ts +182 -0
- package/templates/base/src/config/color-schemes.ts +128 -0
- package/templates/base/src/config/frontmatter-preview-defaults.ts +24 -0
- package/templates/base/src/config/frontmatter-preview-renderers.tsx +46 -0
- package/templates/base/src/config/i18n.ts +225 -0
- package/templates/base/src/config/settings-types.ts +162 -0
- package/templates/base/src/config/sidebars.ts +66 -0
- package/templates/base/src/config/tag-vocabulary-types.ts +39 -0
- package/templates/base/src/config/tag-vocabulary.ts +20 -0
- package/templates/base/src/hooks/use-active-heading.ts +133 -0
- package/templates/base/src/plugins/docs-source-map.ts +103 -0
- package/templates/base/src/plugins/hast-utils.ts +10 -0
- package/templates/base/src/plugins/rehype-code-title.ts +50 -0
- package/templates/base/src/plugins/rehype-heading-links.ts +53 -0
- package/templates/base/src/plugins/rehype-image-enlarge.ts +113 -0
- package/templates/base/src/plugins/rehype-mermaid.ts +41 -0
- package/templates/base/src/plugins/rehype-strip-md-extension.ts +58 -0
- package/templates/base/src/plugins/remark-admonitions.ts +99 -0
- package/templates/base/src/plugins/remark-resolve-markdown-links.ts +127 -0
- package/templates/base/src/plugins/url-utils.ts +4 -0
- package/templates/base/src/styles/global.css +1066 -0
- package/templates/base/src/types/docs-entry.ts +39 -0
- package/templates/base/src/types/heading.ts +5 -0
- package/templates/base/src/types/locale.ts +10 -0
- package/templates/base/src/utils/base.ts +139 -0
- package/templates/base/src/utils/content-files.ts +106 -0
- package/templates/base/src/utils/dedent.ts +24 -0
- package/templates/base/src/utils/docs.ts +335 -0
- package/templates/base/src/utils/git-info.ts +70 -0
- package/templates/base/src/utils/github.ts +19 -0
- package/templates/base/src/utils/header-right-items.ts +38 -0
- package/templates/base/src/utils/nav-scope.ts +63 -0
- package/templates/base/src/utils/sidebar.ts +104 -0
- package/templates/base/src/utils/slug.ts +10 -0
- package/templates/base/src/utils/smart-break.tsx +126 -0
- package/templates/base/src/utils/tags.ts +126 -0
- package/templates/base/tsconfig.json +36 -0
- package/templates/features/bodyFootUtil/files/src/utils/github.ts +19 -0
- package/templates/features/claudeResources/files/plugins/claude-resources-plugin.mjs +137 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/escape-for-mdx.test.ts +34 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/__tests__/generate.test.ts +376 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/escape-for-mdx.ts +93 -0
- package/templates/features/claudeResources/files/src/integrations/claude-resources/generate.ts +586 -0
- package/templates/features/designTokenPanel/files/src/components/design-token-panel-bootstrap.tsx +15 -0
- package/templates/features/designTokenPanel/files/src/config/design-token-panel-config.ts +99 -0
- package/templates/features/designTokenPanel/files/src/config/design-tokens-manifest.ts +177 -0
- package/templates/features/designTokenPanel/files/src/lib/design-token-panel-bootstrap.ts +50 -0
- package/templates/features/docHistory/files/plugins/doc-history-plugin.mjs +99 -0
- package/templates/features/docHistory/files/src/components/doc-history.tsx +598 -0
- package/templates/features/docHistory/files/src/types/doc-history.ts +23 -0
- package/templates/features/docHistory/files/src/utils/doc-history.ts +180 -0
- package/templates/features/docTags/files/pages/[locale]/docs/tags/[tag].tsx +116 -0
- package/templates/features/docTags/files/pages/[locale]/docs/tags/index.tsx +99 -0
- package/templates/features/docTags/files/pages/docs/tags/[tag].tsx +101 -0
- package/templates/features/docTags/files/pages/docs/tags/index.tsx +86 -0
- package/templates/features/i18n/files/pages/[locale]/docs/[...slug].tsx +467 -0
- package/templates/features/i18n/files/pages/[locale]/index.tsx +213 -0
- package/templates/features/imageEnlarge/files/src/components/image-enlarge.tsx +248 -0
- package/templates/features/llmsTxt/files/plugins/llms-txt-plugin.mjs +74 -0
- package/templates/features/sidebarResizer/files/src/scripts/sidebar-resizer.ts +185 -0
- package/templates/features/sidebarToggle/files/src/components/desktop-sidebar-toggle.tsx +126 -0
- package/templates/features/tagGovernance/files/scripts/tags-audit.ts +576 -0
- package/templates/features/tagGovernance/files/scripts/tags-suggest.ts +428 -0
- package/templates/features/tauri/files/src/components/find-bar.tsx +122 -0
- package/templates/features/tauri/files/src/components/find-in-page-init.tsx +53 -0
- package/templates/features/tauri/files/src/utils/find-in-page.ts +175 -0
- package/templates/features/tauri/files/src-tauri/Cargo.toml +14 -0
- package/templates/features/tauri/files/src-tauri/build.rs +3 -0
- package/templates/features/tauri/files/src-tauri/capabilities/default.json +11 -0
- package/templates/features/tauri/files/src-tauri/src/main.rs +250 -0
- package/templates/features/tauri/files/src-tauri/tauri.conf.json +25 -0
- package/templates/features/tauriDev/files/src-tauri-dev/Cargo.toml +15 -0
- package/templates/features/tauriDev/files/src-tauri-dev/build.rs +3 -0
- package/templates/features/tauriDev/files/src-tauri-dev/capabilities/default.json +7 -0
- package/templates/features/tauriDev/files/src-tauri-dev/frontend/index.html +187 -0
- package/templates/features/tauriDev/files/src-tauri-dev/icons/icon.png +0 -0
- package/templates/features/tauriDev/files/src-tauri-dev/src/main.rs +995 -0
- package/templates/features/tauriDev/files/src-tauri-dev/tauri.conf.json +22 -0
- package/templates/features/tauriDev/files/src-tauri-dev/test-launch.sh +65 -0
- package/templates/features/versioning/files/pages/[locale]/docs/versions.tsx +100 -0
- package/templates/features/versioning/files/pages/docs/versions.tsx +78 -0
- package/templates/features/versioning/files/pages/v/[version]/docs/[...slug].tsx +451 -0
- 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,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
|
+
"&": "&",
|
|
44
|
+
"<": "<",
|
|
45
|
+
">": ">",
|
|
46
|
+
'"': """,
|
|
47
|
+
"'": "'",
|
|
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
|
+
}
|