@hraness/wordcell 0.21.3 → 0.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 (81) hide show
  1. package/README.md +170 -64
  2. package/dist/{authority-4sp9d552.js → authority-1pgftmwq.js} +55 -10
  3. package/dist/benchmark.js +3 -3
  4. package/dist/capture.js +6 -5
  5. package/dist/cli.js +37 -31
  6. package/dist/clip/acquire.js +3 -2
  7. package/dist/clip/cli.js +6 -5
  8. package/dist/clip/doctor.js +4 -3
  9. package/dist/clip/jobs.js +3 -3
  10. package/dist/clip/refresh.js +1 -1
  11. package/dist/evaluation-builder.js +18 -17
  12. package/dist/evaluation-kb.js +14 -13
  13. package/dist/graph-authority.js +3 -3
  14. package/dist/graph-percolation.js +5 -5
  15. package/dist/index-0dqfnrz9.js +1985 -0
  16. package/dist/{index-0k2x4nn9.js → index-1tm7bgx7.js} +16 -4
  17. package/dist/{index-vdw5yjfa.js → index-2hf27mws.js} +108 -18
  18. package/dist/index-3agn8scn.js +306 -0
  19. package/dist/{index-0rfcqab6.js → index-4ag64rc1.js} +555 -59
  20. package/dist/index-4knsp9qj.js +34 -0
  21. package/dist/index-66pshdtx.js +341 -0
  22. package/dist/index-9rf81m0p.js +368 -0
  23. package/dist/{index-59ssmqqm.js → index-a6kbv718.js} +1 -1
  24. package/dist/{index-51bap0mq.js → index-avbce90h.js} +9 -9
  25. package/dist/{index-9zcpdbjg.js → index-de2w8crk.js} +13 -13
  26. package/dist/{index-pbr6rv47.js → index-e6p9e926.js} +6 -6
  27. package/dist/{index-bcfn9xah.js → index-famy7fhs.js} +5 -5
  28. package/dist/{index-van48n3g.js → index-fc4dr114.js} +4 -2
  29. package/dist/{index-jzcws0zn.js → index-g5vsqmdy.js} +27 -57
  30. package/dist/{index-115b07ap.js → index-hcw140eb.js} +5 -5
  31. package/dist/index-j70m75wd.js +217 -0
  32. package/dist/{index-3ehtt59c.js → index-qwgsmtsz.js} +1 -1
  33. package/dist/{index-bgfzwt4h.js → index-t2bs9xdr.js} +4 -4
  34. package/dist/index.js +285 -93
  35. package/dist/portfolio.js +14 -13
  36. package/dist/publish-model.js +36 -0
  37. package/dist/publish-reader/reader.css +1042 -0
  38. package/dist/publish-reader/reader.js +1534 -0
  39. package/dist/publish-reader/theme.js +92 -0
  40. package/dist/publish-search.js +52 -0
  41. package/dist/publish.js +36 -0
  42. package/dist/rerank-typesafe.js +13 -0
  43. package/dist/rerank.js +14 -0
  44. package/dist/sdk.js +14 -11
  45. package/dist/search.js +2 -2
  46. package/dist/semantic.js +4 -4
  47. package/dist/{support-vm4mkw0k.js → support-mywv1em0.js} +38 -32
  48. package/dist/workflows/decision-context.js +13 -12
  49. package/dist/workflows/index.js +13 -12
  50. package/package.json +63 -5
  51. package/skills/wordcell/AGENTS.md +1 -0
  52. package/skills/wordcell/SKILL.md +2 -1
  53. package/skills/wordcell/agents/openai.yaml +1 -1
  54. package/skills/wordcell/references/publish.md +93 -0
  55. package/skills/wordcell/references/query.md +22 -0
  56. package/src/cli-program.ts +347 -9
  57. package/src/index.ts +4 -0
  58. package/src/oh/authority.ts +14 -5
  59. package/src/oh/canonical-rust.ts +127 -0
  60. package/src/oh/projection-rust.ts +63 -0
  61. package/src/oh-adoption.ts +7 -1
  62. package/src/publish-graph.ts +0 -0
  63. package/src/publish-index.ts +195 -0
  64. package/src/publish-markdown.ts +617 -0
  65. package/src/publish-model.ts +599 -0
  66. package/src/publish-nav.ts +204 -0
  67. package/src/publish-pages.ts +382 -0
  68. package/src/publish-reader/reader.css +1042 -0
  69. package/src/publish-reader/reader.ts +1017 -0
  70. package/src/publish-reader/theme.ts +97 -0
  71. package/src/publish-search.ts +418 -0
  72. package/src/publish-select.ts +419 -0
  73. package/src/publish-theme.ts +106 -0
  74. package/src/publish.ts +904 -0
  75. package/src/rerank-credentials.ts +56 -0
  76. package/src/rerank-typesafe.ts +472 -0
  77. package/src/rerank.ts +389 -0
  78. package/src/sdk.ts +157 -6
  79. package/src/serve.ts +180 -0
  80. package/src/vault.ts +23 -9
  81. package/dist/{index-why54q5p.js → index-gm9t95d9.js} +3 -3
@@ -0,0 +1,1985 @@
1
+ // @bun
2
+ import {
3
+ scanVault
4
+ } from "./index-1tm7bgx7.js";
5
+ import {
6
+ navigateLinks
7
+ } from "./index-d13v9ckt.js";
8
+ import {
9
+ queryVault
10
+ } from "./index-48pz4jpc.js";
11
+ import {
12
+ findKbPackageRoot
13
+ } from "./index-4knsp9qj.js";
14
+ import {
15
+ WORDCELL_SITE_CATALOG_FORMAT_V1,
16
+ WORDCELL_SITE_DOCS_FORMAT_V1,
17
+ WORDCELL_SITE_FORMAT_V1,
18
+ WORDCELL_SITE_GRAPH_FORMAT_V1,
19
+ WORDCELL_SITE_LIMITS_V1,
20
+ WORDCELL_SITE_NOTE_FORMAT_V1,
21
+ WORDCELL_SITE_POSTINGS_FORMAT_V1,
22
+ WORDCELL_SITE_TERMS_FORMAT_V1,
23
+ parseSiteCatalogV1,
24
+ parseSiteDocsV1,
25
+ parseSiteGraphV1,
26
+ parseSiteManifestV1,
27
+ parseSiteNoteV1,
28
+ parseSitePostingsV1,
29
+ parseSiteTermsV1
30
+ } from "./index-66pshdtx.js";
31
+ import {
32
+ publishNormalize,
33
+ publishShardName
34
+ } from "./index-3agn8scn.js";
35
+ import {
36
+ parseLocalAttachmentReferences,
37
+ validateMarkdownAttachments
38
+ } from "./index-x3fthpsc.js";
39
+ import {
40
+ lookupNote,
41
+ normalizeVaultPath
42
+ } from "./index-ekpwvbra.js";
43
+
44
+ // src/publish.ts
45
+ import { createHash } from "crypto";
46
+ import { lstat, mkdir, mkdtemp, readdir, readFile, realpath, rename, rm, writeFile } from "fs/promises";
47
+ import { basename, dirname, isAbsolute, posix as posix2, relative, resolve, sep } from "path";
48
+ import { canonicalJson as canonicalJson2, canonicalSha256 } from "@hraness/oh";
49
+
50
+ // src/publish-index.ts
51
+ import { canonicalJson } from "@hraness/oh";
52
+ var TERM_PATTERN = /[\p{L}\p{N}][\p{L}\p{N}._/-]*/gu;
53
+ function bounded(value, maximumBytes) {
54
+ if (Buffer.byteLength(value, "utf8") <= maximumBytes)
55
+ return { text: value, truncated: false };
56
+ const bytes = Buffer.from(value, "utf8");
57
+ let end = maximumBytes;
58
+ while (end > 0 && ((bytes[end] ?? 0) & 192) === 128)
59
+ end -= 1;
60
+ return { text: bytes.subarray(0, end).toString("utf8"), truncated: true };
61
+ }
62
+ function previewText(note) {
63
+ const basis = note.summary !== "" ? note.summary : note.searchableText;
64
+ const collapsed = basis.replace(/\s+/gu, " ").trim();
65
+ return bounded(collapsed, WORDCELL_SITE_LIMITS_V1.docPreviewBytes).text;
66
+ }
67
+ function indexableTerms(text) {
68
+ const terms = new Set;
69
+ for (const match of text.matchAll(TERM_PATTERN))
70
+ terms.add(match[0]);
71
+ return [...terms];
72
+ }
73
+ function buildSiteIndex(notes, slugById, options = {}) {
74
+ const indexContent = options.indexContent !== false;
75
+ const fields = [];
76
+ const inlineTexts = [];
77
+ let textTruncated = false;
78
+ let inlineTotal = 0;
79
+ for (const note of notes) {
80
+ const title = bounded(publishNormalize(note.title), WORDCELL_SITE_LIMITS_V1.fieldTextBytes);
81
+ const aliases = bounded(note.aliases.map(publishNormalize).join(`
82
+ `), WORDCELL_SITE_LIMITS_V1.fieldTextBytes);
83
+ const path = bounded(`${publishNormalize(note.path)}
84
+ ${publishNormalize(note.id)}`, WORDCELL_SITE_LIMITS_V1.fieldTextBytes);
85
+ const tags = bounded(note.tags.map(publishNormalize).join(`
86
+ `), WORDCELL_SITE_LIMITS_V1.fieldTextBytes);
87
+ const metadata = bounded(publishNormalize(canonicalJson(note.metadata)), WORDCELL_SITE_LIMITS_V1.fieldTextBytes);
88
+ textTruncated ||= title.truncated || aliases.truncated || path.truncated || tags.truncated || metadata.truncated;
89
+ fields.push({ t: title.text, a: aliases.text, p: path.text, g: tags.text, m: metadata.text });
90
+ if (indexContent) {
91
+ const inline = bounded(publishNormalize(note.searchableText), WORDCELL_SITE_LIMITS_V1.inlineTextBytes);
92
+ textTruncated ||= inline.truncated;
93
+ inlineTexts.push(inline.text);
94
+ inlineTotal += Buffer.byteLength(inline.text, "utf8");
95
+ }
96
+ }
97
+ const mode = !indexContent ? "none" : inlineTotal <= WORDCELL_SITE_LIMITS_V1.inlineTotalBytes ? "inline" : "shards";
98
+ const allTerms = new Set;
99
+ const contentPostings = new Map;
100
+ const docs = [];
101
+ for (const [index, note] of notes.entries()) {
102
+ const slug = slugById.get(note.id);
103
+ if (slug === undefined)
104
+ throw new Error(`Missing published slug for ${note.id}.`);
105
+ const field = fields[index];
106
+ if (field === undefined)
107
+ throw new Error(`Missing indexed fields for ${note.id}.`);
108
+ const inline = mode === "inline" ? inlineTexts[index] : undefined;
109
+ const fieldTerms = indexableTerms(`${field.t}
110
+ ${field.a}
111
+ ${field.p}
112
+ ${field.g}
113
+ ${field.m}`);
114
+ for (const term of fieldTerms)
115
+ allTerms.add(term);
116
+ if (mode !== "none") {
117
+ const content = inlineTexts[index] ?? "";
118
+ for (const term of indexableTerms(content)) {
119
+ allTerms.add(term);
120
+ if (mode === "shards") {
121
+ const list = contentPostings.get(term) ?? [];
122
+ list.push(index);
123
+ contentPostings.set(term, list);
124
+ }
125
+ }
126
+ }
127
+ docs.push({
128
+ i: index,
129
+ s: slug,
130
+ t: note.title,
131
+ p: previewText(note),
132
+ f: field,
133
+ ...inline === undefined ? {} : { x: inline }
134
+ });
135
+ }
136
+ let termsTruncated = false;
137
+ let sortedTerms = [...allTerms].toSorted((left, right) => left.localeCompare(right));
138
+ if (sortedTerms.length > WORDCELL_SITE_LIMITS_V1.indexTerms) {
139
+ sortedTerms = sortedTerms.slice(0, WORDCELL_SITE_LIMITS_V1.indexTerms);
140
+ termsTruncated = true;
141
+ }
142
+ const admitted = new Set(sortedTerms);
143
+ const postings = new Map;
144
+ if (mode === "shards") {
145
+ const grouped = new Map;
146
+ for (const [term, docIds] of [...contentPostings.entries()].toSorted(([a], [b]) => a.localeCompare(b))) {
147
+ if (!admitted.has(term))
148
+ continue;
149
+ const shard = publishShardName(term);
150
+ const bucket = grouped.get(shard) ?? Object.create(null);
151
+ bucket[term] = docIds;
152
+ grouped.set(shard, bucket);
153
+ }
154
+ for (const [shard, bucket] of [...grouped.entries()].toSorted(([a], [b]) => a.localeCompare(b))) {
155
+ postings.set(shard, {
156
+ format: WORDCELL_SITE_POSTINGS_FORMAT_V1,
157
+ shard,
158
+ postings: Object.freeze(bucket)
159
+ });
160
+ }
161
+ }
162
+ return {
163
+ docs: {
164
+ format: WORDCELL_SITE_DOCS_FORMAT_V1,
165
+ content: mode,
166
+ docs
167
+ },
168
+ terms: {
169
+ format: WORDCELL_SITE_TERMS_FORMAT_V1,
170
+ terms: sortedTerms
171
+ },
172
+ postings,
173
+ termsCount: sortedTerms.length,
174
+ termsTruncated,
175
+ textTruncated,
176
+ mode
177
+ };
178
+ }
179
+
180
+ // src/publish-markdown.ts
181
+ var VOID_SLUG = /[^\p{L}\p{N}._~-]+/gu;
182
+ function escapeHtml(value) {
183
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
184
+ }
185
+ function escapeAttribute(value) {
186
+ return escapeHtml(value);
187
+ }
188
+ function anchorId(text, used) {
189
+ const base = text.normalize("NFC").toLocaleLowerCase("en-US").replace(VOID_SLUG, "-").replace(/^[-._~]+|[-._~]+$/gu, "") || "section";
190
+ const seen = used.get(base) ?? 0;
191
+ used.set(base, seen + 1);
192
+ return seen === 0 ? base : `${base}-${seen + 1}`;
193
+ }
194
+ function isExternalUrl(value) {
195
+ return /^(?:https?|mailto):/iu.test(value);
196
+ }
197
+ function isSafeUrl(value) {
198
+ const trimmed = value.trim();
199
+ return !/^[a-z][a-z\d+.-]*:/iu.test(trimmed) || isExternalUrl(trimmed);
200
+ }
201
+ function splitWikiTarget(raw) {
202
+ const pipe = raw.indexOf("|");
203
+ const alias = pipe === -1 ? undefined : raw.slice(pipe + 1).trim();
204
+ const head = pipe === -1 ? raw : raw.slice(0, pipe);
205
+ const hash = head.indexOf("#");
206
+ const caret = head.indexOf("^");
207
+ let cut = head.length;
208
+ if (hash !== -1)
209
+ cut = Math.min(cut, hash);
210
+ if (caret !== -1)
211
+ cut = Math.min(cut, caret);
212
+ const target = head.slice(0, cut).trim();
213
+ const fragment = hash === -1 ? "" : head.slice(hash + 1).split("^")[0]?.trim() ?? "";
214
+ return { target, fragment, ...alias === undefined ? {} : { alias } };
215
+ }
216
+ function wikiDisplay(raw) {
217
+ const { target, fragment, alias } = splitWikiTarget(raw);
218
+ return {
219
+ target,
220
+ fragment,
221
+ label: alias ?? (fragment === "" ? target : `${target} > ${fragment}`)
222
+ };
223
+ }
224
+ function renderInline(text, ctx) {
225
+ let output = "";
226
+ let cursor = 0;
227
+ const length = text.length;
228
+ const pushText = (value) => {
229
+ output += escapeHtml(value);
230
+ };
231
+ while (cursor < length) {
232
+ const rest = text.slice(cursor);
233
+ if (rest[0] === "`") {
234
+ const ticks = /^`+/u.exec(rest)?.[0] ?? "`";
235
+ const closing = rest.indexOf(ticks, ticks.length);
236
+ if (closing !== -1) {
237
+ const code = rest.slice(ticks.length, closing).replace(/\s+/gu, " ");
238
+ output += `<code>${escapeHtml(code)}</code>`;
239
+ cursor += ticks.length + closing + ticks.length;
240
+ continue;
241
+ }
242
+ pushText("`");
243
+ cursor += 1;
244
+ continue;
245
+ }
246
+ if (rest.startsWith("![[")) {
247
+ const closing = rest.indexOf("]]");
248
+ if (closing !== -1) {
249
+ const raw = rest.slice(3, closing);
250
+ const { target, fragment, label } = wikiDisplay(raw);
251
+ if (target === "" && fragment !== "") {
252
+ output += `<a href="#${escapeAttribute(fragment.normalize("NFC").toLocaleLowerCase("en-US").replace(VOID_SLUG, "-"))}">${escapeHtml(fragment)}</a>`;
253
+ cursor += closing + 2;
254
+ continue;
255
+ }
256
+ const asset = ctx.resolveAsset(target, ctx.source);
257
+ if (asset !== undefined) {
258
+ if (/\.(?:apng|avif|bmp|gif|heic|heif|jpe?g|png|svg|webp)$/iu.test(asset)) {
259
+ output += `<img src="${escapeAttribute(asset)}" alt="${escapeAttribute(label)}" loading="lazy">`;
260
+ } else {
261
+ output += `<a class="asset" href="${escapeAttribute(asset)}">${escapeHtml(label)}</a>`;
262
+ }
263
+ } else {
264
+ const note = ctx.resolveNote(target);
265
+ if (note === undefined) {
266
+ output += `<span class="unresolved">${escapeHtml(label)}</span>`;
267
+ } else {
268
+ const hash = fragment === "" ? "" : `#${anchorId(fragment, new Map)}`;
269
+ output += `<a href="${escapeAttribute(ctx.noteHref(note.slug) + hash)}">${escapeHtml(label === "" ? note.title : label)}</a>`;
270
+ }
271
+ }
272
+ cursor += closing + 2;
273
+ continue;
274
+ }
275
+ pushText("!");
276
+ cursor += 1;
277
+ continue;
278
+ }
279
+ if (rest.startsWith("[[")) {
280
+ const closing = rest.indexOf("]]");
281
+ if (closing !== -1) {
282
+ const raw = rest.slice(2, closing);
283
+ const { target, fragment, label } = wikiDisplay(raw);
284
+ if (target === "" && fragment !== "") {
285
+ output += `<a href="#${escapeAttribute(fragment.normalize("NFC").toLocaleLowerCase("en-US").replace(VOID_SLUG, "-"))}">${escapeHtml(fragment)}</a>`;
286
+ cursor += closing + 2;
287
+ continue;
288
+ }
289
+ const note = ctx.resolveNote(target);
290
+ if (note === undefined) {
291
+ output += `<span class="unresolved">${escapeHtml(label)}</span>`;
292
+ } else {
293
+ const hash = fragment === "" ? "" : `#${anchorId(fragment, new Map)}`;
294
+ output += `<a href="${escapeAttribute(ctx.noteHref(note.slug) + hash)}">${escapeHtml(label === "" ? note.title : label)}</a>`;
295
+ }
296
+ cursor += closing + 2;
297
+ continue;
298
+ }
299
+ pushText("[");
300
+ cursor += 1;
301
+ continue;
302
+ }
303
+ if (rest.startsWith("![")) {
304
+ const closing = findClosingBracket(rest, 1);
305
+ if (closing !== -1 && rest[closing + 1] === "(") {
306
+ const destination = readDestination(rest, closing + 2);
307
+ if (destination !== null) {
308
+ const alt = rest.slice(2, closing);
309
+ const asset = ctx.resolveAsset(destination.target, ctx.source);
310
+ if (asset !== undefined) {
311
+ output += `<img src="${escapeAttribute(asset)}" alt="${escapeAttribute(alt)}" loading="lazy">`;
312
+ } else if (isExternalUrl(destination.target)) {
313
+ output += `<a class="asset" href="${escapeAttribute(destination.target)}" rel="noopener noreferrer">${escapeHtml(alt === "" ? destination.target : alt)}</a>`;
314
+ } else {
315
+ output += `<span class="unresolved">${escapeHtml(alt)}</span>`;
316
+ }
317
+ cursor += destination.end;
318
+ continue;
319
+ }
320
+ }
321
+ pushText("!");
322
+ cursor += 1;
323
+ continue;
324
+ }
325
+ if (rest[0] === "[") {
326
+ const closing = findClosingBracket(rest, 0);
327
+ if (closing !== -1 && rest[closing + 1] === "(") {
328
+ const destination = readDestination(rest, closing + 2);
329
+ if (destination !== null) {
330
+ const label = renderInline(rest.slice(1, closing), ctx);
331
+ const target = destination.target;
332
+ const asset = ctx.resolveAsset(target, ctx.source);
333
+ if (asset !== undefined) {
334
+ output += `<a class="asset" href="${escapeAttribute(asset)}">${label}</a>`;
335
+ } else if (isSafeUrl(target)) {
336
+ const external = isExternalUrl(target);
337
+ const note = external ? undefined : ctx.resolveNote(target.replace(/\.md$/iu, ""));
338
+ if (note !== undefined) {
339
+ const hash = destination.fragment === "" ? "" : `#${anchorId(destination.fragment, new Map)}`;
340
+ output += `<a href="${escapeAttribute(ctx.noteHref(note.slug) + hash)}">${label}</a>`;
341
+ } else {
342
+ output += `<a href="${escapeAttribute(target)}"${external ? ' rel="noopener noreferrer"' : ""}>${label}</a>`;
343
+ }
344
+ } else {
345
+ output += label;
346
+ }
347
+ cursor += destination.end;
348
+ continue;
349
+ }
350
+ }
351
+ pushText("[");
352
+ cursor += 1;
353
+ continue;
354
+ }
355
+ if (rest[0] === "<") {
356
+ const match = /^<([a-z][a-z\d+.-]*:[^>\s]+)>/iu.exec(rest);
357
+ if (match !== null && isExternalUrl(match[1] ?? "")) {
358
+ output += `<a href="${escapeAttribute(match[1] ?? "")}" rel="noopener noreferrer">${escapeHtml(match[1] ?? "")}</a>`;
359
+ cursor += (match[0] ?? "").length;
360
+ continue;
361
+ }
362
+ const tagMatch = /^<\/?[a-z][^>]*>/iu.exec(rest);
363
+ if (tagMatch !== null) {
364
+ pushText(tagMatch[0]);
365
+ cursor += tagMatch[0].length;
366
+ continue;
367
+ }
368
+ pushText("<");
369
+ cursor += 1;
370
+ continue;
371
+ }
372
+ const emphasis = /^(?:\*\*|__|\*|~~|==|_)/u.exec(rest)?.[0];
373
+ if (emphasis !== undefined && emphasis !== "_") {
374
+ const closing = rest.indexOf(emphasis, emphasis.length);
375
+ if (closing > emphasis.length) {
376
+ const inner = renderInline(rest.slice(emphasis.length, closing), ctx);
377
+ const tag = emphasis === "**" || emphasis === "__" ? "strong" : emphasis === "~~" ? "del" : emphasis === "==" ? "mark" : "em";
378
+ output += `<${tag}>${inner}</${tag}>`;
379
+ cursor += closing + emphasis.length;
380
+ continue;
381
+ }
382
+ }
383
+ if (emphasis === "_") {
384
+ const closing = rest.indexOf("_", 1);
385
+ if (closing > 1) {
386
+ output += `<em>${renderInline(rest.slice(1, closing), ctx)}</em>`;
387
+ cursor += closing + 1;
388
+ continue;
389
+ }
390
+ }
391
+ if (rest[0] === "\\" && length > cursor + 1 && /[\\`*[\]#|!><~=_-]/u.test(rest[1] ?? "")) {
392
+ pushText(rest[1] ?? "");
393
+ cursor += 2;
394
+ continue;
395
+ }
396
+ pushText(rest[0] ?? "");
397
+ cursor += 1;
398
+ }
399
+ return output;
400
+ }
401
+ function findClosingBracket(text, opening) {
402
+ for (let index = opening + 1;index < text.length; index += 1) {
403
+ if (text[index] === "\\") {
404
+ index += 1;
405
+ continue;
406
+ }
407
+ if (text[index] === "]")
408
+ return index;
409
+ if (text[index] === "[" || text[index] === `
410
+ `)
411
+ return -1;
412
+ }
413
+ return -1;
414
+ }
415
+ function readDestination(text, start) {
416
+ let cursor = start;
417
+ while (text[cursor] === " " || text[cursor] === "\t")
418
+ cursor += 1;
419
+ let target = "";
420
+ if (text[cursor] === "<") {
421
+ const end = text.indexOf(">", cursor + 1);
422
+ if (end === -1)
423
+ return null;
424
+ target = text.slice(cursor + 1, end);
425
+ cursor = end + 1;
426
+ } else {
427
+ let depth = 0;
428
+ const begin = cursor;
429
+ for (;cursor < text.length; cursor += 1) {
430
+ const character = text[cursor];
431
+ if (character === "\\") {
432
+ cursor += 1;
433
+ continue;
434
+ }
435
+ if (character === "(")
436
+ depth += 1;
437
+ else if (character === ")") {
438
+ if (depth === 0)
439
+ break;
440
+ depth -= 1;
441
+ } else if (character === " " || character === "\t" || character === `
442
+ `) {
443
+ break;
444
+ }
445
+ }
446
+ target = text.slice(begin, cursor);
447
+ }
448
+ while (text[cursor] === " " || text[cursor] === "\t")
449
+ cursor += 1;
450
+ const quoteChar = text[cursor];
451
+ if (quoteChar === '"' || quoteChar === "'") {
452
+ const end = text.indexOf(quoteChar, cursor + 1);
453
+ if (end !== -1)
454
+ cursor = end + 1;
455
+ while (text[cursor] === " " || text[cursor] === "\t")
456
+ cursor += 1;
457
+ }
458
+ if (text[cursor] !== ")")
459
+ return null;
460
+ const hash = target.indexOf("#");
461
+ const fragment = hash === -1 ? "" : target.slice(hash + 1);
462
+ return {
463
+ target: hash === -1 ? target : target.slice(0, hash),
464
+ fragment: decodeURIComponentSafe(fragment),
465
+ end: cursor + 1
466
+ };
467
+ }
468
+ function decodeURIComponentSafe(value) {
469
+ try {
470
+ return decodeURIComponent(value);
471
+ } catch {
472
+ return value;
473
+ }
474
+ }
475
+ function listMarker(line) {
476
+ const match = /^(\s*)(?:([-+*])|(\d{1,9})[.)])(\s+)/u.exec(line);
477
+ if (match === null)
478
+ return;
479
+ const indent = (match[1] ?? "").replaceAll("\t", " ").length;
480
+ const body = line.slice((match[0] ?? "").length);
481
+ const task = /^\[([ xX])\]\s+/u.exec(body);
482
+ return {
483
+ indent,
484
+ ordered: match[3] !== undefined,
485
+ ...task === null ? {} : { checked: (task[1] ?? " ").toLowerCase() === "x" }
486
+ };
487
+ }
488
+ function isTableDivider(line) {
489
+ return /^\s*\|?(?:\s*:?-{1,}:?\s*\|)+\s*:?-{0,}:?\s*$/u.test(line);
490
+ }
491
+ function tableCells(line) {
492
+ const trimmed = line.trim().replace(/^\|/u, "").replace(/\|$/u, "");
493
+ const cells = [];
494
+ let current = "";
495
+ for (let index = 0;index < trimmed.length; index += 1) {
496
+ const character = trimmed[index];
497
+ if (character === "\\" && trimmed[index + 1] === "|") {
498
+ current += "|";
499
+ index += 1;
500
+ continue;
501
+ }
502
+ if (character === "|") {
503
+ cells.push(current);
504
+ current = "";
505
+ continue;
506
+ }
507
+ current += character;
508
+ }
509
+ cells.push(current);
510
+ return cells.map((cell) => cell.trim());
511
+ }
512
+ function renderMarkdownToHtml(content, ctx) {
513
+ const lines = content.split(`
514
+ `);
515
+ let start = 0;
516
+ if (lines[0]?.trim() === "---") {
517
+ for (let index2 = 1;index2 < lines.length; index2 += 1) {
518
+ if (lines[index2]?.trim() === "---" || lines[index2]?.trim() === "...") {
519
+ start = index2 + 1;
520
+ break;
521
+ }
522
+ }
523
+ }
524
+ const anchors = new Map;
525
+ const html = [];
526
+ let index = start;
527
+ let inComment = false;
528
+ const paragraph = [];
529
+ const flushParagraph = () => {
530
+ if (paragraph.length === 0)
531
+ return;
532
+ html.push(`<p>${renderInline(paragraph.join(`
533
+ `).replace(/\n/gu, " "), ctx)}</p>`);
534
+ paragraph.length = 0;
535
+ };
536
+ while (index < lines.length) {
537
+ const line = lines[index] ?? "";
538
+ if (inComment) {
539
+ const close = line.indexOf("-->");
540
+ index += 1;
541
+ if (close !== -1)
542
+ inComment = false;
543
+ continue;
544
+ }
545
+ if (line.trim().startsWith("%%") && line.trim().endsWith("%%") && line.trim().length > 3) {
546
+ index += 1;
547
+ continue;
548
+ }
549
+ const fence = /^\s{0,3}(`{3,}|~{3,})([^`]*)$/u.exec(line);
550
+ if (fence !== null) {
551
+ flushParagraph();
552
+ const marker2 = fence[1] ?? "```";
553
+ const language = (fence[2] ?? "").trim().split(/\s/u)[0] ?? "";
554
+ const body = [];
555
+ index += 1;
556
+ while (index < lines.length) {
557
+ const candidate = lines[index] ?? "";
558
+ const close = /^\s{0,3}(`{3,}|~{3,})\s*$/u.exec(candidate);
559
+ if (close !== null && (close[1] ?? "")[0] === marker2[0] && (close[1] ?? "").length >= marker2.length) {
560
+ break;
561
+ }
562
+ body.push(candidate);
563
+ index += 1;
564
+ }
565
+ index += 1;
566
+ const className = language === "" ? "" : ` class="language-${escapeAttribute(language)}"`;
567
+ html.push(`<pre><code${className}>${escapeHtml(body.join(`
568
+ `))}</code></pre>`);
569
+ continue;
570
+ }
571
+ if (line.trim() === "") {
572
+ flushParagraph();
573
+ index += 1;
574
+ continue;
575
+ }
576
+ const heading = /^(#{1,6})\s+(.*)$/u.exec(line);
577
+ if (heading !== null) {
578
+ flushParagraph();
579
+ const level = (heading[1] ?? "#").length;
580
+ const body = renderInline((heading[2] ?? "").replace(/\s+#+$/u, ""), ctx);
581
+ const id = anchorId(stripMarkup(body), anchors);
582
+ html.push(`<h${level} id="${escapeAttribute(id)}">${body}</h${level}>`);
583
+ index += 1;
584
+ continue;
585
+ }
586
+ if (/^\s{0,3}(?:\*\s*){3,}$/u.test(line) || /^\s{0,3}(?:-\s*){3,}$/u.test(line) || /^\s{0,3}(?:_\s*){3,}$/u.test(line)) {
587
+ flushParagraph();
588
+ html.push("<hr>");
589
+ index += 1;
590
+ continue;
591
+ }
592
+ if (/^\s{0,3}>/u.test(line)) {
593
+ flushParagraph();
594
+ const quote = [];
595
+ while (index < lines.length && /^\s{0,3}>/u.test(lines[index] ?? "")) {
596
+ quote.push((lines[index] ?? "").replace(/^\s{0,3}>\s?/u, ""));
597
+ index += 1;
598
+ }
599
+ html.push(`<blockquote>${renderMarkdownToHtml(quote.join(`
600
+ `), ctx)}</blockquote>`);
601
+ continue;
602
+ }
603
+ if (line.includes("|") && index + 1 < lines.length && isTableDivider(lines[index + 1] ?? "")) {
604
+ flushParagraph();
605
+ const header = tableCells(line);
606
+ index += 2;
607
+ const rows = [];
608
+ while (index < lines.length && (lines[index] ?? "").includes("|") && (lines[index] ?? "").trim() !== "") {
609
+ rows.push(lines[index] ?? "");
610
+ index += 1;
611
+ }
612
+ const head = header.map((cell) => `<th>${renderInline(cell, ctx)}</th>`).join("");
613
+ const bodyRows = rows.map((row) => `<tr>${tableCells(row).map((cell) => `<td>${renderInline(cell, ctx)}</td>`).join("")}</tr>`).join("");
614
+ html.push(`<table><thead><tr>${head}</tr></thead><tbody>${bodyRows}</tbody></table>`);
615
+ continue;
616
+ }
617
+ const marker = listMarker(line);
618
+ if (marker !== undefined) {
619
+ flushParagraph();
620
+ const items = [];
621
+ let ordered = marker.ordered;
622
+ const baseIndent = marker.indent;
623
+ const stack = [];
624
+ while (index < lines.length) {
625
+ const current = listMarker(lines[index] ?? "");
626
+ const raw = lines[index] ?? "";
627
+ if (current === undefined) {
628
+ if (raw.trim() === "") {
629
+ index += 1;
630
+ break;
631
+ }
632
+ const continuationIndent = raw.length - raw.trimStart().length;
633
+ if (items.length > 0 && continuationIndent > baseIndent) {
634
+ items.push(`
635
+ ${renderInline(raw.trim(), ctx)}`);
636
+ index += 1;
637
+ continue;
638
+ }
639
+ break;
640
+ }
641
+ const body = raw.slice(raw.search(/\S/u));
642
+ const itemBody = body.replace(/^(?:[-+*]|\d{1,9}[.)])\s+/u, "");
643
+ const task = /^\[([ xX])\]\s+/u.exec(itemBody);
644
+ const content2 = task === null ? itemBody : itemBody.slice((task[0] ?? "").length);
645
+ const checkbox = task === null ? "" : `<input type="checkbox" disabled${(task[1] ?? " ").toLowerCase() === "x" ? " checked" : ""}> `;
646
+ const top = stack[stack.length - 1];
647
+ if (current.indent > (top?.indent ?? baseIndent) && stack.length < 8) {
648
+ stack.push({ indent: current.indent, ordered: current.ordered });
649
+ items.push(`<${current.ordered ? "ol" : "ul"}>`);
650
+ }
651
+ while (stack.length > 0 && current.indent < (stack[stack.length - 1]?.indent ?? baseIndent)) {
652
+ const popped = stack.pop();
653
+ items.push(`</${popped?.ordered === true ? "ol" : "ul"}>`);
654
+ }
655
+ items.push(`<li>${checkbox}${renderInline(content2, ctx)}</li>`);
656
+ index += 1;
657
+ }
658
+ while (stack.length > 0) {
659
+ const popped = stack.pop();
660
+ items.push(`</${popped?.ordered === true ? "ol" : "ul"}>`);
661
+ }
662
+ html.push(`<${ordered ? "ol" : "ul"}>${items.join("")}</${ordered ? "ol" : "ul"}>`);
663
+ continue;
664
+ }
665
+ const commentStart = line.indexOf("<!--");
666
+ if (commentStart !== -1) {
667
+ const close = line.indexOf("-->", commentStart + 4);
668
+ if (close === -1)
669
+ inComment = true;
670
+ index += 1;
671
+ continue;
672
+ }
673
+ if (/^(?: {4}|\t)/u.test(line)) {
674
+ flushParagraph();
675
+ const body = [];
676
+ while (index < lines.length && /^(?: {4}|\t| *$)/u.test(lines[index] ?? "")) {
677
+ body.push((lines[index] ?? "").replace(/^(?: {4}|\t)/u, ""));
678
+ index += 1;
679
+ }
680
+ html.push(`<pre><code>${escapeHtml(body.join(`
681
+ `).replace(/\n+$/u, ""))}</code></pre>`);
682
+ continue;
683
+ }
684
+ paragraph.push(line);
685
+ index += 1;
686
+ }
687
+ flushParagraph();
688
+ return html.join(`
689
+ `);
690
+ }
691
+ function stripMarkup(html) {
692
+ let stripped = "";
693
+ let inTag = false;
694
+ for (const character of html) {
695
+ if (character === "<")
696
+ inTag = true;
697
+ else if (character === ">")
698
+ inTag = false;
699
+ else if (!inTag)
700
+ stripped += character;
701
+ }
702
+ return stripped.replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", '"').replaceAll("&#39;", "'").replaceAll("&amp;", "&");
703
+ }
704
+
705
+ // src/publish-nav.ts
706
+ var SITE_NAV_GROUP_LIMIT = 96;
707
+ var SITE_NAV_NODE_BUDGET = 768;
708
+ var SITE_TOC_LIMIT = 128;
709
+ function compareNavNodes(left, right) {
710
+ return Number(right.children.length > 0) - Number(left.children.length > 0) || (left.label < right.label ? -1 : left.label > right.label ? 1 : 0) || (left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
711
+ }
712
+ function siteNavFromCatalog(entries) {
713
+ const root = new Map;
714
+ for (const entry of entries) {
715
+ if (entry.s === "")
716
+ continue;
717
+ const segments = entry.s.split("/");
718
+ let level = root;
719
+ let path = "";
720
+ for (const [index, segment] of segments.entries()) {
721
+ path = path === "" ? segment : `${path}/${segment}`;
722
+ let node = level.get(segment);
723
+ if (node === undefined) {
724
+ node = { path, label: segment, children: new Map };
725
+ level.set(segment, node);
726
+ }
727
+ if (index === segments.length - 1) {
728
+ node.slug = entry.s;
729
+ node.label = entry.t;
730
+ }
731
+ level = node.children;
732
+ }
733
+ }
734
+ const freeze = (level) => [...level.values()].map((node) => ({
735
+ path: node.path,
736
+ ...node.slug === undefined ? {} : { slug: node.slug },
737
+ label: node.label,
738
+ children: freeze(node.children)
739
+ })).toSorted(compareNavNodes);
740
+ return { nodes: freeze(root), hiddenRoots: 0 };
741
+ }
742
+ function pruneSiteNav(tree, current, groupLimit = SITE_NAV_GROUP_LIMIT, nodeBudget = SITE_NAV_NODE_BUDGET) {
743
+ const onPath = (path) => current !== undefined && current !== "" && (path === current || current.startsWith(`${path}/`));
744
+ const nearCurrent = (path) => current !== undefined && current !== "" && (path === current || current.startsWith(`${path}/`) || path.startsWith(`${current}/`));
745
+ const keepNode = (node) => {
746
+ if (node.path.split("/").length <= 2)
747
+ return true;
748
+ if (nearCurrent(node.path))
749
+ return true;
750
+ const parent = node.path.slice(0, node.path.lastIndexOf("/"));
751
+ return parent !== "" && onPath(parent);
752
+ };
753
+ const budget = { left: nodeBudget };
754
+ const pruneLevel = (nodes) => {
755
+ const kept = nodes.filter(keepNode);
756
+ const required = kept.filter((node) => onPath(node.path) || node.path === current);
757
+ const requiredSet = new Set(required);
758
+ const fillable = Math.max(0, groupLimit - required.length);
759
+ const optional = kept.filter((node) => !requiredSet.has(node)).slice(0, fillable);
760
+ const shown = new Set([...required, ...optional]);
761
+ let hidden = nodes.length - shown.size;
762
+ const out = [];
763
+ for (const node of kept) {
764
+ if (!shown.has(node))
765
+ continue;
766
+ if (budget.left <= 0 && !onPath(node.path)) {
767
+ hidden += 1;
768
+ continue;
769
+ }
770
+ budget.left -= 1;
771
+ const children = node.children.length === 0 ? { nodes: [], hiddenRoots: 0 } : pruneLevel(node.children);
772
+ out.push({
773
+ path: node.path,
774
+ ...node.slug === undefined ? {} : { slug: node.slug },
775
+ label: node.label,
776
+ children: children.nodes,
777
+ ...children.hiddenRoots === 0 ? {} : { hiddenChildren: children.hiddenRoots }
778
+ });
779
+ }
780
+ return { nodes: out, hiddenRoots: hidden };
781
+ };
782
+ return pruneLevel(tree.nodes);
783
+ }
784
+ function siteBreadcrumbs(slug, titleBySlug) {
785
+ if (slug === "")
786
+ return [];
787
+ const segments = slug.split("/");
788
+ return segments.map((segment, index) => {
789
+ const path = segments.slice(0, index + 1).join("/");
790
+ const current = index === segments.length - 1;
791
+ return {
792
+ label: titleBySlug.get(path) ?? segment,
793
+ ...current || !titleBySlug.has(path) ? {} : { slug: path },
794
+ current
795
+ };
796
+ });
797
+ }
798
+ var HEADING_PATTERN = /<h([1-6]) id="([^"]{0,256})">([\s\S]*?)<\/h\1>/gu;
799
+ function siteTocFromHtml(html, limit = SITE_TOC_LIMIT) {
800
+ const entries = [];
801
+ for (const match of html.matchAll(HEADING_PATTERN)) {
802
+ if (entries.length >= limit)
803
+ break;
804
+ entries.push({
805
+ level: Number(match[1]),
806
+ id: match[2] ?? "",
807
+ text: stripMarkup(match[3] ?? "").replace(/\s+/gu, " ").trim()
808
+ });
809
+ }
810
+ return entries;
811
+ }
812
+
813
+ // src/publish-pages.ts
814
+ var SITE_CONTENT_SECURITY_POLICY = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'";
815
+ function relativePrefix(slug) {
816
+ if (slug === "")
817
+ return "";
818
+ const depth = slug.split("/").length;
819
+ return "../".repeat(depth + 1);
820
+ }
821
+ function noteHref(rel, slug) {
822
+ return slug === "" ? rel || "./" : `${rel}n/${encodeURI(slug)}/`;
823
+ }
824
+ function head(title, ctx, extra = []) {
825
+ const lines = [
826
+ `<meta charset="utf-8">`,
827
+ `<meta name="viewport" content="width=device-width, initial-scale=1">`,
828
+ `<title>${escapeHtml(title === "" ? ctx.site.title : `${title} \u2014 ${ctx.site.title}`)}</title>`,
829
+ `<meta name="generator" content="${escapeAttribute(ctx.generator)}">`,
830
+ `<meta name="wordcell:base" content="${escapeAttribute(ctx.rel || "./")}">`,
831
+ `<meta http-equiv="Content-Security-Policy" content="${escapeAttribute(SITE_CONTENT_SECURITY_POLICY)}">`,
832
+ `<script src="${escapeAttribute(ctx.rel)}reader/theme.js"></script>`,
833
+ `<link rel="stylesheet" href="${escapeAttribute(ctx.rel)}reader/reader.css">`,
834
+ `<script type="module" src="${escapeAttribute(ctx.rel)}reader/reader.js"></script>`
835
+ ];
836
+ if (ctx.site.description !== undefined) {
837
+ lines.push(`<meta name="description" content="${escapeAttribute(ctx.site.description)}">`);
838
+ }
839
+ if (ctx.noindex)
840
+ lines.push(`<meta name="robots" content="noindex">`);
841
+ lines.push(...extra);
842
+ return lines.join(`
843
+ `);
844
+ }
845
+ function navLink(rel, node, current, extraClass = "") {
846
+ const isCurrent = node.slug !== undefined && node.slug === current;
847
+ const classes = `site-tree-link${isCurrent ? " site-tree-current" : ""}${extraClass}`;
848
+ return `<a class="${classes}" href="${escapeAttribute(noteHref(rel, node.slug ?? ""))}"${isCurrent ? ` aria-current="page"` : ""}>${escapeHtml(node.label)}</a>`;
849
+ }
850
+ function navItems(nodes, current, rel, depth) {
851
+ const items = nodes.map((node) => {
852
+ const grouped = node.children.length > 0 || (node.hiddenChildren ?? 0) > 0;
853
+ if (!grouped) {
854
+ const body = node.slug === undefined ? `<span class="site-tree-empty">${escapeHtml(node.label)}</span>` : navLink(rel, node, current);
855
+ return ` <li class="site-tree-leaf">${body}</li>`;
856
+ }
857
+ const onPath = current !== "" && (current === node.path || current.startsWith(`${node.path}/`));
858
+ const open = depth === 0 || onPath;
859
+ const self = node.slug === undefined ? "" : ` <li class="site-tree-self">${navLink(rel, node, current)}</li>
860
+ `;
861
+ const more = (node.hiddenChildren ?? 0) === 0 ? "" : ` <li class="site-tree-more"><a href="${escapeAttribute(rel)}#catalog">+${String(node.hiddenChildren)} more</a></li>
862
+ `;
863
+ return ` <li class="site-tree-group">
864
+ <details${open ? " open" : ""}>
865
+ <summary>${escapeHtml(node.label)}</summary>
866
+ <ul>
867
+ ${self}${navItems(node.children, current, rel, depth + 1)}${more} </ul>
868
+ </details>
869
+ </li>`;
870
+ });
871
+ return items.join(`
872
+ `) + (items.length === 0 ? "" : `
873
+ `);
874
+ }
875
+ function renderSiteNav(tree, current, rel) {
876
+ if (tree.nodes.length === 0)
877
+ return "";
878
+ const more = tree.hiddenRoots === 0 ? "" : ` <li class="site-tree-more"><a href="${escapeAttribute(rel)}#catalog">+${String(tree.hiddenRoots)} more</a></li>
879
+ `;
880
+ return ` <nav class="site-nav" aria-label="Published notes">
881
+ <ul class="site-tree">
882
+ ${navItems(tree.nodes, current, rel, 0)}${more} </ul>
883
+ </nav>`;
884
+ }
885
+ function breadcrumbsHtml(ctx) {
886
+ const crumbs = siteBreadcrumbs(ctx.current, ctx.titles);
887
+ if (crumbs.length === 0)
888
+ return "";
889
+ const items = [
890
+ ` <li><a href="${escapeAttribute(ctx.rel || "./")}">Home</a></li>`,
891
+ ...crumbs.map((crumb) => {
892
+ if (crumb.current) {
893
+ return ` <li><span aria-current="page">${escapeHtml(crumb.label)}</span></li>`;
894
+ }
895
+ if (crumb.slug === undefined) {
896
+ return ` <li><span>${escapeHtml(crumb.label)}</span></li>`;
897
+ }
898
+ return ` <li><a href="${escapeAttribute(noteHref(ctx.rel, crumb.slug))}">${escapeHtml(crumb.label)}</a></li>`;
899
+ })
900
+ ];
901
+ return ` <nav class="breadcrumbs" aria-label="Breadcrumb">
902
+ <ol>
903
+ ${items.join(`
904
+ `)}
905
+ </ol>
906
+ </nav>
907
+ `;
908
+ }
909
+ function tocHtml(bodyHtml) {
910
+ const toc = siteTocFromHtml(bodyHtml);
911
+ if (toc.length < 2)
912
+ return "";
913
+ const items = toc.map((entry) => ` <li class="toc-level-${String(Math.min(entry.level, 4))}"><a href="#${escapeAttribute(entry.id)}">${escapeHtml(entry.text)}</a></li>`);
914
+ return ` <nav class="note-toc" aria-label="On this page">
915
+ <h2>On this page</h2>
916
+ <ol>
917
+ ${items.join(`
918
+ `)}
919
+ </ol>
920
+ </nav>`;
921
+ }
922
+ function chrome(title, main, ctx, extra = [], options = {}) {
923
+ const nav = renderSiteNav(pruneSiteNav(ctx.nav, ctx.current), ctx.current, ctx.rel);
924
+ const graphCurrent = options.graph === true ? ` aria-current="page"` : "";
925
+ const layoutClass = nav === "" ? "site-layout site-layout-flat" : "site-layout";
926
+ return `<!doctype html>
927
+ <html lang="en">
928
+ <head>
929
+ ${head(title, ctx, extra)}
930
+ </head>
931
+ <body>
932
+ <a class="skip-link" href="#wordcell-main">Skip to content</a>
933
+ <header class="site-header">
934
+ <a class="site-title" href="${escapeAttribute(ctx.rel || "./")}">${escapeHtml(ctx.site.title)}</a>
935
+ <nav class="site-actions" aria-label="Site">
936
+ <a class="graph-link" href="${escapeAttribute(ctx.rel)}graph/"${graphCurrent}>Graph</a>
937
+ <button type="button" class="search-button" data-wordcell-search aria-keyshortcuts="/">
938
+ <span>Search</span><kbd>/</kbd>
939
+ </button>
940
+ </nav>
941
+ </header>
942
+ <div class="${layoutClass}">
943
+ ${nav}
944
+ <main id="wordcell-main">
945
+ ${main}
946
+ </main>
947
+ </div>
948
+ <footer class="site-footer">
949
+ <span>Published with <a href="https://wordcell.io" rel="noopener noreferrer">Wordcell</a>.</span>
950
+ </footer>
951
+ </body>
952
+ </html>
953
+ `;
954
+ }
955
+ function propertiesBlock(note) {
956
+ const rows = [];
957
+ const type = note.metadata["type"];
958
+ if (typeof type === "string" && type !== "") {
959
+ rows.push(`<span class="note-type">${escapeHtml(type)}</span>`);
960
+ }
961
+ for (const tag of note.tags) {
962
+ rows.push(`<span class="note-tag">${escapeHtml(tag)}</span>`);
963
+ }
964
+ if (rows.length === 0)
965
+ return "";
966
+ return `<div class="note-meta">${rows.join(`
967
+ `)}</div>`;
968
+ }
969
+ function linkList(title, links, rel) {
970
+ if (links.length === 0)
971
+ return "";
972
+ const items = links.map((link) => ` <li><a href="${escapeAttribute(noteHref(rel, link.s))}">${escapeHtml(link.t)}</a></li>`).join(`
973
+ `);
974
+ return ` <section class="note-links">
975
+ <h2>${escapeHtml(title)}</h2>
976
+ <ul>
977
+ ${items}
978
+ </ul>
979
+ </section>`;
980
+ }
981
+ function relationList(title, relations, rel) {
982
+ if (relations.length === 0)
983
+ return "";
984
+ const items = relations.map((relation) => ` <li><span class="predicate">${escapeHtml(relation.p)}</span> <a href="${escapeAttribute(noteHref(rel, relation.s))}">${escapeHtml(relation.t)}</a></li>`).join(`
985
+ `);
986
+ return ` <section class="note-relations">
987
+ <h2>${escapeHtml(title)}</h2>
988
+ <ul>
989
+ ${items}
990
+ </ul>
991
+ </section>`;
992
+ }
993
+ function notePageTitle(title, bodyHtml) {
994
+ const leading = /^\s*(<h1 id="[^"]*">([^<]*)<\/h1>)/u.exec(bodyHtml);
995
+ if (leading !== null && leading[2] === escapeHtml(title)) {
996
+ return { heading: leading[1] ?? "", body: bodyHtml.slice(leading[0].length) };
997
+ }
998
+ return { heading: `<h1>${escapeHtml(title)}</h1>`, body: bodyHtml };
999
+ }
1000
+ function renderNotePage(note, bodyHtml, sides, ctx) {
1001
+ const meta = propertiesBlock(note);
1002
+ const aliasRow = note.aliases.length === 0 ? "" : `<p class="note-aliases">Also known as ${escapeHtml(note.aliases.join(", "))}</p>`;
1003
+ const aside = [
1004
+ linkList("Linked from", sides.backlinks, ctx.rel),
1005
+ relationList("Relations", sides.relations, ctx.rel),
1006
+ relationList("Referenced by", sides.relationBacklinks, ctx.rel)
1007
+ ].filter((section) => section !== "").join(`
1008
+ `);
1009
+ const toc = tocHtml(bodyHtml);
1010
+ const title = notePageTitle(note.title, bodyHtml);
1011
+ const article = ` <article class="note">
1012
+ ${title.heading}
1013
+ ${meta}
1014
+ ${aliasRow}
1015
+ <div class="note-body">
1016
+ ${title.body}
1017
+ </div>
1018
+ </article>${aside === "" ? "" : `
1019
+ <aside class="note-aside">
1020
+ ${aside}
1021
+ </aside>`}`;
1022
+ const main = `${breadcrumbsHtml(ctx)} <div class="note-columns">
1023
+ ${article}
1024
+ ${toc}
1025
+ </div>`;
1026
+ return chrome(note.title, main, ctx);
1027
+ }
1028
+ function renderLandingPage(indexBodyHtml, entries, ctx) {
1029
+ const groups = new Map;
1030
+ for (const entry of entries) {
1031
+ const top = entry.s.split("/")[0] ?? "";
1032
+ const group = groups.get(top) ?? [];
1033
+ group.push(entry);
1034
+ groups.set(top, group);
1035
+ }
1036
+ const sections = [...groups.entries()].toSorted(([left], [right]) => left.localeCompare(right)).map(([group, members]) => {
1037
+ const items = members.map((entry) => ` <li><a href="${escapeAttribute(noteHref(ctx.rel, entry.s))}">${escapeHtml(entry.t)}</a></li>`).join(`
1038
+ `);
1039
+ return ` <section class="catalog-group">
1040
+ <h2>${escapeHtml(group === "" ? "Notes" : group)}</h2>
1041
+ <ul>
1042
+ ${items}
1043
+ </ul>
1044
+ </section>`;
1045
+ }).join(`
1046
+ `);
1047
+ const body = indexBodyHtml === undefined || indexBodyHtml === "" ? "" : ` <div class="note-body index-body">
1048
+ ${indexBodyHtml}
1049
+ </div>
1050
+ `;
1051
+ const main = `${body} <nav class="catalog" id="catalog" aria-label="All published notes">
1052
+ ${sections}
1053
+ </nav>`;
1054
+ return chrome("", main, ctx, [
1055
+ `<meta name="wordcell:landing" content="true">`
1056
+ ]);
1057
+ }
1058
+ function renderGraphPage(entries, edgeCount, ctx) {
1059
+ const items = entries.map((entry) => ` <li><a href="${escapeAttribute(noteHref(ctx.rel, entry.s))}">${escapeHtml(entry.t)}</a></li>`).join(`
1060
+ `);
1061
+ const main = ` <article class="note graph-page">
1062
+ <h1>Graph</h1>
1063
+ <p class="graph-lede">${String(entries.length)} notes and ${String(edgeCount)} links. Drag to pan, scroll or pinch to zoom, click a node to open the note.</p>
1064
+ <div class="graph-shell" data-wordcell-graph>
1065
+ <canvas class="graph-canvas" data-wordcell-graph-canvas width="1280" height="720" tabindex="0" role="img" aria-label="Interactive map of ${String(entries.length)} published notes"></canvas>
1066
+ <div class="graph-status" data-wordcell-graph-status>The interactive map needs JavaScript. Every note is listed below.</div>
1067
+ <div class="graph-toolbar">
1068
+ <button type="button" class="graph-reset" data-wordcell-graph-reset>Reset view</button>
1069
+ <span class="graph-legend"><span class="graph-edge graph-edge-link"></span> link <span class="graph-edge graph-edge-relation"></span> typed relation</span>
1070
+ </div>
1071
+ </div>
1072
+ <h2>All notes</h2>
1073
+ <ul class="graph-index">
1074
+ ${items}
1075
+ </ul>
1076
+ </article>`;
1077
+ return chrome("Graph", main, ctx, [], { graph: true });
1078
+ }
1079
+ function renderNotFoundPage(ctx) {
1080
+ return chrome("Not found", ` <article class="note">
1081
+ <h1>Not found</h1>
1082
+ <p>This page is not part of the published site.</p>
1083
+ </article>`, { ...ctx, noindex: true });
1084
+ }
1085
+ function renderRobotsTxt(noindex) {
1086
+ return noindex ? `User-agent: *
1087
+ Disallow: /
1088
+ ` : `User-agent: *
1089
+ Allow: /
1090
+ `;
1091
+ }
1092
+ function renderSitemapXml(slugs, baseUrl) {
1093
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
1094
+ const urls = slugs.map((slug) => ` <url><loc>${escapeHtml(base + (slug === "" ? "" : `n/${encodeURI(slug)}/`))}</loc></url>`).join(`
1095
+ `);
1096
+ return `<?xml version="1.0" encoding="UTF-8"?>
1097
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
1098
+ ${urls}
1099
+ </urlset>
1100
+ `;
1101
+ }
1102
+
1103
+ // src/publish-select.ts
1104
+ import {
1105
+ posix
1106
+ } from "path";
1107
+ var MAX_PUBLISH_SELECTORS = 256;
1108
+ var MAX_PUBLISH_FROM_NOTES = 1000;
1109
+ var MAX_PUBLISH_SELECTOR_BYTES = 1024;
1110
+ var MAX_PUBLISH_GLOB_WORK = 1e8;
1111
+ var MAX_PUBLISH_SLUG_BYTES = 1024;
1112
+ function normalizeSelectorPrefix(value, flag) {
1113
+ const trimmed = value.trim().replaceAll("\\", "/");
1114
+ if (trimmed === "" || Buffer.byteLength(trimmed, "utf8") > MAX_PUBLISH_SELECTOR_BYTES || /[\u0000-\u001f\u007f]/u.test(trimmed)) {
1115
+ throw new TypeError(`${flag} must be bounded text without control characters`);
1116
+ }
1117
+ const normalized = posix.normalize(trimmed).replace(/^\.\//u, "").replace(/\/+$/u, "");
1118
+ if (normalized === "" || trimmed.split("/").includes("..") || /^[a-z]:\//iu.test(normalized) || normalized.startsWith("/") || normalized === ".." || normalized.startsWith("../") || normalized.split("/").includes("..")) {
1119
+ throw new TypeError(`${flag} must be a vault-relative path prefix or note id`);
1120
+ }
1121
+ return normalized;
1122
+ }
1123
+ function selectorIncludes(prefixes, id) {
1124
+ return prefixes.some((prefix) => prefix === "." || id === prefix || id.startsWith(`${prefix}/`) || prefix.endsWith(".md") && id === prefix.slice(0, -3));
1125
+ }
1126
+ function publishGlob(value, flag) {
1127
+ const pattern = value.trim().replaceAll("\\", "/").replace(/^\.\//u, "");
1128
+ const segments = pattern.split("/");
1129
+ if (pattern === "" || /^[a-z]:\//iu.test(pattern) || Buffer.byteLength(pattern, "utf8") > MAX_PUBLISH_SELECTOR_BYTES || /[\u0000-\u001f\u007f\[\]{}]/u.test(pattern) || segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment.includes("**") && segment !== "**")) {
1130
+ throw new TypeError(`${flag} must be a vault-relative glob using *, ?, or whole-segment **`);
1131
+ }
1132
+ return segments;
1133
+ }
1134
+ function spendGlobWork(budget, cells) {
1135
+ if (cells > budget.remaining) {
1136
+ throw new RangeError(`Publish glob matching exceeds the ${MAX_PUBLISH_GLOB_WORK}-cell work budget; reduce the number or complexity of globs, or use --include/--exclude path prefixes.`);
1137
+ }
1138
+ budget.remaining -= cells;
1139
+ }
1140
+ function wildcardSegment(pattern, value, budget) {
1141
+ const tokens = Array.from(pattern);
1142
+ const characters = Array.from(value);
1143
+ spendGlobWork(budget, tokens.length * (characters.length + 1));
1144
+ let previous = new Uint8Array(characters.length + 1);
1145
+ previous[0] = 1;
1146
+ for (const token of tokens) {
1147
+ const current = new Uint8Array(characters.length + 1);
1148
+ if (token === "*")
1149
+ current[0] = previous[0] ?? 0;
1150
+ for (let index = 1;index <= characters.length; index += 1) {
1151
+ current[index] = token === "*" ? previous[index] || current[index - 1] ? 1 : 0 : previous[index - 1] && (token === "?" || token === characters[index - 1]) ? 1 : 0;
1152
+ }
1153
+ previous = current;
1154
+ }
1155
+ return previous[characters.length] === 1;
1156
+ }
1157
+ function globMatches(pattern, path, budget) {
1158
+ const segments = path.split("/");
1159
+ spendGlobWork(budget, pattern.length * (segments.length + 1));
1160
+ let previous = new Uint8Array(segments.length + 1);
1161
+ previous[0] = 1;
1162
+ for (const token of pattern) {
1163
+ const current = new Uint8Array(segments.length + 1);
1164
+ if (token === "**")
1165
+ current[0] = previous[0] ?? 0;
1166
+ for (let index = 1;index <= segments.length; index += 1) {
1167
+ current[index] = token === "**" ? previous[index] || current[index - 1] ? 1 : 0 : previous[index - 1] && wildcardSegment(token, segments[index - 1] ?? "", budget) ? 1 : 0;
1168
+ }
1169
+ previous = current;
1170
+ }
1171
+ return previous[segments.length] === 1;
1172
+ }
1173
+ function selectorGlobs(patterns, note, budget) {
1174
+ return patterns.some((pattern) => globMatches(pattern, note.id, budget) || globMatches(pattern, note.path, budget));
1175
+ }
1176
+ function isPrivate(note) {
1177
+ return note.metadata["publish"] === false;
1178
+ }
1179
+ var SLUG_UNSAFE = /[^\p{L}\p{N}._~-]+/gu;
1180
+ var SLUG_EDGE = /^[-._~]+|[-._~]+$/gu;
1181
+ function publishSlugSegment(segment) {
1182
+ const cleaned = segment.normalize("NFC").toLocaleLowerCase("en-US").replace(SLUG_UNSAFE, "-").replace(SLUG_EDGE, "").replace(/-{2,}/gu, "-");
1183
+ return cleaned === "" ? "x" : cleaned;
1184
+ }
1185
+ function derivePublishSlugs(ids) {
1186
+ const sorted = [...ids].toSorted((left, right) => left.localeCompare(right));
1187
+ const slugById = new Map;
1188
+ const used = new Set;
1189
+ for (const id of sorted) {
1190
+ const segments = id.split("/").map(publishSlugSegment);
1191
+ let slug = id === "index" ? "" : segments.join("/");
1192
+ if (slug !== "" && used.has(slug)) {
1193
+ let suffix = 2;
1194
+ while (used.has(`${slug}-${suffix}`))
1195
+ suffix += 1;
1196
+ slug = `${slug}-${suffix}`;
1197
+ }
1198
+ if (Buffer.byteLength(slug, "utf8") > MAX_PUBLISH_SLUG_BYTES) {
1199
+ throw new RangeError(`Published slug for ${JSON.stringify(id)} exceeds the byte limit.`);
1200
+ }
1201
+ used.add(slug);
1202
+ slugById.set(id, slug);
1203
+ }
1204
+ return slugById;
1205
+ }
1206
+ function publishAssetTarget(rawTarget) {
1207
+ const withoutDecoration = rawTarget.trim().split(/[?#^]/u, 1)[0] ?? "";
1208
+ if (withoutDecoration === "" || withoutDecoration.startsWith("#"))
1209
+ return;
1210
+ let decoded;
1211
+ try {
1212
+ decoded = decodeURIComponent(withoutDecoration).replace(/\\([\\()[\] ])/gu, "$1").normalize("NFC");
1213
+ } catch {
1214
+ return;
1215
+ }
1216
+ if (decoded === "" || /^(?:[a-z][a-z\d+.-]*:|\/\/)/iu.test(decoded))
1217
+ return;
1218
+ return decoded;
1219
+ }
1220
+ function selectPublishNotes(notes, analysis, input = {}) {
1221
+ const selectorCount = (input.includes?.length ?? 0) + (input.excludes?.length ?? 0) + (input.includeGlobs?.length ?? 0) + (input.excludeGlobs?.length ?? 0);
1222
+ if (selectorCount > MAX_PUBLISH_SELECTORS) {
1223
+ throw new RangeError(`Publish selectors may contain at most ${MAX_PUBLISH_SELECTORS} entries.`);
1224
+ }
1225
+ const includes = (input.includes ?? []).map((value) => normalizeSelectorPrefix(value, "--include"));
1226
+ const excludes = (input.excludes ?? []).map((value) => normalizeSelectorPrefix(value, "--exclude"));
1227
+ const includeGlobs = (input.includeGlobs ?? []).map((value) => publishGlob(value, "--include-glob"));
1228
+ const excludeGlobs = (input.excludeGlobs ?? []).map((value) => publishGlob(value, "--exclude-glob"));
1229
+ const filters = input.filters ?? [];
1230
+ const tags = input.tags ?? [];
1231
+ const repositoryScopes = input.repositoryScopes ?? [];
1232
+ const globBudget = { remaining: MAX_PUBLISH_GLOB_WORK };
1233
+ const candidates = new Set;
1234
+ const hasPositive = includes.length > 0 || includeGlobs.length > 0 || filters.length > 0 || tags.length > 0 || repositoryScopes.length > 0 || input.from !== undefined;
1235
+ for (const note of notes) {
1236
+ if (selectorIncludes(includes, note.id) || selectorGlobs(includeGlobs, note, globBudget))
1237
+ candidates.add(note.id);
1238
+ }
1239
+ if (filters.length > 0 || tags.length > 0 || repositoryScopes.length > 0) {
1240
+ for (const row of queryVault(notes, analysis, { filters, tags, repositoryScopes })) {
1241
+ candidates.add(row.id);
1242
+ }
1243
+ }
1244
+ if (input.from !== undefined) {
1245
+ const lookup = lookupNote(notes, input.from.note);
1246
+ if (lookup.kind === "missing") {
1247
+ throw new Error(`Publish seed ${JSON.stringify(input.from.note)} was not found.`);
1248
+ }
1249
+ if (lookup.kind === "ambiguous") {
1250
+ throw new Error(`Publish seed ${JSON.stringify(input.from.note)} is ambiguous: ` + lookup.candidates.map(({ path }) => path).join(", "));
1251
+ }
1252
+ const neighborhood = navigateLinks(notes, analysis, lookup.note, {
1253
+ direction: input.from.direction,
1254
+ depth: input.from.depth,
1255
+ limit: MAX_PUBLISH_FROM_NOTES
1256
+ });
1257
+ if (neighborhood.truncated) {
1258
+ throw new RangeError(`Publish neighborhood exceeds the ${MAX_PUBLISH_FROM_NOTES}-note or connection limit; narrow --depth or select explicit paths instead.`);
1259
+ }
1260
+ for (const node of neighborhood.nodes)
1261
+ candidates.add(node.id);
1262
+ }
1263
+ let excludedPrivate = 0;
1264
+ let excludedBySelection = 0;
1265
+ const selected = [];
1266
+ for (const note of notes) {
1267
+ const chosen = hasPositive ? candidates.has(note.id) : true;
1268
+ const excluded = selectorIncludes(excludes, note.id) || selectorGlobs(excludeGlobs, note, globBudget);
1269
+ if (!chosen || excluded) {
1270
+ excludedBySelection += 1;
1271
+ continue;
1272
+ }
1273
+ if (isPrivate(note)) {
1274
+ excludedPrivate += 1;
1275
+ continue;
1276
+ }
1277
+ selected.push(note);
1278
+ }
1279
+ selected.sort((left, right) => left.id.localeCompare(right.id));
1280
+ const selectedIds = new Set(selected.map(({ id }) => id));
1281
+ const slugById = derivePublishSlugs(selected.map(({ id }) => id));
1282
+ const idByPath = new Map(notes.map((note) => [note.path, note.id]));
1283
+ let droppedExternalLinks = 0;
1284
+ const links = [];
1285
+ const backlinksById = new Map;
1286
+ for (const link of analysis.contextualLinks) {
1287
+ const sourceId = idByPath.get(link.source);
1288
+ const targetId = idByPath.get(link.target);
1289
+ if (sourceId === undefined || targetId === undefined)
1290
+ continue;
1291
+ const sourceInside = selectedIds.has(sourceId);
1292
+ const targetInside = selectedIds.has(targetId);
1293
+ if (sourceInside && !targetInside)
1294
+ droppedExternalLinks += 1;
1295
+ if (!sourceInside || !targetInside)
1296
+ continue;
1297
+ links.push({ source: sourceId, target: targetId });
1298
+ const list = backlinksById.get(targetId) ?? [];
1299
+ list.push({ source: sourceId, target: targetId, line: link.line });
1300
+ backlinksById.set(targetId, list);
1301
+ }
1302
+ let droppedExternalRelations = 0;
1303
+ const relationsById = new Map;
1304
+ const relationBacklinksById = new Map;
1305
+ for (const relation of analysis.authoredRelations) {
1306
+ const sourceInside = selectedIds.has(relation.source);
1307
+ const targetInside = selectedIds.has(relation.target);
1308
+ if (sourceInside && !targetInside)
1309
+ droppedExternalRelations += 1;
1310
+ if (!sourceInside || !targetInside)
1311
+ continue;
1312
+ const outbound = relationsById.get(relation.source) ?? [];
1313
+ outbound.push(relation);
1314
+ relationsById.set(relation.source, outbound);
1315
+ const inbound = relationBacklinksById.get(relation.target) ?? [];
1316
+ inbound.push(relation);
1317
+ relationBacklinksById.set(relation.target, inbound);
1318
+ }
1319
+ const attachmentsById = new Map;
1320
+ for (const note of selected) {
1321
+ const parsed = parseLocalAttachmentReferences(note.path, note.content);
1322
+ if (parsed.references.length > 0)
1323
+ attachmentsById.set(note.id, parsed.references);
1324
+ }
1325
+ const descriptor = {
1326
+ includes: [],
1327
+ excludes: [],
1328
+ includeCount: includes.length,
1329
+ excludeCount: excludes.length,
1330
+ includeGlobCount: includeGlobs.length,
1331
+ excludeGlobCount: excludeGlobs.length,
1332
+ fromCount: input.from === undefined ? 0 : 1,
1333
+ filterCount: filters.length,
1334
+ tagCount: tags.length,
1335
+ scopeCount: repositoryScopes.length
1336
+ };
1337
+ return {
1338
+ notes: selected,
1339
+ slugById,
1340
+ selected: selectedIds,
1341
+ links,
1342
+ backlinksById,
1343
+ relationsById,
1344
+ relationBacklinksById,
1345
+ attachmentsById,
1346
+ excludedPrivate,
1347
+ excludedBySelection,
1348
+ droppedExternalLinks,
1349
+ droppedExternalRelations,
1350
+ descriptor
1351
+ };
1352
+ }
1353
+
1354
+ // src/publish.ts
1355
+ var WORDCELL_PUBLISH_GENERATOR = "@hraness/wordcell";
1356
+ function serializeSiteFile(value) {
1357
+ return `${canonicalJson2(value)}
1358
+ `;
1359
+ }
1360
+ function setSiteJson(files, path, value, parse) {
1361
+ try {
1362
+ parse(value);
1363
+ } catch (error) {
1364
+ throw new Error(`Cannot publish ${path}: ${error instanceof Error ? error.message : String(error)}`);
1365
+ }
1366
+ files.set(path, encodeUtf8(serializeSiteFile(value)));
1367
+ }
1368
+ var SITE_PATHS = Object.freeze({
1369
+ catalog: "catalog.json",
1370
+ graph: "graph.json",
1371
+ docs: "index/docs.json",
1372
+ terms: "index/terms.json",
1373
+ postingsPrefix: "index/c/",
1374
+ notePrefix: "n/",
1375
+ assetPrefix: "assets/",
1376
+ readerPrefix: "reader/"
1377
+ });
1378
+ var MAX_SITE_BYTES = 1024 * 1024 * 1024;
1379
+ var DEFAULT_PUBLISH_LIST_LIMIT = 20;
1380
+ var MAX_PUBLISH_LIST_LIMIT = 1000;
1381
+ var MAX_PUBLISH_LIST_BYTES = 16384;
1382
+ var MAX_PUBLISH_PATH_COMPONENT_BYTES = 255;
1383
+ function validateSitePaths(paths) {
1384
+ const files = new Set(paths);
1385
+ for (const path of files) {
1386
+ const segments = path.split("/");
1387
+ if (/[:\\\u0000-\u001f\u007f]/u.test(path) || segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
1388
+ throw new Error(`Cannot publish unsafe artifact path ${JSON.stringify(path)}.`);
1389
+ }
1390
+ let ancestor = "";
1391
+ for (const [index, segment] of segments.entries()) {
1392
+ if (Buffer.byteLength(segment, "utf8") > MAX_PUBLISH_PATH_COMPONENT_BYTES) {
1393
+ throw new RangeError(`Cannot publish ${JSON.stringify(path)}: a path component exceeds ${MAX_PUBLISH_PATH_COMPONENT_BYTES} UTF-8 bytes; shorten the source note or attachment filename.`);
1394
+ }
1395
+ ancestor = ancestor === "" ? segment : `${ancestor}/${segment}`;
1396
+ if (index < segments.length - 1 && files.has(ancestor)) {
1397
+ throw new Error(`Cannot publish ${JSON.stringify(path)}: ${JSON.stringify(ancestor)} is both a file and a parent directory; rename one of the conflicting source notes.`);
1398
+ }
1399
+ }
1400
+ }
1401
+ }
1402
+ function publishListLimit(value) {
1403
+ const limit = value ?? DEFAULT_PUBLISH_LIST_LIMIT;
1404
+ if (!Number.isSafeInteger(limit) || limit < 0 || limit > MAX_PUBLISH_LIST_LIMIT) {
1405
+ throw new RangeError(`--list-limit must be an integer from 0 through ${MAX_PUBLISH_LIST_LIMIT}`);
1406
+ }
1407
+ return limit;
1408
+ }
1409
+ function encodeUtf8(text) {
1410
+ return new TextEncoder().encode(text);
1411
+ }
1412
+ function sha256Hex(bytes) {
1413
+ return createHash("sha256").update(bytes).digest("hex");
1414
+ }
1415
+ function normalizeBasePath(input) {
1416
+ const raw = (input ?? "/").trim();
1417
+ const prefixed = raw.startsWith("/") ? raw : `/${raw}`;
1418
+ return prefixed.endsWith("/") ? prefixed : `${prefixed}/`;
1419
+ }
1420
+ function normalizeBaseUrl(input) {
1421
+ if (input === undefined)
1422
+ return;
1423
+ let url;
1424
+ try {
1425
+ url = new URL(input);
1426
+ } catch {
1427
+ throw new Error(`--base-url must be an absolute http(s) URL, got ${JSON.stringify(input)}.`);
1428
+ }
1429
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
1430
+ throw new Error(`--base-url must use http or https, got ${url.protocol}.`);
1431
+ }
1432
+ return url.toString().replace(/\/+$/u, "");
1433
+ }
1434
+ function boundedNormalized(text, maximumBytes) {
1435
+ const normalized = publishNormalize(text);
1436
+ if (Buffer.byteLength(normalized, "utf8") <= maximumBytes) {
1437
+ return { text: normalized, truncated: false };
1438
+ }
1439
+ const bytes = Buffer.from(normalized, "utf8");
1440
+ let end = maximumBytes;
1441
+ while (end > 0 && ((bytes[end] ?? 0) & 192) === 128)
1442
+ end -= 1;
1443
+ return { text: bytes.subarray(0, end).toString("utf8"), truncated: true };
1444
+ }
1445
+ async function projectVault(snapshot, options, io) {
1446
+ const listLimit = publishListLimit(options.listLimit);
1447
+ const selection = selectPublishNotes(snapshot.notes, snapshot.analysis, options.selection ?? {});
1448
+ if (selection.notes.length > WORDCELL_SITE_LIMITS_V1.notes) {
1449
+ throw new RangeError(`Publish selection exceeds the ${WORDCELL_SITE_LIMITS_V1.notes}-note contract limit.`);
1450
+ }
1451
+ const noteById = new Map(selection.notes.map((note) => [note.id, note]));
1452
+ const slugFor = (id) => {
1453
+ const slug = selection.slugById.get(id);
1454
+ if (slug === undefined)
1455
+ throw new Error(`Selected note ${id} has no slug.`);
1456
+ return slug;
1457
+ };
1458
+ const assetByPath = new Map;
1459
+ let assetsSkipped = 0;
1460
+ let assetsTruncated = false;
1461
+ for (const note of selection.notes) {
1462
+ const refs = selection.attachmentsById.get(note.id) ?? [];
1463
+ for (const ref of refs) {
1464
+ const path = io.resolveAssetPath(note.path, ref.target);
1465
+ if (path === undefined || assetByPath.has(path))
1466
+ continue;
1467
+ if (assetByPath.size >= WORDCELL_SITE_LIMITS_V1.assets) {
1468
+ assetsTruncated = true;
1469
+ continue;
1470
+ }
1471
+ const bytes = await io.readAsset(path);
1472
+ if (bytes === undefined || bytes.byteLength > WORDCELL_SITE_LIMITS_V1.assetBytes) {
1473
+ assetsSkipped += 1;
1474
+ continue;
1475
+ }
1476
+ const hash = sha256Hex(bytes).slice(0, 16);
1477
+ const dot = path.lastIndexOf(".");
1478
+ const extension = dot === -1 ? "bin" : path.slice(dot + 1).toLowerCase();
1479
+ assetByPath.set(path, { name: `${hash}.${extension}`, bytes });
1480
+ }
1481
+ }
1482
+ const linkEntries = (links, id, key) => links.filter((link) => link[key] === id).map((link) => {
1483
+ const other = key === "source" ? link.target : link.source;
1484
+ const note = noteById.get(other);
1485
+ return note === undefined ? undefined : { s: slugFor(note.id), t: note.title };
1486
+ }).filter((entry) => entry !== undefined).toSorted((a, b) => a.s.localeCompare(b.s) || a.t.localeCompare(b.t));
1487
+ const relationEntries = (relations, id, key) => relations.filter((relation) => relation[key] === id).map((relation) => {
1488
+ const other = key === "source" ? relation.target : relation.source;
1489
+ const note = noteById.get(other);
1490
+ return note === undefined ? undefined : { p: relation.predicate, s: slugFor(note.id), t: note.title };
1491
+ }).filter((entry) => entry !== undefined).toSorted((a, b) => a.s.localeCompare(b.s) || a.p.localeCompare(b.p) || a.t.localeCompare(b.t));
1492
+ const payloadBySlug = new Map;
1493
+ let anyTextTruncated = false;
1494
+ for (const note of selection.notes) {
1495
+ const slug = slugFor(note.id);
1496
+ const text = boundedNormalized(note.searchableText, WORDCELL_SITE_LIMITS_V1.noteTextBytes);
1497
+ anyTextTruncated ||= text.truncated;
1498
+ const payload = {
1499
+ format: WORDCELL_SITE_NOTE_FORMAT_V1,
1500
+ id: note.id,
1501
+ slug,
1502
+ title: note.title,
1503
+ aliases: note.aliases,
1504
+ ...typeof note.metadata["type"] === "string" && note.metadata["type"] !== "" ? { type: note.metadata["type"] } : {},
1505
+ tags: note.tags,
1506
+ summary: note.summary,
1507
+ text: text.text,
1508
+ textTruncated: text.truncated,
1509
+ links: linkEntries(selection.links, note.id, "source"),
1510
+ backlinks: linkEntries(selection.links, note.id, "target"),
1511
+ relations: relationEntries(selection.relationsById.get(note.id) ?? [], note.id, "source"),
1512
+ relationBacklinks: relationEntries(selection.relationBacklinksById.get(note.id) ?? [], note.id, "target")
1513
+ };
1514
+ payloadBySlug.set(slug, payload);
1515
+ }
1516
+ const resolveNote = (source) => (rawTarget) => {
1517
+ if (rawTarget === "")
1518
+ return;
1519
+ const found = lookupNote(selection.notes, rawTarget);
1520
+ const note = found.kind === "found" ? found.note : rawTarget.startsWith(".") ? (() => {
1521
+ const joined = normalizeVaultPath(posix2.join(posix2.dirname(source.path), rawTarget));
1522
+ const retry = lookupNote(selection.notes, joined);
1523
+ return retry.kind === "found" ? retry.note : undefined;
1524
+ })() : undefined;
1525
+ return note === undefined ? undefined : { slug: slugFor(note.id), title: note.title };
1526
+ };
1527
+ const bodyBySlug = new Map;
1528
+ for (const note of selection.notes) {
1529
+ const slug = slugFor(note.id);
1530
+ const rel = relativePrefix(slug);
1531
+ const ctx = {
1532
+ source: note.path,
1533
+ resolveNote: resolveNote(note),
1534
+ resolveAsset: (rawTarget, from) => {
1535
+ const decoded = publishAssetTarget(rawTarget);
1536
+ if (decoded === undefined)
1537
+ return;
1538
+ const path = io.resolveAssetPath(from, decoded);
1539
+ if (path === undefined)
1540
+ return;
1541
+ const asset = assetByPath.get(path);
1542
+ return asset === undefined ? undefined : `${rel}${SITE_PATHS.assetPrefix}${asset.name}`;
1543
+ },
1544
+ noteHref: (slugTarget) => slugTarget === "" ? rel || "./" : `${rel}n/${encodeURI(slugTarget)}/`
1545
+ };
1546
+ bodyBySlug.set(slug, renderMarkdownToHtml(note.content, ctx));
1547
+ }
1548
+ const site = {
1549
+ title: options.title?.trim() || basename(resolve(snapshot.root)) || "Vault",
1550
+ basePath: normalizeBasePath(options.basePath),
1551
+ ...options.description === undefined ? {} : { description: options.description }
1552
+ };
1553
+ const noindex = options.noindex === true;
1554
+ const catalog = {
1555
+ format: WORDCELL_SITE_CATALOG_FORMAT_V1,
1556
+ entries: selection.notes.map((note, index2) => ({
1557
+ i: index2,
1558
+ s: slugFor(note.id),
1559
+ t: note.title,
1560
+ ...typeof note.metadata["type"] === "string" && note.metadata["type"] !== "" ? { type: note.metadata["type"] } : {},
1561
+ ...note.tags.length === 0 ? {} : { g: note.tags }
1562
+ }))
1563
+ };
1564
+ const nav = siteNavFromCatalog(catalog.entries);
1565
+ const titles = new Map(catalog.entries.map((entry) => [entry.s, entry.t]));
1566
+ const edgeIndexById = new Map(selection.notes.map((note, index2) => [note.id, index2]));
1567
+ const rawEdges = [
1568
+ ...selection.links.map((link) => ({
1569
+ s: edgeIndexById.get(link.source) ?? -1,
1570
+ t: edgeIndexById.get(link.target) ?? -1,
1571
+ k: "link"
1572
+ })),
1573
+ ...selection.notes.flatMap((note) => (selection.relationsById.get(note.id) ?? []).map((relation) => ({
1574
+ s: edgeIndexById.get(relation.source) ?? -1,
1575
+ t: edgeIndexById.get(relation.target) ?? -1,
1576
+ k: "relation",
1577
+ p: relation.predicate
1578
+ })))
1579
+ ];
1580
+ const edges = rawEdges.filter((edge) => edge.s >= 0 && edge.t >= 0).toSorted((a, b) => a.s - b.s || a.t - b.t || a.k.localeCompare(b.k) || (a.p ?? "").localeCompare(b.p ?? ""));
1581
+ const files = new Map;
1582
+ for (const note of selection.notes) {
1583
+ const slug = slugFor(note.id);
1584
+ if (slug !== "") {
1585
+ setSiteJson(files, `n/${slug}.json`, payloadBySlug.get(slug), parseSiteNoteV1);
1586
+ const ctx = {
1587
+ site,
1588
+ rel: relativePrefix(slug),
1589
+ noindex,
1590
+ generator: WORDCELL_PUBLISH_GENERATOR,
1591
+ nav,
1592
+ current: slug,
1593
+ titles
1594
+ };
1595
+ const payload = payloadBySlug.get(slug);
1596
+ if (payload === undefined)
1597
+ throw new Error(`Missing payload for ${slug}.`);
1598
+ files.set(`n/${slug}/index.html`, encodeUtf8(renderNotePage(note, bodyBySlug.get(slug) ?? "", {
1599
+ links: payload.links,
1600
+ backlinks: payload.backlinks,
1601
+ relations: payload.relations,
1602
+ relationBacklinks: payload.relationBacklinks
1603
+ }, ctx)));
1604
+ }
1605
+ }
1606
+ const indexPayload = payloadBySlug.get("");
1607
+ if (indexPayload !== undefined) {
1608
+ setSiteJson(files, "index.json", indexPayload, parseSiteNoteV1);
1609
+ }
1610
+ const landingCtx = {
1611
+ site,
1612
+ rel: "",
1613
+ noindex,
1614
+ generator: WORDCELL_PUBLISH_GENERATOR,
1615
+ nav,
1616
+ current: "",
1617
+ titles
1618
+ };
1619
+ files.set("index.html", encodeUtf8(renderLandingPage(indexPayload === undefined ? undefined : bodyBySlug.get(""), catalog.entries, landingCtx)));
1620
+ files.set("graph/index.html", encodeUtf8(renderGraphPage(catalog.entries, edges.length, { ...landingCtx, rel: "../" })));
1621
+ files.set("404.html", encodeUtf8(renderNotFoundPage(landingCtx)));
1622
+ files.set("robots.txt", encodeUtf8(renderRobotsTxt(noindex)));
1623
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
1624
+ if (baseUrl !== undefined) {
1625
+ const slugs = selection.notes.map((note) => slugFor(note.id));
1626
+ files.set("sitemap.xml", encodeUtf8(renderSitemapXml(slugs, `${baseUrl}${site.basePath === "/" ? "" : site.basePath.replace(/\/$/u, "")}`)));
1627
+ }
1628
+ setSiteJson(files, SITE_PATHS.catalog, catalog, parseSiteCatalogV1);
1629
+ const index = buildSiteIndex(selection.notes, selection.slugById, {
1630
+ ...options.indexContent === undefined ? {} : { indexContent: options.indexContent }
1631
+ });
1632
+ setSiteJson(files, SITE_PATHS.docs, index.docs, parseSiteDocsV1);
1633
+ setSiteJson(files, SITE_PATHS.terms, index.terms, parseSiteTermsV1);
1634
+ for (const [shard, postings] of index.postings) {
1635
+ setSiteJson(files, `${SITE_PATHS.postingsPrefix}${shard}.json`, postings, parseSitePostingsV1);
1636
+ }
1637
+ setSiteJson(files, SITE_PATHS.graph, {
1638
+ format: WORDCELL_SITE_GRAPH_FORMAT_V1,
1639
+ edges
1640
+ }, parseSiteGraphV1);
1641
+ for (const [path, asset] of [...assetByPath.entries()].toSorted(([a], [b]) => a.localeCompare(b))) {
1642
+ files.set(`${SITE_PATHS.assetPrefix}${asset.name}`, asset.bytes);
1643
+ }
1644
+ const reader = io.readerFiles === undefined ? await defaultReaderFiles() : await io.readerFiles();
1645
+ for (const [name, bytes] of reader)
1646
+ files.set(`${SITE_PATHS.readerPrefix}${name}`, bytes);
1647
+ const generatedAt = (options.now ?? (() => new Date))();
1648
+ const digest = canonicalSha256({
1649
+ format: "hraness.wordcell.site-source.v1",
1650
+ notes: selection.notes.map((note) => ({
1651
+ id: note.id,
1652
+ slug: slugFor(note.id),
1653
+ sha256: sha256Hex(encodeUtf8(note.content))
1654
+ }))
1655
+ });
1656
+ let totalBytes = 0;
1657
+ let assetBytes = 0;
1658
+ for (const [path, bytes] of files) {
1659
+ totalBytes += bytes.byteLength;
1660
+ if (path.startsWith(SITE_PATHS.assetPrefix))
1661
+ assetBytes += bytes.byteLength;
1662
+ }
1663
+ if (assetBytes > WORDCELL_SITE_LIMITS_V1.totalAssetBytes) {
1664
+ throw new RangeError(`Published attachments exceed the ${WORDCELL_SITE_LIMITS_V1.totalAssetBytes}-byte budget.`);
1665
+ }
1666
+ if (totalBytes > MAX_SITE_BYTES) {
1667
+ throw new RangeError(`Published site exceeds the ${MAX_SITE_BYTES}-byte artifact budget.`);
1668
+ }
1669
+ const truncated = {
1670
+ ...index.termsTruncated ? { terms: true } : {},
1671
+ ...index.textTruncated || anyTextTruncated ? { text: true } : {},
1672
+ ...assetsTruncated ? { assets: true } : {}
1673
+ };
1674
+ const manifest = {
1675
+ format: WORDCELL_SITE_FORMAT_V1,
1676
+ site,
1677
+ generated: {
1678
+ by: WORDCELL_PUBLISH_GENERATOR,
1679
+ version: io.version ?? "0.0.0-dev",
1680
+ ...options.deterministic === true ? {} : { at: generatedAt.toISOString() }
1681
+ },
1682
+ source: {
1683
+ selection: selection.descriptor,
1684
+ notes: selection.notes.length,
1685
+ digest: `sha256:${digest}`
1686
+ },
1687
+ paths: SITE_PATHS,
1688
+ search: {
1689
+ mode: "exact",
1690
+ content: index.mode,
1691
+ shards: index.postings.size,
1692
+ hash: "fnv1a32-8bit"
1693
+ },
1694
+ counts: {
1695
+ notes: selection.notes.length,
1696
+ assets: assetByPath.size,
1697
+ bytes: totalBytes
1698
+ },
1699
+ truncated
1700
+ };
1701
+ setSiteJson(files, "manifest.json", manifest, parseSiteManifestV1);
1702
+ validateSitePaths(files.keys());
1703
+ const ordered = new Map([...files.entries()].toSorted(([a], [b]) => a.localeCompare(b)));
1704
+ const listedIds = [];
1705
+ let listedBytes = 0;
1706
+ for (const note of selection.notes) {
1707
+ const bytes = Buffer.byteLength(note.id, "utf8");
1708
+ if (listedIds.length >= listLimit || listedBytes + bytes > MAX_PUBLISH_LIST_BYTES)
1709
+ break;
1710
+ listedIds.push(note.id);
1711
+ listedBytes += bytes;
1712
+ }
1713
+ const report = {
1714
+ format: WORDCELL_SITE_FORMAT_V1,
1715
+ out: "",
1716
+ deterministic: options.deterministic === true,
1717
+ selection: {
1718
+ ids: listedIds,
1719
+ total: selection.notes.length,
1720
+ truncated: listedIds.length < selection.notes.length,
1721
+ digest: manifest.source.digest
1722
+ },
1723
+ files: ordered.size,
1724
+ bytes: totalBytes,
1725
+ notes: {
1726
+ published: selection.notes.length,
1727
+ excludedPrivate: selection.excludedPrivate,
1728
+ excludedBySelection: selection.excludedBySelection
1729
+ },
1730
+ links: {
1731
+ kept: selection.links.length,
1732
+ droppedExternal: selection.droppedExternalLinks + selection.droppedExternalRelations
1733
+ },
1734
+ assets: {
1735
+ count: assetByPath.size,
1736
+ bytes: assetBytes,
1737
+ skipped: assetsSkipped
1738
+ },
1739
+ search: {
1740
+ content: index.mode,
1741
+ terms: index.termsCount,
1742
+ truncated: index.termsTruncated || index.textTruncated
1743
+ }
1744
+ };
1745
+ return { files: ordered, manifest, report };
1746
+ }
1747
+ function withinRoot(root, candidate) {
1748
+ const path = relative(root, candidate);
1749
+ return path === "" || !path.startsWith(`..${sep}`) && path !== ".." && !isAbsolute(path);
1750
+ }
1751
+ async function canonicalOutputPath(path) {
1752
+ let ancestor = path;
1753
+ const suffix = [];
1754
+ for (;; ) {
1755
+ try {
1756
+ const stat = await lstat(ancestor);
1757
+ if (ancestor === path && stat.isSymbolicLink()) {
1758
+ throw new Error("--out must not be a symbolic link.");
1759
+ }
1760
+ return resolve(await realpath(ancestor), ...suffix);
1761
+ } catch (error) {
1762
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT")
1763
+ throw error;
1764
+ const parent = dirname(ancestor);
1765
+ if (parent === ancestor)
1766
+ throw error;
1767
+ suffix.unshift(basename(ancestor));
1768
+ ancestor = parent;
1769
+ }
1770
+ }
1771
+ }
1772
+ function assertSeparateOutput(root, out) {
1773
+ if (withinRoot(root, out)) {
1774
+ throw new Error("--out must not be the vault root or a directory inside it.");
1775
+ }
1776
+ if (withinRoot(out, root)) {
1777
+ throw new Error("--out must not contain the vault root; replacement would delete the vault.");
1778
+ }
1779
+ }
1780
+ async function existingPath(path) {
1781
+ try {
1782
+ return await lstat(path);
1783
+ } catch (error) {
1784
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
1785
+ return;
1786
+ throw error;
1787
+ }
1788
+ }
1789
+ function sameOutputIdentity(expected, actual) {
1790
+ if (expected === undefined || actual === undefined)
1791
+ return expected === actual;
1792
+ return actual.isDirectory() && !actual.isSymbolicLink() && expected.dev === actual.dev && expected.ino === actual.ino && expected.mtimeMs === actual.mtimeMs && expected.ctimeMs === actual.ctimeMs;
1793
+ }
1794
+ async function writeProjectedSite(root, out, files, force) {
1795
+ const expected = await existingPath(out);
1796
+ if (expected !== undefined) {
1797
+ if (!expected.isDirectory() || expected.isSymbolicLink()) {
1798
+ throw new Error(`--out ${out} exists and is not a directory.`);
1799
+ }
1800
+ if (!force && (await readdir(out)).length > 0) {
1801
+ throw new Error(`--out ${out} is not empty (pass --force to overwrite).`);
1802
+ }
1803
+ }
1804
+ const checkOutput = async () => {
1805
+ const canonical = await canonicalOutputPath(out);
1806
+ assertSeparateOutput(await realpath(root), canonical);
1807
+ if (canonical !== out || !sameOutputIdentity(expected, await existingPath(out))) {
1808
+ throw new Error(`--out ${out} changed while publishing; leave it unchanged and retry.`);
1809
+ }
1810
+ };
1811
+ await checkOutput();
1812
+ await mkdir(dirname(out), { recursive: true });
1813
+ const stageRoot = await mkdtemp(resolve(dirname(out), ".wordcell-stage-"));
1814
+ const stage = resolve(stageRoot, basename(out));
1815
+ let backupRoot;
1816
+ let backup;
1817
+ let backupHoldsOutput = false;
1818
+ let primaryError;
1819
+ try {
1820
+ await mkdir(stage);
1821
+ for (const [path, bytes] of files) {
1822
+ const absolute = resolve(stage, path);
1823
+ if (!withinRoot(stage, absolute))
1824
+ throw new Error(`Refusing to write outside staged output: ${path}`);
1825
+ await mkdir(resolve(stage, posix2.dirname(path)), { recursive: true });
1826
+ await writeFile(absolute, bytes, { flag: "wx" });
1827
+ }
1828
+ if (expected !== undefined) {
1829
+ backupRoot = await mkdtemp(resolve(dirname(out), ".wordcell-backup-"));
1830
+ backup = resolve(backupRoot, basename(out));
1831
+ }
1832
+ await checkOutput();
1833
+ if (backup !== undefined) {
1834
+ await rename(out, backup);
1835
+ backupHoldsOutput = true;
1836
+ }
1837
+ try {
1838
+ await rename(stage, out);
1839
+ } catch (promotionError) {
1840
+ if (backup !== undefined && backupHoldsOutput) {
1841
+ try {
1842
+ if (await existingPath(out) !== undefined) {
1843
+ throw new Error("another entry appeared at the output path");
1844
+ }
1845
+ await rename(backup, out);
1846
+ backupHoldsOutput = false;
1847
+ } catch (restoreError) {
1848
+ throw new Error(`Could not promote the new site or restore --out; the previous site is preserved at ${backup}. ` + `Restore failed: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}. ` + `Promotion failed: ${promotionError instanceof Error ? promotionError.message : String(promotionError)}`);
1849
+ }
1850
+ }
1851
+ throw promotionError;
1852
+ }
1853
+ if (backupRoot !== undefined) {
1854
+ try {
1855
+ await rm(backupRoot, { recursive: true });
1856
+ backupHoldsOutput = false;
1857
+ backupRoot = undefined;
1858
+ } catch (error) {
1859
+ throw new Error(`Published the new site, but previous-site backup cleanup is incomplete at ${backup}; cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
1860
+ }
1861
+ }
1862
+ } catch (error) {
1863
+ primaryError = error;
1864
+ throw error;
1865
+ } finally {
1866
+ try {
1867
+ await rm(stageRoot, { recursive: true, force: true });
1868
+ } catch (error) {
1869
+ throw new Error(`Could not remove staging directory ${stageRoot}` + `${backupHoldsOutput ? `; retained backup: ${backup}` : ""}. ` + `${error instanceof Error ? error.message : String(error)}` + `${primaryError === undefined ? "" : `; original failure: ${primaryError instanceof Error ? primaryError.message : String(primaryError)}`}`);
1870
+ }
1871
+ if (backupRoot !== undefined && !backupHoldsOutput) {
1872
+ await rm(backupRoot, { recursive: true, force: true });
1873
+ }
1874
+ }
1875
+ }
1876
+ var READER_FILES = ["reader.js", "reader.css", "theme.js"];
1877
+ async function defaultReaderFiles() {
1878
+ const packageRoot = findKbPackageRoot();
1879
+ const files = new Map;
1880
+ for (const name of READER_FILES) {
1881
+ for (const candidate of [
1882
+ `${packageRoot}/dist/publish-reader/${name}`,
1883
+ `${packageRoot}/src/publish-reader/${name}`
1884
+ ]) {
1885
+ try {
1886
+ files.set(name, new Uint8Array(await readFile(candidate)));
1887
+ break;
1888
+ } catch {
1889
+ continue;
1890
+ }
1891
+ }
1892
+ }
1893
+ return files;
1894
+ }
1895
+ async function defaultVersion() {
1896
+ try {
1897
+ const manifest = JSON.parse(await readFile(`${findKbPackageRoot()}/package.json`, "utf8"));
1898
+ if (typeof manifest === "object" && manifest !== null && "version" in manifest && typeof manifest.version === "string") {
1899
+ return manifest.version;
1900
+ }
1901
+ } catch {}
1902
+ return "0.0.0-dev";
1903
+ }
1904
+ async function publishVault(options) {
1905
+ publishListLimit(options.listLimit);
1906
+ const requestedRoot = resolve(options.root);
1907
+ const requestedOut = resolve(options.out);
1908
+ assertSeparateOutput(requestedRoot, requestedOut);
1909
+ const resolvedRoot = await realpath(requestedRoot);
1910
+ const out = await canonicalOutputPath(requestedOut);
1911
+ assertSeparateOutput(resolvedRoot, out);
1912
+ const snapshot = await scanVault(resolvedRoot, {
1913
+ mentionScope: false,
1914
+ ...options.index === undefined ? {} : { index: options.index }
1915
+ });
1916
+ const selection = selectPublishNotes(snapshot.notes, snapshot.analysis, options.selection ?? {});
1917
+ if (selection.notes.length === 0 && !options.dryRun) {
1918
+ throw new Error("Publish selection is empty; check the selectors with --dry-run before replacing output.");
1919
+ }
1920
+ const validation = await validateMarkdownAttachments({
1921
+ root: resolvedRoot,
1922
+ documents: selection.notes.map((note) => ({ path: note.path, content: note.content }))
1923
+ });
1924
+ const resolvedByRef = new Map;
1925
+ for (const attachment of validation.attachments) {
1926
+ resolvedByRef.set(`${attachment.source}\x00${attachment.target}`, attachment.path);
1927
+ }
1928
+ const missing = new Set(validation.issues.filter((issue) => issue.kind === "missing" || issue.kind === "ambiguous" || issue.kind === "case-mismatch" || issue.kind === "case-collision").map((issue) => `${issue.source}\x00${issue.target}`));
1929
+ const io = {
1930
+ resolveAssetPath: (source, target) => {
1931
+ const key = `${source}\x00${target}`;
1932
+ if (missing.has(key))
1933
+ return;
1934
+ return resolvedByRef.get(key);
1935
+ },
1936
+ readAsset: async (path) => {
1937
+ const absolute = resolve(resolvedRoot, path);
1938
+ if (!withinRoot(resolvedRoot, absolute))
1939
+ return;
1940
+ let stat;
1941
+ try {
1942
+ stat = await lstat(absolute);
1943
+ } catch {
1944
+ return;
1945
+ }
1946
+ if (!stat.isFile() || stat.isSymbolicLink())
1947
+ return;
1948
+ if (stat.size > WORDCELL_SITE_LIMITS_V1.assetBytes)
1949
+ return;
1950
+ return new Uint8Array(await readFile(absolute));
1951
+ },
1952
+ version: await defaultVersion()
1953
+ };
1954
+ const projection = await projectVault(snapshot, options, io);
1955
+ const report = { ...projection.report, out };
1956
+ if (!options.dryRun) {
1957
+ await writeProjectedSite(resolvedRoot, out, projection.files, options.force === true);
1958
+ }
1959
+ return { ...projection, report, out };
1960
+ }
1961
+ function renderPublishReportText(report, dryRun) {
1962
+ const lines = [
1963
+ `${dryRun ? "Publish plan" : "Published"} ${report.notes.published} note${report.notes.published === 1 ? "" : "s"}` + `${report.out === "" ? "" : ` \u2192 ${report.out}`}`,
1964
+ ` bytes: ${report.bytes.toLocaleString("en-US")} across ${report.files} files`,
1965
+ ` attachments: ${report.assets.count} copied, ${report.assets.skipped} skipped`,
1966
+ ` links: ${report.links.kept} kept, ${report.links.droppedExternal} dropped (targets outside the selection)`,
1967
+ ` excluded: ${report.notes.excludedPrivate} private, ${report.notes.excludedBySelection} by selection`,
1968
+ ` search: ${report.search.content} content index, ${report.search.terms.toLocaleString("en-US")} terms`
1969
+ ];
1970
+ if (report.selection.ids.length > 0) {
1971
+ lines.push(` selected ids: ${report.selection.ids.join(", ")}`);
1972
+ }
1973
+ if (report.selection.truncated) {
1974
+ lines.push(` selected ids shown: ${report.selection.ids.length} of ${report.selection.total} (use --list-limit to change the report bound)`);
1975
+ }
1976
+ lines.push(` source: ${report.selection.digest}`);
1977
+ if (report.search.truncated) {
1978
+ lines.push(" warning: search index truncated at contract limits");
1979
+ }
1980
+ return `${lines.join(`
1981
+ `)}
1982
+ `;
1983
+ }
1984
+
1985
+ export { WORDCELL_PUBLISH_GENERATOR, serializeSiteFile, MAX_SITE_BYTES, DEFAULT_PUBLISH_LIST_LIMIT, MAX_PUBLISH_LIST_LIMIT, MAX_PUBLISH_LIST_BYTES, MAX_PUBLISH_PATH_COMPONENT_BYTES, projectVault, publishVault, renderPublishReportText };