okf-catalog 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/CHANGELOG.md +24 -0
- package/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +87 -0
- package/dist/bundle/cat-file.js +94 -0
- package/dist/bundle/cat-file.js.map +1 -0
- package/dist/bundle/contract.js +60 -0
- package/dist/bundle/contract.js.map +1 -0
- package/dist/bundle/frontmatter.js +106 -0
- package/dist/bundle/frontmatter.js.map +1 -0
- package/dist/bundle/git-tree.js +109 -0
- package/dist/bundle/git-tree.js.map +1 -0
- package/dist/bundle/index-file.js +90 -0
- package/dist/bundle/index-file.js.map +1 -0
- package/dist/bundle/links.js +57 -0
- package/dist/bundle/links.js.map +1 -0
- package/dist/bundle/load.js +363 -0
- package/dist/bundle/load.js.map +1 -0
- package/dist/bundle/manifest.js +95 -0
- package/dist/bundle/manifest.js.map +1 -0
- package/dist/bundle/markdown.js +210 -0
- package/dist/bundle/markdown.js.map +1 -0
- package/dist/bundle/model.js +7 -0
- package/dist/bundle/model.js.map +1 -0
- package/dist/bundle/page.js +374 -0
- package/dist/bundle/page.js.map +1 -0
- package/dist/bundle/paths.js +36 -0
- package/dist/bundle/paths.js.map +1 -0
- package/dist/bundle/reserved.js +56 -0
- package/dist/bundle/reserved.js.map +1 -0
- package/dist/bundle/timestamp.js +49 -0
- package/dist/bundle/timestamp.js.map +1 -0
- package/dist/catalog/model.js +32 -0
- package/dist/catalog/model.js.map +1 -0
- package/dist/catalog/nearest.js +56 -0
- package/dist/catalog/nearest.js.map +1 -0
- package/dist/catalog/outputs.js +438 -0
- package/dist/catalog/outputs.js.map +1 -0
- package/dist/catalog/provenance.js +37 -0
- package/dist/catalog/provenance.js.map +1 -0
- package/dist/catalog/runtime.js +2 -0
- package/dist/catalog/runtime.js.map +1 -0
- package/dist/catalog/text.js +140 -0
- package/dist/catalog/text.js.map +1 -0
- package/dist/cli.js +81 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/check.js +155 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/pack.js +182 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/serve.js +452 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/config/company-config.js +272 -0
- package/dist/config/company-config.js.map +1 -0
- package/dist/derive/derived-document.js +42 -0
- package/dist/derive/derived-document.js.map +1 -0
- package/dist/engine/qmd-render.js +54 -0
- package/dist/engine/qmd-render.js.map +1 -0
- package/dist/engine/qmd.js +241 -0
- package/dist/engine/qmd.js.map +1 -0
- package/dist/fs/cache-dir.js +140 -0
- package/dist/fs/cache-dir.js.map +1 -0
- package/dist/fs/company-lock.js +162 -0
- package/dist/fs/company-lock.js.map +1 -0
- package/dist/fs/walk.js +168 -0
- package/dist/fs/walk.js.map +1 -0
- package/dist/log.js +65 -0
- package/dist/log.js.map +1 -0
- package/dist/mcp/server.js +30 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.js +45 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/tools.js +249 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/report/report.js +43 -0
- package/dist/report/report.js.map +1 -0
- package/dist/search/engine.js +2 -0
- package/dist/search/engine.js.map +1 -0
- package/dist/search/query.js +91 -0
- package/dist/search/query.js.map +1 -0
- package/dist/search/search.js +224 -0
- package/dist/search/search.js.map +1 -0
- package/dist/search/snippet.js +95 -0
- package/dist/search/snippet.js.map +1 -0
- package/dist/serve/poller.js +119 -0
- package/dist/serve/poller.js.map +1 -0
- package/dist/serve/runtime.js +390 -0
- package/dist/serve/runtime.js.map +1 -0
- package/dist/source/git-runner.js +232 -0
- package/dist/source/git-runner.js.map +1 -0
- package/dist/source/git.js +457 -0
- package/dist/source/git.js.map +1 -0
- package/dist/source/local.js +20 -0
- package/dist/source/local.js.map +1 -0
- package/dist/source/source.js +2 -0
- package/dist/source/source.js.map +1 -0
- package/npm-shrinkwrap.json +5482 -0
- package/package.json +80 -0
- package/plugin/claude-code/.claude-plugin/plugin.json +15 -0
- package/plugin/claude-code/.mcp.json +10 -0
- package/plugin/claude-code/skills/okf-catalog/SKILL.md +15 -0
- package/recipes/publish/README.md +32 -0
- package/recipes/publish/checkers.lock +435 -0
- package/recipes/publish/checkers.txt +5 -0
- package/recipes/publish/pack.sh +60 -0
- package/recipes/publish/publish.yml +88 -0
- package/recipes/publish/push.sh +63 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import { z } from "zod/v4";
|
|
5
|
+
import { DEFAULT_CAPS } from "../bundle/model.js";
|
|
6
|
+
import { isSafeRelativePath } from "../bundle/paths.js";
|
|
7
|
+
import { safe } from "../catalog/text.js";
|
|
8
|
+
const COMPANY = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
9
|
+
/** The scp-like repository form: a user, a host, a colon and a path that is not an option. */
|
|
10
|
+
const SCP_LIKE = /^[A-Za-z0-9._][A-Za-z0-9._-]*@[A-Za-z0-9.-]+:[^-\s/][^\s]*$/;
|
|
11
|
+
/** A branch name as one plain ref segment or a few: letters, digits, `.`, `_`, `-` and `/`, nothing a refspec or an option could misread. */
|
|
12
|
+
const BRANCH = /^[A-Za-z0-9_][A-Za-z0-9._/-]*$/;
|
|
13
|
+
/** Why a repository string is not one the server will fetch (D50), or undefined when it is. */
|
|
14
|
+
export function repositoryProblem(value, options = {}) {
|
|
15
|
+
if (SCP_LIKE.test(value))
|
|
16
|
+
return undefined;
|
|
17
|
+
let url;
|
|
18
|
+
try {
|
|
19
|
+
url = new URL(value);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return "must be an https:// or ssh:// URL, or user@host:path";
|
|
23
|
+
}
|
|
24
|
+
if (url.protocol === "file:" && options.allowFileRepositories === true)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (url.protocol !== "https:" && url.protocol !== "ssh:")
|
|
27
|
+
return "must be an https:// or ssh:// URL, or user@host:path (a local path or another scheme is not fetched)";
|
|
28
|
+
if (url.hostname.length === 0)
|
|
29
|
+
return "must name a host";
|
|
30
|
+
if (url.password.length > 0)
|
|
31
|
+
return "must not carry a password; use a credential helper";
|
|
32
|
+
if (url.search.length > 0 || url.hash.length > 0)
|
|
33
|
+
return "must not carry a query or a fragment (a token in a URL would reach the log)";
|
|
34
|
+
if (url.protocol === "https:" && url.username.length > 0)
|
|
35
|
+
return "must not carry a user name over https; use a credential helper";
|
|
36
|
+
if (url.username.startsWith("-"))
|
|
37
|
+
return "must not carry a user name that looks like an option";
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
/** Why a branch name is refused (D50), or undefined when it is accepted. */
|
|
41
|
+
export function branchProblem(value) {
|
|
42
|
+
if (!BRANCH.test(value) || value.includes("..") || value.includes("//") || value.includes("@{"))
|
|
43
|
+
return "must be a plain branch name of letters, digits, '.', '_', '-' and '/'";
|
|
44
|
+
if (value.endsWith("/") || value.endsWith(".") || value.includes("/."))
|
|
45
|
+
return "must not end in '/' or '.', and no segment may start with '.'";
|
|
46
|
+
if (value === "HEAD" || value.split("/").some((segment) => segment.endsWith(".lock")))
|
|
47
|
+
return "must not be HEAD, and no segment may end in '.lock'";
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
/** Why a bundle path is refused (D50), or undefined: `.` or a safe relative path with no dot-leading segment. */
|
|
51
|
+
export function bundlePathProblem(value) {
|
|
52
|
+
if (value === ".")
|
|
53
|
+
return undefined;
|
|
54
|
+
if (!isSafeRelativePath(value) || value.endsWith("/"))
|
|
55
|
+
return "must be '.' or a relative path with no empty, '.', '..' or backslash segments";
|
|
56
|
+
if (value.split("/").some((segment) => segment.startsWith(".")))
|
|
57
|
+
return "must not name a hidden folder (a segment starting with '.')";
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const DURATION = /^(\d+)(s|m|h)$/;
|
|
61
|
+
const MIN_INTERVAL_MS = 30_000;
|
|
62
|
+
const MAX_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
63
|
+
const CAP_CEILINGS = {
|
|
64
|
+
file_bytes: 64 * 1024 * 1024,
|
|
65
|
+
files: 200_000,
|
|
66
|
+
tree_bytes: 8 * 1024 * 1024 * 1024,
|
|
67
|
+
};
|
|
68
|
+
const Raw = z.strictObject({
|
|
69
|
+
company: z.string(),
|
|
70
|
+
source: z.strictObject({
|
|
71
|
+
local: z.string().min(1).optional(),
|
|
72
|
+
repository: z.string().min(1).optional(),
|
|
73
|
+
branch: z.string().min(1).optional(),
|
|
74
|
+
bundle_path: z.string().min(1).optional(),
|
|
75
|
+
}),
|
|
76
|
+
serve: z
|
|
77
|
+
.strictObject({
|
|
78
|
+
admit: z.array(z.string()).optional(),
|
|
79
|
+
dev: z.boolean().optional(),
|
|
80
|
+
pull_interval: z.string().optional(),
|
|
81
|
+
limit_default: z.number().int().min(1).max(25).optional(),
|
|
82
|
+
})
|
|
83
|
+
.optional(),
|
|
84
|
+
caps: z
|
|
85
|
+
.strictObject({
|
|
86
|
+
file_bytes: z.number().int().min(1).max(CAP_CEILINGS.file_bytes).optional(),
|
|
87
|
+
files: z.number().int().min(1).max(CAP_CEILINGS.files).optional(),
|
|
88
|
+
tree_bytes: z.number().int().min(1).max(CAP_CEILINGS.tree_bytes).optional(),
|
|
89
|
+
})
|
|
90
|
+
.optional(),
|
|
91
|
+
types: z.array(z.string().min(1)).optional(),
|
|
92
|
+
spec_text: z.enum(["2026-08-15", "2026-08-21"]).optional(),
|
|
93
|
+
});
|
|
94
|
+
function issueText(issue) {
|
|
95
|
+
const path = issue.path.map(String).join(".");
|
|
96
|
+
if (issue.code === "unrecognized_keys") {
|
|
97
|
+
return issue.keys.map((k) => `${path === "" ? k : `${path}.${k}`}: unknown key`).join("; ");
|
|
98
|
+
}
|
|
99
|
+
return `${path === "" ? "document" : path}: ${issue.message}`;
|
|
100
|
+
}
|
|
101
|
+
/** `~/x` against the home folder, an absolute path as is, anything else against the configuration file's folder. */
|
|
102
|
+
function resolvePath(value, configDir, home) {
|
|
103
|
+
if (value === "~")
|
|
104
|
+
return home;
|
|
105
|
+
if (value.startsWith("~/"))
|
|
106
|
+
return join(home, value.slice(2));
|
|
107
|
+
if (isAbsolute(value))
|
|
108
|
+
return value;
|
|
109
|
+
return resolve(configDir, value);
|
|
110
|
+
}
|
|
111
|
+
function durationMs(text) {
|
|
112
|
+
const m = DURATION.exec(text);
|
|
113
|
+
if (m === null)
|
|
114
|
+
return undefined;
|
|
115
|
+
const n = Number(m[1]);
|
|
116
|
+
const unit = m[2] === "s" ? 1000 : m[2] === "m" ? 60_000 : 3_600_000;
|
|
117
|
+
return n * unit;
|
|
118
|
+
}
|
|
119
|
+
/** Parses one company's YAML configuration. Every problem is a sentence naming the key; nothing is guessed. */
|
|
120
|
+
export function parseCompanyConfig(text, configDir, home, parseOptions = {}) {
|
|
121
|
+
let document;
|
|
122
|
+
try {
|
|
123
|
+
document = parseYaml(text, { version: "1.2" });
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
// The parser's message carries a code frame of the file on later lines: the first line names the problem.
|
|
127
|
+
const first = error.message.split("\n")[0] ?? "";
|
|
128
|
+
return { ok: false, problems: [`the configuration is not valid YAML: ${safe(first)}`] };
|
|
129
|
+
}
|
|
130
|
+
if (document === null || typeof document !== "object" || Array.isArray(document)) {
|
|
131
|
+
return { ok: false, problems: ["the configuration must be a mapping of keys to values"] };
|
|
132
|
+
}
|
|
133
|
+
const parsed = Raw.safeParse(document);
|
|
134
|
+
if (!parsed.success)
|
|
135
|
+
return { ok: false, problems: parsed.error.issues.map(issueText) };
|
|
136
|
+
const raw = parsed.data;
|
|
137
|
+
const problems = [];
|
|
138
|
+
if (!COMPANY.test(raw.company)) {
|
|
139
|
+
problems.push("company: must be one lower-case path segment (letters, digits and hyphens, starting with a letter or digit, at most 63 characters)");
|
|
140
|
+
}
|
|
141
|
+
const hasLocal = raw.source.local !== undefined;
|
|
142
|
+
const hasGit = raw.source.repository !== undefined;
|
|
143
|
+
if (hasLocal && hasGit)
|
|
144
|
+
problems.push("source: exactly one of local or repository, not both");
|
|
145
|
+
if (!hasLocal && !hasGit)
|
|
146
|
+
problems.push("source: local or repository is required");
|
|
147
|
+
if (hasLocal && (raw.source.branch !== undefined || raw.source.bundle_path !== undefined)) {
|
|
148
|
+
problems.push("source: branch and bundle_path belong to a repository source");
|
|
149
|
+
}
|
|
150
|
+
if (hasGit) {
|
|
151
|
+
const repository = repositoryProblem(raw.source.repository ?? "", parseOptions);
|
|
152
|
+
if (repository !== undefined)
|
|
153
|
+
problems.push(`source.repository: ${repository}`);
|
|
154
|
+
const branch = branchProblem(raw.source.branch ?? "published");
|
|
155
|
+
if (branch !== undefined)
|
|
156
|
+
problems.push(`source.branch: ${branch}`);
|
|
157
|
+
const bundle = bundlePathProblem(raw.source.bundle_path ?? ".");
|
|
158
|
+
if (bundle !== undefined)
|
|
159
|
+
problems.push(`source.bundle_path: ${bundle}`);
|
|
160
|
+
}
|
|
161
|
+
const admit = raw.serve?.admit ?? ["stable", "deprecated"];
|
|
162
|
+
if (admit.length === 0)
|
|
163
|
+
problems.push("serve.admit: at least one status is required");
|
|
164
|
+
for (const status of admit) {
|
|
165
|
+
if (status === "draft")
|
|
166
|
+
problems.push("serve.admit: draft is admitted only through serve.dev");
|
|
167
|
+
else if (status !== "stable" && status !== "deprecated")
|
|
168
|
+
problems.push(`serve.admit: ${JSON.stringify(status)} is not a status`);
|
|
169
|
+
}
|
|
170
|
+
const dev = raw.serve?.dev ?? false;
|
|
171
|
+
if (dev && !hasLocal)
|
|
172
|
+
problems.push("serve.dev: allowed only with source.local");
|
|
173
|
+
let pullIntervalMs = 600_000;
|
|
174
|
+
if (raw.serve?.pull_interval !== undefined) {
|
|
175
|
+
const ms = durationMs(raw.serve.pull_interval);
|
|
176
|
+
if (ms === undefined) {
|
|
177
|
+
problems.push("serve.pull_interval: write a number and a unit, such as 30s, 10m or 2h");
|
|
178
|
+
}
|
|
179
|
+
else if (ms < MIN_INTERVAL_MS || ms > MAX_INTERVAL_MS) {
|
|
180
|
+
problems.push("serve.pull_interval: must be between 30s and 24h");
|
|
181
|
+
}
|
|
182
|
+
else
|
|
183
|
+
pullIntervalMs = ms;
|
|
184
|
+
}
|
|
185
|
+
if (problems.length > 0)
|
|
186
|
+
return { ok: false, problems };
|
|
187
|
+
const caps = {
|
|
188
|
+
fileBytes: raw.caps?.file_bytes ?? DEFAULT_CAPS.fileBytes,
|
|
189
|
+
files: raw.caps?.files ?? DEFAULT_CAPS.files,
|
|
190
|
+
treeBytes: raw.caps?.tree_bytes ?? DEFAULT_CAPS.treeBytes,
|
|
191
|
+
};
|
|
192
|
+
const config = {
|
|
193
|
+
company: raw.company,
|
|
194
|
+
source: raw.source.local !== undefined
|
|
195
|
+
? {
|
|
196
|
+
kind: "local",
|
|
197
|
+
path: resolvePath(raw.source.local, configDir, home),
|
|
198
|
+
configured: raw.source.local,
|
|
199
|
+
}
|
|
200
|
+
: {
|
|
201
|
+
kind: "git",
|
|
202
|
+
repository: raw.source.repository ?? "",
|
|
203
|
+
branch: raw.source.branch ?? "published",
|
|
204
|
+
bundlePath: raw.source.bundle_path ?? ".",
|
|
205
|
+
},
|
|
206
|
+
serve: {
|
|
207
|
+
admit: admit,
|
|
208
|
+
dev,
|
|
209
|
+
pullIntervalMs,
|
|
210
|
+
limitDefault: raw.serve?.limit_default ?? 8,
|
|
211
|
+
},
|
|
212
|
+
integrity: dev ? "none" : "require-manifest",
|
|
213
|
+
caps,
|
|
214
|
+
specText: raw.spec_text ?? "2026-08-15",
|
|
215
|
+
};
|
|
216
|
+
if (raw.types !== undefined && raw.types.length > 0)
|
|
217
|
+
config.types = raw.types;
|
|
218
|
+
return { ok: true, config };
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Where the configuration is: the flag, then `OKF_CATALOG_CONFIG`, then `okf-catalog.yaml` in the working
|
|
222
|
+
* folder. A variable that is set but empty, or that still holds a `${...}` placeholder, is an error and never
|
|
223
|
+
* falls through, so a checked-out repository can never choose the bundle by accident.
|
|
224
|
+
*/
|
|
225
|
+
export function discoverConfigPath(flag, env, cwd, home) {
|
|
226
|
+
// A leading `~/` is expanded as it is inside the file; nothing else is interpreted.
|
|
227
|
+
const expand = (value) => value.startsWith("~/") ? join(home, value.slice(2)) : resolve(cwd, value);
|
|
228
|
+
if (flag !== undefined)
|
|
229
|
+
return { path: expand(flag), rule: "flag" };
|
|
230
|
+
if (Object.hasOwn(env, "OKF_CATALOG_CONFIG")) {
|
|
231
|
+
const value = (env.OKF_CATALOG_CONFIG ?? "").trim();
|
|
232
|
+
if (value.length === 0 || value.includes("${")) {
|
|
233
|
+
return {
|
|
234
|
+
error: "OKF_CATALOG_CONFIG is empty or still holds a placeholder; set it to the configuration file's path, put okf-catalog.yaml in the project folder, or pass --config",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return { path: expand(value), rule: "env" };
|
|
238
|
+
}
|
|
239
|
+
return { path: join(cwd, "okf-catalog.yaml"), rule: "cwd" };
|
|
240
|
+
}
|
|
241
|
+
/** Reads and parses a configuration file; relative paths inside it resolve against the file's folder. */
|
|
242
|
+
/** A configuration is a short file; anything over this is refused unread. */
|
|
243
|
+
export const CONFIG_SIZE_CAP = 1024 * 1024;
|
|
244
|
+
export function readCompanyConfig(path, home, parseOptions = {}) {
|
|
245
|
+
let text;
|
|
246
|
+
try {
|
|
247
|
+
const stat = statSync(path);
|
|
248
|
+
if (!stat.isFile()) {
|
|
249
|
+
return { ok: false, problems: [`the configuration path ${path} is not a regular file`] };
|
|
250
|
+
}
|
|
251
|
+
if (stat.size > CONFIG_SIZE_CAP) {
|
|
252
|
+
return {
|
|
253
|
+
ok: false,
|
|
254
|
+
problems: [
|
|
255
|
+
`the configuration file ${path} is too large (${stat.size} bytes, over ${CONFIG_SIZE_CAP})`,
|
|
256
|
+
],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
text = readFileSync(path, "utf8");
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
const code = error.code;
|
|
263
|
+
const why = code === "ENOENT"
|
|
264
|
+
? "does not exist"
|
|
265
|
+
: code === "EISDIR"
|
|
266
|
+
? "is a folder, not a file"
|
|
267
|
+
: `cannot be read (${code ?? "error"})`;
|
|
268
|
+
return { ok: false, problems: [`the configuration file ${path} ${why}`] };
|
|
269
|
+
}
|
|
270
|
+
return parseCompanyConfig(text, dirname(path), home, parseOptions);
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=company-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"company-config.js","sourceRoot":"","sources":["../../src/config/company-config.ts"],"names":[],"mappings":"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","sourcesContent":["import { readFileSync, statSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport { z } from \"zod/v4\";\nimport { type Caps, DEFAULT_CAPS, type SpecText, type Status } from \"../bundle/model.js\";\nimport { isSafeRelativePath } from \"../bundle/paths.js\";\nimport { safe } from \"../catalog/text.js\";\n\n/** Configuration as the server uses it: paths resolved, defaults applied, `dev` mapped onto drafts and integrity. */\nexport interface CompanyConfig {\n company: string;\n source:\n | { kind: \"local\"; path: string; configured: string }\n | { kind: \"git\"; repository: string; branch: string; bundlePath: string };\n serve: { admit: Status[]; dev: boolean; pullIntervalMs: number; limitDefault: number };\n integrity: \"require-manifest\" | \"none\";\n caps: Caps;\n types?: string[];\n specText: SpecText;\n}\n\nexport type ConfigResult = { ok: true; config: CompanyConfig } | { ok: false; problems: string[] };\n\nconst COMPANY = /^[a-z0-9][a-z0-9-]{0,62}$/;\n/** The scp-like repository form: a user, a host, a colon and a path that is not an option. */\nconst SCP_LIKE = /^[A-Za-z0-9._][A-Za-z0-9._-]*@[A-Za-z0-9.-]+:[^-\\s/][^\\s]*$/;\n/** A branch name as one plain ref segment or a few: letters, digits, `.`, `_`, `-` and `/`, nothing a refspec or an option could misread. */\nconst BRANCH = /^[A-Za-z0-9_][A-Za-z0-9._/-]*$/;\n\n/** What a reader of the configuration may relax; only the suite does. */\nexport interface ParseOptions {\n /** Accept `file://` repositories (a test-only setting, paired with the `file` transport protocol). */\n allowFileRepositories?: boolean;\n}\n\n/** Why a repository string is not one the server will fetch (D50), or undefined when it is. */\nexport function repositoryProblem(value: string, options: ParseOptions = {}): string | undefined {\n if (SCP_LIKE.test(value)) return undefined;\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return \"must be an https:// or ssh:// URL, or user@host:path\";\n }\n if (url.protocol === \"file:\" && options.allowFileRepositories === true) return undefined;\n if (url.protocol !== \"https:\" && url.protocol !== \"ssh:\")\n return \"must be an https:// or ssh:// URL, or user@host:path (a local path or another scheme is not fetched)\";\n if (url.hostname.length === 0) return \"must name a host\";\n if (url.password.length > 0) return \"must not carry a password; use a credential helper\";\n if (url.search.length > 0 || url.hash.length > 0)\n return \"must not carry a query or a fragment (a token in a URL would reach the log)\";\n if (url.protocol === \"https:\" && url.username.length > 0)\n return \"must not carry a user name over https; use a credential helper\";\n if (url.username.startsWith(\"-\")) return \"must not carry a user name that looks like an option\";\n return undefined;\n}\n\n/** Why a branch name is refused (D50), or undefined when it is accepted. */\nexport function branchProblem(value: string): string | undefined {\n if (!BRANCH.test(value) || value.includes(\"..\") || value.includes(\"//\") || value.includes(\"@{\"))\n return \"must be a plain branch name of letters, digits, '.', '_', '-' and '/'\";\n if (value.endsWith(\"/\") || value.endsWith(\".\") || value.includes(\"/.\"))\n return \"must not end in '/' or '.', and no segment may start with '.'\";\n if (value === \"HEAD\" || value.split(\"/\").some((segment) => segment.endsWith(\".lock\")))\n return \"must not be HEAD, and no segment may end in '.lock'\";\n return undefined;\n}\n\n/** Why a bundle path is refused (D50), or undefined: `.` or a safe relative path with no dot-leading segment. */\nexport function bundlePathProblem(value: string): string | undefined {\n if (value === \".\") return undefined;\n if (!isSafeRelativePath(value) || value.endsWith(\"/\"))\n return \"must be '.' or a relative path with no empty, '.', '..' or backslash segments\";\n if (value.split(\"/\").some((segment) => segment.startsWith(\".\")))\n return \"must not name a hidden folder (a segment starting with '.')\";\n return undefined;\n}\nconst DURATION = /^(\\d+)(s|m|h)$/;\nconst MIN_INTERVAL_MS = 30_000;\nconst MAX_INTERVAL_MS = 24 * 60 * 60 * 1000;\nconst CAP_CEILINGS = {\n file_bytes: 64 * 1024 * 1024,\n files: 200_000,\n tree_bytes: 8 * 1024 * 1024 * 1024,\n};\n\nconst Raw = z.strictObject({\n company: z.string(),\n source: z.strictObject({\n local: z.string().min(1).optional(),\n repository: z.string().min(1).optional(),\n branch: z.string().min(1).optional(),\n bundle_path: z.string().min(1).optional(),\n }),\n serve: z\n .strictObject({\n admit: z.array(z.string()).optional(),\n dev: z.boolean().optional(),\n pull_interval: z.string().optional(),\n limit_default: z.number().int().min(1).max(25).optional(),\n })\n .optional(),\n caps: z\n .strictObject({\n file_bytes: z.number().int().min(1).max(CAP_CEILINGS.file_bytes).optional(),\n files: z.number().int().min(1).max(CAP_CEILINGS.files).optional(),\n tree_bytes: z.number().int().min(1).max(CAP_CEILINGS.tree_bytes).optional(),\n })\n .optional(),\n types: z.array(z.string().min(1)).optional(),\n spec_text: z.enum([\"2026-08-15\", \"2026-08-21\"]).optional(),\n});\n\nfunction issueText(issue: z.core.$ZodIssue): string {\n const path = issue.path.map(String).join(\".\");\n if (issue.code === \"unrecognized_keys\") {\n return issue.keys.map((k) => `${path === \"\" ? k : `${path}.${k}`}: unknown key`).join(\"; \");\n }\n return `${path === \"\" ? \"document\" : path}: ${issue.message}`;\n}\n\n/** `~/x` against the home folder, an absolute path as is, anything else against the configuration file's folder. */\nfunction resolvePath(value: string, configDir: string, home: string): string {\n if (value === \"~\") return home;\n if (value.startsWith(\"~/\")) return join(home, value.slice(2));\n if (isAbsolute(value)) return value;\n return resolve(configDir, value);\n}\n\nfunction durationMs(text: string): number | undefined {\n const m = DURATION.exec(text);\n if (m === null) return undefined;\n const n = Number(m[1]);\n const unit = m[2] === \"s\" ? 1000 : m[2] === \"m\" ? 60_000 : 3_600_000;\n return n * unit;\n}\n\n/** Parses one company's YAML configuration. Every problem is a sentence naming the key; nothing is guessed. */\nexport function parseCompanyConfig(\n text: string,\n configDir: string,\n home: string,\n parseOptions: ParseOptions = {},\n): ConfigResult {\n let document: unknown;\n try {\n document = parseYaml(text, { version: \"1.2\" });\n } catch (error) {\n // The parser's message carries a code frame of the file on later lines: the first line names the problem.\n const first = (error as Error).message.split(\"\\n\")[0] ?? \"\";\n return { ok: false, problems: [`the configuration is not valid YAML: ${safe(first)}`] };\n }\n if (document === null || typeof document !== \"object\" || Array.isArray(document)) {\n return { ok: false, problems: [\"the configuration must be a mapping of keys to values\"] };\n }\n const parsed = Raw.safeParse(document);\n if (!parsed.success) return { ok: false, problems: parsed.error.issues.map(issueText) };\n const raw = parsed.data;\n const problems: string[] = [];\n if (!COMPANY.test(raw.company)) {\n problems.push(\n \"company: must be one lower-case path segment (letters, digits and hyphens, starting with a letter or digit, at most 63 characters)\",\n );\n }\n const hasLocal = raw.source.local !== undefined;\n const hasGit = raw.source.repository !== undefined;\n if (hasLocal && hasGit) problems.push(\"source: exactly one of local or repository, not both\");\n if (!hasLocal && !hasGit) problems.push(\"source: local or repository is required\");\n if (hasLocal && (raw.source.branch !== undefined || raw.source.bundle_path !== undefined)) {\n problems.push(\"source: branch and bundle_path belong to a repository source\");\n }\n if (hasGit) {\n const repository = repositoryProblem(raw.source.repository ?? \"\", parseOptions);\n if (repository !== undefined) problems.push(`source.repository: ${repository}`);\n const branch = branchProblem(raw.source.branch ?? \"published\");\n if (branch !== undefined) problems.push(`source.branch: ${branch}`);\n const bundle = bundlePathProblem(raw.source.bundle_path ?? \".\");\n if (bundle !== undefined) problems.push(`source.bundle_path: ${bundle}`);\n }\n const admit = raw.serve?.admit ?? [\"stable\", \"deprecated\"];\n if (admit.length === 0) problems.push(\"serve.admit: at least one status is required\");\n for (const status of admit) {\n if (status === \"draft\") problems.push(\"serve.admit: draft is admitted only through serve.dev\");\n else if (status !== \"stable\" && status !== \"deprecated\")\n problems.push(`serve.admit: ${JSON.stringify(status)} is not a status`);\n }\n const dev = raw.serve?.dev ?? false;\n if (dev && !hasLocal) problems.push(\"serve.dev: allowed only with source.local\");\n let pullIntervalMs = 600_000;\n if (raw.serve?.pull_interval !== undefined) {\n const ms = durationMs(raw.serve.pull_interval);\n if (ms === undefined) {\n problems.push(\"serve.pull_interval: write a number and a unit, such as 30s, 10m or 2h\");\n } else if (ms < MIN_INTERVAL_MS || ms > MAX_INTERVAL_MS) {\n problems.push(\"serve.pull_interval: must be between 30s and 24h\");\n } else pullIntervalMs = ms;\n }\n if (problems.length > 0) return { ok: false, problems };\n const caps: Caps = {\n fileBytes: raw.caps?.file_bytes ?? DEFAULT_CAPS.fileBytes,\n files: raw.caps?.files ?? DEFAULT_CAPS.files,\n treeBytes: raw.caps?.tree_bytes ?? DEFAULT_CAPS.treeBytes,\n };\n const config: CompanyConfig = {\n company: raw.company,\n source:\n raw.source.local !== undefined\n ? {\n kind: \"local\",\n path: resolvePath(raw.source.local, configDir, home),\n configured: raw.source.local,\n }\n : {\n kind: \"git\",\n repository: raw.source.repository ?? \"\",\n branch: raw.source.branch ?? \"published\",\n bundlePath: raw.source.bundle_path ?? \".\",\n },\n serve: {\n admit: admit as Status[],\n dev,\n pullIntervalMs,\n limitDefault: raw.serve?.limit_default ?? 8,\n },\n integrity: dev ? \"none\" : \"require-manifest\",\n caps,\n specText: raw.spec_text ?? \"2026-08-15\",\n };\n if (raw.types !== undefined && raw.types.length > 0) config.types = raw.types;\n return { ok: true, config };\n}\n\nexport type Discovery = { path: string; rule: \"flag\" | \"env\" | \"cwd\" } | { error: string };\n\n/**\n * Where the configuration is: the flag, then `OKF_CATALOG_CONFIG`, then `okf-catalog.yaml` in the working\n * folder. A variable that is set but empty, or that still holds a `${...}` placeholder, is an error and never\n * falls through, so a checked-out repository can never choose the bundle by accident.\n */\nexport function discoverConfigPath(\n flag: string | undefined,\n env: NodeJS.ProcessEnv,\n cwd: string,\n home: string,\n): Discovery {\n // A leading `~/` is expanded as it is inside the file; nothing else is interpreted.\n const expand = (value: string): string =>\n value.startsWith(\"~/\") ? join(home, value.slice(2)) : resolve(cwd, value);\n if (flag !== undefined) return { path: expand(flag), rule: \"flag\" };\n if (Object.hasOwn(env, \"OKF_CATALOG_CONFIG\")) {\n const value = (env.OKF_CATALOG_CONFIG ?? \"\").trim();\n if (value.length === 0 || value.includes(\"${\")) {\n return {\n error:\n \"OKF_CATALOG_CONFIG is empty or still holds a placeholder; set it to the configuration file's path, put okf-catalog.yaml in the project folder, or pass --config\",\n };\n }\n return { path: expand(value), rule: \"env\" };\n }\n return { path: join(cwd, \"okf-catalog.yaml\"), rule: \"cwd\" };\n}\n\n/** Reads and parses a configuration file; relative paths inside it resolve against the file's folder. */\n/** A configuration is a short file; anything over this is refused unread. */\nexport const CONFIG_SIZE_CAP = 1024 * 1024;\n\nexport function readCompanyConfig(\n path: string,\n home: string,\n parseOptions: ParseOptions = {},\n): ConfigResult {\n let text: string;\n try {\n const stat = statSync(path);\n if (!stat.isFile()) {\n return { ok: false, problems: [`the configuration path ${path} is not a regular file`] };\n }\n if (stat.size > CONFIG_SIZE_CAP) {\n return {\n ok: false,\n problems: [\n `the configuration file ${path} is too large (${stat.size} bytes, over ${CONFIG_SIZE_CAP})`,\n ],\n };\n }\n text = readFileSync(path, \"utf8\");\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n const why =\n code === \"ENOENT\"\n ? \"does not exist\"\n : code === \"EISDIR\"\n ? \"is a folder, not a file\"\n : `cannot be read (${code ?? \"error\"})`;\n return { ok: false, problems: [`the configuration file ${path} ${why}`] };\n }\n return parseCompanyConfig(text, dirname(path), home, parseOptions);\n}\n"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const normalise = (s) => s.replace(/\s+/g, " ").trim().toLowerCase();
|
|
2
|
+
const ATX_HEADING = /^(?:[ \t]*\n)* {0,3}#{1,6}[ \t]+([^\n]*?)(?:[ \t]+#+)?[ \t]*\n+/;
|
|
3
|
+
const SETEXT_HEADING = /^(?:[ \t]*\n)* {0,3}([^\n]+?)[ \t]*\n {0,3}=+[ \t]*\n+/;
|
|
4
|
+
/**
|
|
5
|
+
* Removes a leading heading whose text equals the title, so the rendered copy carries the title once. ATX or
|
|
6
|
+
* setext, at most three spaces of indentation (four is a code block), and closing hashes only after a space, as
|
|
7
|
+
* CommonMark reads them: `# C#` under the title C stays.
|
|
8
|
+
*/
|
|
9
|
+
function withoutDuplicateHeading(body, title) {
|
|
10
|
+
for (const pattern of [ATX_HEADING, SETEXT_HEADING]) {
|
|
11
|
+
const match = pattern.exec(body);
|
|
12
|
+
if (match !== null && normalise(match[1] ?? "") === normalise(title)) {
|
|
13
|
+
return body.slice(match[0].length);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return body;
|
|
17
|
+
}
|
|
18
|
+
export function deriveDocument(page) {
|
|
19
|
+
const metadata = {
|
|
20
|
+
okf_type: page.type,
|
|
21
|
+
okf_status: page.status,
|
|
22
|
+
okf_tags: page.tags,
|
|
23
|
+
};
|
|
24
|
+
if (page.staleAfter !== undefined)
|
|
25
|
+
metadata.okf_stale_after = page.staleAfter.raw;
|
|
26
|
+
metadata.okf_trust = page.trust;
|
|
27
|
+
if (page.latestVerification !== undefined)
|
|
28
|
+
metadata.okf_verified_by = page.latestVerification.by;
|
|
29
|
+
metadata.okf_source_count = page.sources.length;
|
|
30
|
+
const doc = {
|
|
31
|
+
path: page.path,
|
|
32
|
+
title: page.title,
|
|
33
|
+
type: page.type,
|
|
34
|
+
tags: page.tags,
|
|
35
|
+
metadata,
|
|
36
|
+
body: withoutDuplicateHeading(page.body, page.title),
|
|
37
|
+
};
|
|
38
|
+
if (page.description !== undefined)
|
|
39
|
+
doc.description = page.description;
|
|
40
|
+
return doc;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=derived-document.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derived-document.js","sourceRoot":"","sources":["../../src/derive/derived-document.ts"],"names":[],"mappings":"AAaA,MAAM,SAAS,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAErF,MAAM,WAAW,GAAG,iEAAiE,CAAC;AACtF,MAAM,cAAc,GAAG,wDAAwD,CAAC;AAEhF;;;;GAIG;AACH,SAAS,uBAAuB,CAAC,IAAY,EAAE,KAAa;IAC1D,KAAK,MAAM,OAAO,IAAI,CAAC,WAAW,EAAE,cAAc,CAAC,EAAE,CAAC;QACpD,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,KAAK,KAAK,IAAI,IAAI,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC;YACrE,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;QACrC,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAU;IACvC,MAAM,QAAQ,GAA+C;QAC3D,QAAQ,EAAE,IAAI,CAAC,IAAI;QACnB,UAAU,EAAE,IAAI,CAAC,MAAM;QACvB,QAAQ,EAAE,IAAI,CAAC,IAAI;KACpB,CAAC;IACF,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;QAAE,QAAQ,CAAC,eAAe,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;IAClF,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC;IAChC,IAAI,IAAI,CAAC,kBAAkB,KAAK,SAAS;QAAE,QAAQ,CAAC,eAAe,GAAG,IAAI,CAAC,kBAAkB,CAAC,EAAE,CAAC;IACjG,QAAQ,CAAC,gBAAgB,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;IAChD,MAAM,GAAG,GAAoB;QAC3B,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,QAAQ;QACR,IAAI,EAAE,uBAAuB,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC;KACrD,CAAC;IACF,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS;QAAE,GAAG,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC;IACvE,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["import type { Page, PagePath } from \"../bundle/model.js\";\n\n/** What the engine indexes for one page: the fields that carry signal, the OKF metadata, and the body. */\nexport interface DerivedDocument {\n path: PagePath;\n title: string;\n description?: string;\n type: string;\n tags: string[];\n metadata: Record<string, string | string[] | number>;\n body: string;\n}\n\nconst normalise = (s: string): string => s.replace(/\\s+/g, \" \").trim().toLowerCase();\n\nconst ATX_HEADING = /^(?:[ \\t]*\\n)* {0,3}#{1,6}[ \\t]+([^\\n]*?)(?:[ \\t]+#+)?[ \\t]*\\n+/;\nconst SETEXT_HEADING = /^(?:[ \\t]*\\n)* {0,3}([^\\n]+?)[ \\t]*\\n {0,3}=+[ \\t]*\\n+/;\n\n/**\n * Removes a leading heading whose text equals the title, so the rendered copy carries the title once. ATX or\n * setext, at most three spaces of indentation (four is a code block), and closing hashes only after a space, as\n * CommonMark reads them: `# C#` under the title C stays.\n */\nfunction withoutDuplicateHeading(body: string, title: string): string {\n for (const pattern of [ATX_HEADING, SETEXT_HEADING]) {\n const match = pattern.exec(body);\n if (match !== null && normalise(match[1] ?? \"\") === normalise(title)) {\n return body.slice(match[0].length);\n }\n }\n return body;\n}\n\nexport function deriveDocument(page: Page): DerivedDocument {\n const metadata: Record<string, string | string[] | number> = {\n okf_type: page.type,\n okf_status: page.status,\n okf_tags: page.tags,\n };\n if (page.staleAfter !== undefined) metadata.okf_stale_after = page.staleAfter.raw;\n metadata.okf_trust = page.trust;\n if (page.latestVerification !== undefined) metadata.okf_verified_by = page.latestVerification.by;\n metadata.okf_source_count = page.sources.length;\n const doc: DerivedDocument = {\n path: page.path,\n title: page.title,\n type: page.type,\n tags: page.tags,\n metadata,\n body: withoutDuplicateHeading(page.body, page.title),\n };\n if (page.description !== undefined) doc.description = page.description;\n return doc;\n}\n"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure half of the qmd adapter: the text qmd indexes for a derived document, and the codec that keeps
|
|
3
|
+
* bundle paths out of the way of qmd's indexer. Imports no qmd, so golden tests and `check` can use it.
|
|
4
|
+
*/
|
|
5
|
+
import { stringify } from "yaml";
|
|
6
|
+
/** Folder names qmd's indexer skips at any depth, exact and case-sensitive. */
|
|
7
|
+
export const SKIPPED_SEGMENTS = new Set([
|
|
8
|
+
"node_modules",
|
|
9
|
+
".git",
|
|
10
|
+
".cache",
|
|
11
|
+
"vendor",
|
|
12
|
+
"dist",
|
|
13
|
+
"build",
|
|
14
|
+
]);
|
|
15
|
+
const collapse = (s) => s.replace(/\s+/g, " ").trim();
|
|
16
|
+
/**
|
|
17
|
+
* `# <title>` first, because qmd takes the first heading as the title and weights it four times the body; then
|
|
18
|
+
* the description, the type value and the tag values on their own lines, with no label words, since a word
|
|
19
|
+
* present in every page would be a universal token; then a blank line and the body.
|
|
20
|
+
*/
|
|
21
|
+
export function renderDocument(doc, options = {}) {
|
|
22
|
+
const lines = [`# ${collapse(doc.title)}`];
|
|
23
|
+
const description = doc.description === undefined ? "" : collapse(doc.description);
|
|
24
|
+
if (description.length > 0)
|
|
25
|
+
lines.push(description);
|
|
26
|
+
const type = collapse(doc.type);
|
|
27
|
+
if (type.length > 0)
|
|
28
|
+
lines.push(type);
|
|
29
|
+
const tags = doc.tags.map(collapse).filter((t) => t.length > 0);
|
|
30
|
+
if (tags.length > 0)
|
|
31
|
+
lines.push(tags.join(" "));
|
|
32
|
+
const body = `${lines.join("\n")}\n\n${doc.body}`;
|
|
33
|
+
if (options.metadataBlock !== true)
|
|
34
|
+
return body;
|
|
35
|
+
return `---\n${stringify({ qmd: { metadata: doc.metadata } })}---\n${body}`;
|
|
36
|
+
}
|
|
37
|
+
/** `%`, `\` and `:` are percent-encoded: qmd splits on the backslash and reads a leading `X:` as a Windows drive. */
|
|
38
|
+
function encodeSegment(segment) {
|
|
39
|
+
const escaped = segment.replace(/%/g, "%25").replace(/\\/g, "%5C").replace(/:/g, "%3A");
|
|
40
|
+
return SKIPPED_SEGMENTS.has(escaped) || escaped.startsWith("_") ? `_${escaped}` : escaped;
|
|
41
|
+
}
|
|
42
|
+
function decodeSegment(segment) {
|
|
43
|
+
const unprefixed = segment.startsWith("_") ? segment.slice(1) : segment;
|
|
44
|
+
return unprefixed.replace(/%3A/g, ":").replace(/%5C/g, "\\").replace(/%25/g, "%");
|
|
45
|
+
}
|
|
46
|
+
/** A bundle path as it is written into the generation tree. */
|
|
47
|
+
export function encodePath(path) {
|
|
48
|
+
return path.split("/").map(encodeSegment).join("/");
|
|
49
|
+
}
|
|
50
|
+
/** A generation-tree path back to its bundle path. */
|
|
51
|
+
export function decodePath(path) {
|
|
52
|
+
return path.split("/").map(decodeSegment).join("/");
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=qmd-render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"qmd-render.js","sourceRoot":"","sources":["../../src/engine/qmd-render.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAGjC,+EAA+E;AAC/E,MAAM,CAAC,MAAM,gBAAgB,GAAwB,IAAI,GAAG,CAAC;IAC3D,cAAc;IACd,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,MAAM;IACN,OAAO;CACR,CAAC,CAAC;AAEH,MAAM,QAAQ,GAAG,CAAC,CAAS,EAAU,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AAEtE;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAC5B,GAAoB,EACpB,OAAO,GAAgC,EAAE;IAEzC,MAAM,KAAK,GAAG,CAAC,KAAK,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC3C,MAAM,WAAW,GAAG,GAAG,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACnF,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAChE,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAChD,MAAM,IAAI,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;IAClD,IAAI,OAAO,CAAC,aAAa,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChD,OAAO,QAAQ,SAAS,CAAC,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,IAAI,EAAE,CAAC;AAC9E,CAAC;AAED,qHAAqH;AACrH,SAAS,aAAa,CAAC,OAAe;IACpC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACxF,OAAO,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;AAC5F,CAAC;AAED,SAAS,aAAa,CAAC,OAAe;IACpC,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;IACxE,OAAO,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACpF,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACtD,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACtD,CAAC","sourcesContent":["/**\n * The pure half of the qmd adapter: the text qmd indexes for a derived document, and the codec that keeps\n * bundle paths out of the way of qmd's indexer. Imports no qmd, so golden tests and `check` can use it.\n */\nimport { stringify } from \"yaml\";\nimport type { DerivedDocument } from \"../derive/derived-document.js\";\n\n/** Folder names qmd's indexer skips at any depth, exact and case-sensitive. */\nexport const SKIPPED_SEGMENTS: ReadonlySet<string> = new Set([\n \"node_modules\",\n \".git\",\n \".cache\",\n \"vendor\",\n \"dist\",\n \"build\",\n]);\n\nconst collapse = (s: string): string => s.replace(/\\s+/g, \" \").trim();\n\n/**\n * `# <title>` first, because qmd takes the first heading as the title and weights it four times the body; then\n * the description, the type value and the tag values on their own lines, with no label words, since a word\n * present in every page would be a universal token; then a blank line and the body.\n */\nexport function renderDocument(\n doc: DerivedDocument,\n options: { metadataBlock?: boolean } = {},\n): string {\n const lines = [`# ${collapse(doc.title)}`];\n const description = doc.description === undefined ? \"\" : collapse(doc.description);\n if (description.length > 0) lines.push(description);\n const type = collapse(doc.type);\n if (type.length > 0) lines.push(type);\n const tags = doc.tags.map(collapse).filter((t) => t.length > 0);\n if (tags.length > 0) lines.push(tags.join(\" \"));\n const body = `${lines.join(\"\\n\")}\\n\\n${doc.body}`;\n if (options.metadataBlock !== true) return body;\n return `---\\n${stringify({ qmd: { metadata: doc.metadata } })}---\\n${body}`;\n}\n\n/** `%`, `\\` and `:` are percent-encoded: qmd splits on the backslash and reads a leading `X:` as a Windows drive. */\nfunction encodeSegment(segment: string): string {\n const escaped = segment.replace(/%/g, \"%25\").replace(/\\\\/g, \"%5C\").replace(/:/g, \"%3A\");\n return SKIPPED_SEGMENTS.has(escaped) || escaped.startsWith(\"_\") ? `_${escaped}` : escaped;\n}\n\nfunction decodeSegment(segment: string): string {\n const unprefixed = segment.startsWith(\"_\") ? segment.slice(1) : segment;\n return unprefixed.replace(/%3A/g, \":\").replace(/%5C/g, \"\\\\\").replace(/%25/g, \"%\");\n}\n\n/** A bundle path as it is written into the generation tree. */\nexport function encodePath(path: string): string {\n return path.split(\"/\").map(encodeSegment).join(\"/\");\n}\n\n/** A generation-tree path back to its bundle path. */\nexport function decodePath(path: string): string {\n return path.split(\"/\").map(decodeSegment).join(\"/\");\n}\n"]}
|