@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.
- package/CHANGELOG.md +23 -0
- package/dist/compiled.css +72 -0
- package/dist/config.d.ts +12 -1
- package/dist/config.js +3 -1
- package/dist/content.css +55 -0
- package/dist/doc-history-area/index.d.ts +2 -0
- package/dist/doc-history-area/index.js +5 -4
- package/dist/doc-metainfo-area/index.d.ts +2 -0
- package/dist/doc-metainfo-area/index.js +8 -3
- package/dist/factory-context/index.d.ts +2 -0
- package/dist/home-intro/index.d.ts +10 -0
- package/dist/home-intro/index.js +20 -0
- package/dist/home-intro/prepare.d.ts +9 -0
- package/dist/home-intro/prepare.js +93 -0
- package/dist/home-intro/resolve.d.ts +5 -0
- package/dist/home-intro/resolve.js +10 -0
- package/dist/home-intro/types.d.ts +10 -0
- package/dist/home-intro/types.js +0 -0
- package/dist/home-page/index.js +42 -22
- package/dist/i18n-defaults/index.js +2 -0
- package/dist/plugins/doc-history.js +7 -3
- package/dist/plugins/img-src-check.d.ts +3 -0
- package/dist/plugins/img-src-check.js +21 -0
- package/dist/plugins/internal/claude-resources/generate.js +2 -2
- package/dist/plugins/internal/doc-history/index.d.ts +6 -1
- package/dist/plugins/internal/doc-history/index.js +1 -0
- package/dist/plugins/internal/img-src-check/index.d.ts +65 -0
- package/dist/plugins/internal/img-src-check/index.js +179 -0
- package/dist/plugins/internal/resource-docs-shared/index.d.ts +1 -1
- package/dist/plugins/internal/resource-docs-shared/index.js +6 -1
- package/dist/plugins/internal/resource-docs-shared/links.d.ts +6 -1
- package/dist/plugins/internal/resource-docs-shared/links.js +21 -3
- package/dist/plugins/internal/resource-docs-shared/skills.js +27 -5
- package/dist/plugins/routes.js +4 -1
- package/dist/preset.d.ts +9 -1
- package/dist/preset.js +16 -2
- package/dist/route-context/index.js +1 -0
- package/dist/route-context-payload/index.d.ts +2 -0
- package/dist/route-context-payload/index.js +2 -1
- package/dist/route-context-payload/types.d.ts +2 -0
- package/dist/safelist.css +1 -1
- package/dist/settings.d.ts +12 -0
- package/package.json +15 -2
- package/routes-src/_virtual.d.ts +3 -1
- 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(
|
|
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
|
-
|
|
52
|
+
options,
|
|
49
53
|
ctx.logger
|
|
50
54
|
);
|
|
51
|
-
const basePrefix = getBasePrefix(
|
|
55
|
+
const basePrefix = getBasePrefix(options.base);
|
|
52
56
|
ctx.register(`${basePrefix}/doc-history`, connectToZfbHandler(middleware));
|
|
53
57
|
}
|
|
54
58
|
};
|
|
@@ -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
|
|
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 {
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
240
|
-
|
|
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
|
}
|
package/dist/plugins/routes.js
CHANGED
|
@@ -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
|
};
|
|
@@ -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
|
}
|