@takazudo/zudo-doc 5.20.0 → 5.22.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 (45) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/compiled.css +72 -0
  3. package/dist/config.d.ts +12 -1
  4. package/dist/config.js +3 -1
  5. package/dist/content.css +55 -0
  6. package/dist/doc-history-area/index.d.ts +2 -0
  7. package/dist/doc-history-area/index.js +5 -4
  8. package/dist/doc-metainfo-area/index.d.ts +2 -0
  9. package/dist/doc-metainfo-area/index.js +8 -3
  10. package/dist/factory-context/index.d.ts +2 -0
  11. package/dist/home-intro/index.d.ts +10 -0
  12. package/dist/home-intro/index.js +20 -0
  13. package/dist/home-intro/prepare.d.ts +9 -0
  14. package/dist/home-intro/prepare.js +93 -0
  15. package/dist/home-intro/resolve.d.ts +5 -0
  16. package/dist/home-intro/resolve.js +10 -0
  17. package/dist/home-intro/types.d.ts +10 -0
  18. package/dist/home-intro/types.js +0 -0
  19. package/dist/home-page/index.js +42 -22
  20. package/dist/i18n-defaults/index.js +2 -0
  21. package/dist/plugins/doc-history.js +7 -3
  22. package/dist/plugins/img-src-check.d.ts +3 -0
  23. package/dist/plugins/img-src-check.js +21 -0
  24. package/dist/plugins/internal/claude-resources/generate.js +2 -2
  25. package/dist/plugins/internal/doc-history/index.d.ts +6 -1
  26. package/dist/plugins/internal/doc-history/index.js +1 -0
  27. package/dist/plugins/internal/img-src-check/index.d.ts +65 -0
  28. package/dist/plugins/internal/img-src-check/index.js +179 -0
  29. package/dist/plugins/internal/resource-docs-shared/index.d.ts +1 -1
  30. package/dist/plugins/internal/resource-docs-shared/index.js +6 -1
  31. package/dist/plugins/internal/resource-docs-shared/links.d.ts +6 -1
  32. package/dist/plugins/internal/resource-docs-shared/links.js +21 -3
  33. package/dist/plugins/internal/resource-docs-shared/skills.js +27 -5
  34. package/dist/plugins/routes.js +4 -1
  35. package/dist/preset.d.ts +9 -1
  36. package/dist/preset.js +16 -2
  37. package/dist/route-context/index.js +1 -0
  38. package/dist/route-context-payload/index.d.ts +2 -0
  39. package/dist/route-context-payload/index.js +2 -1
  40. package/dist/route-context-payload/types.d.ts +2 -0
  41. package/dist/safelist.css +1 -1
  42. package/dist/settings.d.ts +12 -0
  43. package/package.json +15 -2
  44. package/routes-src/_virtual.d.ts +3 -1
  45. package/virtual-modules.d.ts +3 -1
@@ -29,8 +29,10 @@ const plugin = {
29
29
  });
30
30
  },
31
31
  async postBuild(ctx) {
32
+ const options = ctx.options;
33
+ if (options.ui === false) return;
32
34
  try {
33
- await runDocHistoryPostBuild(ctx.options, {
35
+ await runDocHistoryPostBuild(options, {
34
36
  outDir: ctx.outDir,
35
37
  logger: ctx.logger
36
38
  });
@@ -44,11 +46,13 @@ const plugin = {
44
46
  }
45
47
  },
46
48
  devMiddleware(ctx) {
49
+ const options = ctx.options;
50
+ if (options.ui === false) return;
47
51
  const middleware = createDocHistoryDevMiddleware(
48
- ctx.options,
52
+ options,
49
53
  ctx.logger
50
54
  );
51
- const basePrefix = getBasePrefix(ctx.options["base"]);
55
+ const basePrefix = getBasePrefix(options.base);
52
56
  ctx.register(`${basePrefix}/doc-history`, connectToZfbHandler(middleware));
53
57
  }
54
58
  };
@@ -0,0 +1,3 @@
1
+ import type { ZfbPlugin } from "@takazudo/zfb/plugins";
2
+ declare const plugin: ZfbPlugin;
3
+ export default plugin;
@@ -0,0 +1,21 @@
1
+ import { checkImgSrcs } from "./internal/img-src-check/index.js";
2
+ function severity(value) {
3
+ return value === "error" || value === "ignore" ? value : "warn";
4
+ }
5
+ const plugin = {
6
+ name: "img-src-check",
7
+ postBuild(ctx) {
8
+ const onBroken = severity(ctx.options["onBroken"]);
9
+ if (onBroken === "ignore") return;
10
+ checkImgSrcs({
11
+ outDir: ctx.outDir,
12
+ base: typeof ctx.options["base"] === "string" ? ctx.options["base"] : "/",
13
+ onBroken,
14
+ logger: ctx.logger
15
+ });
16
+ }
17
+ };
18
+ var img_src_check_default = plugin;
19
+ export {
20
+ img_src_check_default as default
21
+ };
@@ -133,7 +133,7 @@ sidebar_label: "${escapeTitle(name)}"
133
133
  generated: true
134
134
  ---
135
135
 
136
- ${escapeForMdx(parsed.content.trim())}
136
+ ${escapeForMdx(downgradeRepoRelativeLinks(parsed.content.trim()))}
137
137
  `;
138
138
  fs.writeFileSync(path.join(outputDir, `${name}.mdx`), mdx);
139
139
  }
@@ -196,7 +196,7 @@ generated: true
196
196
  ---
197
197
 
198
198
  ${modelBadge}
199
- ${escapeForMdx(parsed.content.trim())}
199
+ ${escapeForMdx(downgradeRepoRelativeLinks(parsed.content.trim()))}
200
200
  `;
201
201
  fs.writeFileSync(path.join(outputDir, `${fileSlug}.mdx`), mdx);
202
202
  }
@@ -13,6 +13,10 @@ export interface DocHistoryOptions {
13
13
  locales?: Record<string, DocHistoryLocaleConfig>;
14
14
  /** Slug globs excluded from pre-build metadata and post-build history JSON. */
15
15
  exclude?: string[];
16
+ /** Whether the history UI, JSON generation, and dev proxy are enabled. Defaults to `true`. */
17
+ ui?: boolean;
18
+ /** Site base path used when registering the dev proxy route. */
19
+ base?: string;
16
20
  /**
17
21
  * Port the standalone `@takazudo/zudo-doc-history-server` listens on.
18
22
  * Defaults to `4322` to match the server's CLI default. Only used by
@@ -118,7 +122,8 @@ export declare function shouldGeneratePostBuild(env?: NodeJS.ProcessEnv): {
118
122
  * `@takazudo/zudo-doc-history-server` to write per-page git history JSON
119
123
  * files into `<outDir>/doc-history/`.
120
124
  *
121
- * Generation is gated by `shouldGeneratePostBuild` (see its docs): skipped by
125
+ * Generation is gated by `options.ui` and `shouldGeneratePostBuild` (see its
126
+ * docs): `ui: false` is always skipped; otherwise generation is skipped by
122
127
  * default on local builds (opt in with `GEN_DOC_HISTORY=1`), run in CI and
123
128
  * when explicitly opted in, and always suppressed by `SKIP_DOC_HISTORY=1` or
124
129
  * `DOC_HISTORY_SKIP_POSTBUILD=1`.
@@ -70,6 +70,7 @@ function isCiEnv(env) {
70
70
  return env.CI === "true" || env.CI === "1" || env.GITHUB_ACTIONS === "true";
71
71
  }
72
72
  async function runDocHistoryPostBuild(options, ctx) {
73
+ if (options.ui === false) return;
73
74
  const { generate, reason } = shouldGeneratePostBuild();
74
75
  if (!generate) {
75
76
  ctx.logger?.info(`Skipping doc history generation (${reason})`);
@@ -0,0 +1,65 @@
1
+ /** Severity used by the raw image-source check. */
2
+ export type ImgSrcCheckSeverity = "warn" | "error" | "ignore";
3
+ /** Logger surface needed by the scanner's reporting phase. */
4
+ export interface ImgSrcCheckLogger {
5
+ warn(message: string): void;
6
+ }
7
+ /** One raw image reference that could not be resolved to a file. */
8
+ export interface BrokenImgSrc {
9
+ /** HTML page path relative to the build output directory. */
10
+ pagePath: string;
11
+ /** The decoded attribute value as authored in the rendered HTML. */
12
+ src: string;
13
+ /** Why the reference was considered broken. */
14
+ reason: string;
15
+ }
16
+ /** Result returned by the filesystem scanner before reporting. */
17
+ export interface ImgSrcCheckResult {
18
+ /** Number of HTML files visited under `outDir`. */
19
+ htmlFileCount: number;
20
+ /** Number of site-absolute `src` attributes inspected. */
21
+ imageCount: number;
22
+ /** Every broken occurrence, including duplicate references. */
23
+ broken: BrokenImgSrc[];
24
+ }
25
+ /** Options for scanning and reporting built HTML. */
26
+ export interface ImgSrcCheckOptions {
27
+ /** Absolute or relative build output directory. */
28
+ outDir: string;
29
+ /** URL base configured for the build (for example `/docs/`). */
30
+ base?: string;
31
+ /** Broken-reference behavior. Defaults to `warn`. */
32
+ onBroken?: ImgSrcCheckSeverity;
33
+ /** zfb's logger. A missing logger makes the scan quiet. */
34
+ logger?: ImgSrcCheckLogger;
35
+ }
36
+ /**
37
+ * Parse rendered HTML and return the `src` values on real `<img>` elements.
38
+ *
39
+ * parse5 performs HTML tokenisation (including comment/script handling) and
40
+ * decodes character references in attribute values. Walking its element tree
41
+ * therefore avoids false positives from comments, script bodies, and escaped
42
+ * code examples without trying to emulate an HTML parser with regular
43
+ * expressions.
44
+ */
45
+ export declare function extractImgSrcs(html: string): string[];
46
+ /** Normalize a zfb base to a slash-delimited URL prefix. */
47
+ export declare function normalizeImgSrcBase(base: string | undefined): string;
48
+ /**
49
+ * Walk every built HTML file and validate its site-absolute image sources.
50
+ *
51
+ * This is intentionally synchronous: zfb's postBuild hook is async-compatible
52
+ * but the operation is a deterministic local filesystem walk, and a sync
53
+ * implementation keeps result ordering stable for warnings and tests.
54
+ */
55
+ export declare function scanImgSrcs(options: ImgSrcCheckOptions): ImgSrcCheckResult;
56
+ /** Format one warning in a stable, page-first form suitable for zfb output. */
57
+ export declare function formatBrokenImgSrc(reference: BrokenImgSrc): string;
58
+ /**
59
+ * Run the scanner and report every broken occurrence. Error mode reports the
60
+ * complete set first, then throws so zfb fails the build with all diagnostics.
61
+ */
62
+ export declare function checkImgSrcs(options: ImgSrcCheckOptions): ImgSrcCheckResult;
63
+ export declare const extractImageSources: typeof extractImgSrcs;
64
+ export declare const scanImageSources: typeof scanImgSrcs;
65
+ export declare const checkImageSources: typeof checkImgSrcs;
@@ -0,0 +1,179 @@
1
+ import { realpathSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { relative, resolve, sep } from "node:path";
3
+ import { parse } from "parse5";
4
+ const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
5
+ const SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:/u;
6
+ function extractImgSrcs(html) {
7
+ const document = parse(html);
8
+ const srcs = [];
9
+ const visit = (node) => {
10
+ if (!("tagName" in node)) return;
11
+ const element = node;
12
+ if (element.tagName.toLowerCase() === "img" && element.namespaceURI === HTML_NAMESPACE) {
13
+ const src = element.attrs.find((attribute) => attribute.name.toLowerCase() === "src")?.value;
14
+ if (src !== void 0) srcs.push(src);
15
+ }
16
+ for (const child of element.childNodes) visit(child);
17
+ if (element.nodeName === "template") {
18
+ const template = element;
19
+ for (const child of template.content.childNodes) visit(child);
20
+ }
21
+ };
22
+ for (const child of document.childNodes) visit(child);
23
+ return srcs;
24
+ }
25
+ function normalizeImgSrcBase(base) {
26
+ if (!base || base === "/") return "/";
27
+ const withLeadingSlash = base.startsWith("/") ? base : `/${base}`;
28
+ const segments = withLeadingSlash.split("/").filter(Boolean);
29
+ return segments.length === 0 ? "/" : `/${segments.join("/")}/`;
30
+ }
31
+ function isWithin(root, candidate) {
32
+ const rel = relative(root, candidate);
33
+ return rel === "" || rel !== ".." && !rel.startsWith(`..${sep}`) && !rel.startsWith(sep);
34
+ }
35
+ function stripQueryAndFragment(src) {
36
+ const query = src.indexOf("?");
37
+ const fragment = src.indexOf("#");
38
+ const end = [query, fragment].filter((index) => index >= 0).sort((a, b) => a - b)[0];
39
+ return end === void 0 ? src : src.slice(0, end);
40
+ }
41
+ function resolveImgSrc(src, outDir, base, canonicalOutDir) {
42
+ const value = src.trim();
43
+ if (!value.startsWith("/") || value.startsWith("//") || SCHEME_RE.test(value)) {
44
+ return { kind: "skip" };
45
+ }
46
+ const pathPart = stripQueryAndFragment(value);
47
+ let decodedPath;
48
+ try {
49
+ decodedPath = decodeURIComponent(pathPart);
50
+ } catch {
51
+ return { kind: "broken", reason: "malformed percent escape" };
52
+ }
53
+ if (decodedPath.includes("\0")) {
54
+ return { kind: "broken", reason: "invalid path" };
55
+ }
56
+ let relativeUrlPath;
57
+ if (base === "/") {
58
+ relativeUrlPath = decodedPath.slice(1).replace(/^\/+/, "");
59
+ } else {
60
+ const baseWithoutTrailingSlash = base.slice(0, -1);
61
+ if (decodedPath === baseWithoutTrailingSlash || decodedPath.startsWith(base)) {
62
+ relativeUrlPath = decodedPath.slice(base.length).replace(/^\/+/, "");
63
+ } else {
64
+ return { kind: "broken", reason: `outside configured base ${base}` };
65
+ }
66
+ }
67
+ const candidate = resolve(outDir, relativeUrlPath);
68
+ if (!isWithin(outDir, candidate)) {
69
+ return { kind: "broken", reason: "resolves outside the build output directory" };
70
+ }
71
+ let stat;
72
+ try {
73
+ stat = statSync(candidate);
74
+ } catch (error) {
75
+ const code = error.code;
76
+ if (code === "ENOENT" || code === "ENOTDIR") {
77
+ return { kind: "broken", reason: "file does not exist" };
78
+ }
79
+ throw error;
80
+ }
81
+ if (!stat.isFile()) return { kind: "broken", reason: "path is not a file" };
82
+ try {
83
+ if (!isWithin(canonicalOutDir, realpathSync(candidate))) {
84
+ return { kind: "broken", reason: "resolves outside the build output directory" };
85
+ }
86
+ } catch (error) {
87
+ const code = error.code;
88
+ if (code === "ENOENT" || code === "ENOTDIR") {
89
+ return { kind: "broken", reason: "file does not exist" };
90
+ }
91
+ throw error;
92
+ }
93
+ return { kind: "path", path: candidate };
94
+ }
95
+ function listHtmlFiles(outDir) {
96
+ const files = [];
97
+ if (!statSync(outDir).isDirectory()) return files;
98
+ const walk = (dir) => {
99
+ const entries = readdirSync(dir, { withFileTypes: true });
100
+ entries.sort((a, b) => a.name.localeCompare(b.name, "en"));
101
+ for (const entry of entries) {
102
+ const filePath = resolve(dir, entry.name);
103
+ if (entry.isDirectory()) {
104
+ walk(filePath);
105
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith(".html")) {
106
+ files.push(filePath);
107
+ }
108
+ }
109
+ };
110
+ walk(outDir);
111
+ return files;
112
+ }
113
+ function scanImgSrcs(options) {
114
+ const outDir = resolve(options.outDir);
115
+ let canonicalOutDir;
116
+ try {
117
+ canonicalOutDir = realpathSync(outDir);
118
+ } catch (error) {
119
+ const code = error.code;
120
+ if (code === "ENOENT" || code === "ENOTDIR") {
121
+ return { htmlFileCount: 0, imageCount: 0, broken: [] };
122
+ }
123
+ throw error;
124
+ }
125
+ const base = normalizeImgSrcBase(options.base);
126
+ const htmlFiles = listHtmlFiles(outDir);
127
+ const broken = [];
128
+ let imageCount = 0;
129
+ for (const htmlFile of htmlFiles) {
130
+ const pagePath = relative(outDir, htmlFile).split(sep).join("/");
131
+ const html = readFileSync(htmlFile, "utf8");
132
+ if (!/<img\b/iu.test(html)) continue;
133
+ for (const src of extractImgSrcs(html)) {
134
+ const resolved = resolveImgSrc(src, outDir, base, canonicalOutDir);
135
+ if (resolved.kind === "skip") continue;
136
+ imageCount += 1;
137
+ if (resolved.kind === "broken") {
138
+ broken.push({ pagePath, src, reason: resolved.reason });
139
+ }
140
+ }
141
+ }
142
+ return { htmlFileCount: htmlFiles.length, imageCount, broken };
143
+ }
144
+ function formatBrokenImgSrc(reference) {
145
+ return `[img-src-check] Broken image source in ${reference.pagePath}: ${reference.src} (${reference.reason})`;
146
+ }
147
+ function checkImgSrcs(options) {
148
+ const severity = options.onBroken ?? "warn";
149
+ if (severity === "ignore") {
150
+ return { htmlFileCount: 0, imageCount: 0, broken: [] };
151
+ }
152
+ const result = scanImgSrcs(options);
153
+ if (result.broken.length === 0) return result;
154
+ if (options.logger) {
155
+ for (const reference of result.broken) options.logger.warn(formatBrokenImgSrc(reference));
156
+ options.logger.warn(
157
+ `[img-src-check] Found ${result.broken.length} broken image source${result.broken.length === 1 ? "" : "s"} in ${result.htmlFileCount} HTML file${result.htmlFileCount === 1 ? "" : "s"}.`
158
+ );
159
+ }
160
+ if (severity === "error") {
161
+ throw new Error(
162
+ `[img-src-check] Build contains ${result.broken.length} broken image source${result.broken.length === 1 ? "" : "s"}.`
163
+ );
164
+ }
165
+ return result;
166
+ }
167
+ const extractImageSources = extractImgSrcs;
168
+ const scanImageSources = scanImgSrcs;
169
+ const checkImageSources = checkImgSrcs;
170
+ export {
171
+ checkImageSources,
172
+ checkImgSrcs,
173
+ extractImageSources,
174
+ extractImgSrcs,
175
+ formatBrokenImgSrc,
176
+ normalizeImgSrcBase,
177
+ scanImageSources,
178
+ scanImgSrcs
179
+ };
@@ -3,7 +3,7 @@ export { cleanDir, ensureDir, listFiles, removeGeneratedIndex, resolveLocaleDirs
3
3
  export { resolveLabel, resolveResourceLabel, type ResolveResourceLabelOptions, type ResourceTranslations, } from "./labels.js";
4
4
  export { shouldEmitResourceLocaleRoute, type ResourceLocaleRouteOptions, } from "./locale-routes.js";
5
5
  export { assertNotIndexReserved, escapeTitle, formatFrontmatterString, parseFrontmatter, type FrontmatterStringRenderer, type MdxFileWriter, writeCategoryIndex, writeUnlistedSubPage, } from "./mdx.js";
6
- export { isRepoRelativeLink, downgradeRepoRelativeLinks } from "./links.js";
6
+ export { isRepoRelativeLink, rewriteMarkdownLinks, downgradeRepoRelativeLinks, } from "./links.js";
7
7
  export { escapeMarkdownTableCell, renderCodeFence, } from "./markdown-structure.js";
8
8
  export { EXCLUDED_DIR_NAMES, findNamedFiles } from "./walk.js";
9
9
  export { generateSkillsCategory, getScriptDescription, getSkillFileTree, getSkillReferences, type GenerateSkillsCategoryOptions, type RenderExtraHeader, type SkillItem, type SkillReference, } from "./skills.js";
@@ -22,7 +22,11 @@ import {
22
22
  writeCategoryIndex,
23
23
  writeUnlistedSubPage
24
24
  } from "./mdx.js";
25
- import { isRepoRelativeLink, downgradeRepoRelativeLinks } from "./links.js";
25
+ import {
26
+ isRepoRelativeLink,
27
+ rewriteMarkdownLinks,
28
+ downgradeRepoRelativeLinks
29
+ } from "./links.js";
26
30
  import {
27
31
  escapeMarkdownTableCell,
28
32
  renderCodeFence
@@ -57,6 +61,7 @@ export {
57
61
  resolveLabel,
58
62
  resolveLocaleDirs,
59
63
  resolveResourceLabel,
64
+ rewriteMarkdownLinks,
60
65
  shouldEmitResourceLocaleRoute,
61
66
  writeCategoryIndex,
62
67
  writeGeneratedIndex,
@@ -6,6 +6,11 @@
6
6
  * anchor (`#…`), or a scheme (`mailto:`, `tel:`).
7
7
  */
8
8
  export declare function isRepoRelativeLink(url: string): boolean;
9
+ /**
10
+ * Rewrite markdown link destinations outside fenced and inline code.
11
+ * Returning `undefined` from `rewrite` leaves that link unchanged.
12
+ */
13
+ export declare function rewriteMarkdownLinks(content: string, rewrite: (url: string) => string | undefined): string;
9
14
  /**
10
15
  * Downgrade repo-relative markdown links in a mirrored `CLAUDE.md` body to
11
16
  * inline code so they don't dangle in the flattened mirror tree (#2411).
@@ -19,4 +24,4 @@ export declare function isRepoRelativeLink(url: string): boolean;
19
24
  * Code spans are preserved verbatim: a `[x](./y)` inside a fenced block or an
20
25
  * inline-code span is literal text, not a link, and must not be rewritten.
21
26
  */
22
- export declare function downgradeRepoRelativeLinks(content: string): string;
27
+ export declare function downgradeRepoRelativeLinks(content: string, keep?: (url: string) => boolean): string;
@@ -6,7 +6,7 @@ function isRepoRelativeLink(url) {
6
6
  if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmed)) return false;
7
7
  return true;
8
8
  }
9
- function downgradeRepoRelativeLinks(content) {
9
+ function transformMarkdownLinks(content, transform) {
10
10
  const blockPlaceholder = "\0CRLINK_BLOCK_";
11
11
  const inlinePlaceholder = "\0CRLINK_INLINE_";
12
12
  const codeBlocks = [];
@@ -26,7 +26,7 @@ function downgradeRepoRelativeLinks(content) {
26
26
  );
27
27
  const rewritten = withInline.replace(
28
28
  /!?\[([^\]]*)\]\(([^)]+)\)/g,
29
- (match, text, url) => isRepoRelativeLink(url) ? `\`${text}\`` : match
29
+ (match, text, url) => transform(match, text, url)
30
30
  );
31
31
  return rewritten.replace(
32
32
  new RegExp(`${inlinePlaceholder}(\\d+)\0`, "g"),
@@ -38,7 +38,25 @@ function downgradeRepoRelativeLinks(content) {
38
38
  (_, idx) => codeBlocks[Number(idx)] ?? ""
39
39
  );
40
40
  }
41
+ function rewriteMarkdownLinks(content, rewrite) {
42
+ return transformMarkdownLinks(content, (match, _text, url) => {
43
+ const replacement = rewrite(url);
44
+ if (replacement === void 0) return match;
45
+ const urlStart = match.length - url.length - 1;
46
+ if (urlStart < 0 || match.slice(urlStart, urlStart + url.length) !== url) {
47
+ return match;
48
+ }
49
+ return `${match.slice(0, urlStart)}${replacement}${match.slice(urlStart + url.length)}`;
50
+ });
51
+ }
52
+ function downgradeRepoRelativeLinks(content, keep) {
53
+ return transformMarkdownLinks(
54
+ content,
55
+ (match, text, url) => isRepoRelativeLink(url) && !(keep?.(url) ?? false) ? `\`${text}\`` : match
56
+ );
57
+ }
41
58
  export {
42
59
  downgradeRepoRelativeLinks,
43
- isRepoRelativeLink
60
+ isRepoRelativeLink,
61
+ rewriteMarkdownLinks
44
62
  };
@@ -1,6 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { escapeForMdx } from "./escape-for-mdx.js";
4
+ import {
5
+ downgradeRepoRelativeLinks,
6
+ rewriteMarkdownLinks
7
+ } from "./links.js";
4
8
  import {
5
9
  assertNotIndexReserved,
6
10
  escapeTitle,
@@ -236,8 +240,26 @@ ${tree}${linkList}`;
236
240
  );
237
241
  }
238
242
  }
239
- let skillBody = parsed.content.trim();
240
- skillBody = skillBody.replace(/\]\(references\/([^)]+)\.md\)/g, "](./ref-$1)").replace(/\]\(scripts\/([^)]+)\.md\)/g, "](./script-$1)").replace(/\]\(assets\/([^)]+)\.md\)/g, "](./asset-$1)");
243
+ const skillLinkRewrites = /* @__PURE__ */ new Map();
244
+ for (const ref of references) {
245
+ skillLinkRewrites.set(`references/${ref.name}.md`, `./ref-${ref.name}`);
246
+ }
247
+ for (const f of scriptFiles.filter((s) => s.endsWith(".md"))) {
248
+ const slug = f.replace(/\.md$/, "");
249
+ skillLinkRewrites.set(`scripts/${f}`, `./script-${slug}`);
250
+ }
251
+ for (const f of assetFiles.filter((a) => a.endsWith(".md"))) {
252
+ const slug = f.replace(/\.md$/, "");
253
+ skillLinkRewrites.set(`assets/${f}`, `./asset-${slug}`);
254
+ }
255
+ const emittedSkillLinks = new Set(skillLinkRewrites.values());
256
+ const skillBody = downgradeRepoRelativeLinks(
257
+ rewriteMarkdownLinks(
258
+ parsed.content.trim(),
259
+ (url) => skillLinkRewrites.get(url)
260
+ ),
261
+ (url) => emittedSkillLinks.has(url)
262
+ );
241
263
  const body = [
242
264
  extraHeader,
243
265
  fileStructureSection,
@@ -259,7 +281,7 @@ ${body}`;
259
281
  writeUnlistedSubPage(
260
282
  path.join(skillDirOut, `ref-${ref.name}.mdx`),
261
283
  ref.title,
262
- escapeForMdx(ref.content.trim()),
284
+ escapeForMdx(downgradeRepoRelativeLinks(ref.content.trim())),
263
285
  renderFrontmatterString
264
286
  );
265
287
  }
@@ -280,7 +302,7 @@ ${body}`;
280
302
  writeUnlistedSubPage(
281
303
  path.join(skillDirOut, `script-${slug}.mdx`),
282
304
  title,
283
- escapeForMdx(raw.trim()),
305
+ escapeForMdx(downgradeRepoRelativeLinks(raw.trim())),
284
306
  renderFrontmatterString
285
307
  );
286
308
  }
@@ -301,7 +323,7 @@ ${body}`;
301
323
  writeUnlistedSubPage(
302
324
  path.join(skillDirOut, `asset-${slug}.mdx`),
303
325
  title,
304
- escapeForMdx(raw.trim()),
326
+ escapeForMdx(downgradeRepoRelativeLinks(raw.trim())),
305
327
  renderFrontmatterString
306
328
  );
307
329
  }
@@ -1,3 +1,4 @@
1
+ import { prepareHomeIntros } from "../home-intro/prepare.js";
1
2
  import { createRequire } from "node:module";
2
3
  import { existsSync, statSync, readFileSync, cpSync, rmSync, mkdirSync } from "node:fs";
3
4
  import { dirname, basename, join } from "node:path";
@@ -242,6 +243,7 @@ const plugin = definePlugin({
242
243
  async () => {
243
244
  beginLoader("context");
244
245
  if (assetViewer && !highlightCodeReady) await assetBodiesLoader(false);
246
+ const homeIntros = await prepareHomeIntros(settings);
245
247
  const assetManifest = assetViewer ? (await getSnapshot()).manifest : null;
246
248
  return `export const routeContext = ${JSON.stringify({
247
249
  settings,
@@ -249,7 +251,8 @@ const plugin = definePlugin({
249
251
  tagVocabulary,
250
252
  colorSchemes,
251
253
  themePackRegistry,
252
- assetManifest
254
+ assetManifest,
255
+ homeIntros
253
256
  })};
254
257
  `;
255
258
  },
package/dist/preset.d.ts CHANGED
@@ -34,11 +34,12 @@
34
34
  */
35
35
  import { z } from "zod";
36
36
  import type { ColorScheme } from "./color-scheme-utils.js";
37
- import type { AssetViewerIndexingConfig, TagVocabularyEntry, FaviconConfig } from "./settings.js";
37
+ import type { AssetViewerIndexingConfig, TagVocabularyEntry, FaviconConfig, HomeConfig } from "./settings.js";
38
38
  import type { DirectiveSpec } from "@takazudo/zfb/config";
39
39
  /** A single locale's content directory (`settings.locales[code]`). */
40
40
  export interface PresetLocaleConfig {
41
41
  dir: string;
42
+ introMarkdown?: string;
42
43
  }
43
44
  /** A single docs version (`settings.versions[n]`). */
44
45
  export interface PresetVersionConfig {
@@ -94,6 +95,8 @@ export interface PresetSettings {
94
95
  base: string;
95
96
  siteName: string;
96
97
  siteDescription: string;
98
+ /** Serializable home prose requires the preparation virtual module even with host routes. */
99
+ home?: HomeConfig;
97
100
  /**
98
101
  * Home-hero logo. `zudoDocPreset()` doesn't otherwise consume this field —
99
102
  * rendering happens in `home-page/index.tsx` against the full `Settings`
@@ -116,7 +119,11 @@ export interface PresetSettings {
116
119
  onBrokenMarkdownLinks: "warn" | "error" | "ignore";
117
120
  llmsTxt?: boolean;
118
121
  changelogs?: PresetChangelogConfig[] | false;
122
+ /** Metadata fields shown in the doc metadata area. */
123
+ docMetainfoFields?: Array<"created" | "updated" | "author">;
119
124
  docHistory?: boolean;
125
+ /** Whether the doc history dropdown UI and related artifacts are enabled. */
126
+ docHistoryUi?: boolean;
120
127
  docHistoryExclude?: string[];
121
128
  /** Generate package-owned viewer pages for files under the configured asset directory. */
122
129
  assetViewer?: boolean;
@@ -311,3 +318,4 @@ export interface ZudoDocPresetResult {
311
318
  * ```
312
319
  */
313
320
  export declare function zudoDocPreset({ settings, buildDocsSchema, directiveVocabulary, translations, tagVocabulary, colorSchemes, }: ZudoDocPresetArgs): ZudoDocPresetResult;
321
+ export declare function buildMarkdownFeatures(settings: Pick<PresetSettings, "mermaid" | "transclude">, directiveVocabulary: DirectiveVocabulary): Record<string, boolean | Record<string, unknown>>;
package/dist/preset.js CHANGED
@@ -129,6 +129,7 @@ function buildPlugins(settings, routeContext) {
129
129
  Object.entries(settings.locales).map(([code, locale]) => [code, { dir: locale.dir }])
130
130
  );
131
131
  const effectivePackageOwnedRoutes = settings.packageOwnedRoutes ?? true;
132
+ const homeIntro = Boolean(settings.home?.introMarkdown?.trim()) || Object.values(settings.locales).some((locale) => Boolean(locale.introMarkdown?.trim()));
132
133
  const assetViewer = settings.assetViewer === true;
133
134
  const assetViewerDir = settings.assetViewerDir ?? "assets";
134
135
  const assetViewerRoutePrefix = settings.assetViewerRoutePrefix ?? "files";
@@ -177,7 +178,7 @@ function buildPlugins(settings, routeContext) {
177
178
  // the route catalog from `settings.locales` / `settings.versions`. Listed
178
179
  // FIRST so an injected route is registered before the other plugins'
179
180
  // preBuild work runs (ordering is cosmetic — injection happens in `setup`).
180
- ...effectivePackageOwnedRoutes || assetViewer ? [
181
+ ...effectivePackageOwnedRoutes || assetViewer || homeIntro ? [
181
182
  {
182
183
  name: "@takazudo/zudo-doc/plugins/routes",
183
184
  options: {
@@ -240,6 +241,7 @@ function buildPlugins(settings, routeContext) {
240
241
  docsDir: settings.docsDir,
241
242
  locales: localeRecord,
242
243
  base: settings.base,
244
+ ui: settings.docHistoryUi !== false,
243
245
  exclude: settings.docHistoryExclude ?? []
244
246
  }
245
247
  }
@@ -290,9 +292,21 @@ function buildPlugins(settings, routeContext) {
290
292
  changelogs: settings.changelogs.map((changelog) => ({ ...changelog }))
291
293
  }
292
294
  }
293
- ] : []
295
+ ] : [],
296
+ // Raw HTML image sources are checked after the build has emitted every
297
+ // page. Keep this as a bare descriptor so the node-backed scanner never
298
+ // enters the config-evaluation graph; its severity intentionally follows
299
+ // the existing broken-markdown-links setting.
300
+ {
301
+ name: "@takazudo/zudo-doc/plugins/img-src-check",
302
+ options: {
303
+ base: settings.base,
304
+ onBroken: settings.onBrokenMarkdownLinks
305
+ }
306
+ }
294
307
  ];
295
308
  }
296
309
  export {
310
+ buildMarkdownFeatures,
297
311
  zudoDocPreset
298
312
  };
@@ -108,6 +108,7 @@ function createRouteContext(payload, options = {}) {
108
108
  colorSchemes,
109
109
  themePackRegistry,
110
110
  assetManifest,
111
+ homeIntros: payload.homeIntros ?? {},
111
112
  i18n,
112
113
  defaultLocale,
113
114
  locales,
@@ -55,6 +55,8 @@ export interface CreateRouteContextPayloadInput {
55
55
  themePackRegistry?: ThemePackRegistry | null;
56
56
  /** Author-facing asset index replacement. `null` forces the feature inert. */
57
57
  assetManifest?: AssetManifest | null;
58
+ /** Trusted output of the async server home-intro/prepare helper. */
59
+ homeIntros?: import("../home-intro/types.js").PreparedHomeIntros;
58
60
  }
59
61
  /**
60
62
  * Build the serializable payload consumed by `createRouteContext` from plain
@@ -41,7 +41,8 @@ function createRouteContextPayload(input) {
41
41
  tagVocabulary: input.tagVocabulary ?? [],
42
42
  colorSchemes: input.colorSchemes === void 0 ? defaultColorSchemes : input.colorSchemes,
43
43
  themePackRegistry,
44
- assetManifest: input.assetManifest ?? null
44
+ assetManifest: input.assetManifest ?? null,
45
+ homeIntros: input.homeIntros ?? {}
45
46
  };
46
47
  }
47
48
  export {
@@ -149,4 +149,6 @@ export interface RouteContextPayload<S = Settings> {
149
149
  themePackRegistry?: ThemePackRegistry | null;
150
150
  /** Asset-viewer index payload. Omitted by older hosts; omission is inert. */
151
151
  assetManifest?: AssetManifest | null;
152
+ /** Safe per-locale homepage content prepared by the server plugin. */
153
+ homeIntros?: import("../home-intro/types.js").PreparedHomeIntros;
152
154
  }