@hraness/wordcell 0.22.2 → 0.22.4

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.
@@ -31,7 +31,7 @@ import {
31
31
  import {
32
32
  publishNormalize,
33
33
  publishShardName
34
- } from "./index-3agn8scn.js";
34
+ } from "./index-23m6bbjt.js";
35
35
  import {
36
36
  parseLocalAttachmentReferences,
37
37
  validateMarkdownAttachments
@@ -49,133 +49,6 @@ import { canonicalJson as canonicalJson2, canonicalSha256 } from "@hraness/oh";
49
49
 
50
50
  // src/publish-index.ts
51
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
52
 
180
53
  // src/publish-markdown.ts
181
54
  var VOID_SLUG = /[^\p{L}\p{N}._~-]+/gu;
@@ -187,9 +60,15 @@ function escapeAttribute(value) {
187
60
  }
188
61
  function anchorId(text, used) {
189
62
  const base = text.normalize("NFC").toLocaleLowerCase("en-US").replace(VOID_SLUG, "-").replace(/^[-._~]+|[-._~]+$/gu, "") || "section";
190
- const seen = used.get(base) ?? 0;
63
+ let seen = used.get(base) ?? 0;
64
+ let id = seen === 0 ? base : `${base}-${seen + 1}`;
65
+ while (used.has(id)) {
66
+ seen += 1;
67
+ id = `${base}-${seen + 1}`;
68
+ }
191
69
  used.set(base, seen + 1);
192
- return seen === 0 ? base : `${base}-${seen + 1}`;
70
+ used.set(id, 1);
71
+ return id;
193
72
  }
194
73
  function isExternalUrl(value) {
195
74
  return /^(?:https?|mailto):/iu.test(value);
@@ -221,7 +100,41 @@ function wikiDisplay(raw) {
221
100
  label: alias ?? (fragment === "" ? target : `${target} > ${fragment}`)
222
101
  };
223
102
  }
224
- function renderInline(text, ctx) {
103
+ var FOOTNOTE_LIMITS = {
104
+ labelBytes: 128,
105
+ definitions: 256,
106
+ bodyBytes: 8192,
107
+ bodyLines: 32,
108
+ references: 2048
109
+ };
110
+ function footnoteLabel(raw) {
111
+ const label = raw.normalize("NFC");
112
+ if (label.length > FOOTNOTE_LIMITS.labelBytes || !/^[\p{L}\p{M}\p{N}._:-]+$/u.test(label))
113
+ return;
114
+ return new TextEncoder().encode(label).byteLength <= FOOTNOTE_LIMITS.labelBytes ? label : undefined;
115
+ }
116
+ function footnoteNumber(note, footnotes) {
117
+ if (note.number === undefined) {
118
+ note.number = footnotes.numbered.length + 1;
119
+ footnotes.numbered.push(note);
120
+ }
121
+ return note.number;
122
+ }
123
+ function renderFootnoteReference(raw, label, footnotes) {
124
+ const key = footnoteLabel(label);
125
+ const note = key === undefined ? undefined : footnotes.byLabel.get(key);
126
+ if (footnotes.disabled || note === undefined || note === null || note.invalid || footnotes.references >= FOOTNOTE_LIMITS.references) {
127
+ return `<span class="unresolved footnote-reference" aria-label="Unresolved footnote">${escapeHtml(raw)}</span>`;
128
+ }
129
+ const number = footnoteNumber(note, footnotes);
130
+ const id = `wordcell:footnote-ref:${number}:${note.references.length + 1}`;
131
+ note.references.push(id);
132
+ footnotes.references += 1;
133
+ if (footnotes.plain)
134
+ return "";
135
+ return `<sup><a id="${id}" href="#wordcell:footnote:${number}" role="doc-noteref" aria-label="Footnote ${number}">${number}</a></sup>`;
136
+ }
137
+ function renderInline(text, ctx, footnotes) {
225
138
  let output = "";
226
139
  let cursor = 0;
227
140
  const length = text.length;
@@ -236,7 +149,7 @@ function renderInline(text, ctx) {
236
149
  if (closing !== -1) {
237
150
  const code = rest.slice(ticks.length, closing).replace(/\s+/gu, " ");
238
151
  output += `<code>${escapeHtml(code)}</code>`;
239
- cursor += ticks.length + closing + ticks.length;
152
+ cursor += closing + ticks.length;
240
153
  continue;
241
154
  }
242
155
  pushText("`");
@@ -348,6 +261,14 @@ function renderInline(text, ctx) {
348
261
  continue;
349
262
  }
350
263
  }
264
+ if (footnotes !== undefined && rest.startsWith("[^")) {
265
+ const end = rest.indexOf("]", 2);
266
+ if (end !== -1) {
267
+ output += renderFootnoteReference(rest.slice(0, end + 1), rest.slice(2, end), footnotes);
268
+ cursor += end + 1;
269
+ continue;
270
+ }
271
+ }
351
272
  pushText("[");
352
273
  cursor += 1;
353
274
  continue;
@@ -373,7 +294,7 @@ function renderInline(text, ctx) {
373
294
  if (emphasis !== undefined && emphasis !== "_") {
374
295
  const closing = rest.indexOf(emphasis, emphasis.length);
375
296
  if (closing > emphasis.length) {
376
- const inner = renderInline(rest.slice(emphasis.length, closing), ctx);
297
+ const inner = renderInline(rest.slice(emphasis.length, closing), ctx, footnotes);
377
298
  const tag = emphasis === "**" || emphasis === "__" ? "strong" : emphasis === "~~" ? "del" : emphasis === "==" ? "mark" : "em";
378
299
  output += `<${tag}>${inner}</${tag}>`;
379
300
  cursor += closing + emphasis.length;
@@ -383,7 +304,7 @@ function renderInline(text, ctx) {
383
304
  if (emphasis === "_") {
384
305
  const closing = rest.indexOf("_", 1);
385
306
  if (closing > 1) {
386
- output += `<em>${renderInline(rest.slice(1, closing), ctx)}</em>`;
307
+ output += `<em>${renderInline(rest.slice(1, closing), ctx, footnotes)}</em>`;
387
308
  cursor += closing + 1;
388
309
  continue;
389
310
  }
@@ -509,44 +430,125 @@ function tableCells(line) {
509
430
  cells.push(current);
510
431
  return cells.map((cell) => cell.trim());
511
432
  }
512
- function renderMarkdownToHtml(content, ctx) {
433
+ function withoutComments(line, comments, continuesCode) {
434
+ let text = "";
435
+ let cursor = 0;
436
+ while (cursor < line.length) {
437
+ if (comments.code !== undefined) {
438
+ const end = line.indexOf(comments.code, cursor);
439
+ if (end === -1)
440
+ return text + line.slice(cursor);
441
+ const next = end + comments.code.length;
442
+ text += line.slice(cursor, next);
443
+ cursor = next;
444
+ delete comments.code;
445
+ continue;
446
+ }
447
+ if (comments.closing !== undefined) {
448
+ const end = line.indexOf(comments.closing, cursor);
449
+ if (end === -1)
450
+ return text;
451
+ cursor = end + comments.closing.length;
452
+ delete comments.closing;
453
+ continue;
454
+ }
455
+ const rest = line.slice(cursor);
456
+ if (rest[0] === "\\" && rest.length > 1) {
457
+ text += rest.slice(0, 2);
458
+ cursor += 2;
459
+ continue;
460
+ }
461
+ if (rest[0] === "`") {
462
+ const ticks = /^`+/u.exec(rest)?.[0] ?? "`";
463
+ const closing = rest.indexOf(ticks, ticks.length);
464
+ if (closing !== -1) {
465
+ const end = closing + ticks.length;
466
+ text += rest.slice(0, end);
467
+ cursor += end;
468
+ continue;
469
+ }
470
+ if (continuesCode?.(ticks) === true) {
471
+ comments.code = ticks;
472
+ return text + rest;
473
+ }
474
+ }
475
+ if (rest.startsWith("<!--") || rest.startsWith("%%")) {
476
+ comments.closing = rest.startsWith("<!--") ? "-->" : "%%";
477
+ cursor += comments.closing === "-->" ? 4 : 2;
478
+ continue;
479
+ }
480
+ text += rest[0] ?? "";
481
+ cursor += 1;
482
+ }
483
+ return text;
484
+ }
485
+ function paragraphBoundary(line, next) {
486
+ return line.trim() === "" || /^(?: {4}|\t|#{1,6}\s|\s{0,3}>|\s{0,3}(?:`{3,}|~{3,}))/u.test(line) || listMarker(line) !== undefined || line.includes("|") && isTableDivider(next) || /^\s{0,3}(?:(?:\*\s*){3,}|(?:-\s*){3,}|(?:_\s*){3,})$/u.test(line);
487
+ }
488
+ function hasContinuedInlineCode(lines, start, ticks) {
489
+ for (let index = start;index < lines.length; index += 1) {
490
+ const line = lines[index] ?? "";
491
+ if (paragraphBoundary(line, lines[index + 1] ?? ""))
492
+ return false;
493
+ if (line.includes(ticks))
494
+ return true;
495
+ }
496
+ return false;
497
+ }
498
+ function admitFootnote(rawLabel, body, authored, footnotes) {
499
+ const text = body.join(`
500
+ `);
501
+ const label = footnoteLabel(rawLabel);
502
+ const note = {
503
+ body: text,
504
+ authored: authored.join(`
505
+ `),
506
+ invalid: label === undefined || body.length > FOOTNOTE_LIMITS.bodyLines || text.length > FOOTNOTE_LIMITS.bodyBytes || new TextEncoder().encode(text).byteLength > FOOTNOTE_LIMITS.bodyBytes || text.trim() === "",
507
+ references: []
508
+ };
509
+ footnotes.definitions += 1;
510
+ if (footnotes.definitions > FOOTNOTE_LIMITS.definitions) {
511
+ footnotes.disabled = true;
512
+ footnotes.byLabel.clear();
513
+ }
514
+ if (!footnotes.disabled && label !== undefined) {
515
+ if (footnotes.byLabel.has(label)) {
516
+ const prior = footnotes.byLabel.get(label);
517
+ if (prior !== undefined && prior !== null)
518
+ prior.invalid = true;
519
+ note.invalid = true;
520
+ footnotes.byLabel.set(label, null);
521
+ } else {
522
+ footnotes.byLabel.set(label, note.invalid ? null : note);
523
+ }
524
+ }
525
+ return note;
526
+ }
527
+ function parseMarkdownBlocks(content, footnotes, topLevel) {
513
528
  const lines = content.split(`
514
529
  `);
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;
530
+ let index = 0;
531
+ if (topLevel && lines[0]?.trim() === "---") {
532
+ for (let cursor = 1;cursor < lines.length; cursor += 1) {
533
+ if (lines[cursor]?.trim() === "---" || lines[cursor]?.trim() === "...") {
534
+ index = cursor + 1;
520
535
  break;
521
536
  }
522
537
  }
523
538
  }
524
- const anchors = new Map;
525
- const html = [];
526
- let index = start;
527
- let inComment = false;
539
+ const blocks = [];
540
+ const comments = {};
528
541
  const paragraph = [];
529
542
  const flushParagraph = () => {
530
543
  if (paragraph.length === 0)
531
544
  return;
532
- html.push(`<p>${renderInline(paragraph.join(`
533
- `).replace(/\n/gu, " "), ctx)}</p>`);
545
+ blocks.push({ type: "paragraph", text: paragraph.join(" ") });
534
546
  paragraph.length = 0;
535
547
  };
536
548
  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);
549
+ const raw = lines[index] ?? "";
550
+ const inCode = comments.code !== undefined;
551
+ const fence = comments.closing === undefined && !inCode ? /^\s{0,3}(`{3,}|~{3,})([^`]*)$/u.exec(raw) : null;
550
552
  if (fence !== null) {
551
553
  flushParagraph();
552
554
  const marker2 = fence[1] ?? "```";
@@ -556,16 +558,58 @@ function renderMarkdownToHtml(content, ctx) {
556
558
  while (index < lines.length) {
557
559
  const candidate = lines[index] ?? "";
558
560
  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) {
561
+ if (close !== null && (close[1] ?? "")[0] === marker2[0] && (close[1] ?? "").length >= marker2.length)
560
562
  break;
561
- }
562
563
  body.push(candidate);
563
564
  index += 1;
564
565
  }
565
566
  index += 1;
566
- const className = language === "" ? "" : ` class="language-${escapeAttribute(language)}"`;
567
- html.push(`<pre><code${className}>${escapeHtml(body.join(`
568
- `))}</code></pre>`);
567
+ blocks.push({ type: "code", language, text: body.join(`
568
+ `) });
569
+ continue;
570
+ }
571
+ if (comments.closing === undefined && !inCode && /^(?: {4}|\t)/u.test(raw)) {
572
+ flushParagraph();
573
+ const body = [];
574
+ while (index < lines.length && /^(?: {4}|\t| *$)/u.test(lines[index] ?? "")) {
575
+ body.push((lines[index] ?? "").replace(/^(?: {4}|\t)/u, ""));
576
+ index += 1;
577
+ }
578
+ blocks.push({ type: "code", language: "", text: body.join(`
579
+ `).replace(/\n+$/u, "") });
580
+ continue;
581
+ }
582
+ const rawDefinition = comments.closing === undefined && !inCode ? /^ {0,3}\[\^([^\]\n]*)\]:/u.exec(raw) : null;
583
+ if (rawDefinition !== null) {
584
+ flushParagraph();
585
+ const authored = [raw];
586
+ index += 1;
587
+ while (index < lines.length) {
588
+ const next = lines[index] ?? "";
589
+ if (/^(?: {4}|\t)/u.test(next) || next.trim() === "" && /^(?: {4}|\t)/u.test(lines[index + 1] ?? "")) {
590
+ authored.push(next);
591
+ index += 1;
592
+ } else {
593
+ break;
594
+ }
595
+ }
596
+ const clean = withoutComments(authored.join(`
597
+ `), comments).split(`
598
+ `);
599
+ const definition = /^ {0,3}\[\^([^\]\n]*)\]:[ \t]*(.*)$/u.exec(clean[0] ?? "");
600
+ if (!topLevel || definition === null) {
601
+ blocks.push({ type: "literal", text: clean.join(`
602
+ `) });
603
+ } else {
604
+ const body = [definition[2] ?? "", ...clean.slice(1).map((line2) => line2.replace(/^(?: {4}|\t)/u, ""))];
605
+ blocks.push({ type: "footnote", note: admitFootnote(rawDefinition[1] ?? "", body, clean, footnotes) });
606
+ }
607
+ continue;
608
+ }
609
+ const line = comments.closing === undefined && !inCode && /^\s{0,3}>/u.test(raw) ? raw : withoutComments(raw, comments, paragraphBoundary(raw, lines[index + 1] ?? "") ? undefined : (ticks) => hasContinuedInlineCode(lines, index + 1, ticks));
610
+ if (inCode) {
611
+ paragraph.push(line);
612
+ index += 1;
569
613
  continue;
570
614
  }
571
615
  if (line.trim() === "") {
@@ -576,118 +620,231 @@ function renderMarkdownToHtml(content, ctx) {
576
620
  const heading = /^(#{1,6})\s+(.*)$/u.exec(line);
577
621
  if (heading !== null) {
578
622
  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}>`);
623
+ blocks.push({ type: "heading", level: (heading[1] ?? "#").length, text: (heading[2] ?? "").replace(/\s+#+$/u, "") });
583
624
  index += 1;
584
625
  continue;
585
626
  }
586
627
  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
628
  flushParagraph();
588
- html.push("<hr>");
629
+ blocks.push({ type: "break" });
589
630
  index += 1;
590
631
  continue;
591
632
  }
592
633
  if (/^\s{0,3}>/u.test(line)) {
593
634
  flushParagraph();
594
- const quote = [];
635
+ const quote = [line.replace(/^\s{0,3}>\s?/u, "")];
636
+ index += 1;
595
637
  while (index < lines.length && /^\s{0,3}>/u.test(lines[index] ?? "")) {
596
638
  quote.push((lines[index] ?? "").replace(/^\s{0,3}>\s?/u, ""));
597
639
  index += 1;
598
640
  }
599
- html.push(`<blockquote>${renderMarkdownToHtml(quote.join(`
600
- `), ctx)}</blockquote>`);
641
+ blocks.push({ type: "quote", blocks: parseMarkdownBlocks(quote.join(`
642
+ `), footnotes, false) });
601
643
  continue;
602
644
  }
603
645
  if (line.includes("|") && index + 1 < lines.length && isTableDivider(lines[index + 1] ?? "")) {
604
646
  flushParagraph();
605
647
  const header = tableCells(line);
606
- index += 2;
607
648
  const rows = [];
649
+ index += 2;
608
650
  while (index < lines.length && (lines[index] ?? "").includes("|") && (lines[index] ?? "").trim() !== "") {
609
- rows.push(lines[index] ?? "");
651
+ const row = withoutComments(lines[index] ?? "", comments);
652
+ if (row.trim() !== "")
653
+ rows.push(tableCells(row));
610
654
  index += 1;
611
655
  }
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>`);
656
+ blocks.push({ type: "table", header, rows });
615
657
  continue;
616
658
  }
617
659
  const marker = listMarker(line);
618
660
  if (marker !== undefined) {
619
661
  flushParagraph();
620
- const items = [];
621
- let ordered = marker.ordered;
662
+ const parts = [];
622
663
  const baseIndent = marker.indent;
623
664
  const stack = [];
665
+ let currentLine = line;
624
666
  while (index < lines.length) {
625
- const current = listMarker(lines[index] ?? "");
626
- const raw = lines[index] ?? "";
667
+ const current = listMarker(currentLine);
627
668
  if (current === undefined) {
628
- if (raw.trim() === "") {
669
+ if (currentLine.trim() === "") {
629
670
  index += 1;
630
671
  break;
631
672
  }
632
- const continuationIndent = raw.length - raw.trimStart().length;
633
- if (items.length > 0 && continuationIndent > baseIndent) {
634
- items.push(`
635
- ${renderInline(raw.trim(), ctx)}`);
673
+ const continuationIndent = currentLine.length - currentLine.trimStart().length;
674
+ if (parts.length > 0 && continuationIndent > baseIndent) {
675
+ parts.push({ type: "continuation", text: currentLine.trim() });
636
676
  index += 1;
637
- continue;
677
+ } else {
678
+ break;
638
679
  }
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"}>`);
680
+ } else {
681
+ const body = currentLine.slice(currentLine.search(/\S/u));
682
+ const itemBody = body.replace(/^(?:[-+*]|\d{1,9}[.)])\s+/u, "");
683
+ const task = /^\[([ xX])\]\s+/u.exec(itemBody);
684
+ const text = task === null ? itemBody : itemBody.slice((task[0] ?? "").length);
685
+ if (current.indent > (stack[stack.length - 1]?.indent ?? baseIndent) && stack.length < 8) {
686
+ stack.push({ indent: current.indent, ordered: current.ordered });
687
+ parts.push({ type: "open", ordered: current.ordered });
688
+ }
689
+ while (stack.length > 0 && current.indent < (stack[stack.length - 1]?.indent ?? baseIndent)) {
690
+ parts.push({ type: "close", ordered: stack.pop()?.ordered === true });
691
+ }
692
+ parts.push({ type: "item", text, ...task === null ? {} : { checked: (task[1] ?? " ").toLowerCase() === "x" } });
693
+ index += 1;
654
694
  }
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;
695
+ const next = lines[index] ?? "";
696
+ if (listMarker(next) === undefined && next.trim() !== "" && next.length - next.trimStart().length <= baseIndent)
697
+ break;
698
+ currentLine = withoutComments(next, comments);
679
699
  }
680
- html.push(`<pre><code>${escapeHtml(body.join(`
681
- `).replace(/\n+$/u, ""))}</code></pre>`);
700
+ while (stack.length > 0)
701
+ parts.push({ type: "close", ordered: stack.pop()?.ordered === true });
702
+ blocks.push({ type: "list", ordered: marker.ordered, parts });
682
703
  continue;
683
704
  }
684
705
  paragraph.push(line);
685
706
  index += 1;
686
707
  }
687
708
  flushParagraph();
688
- return html.join(`
709
+ return blocks;
710
+ }
711
+ function renderBlocks(blocks, ctx, footnotes, anchors) {
712
+ return blocks.map((block) => {
713
+ switch (block.type) {
714
+ case "paragraph":
715
+ return `<p>${renderInline(block.text, ctx, footnotes)}</p>`;
716
+ case "literal":
717
+ return `<p>${escapeHtml(block.text).replaceAll(`
718
+ `, "<br>")}</p>`;
719
+ case "heading": {
720
+ const body = renderInline(block.text, ctx, footnotes);
721
+ return `<h${block.level} id="${escapeAttribute(anchorId(stripMarkup(body), anchors))}">${body}</h${block.level}>`;
722
+ }
723
+ case "code":
724
+ return `<pre><code${block.language === "" ? "" : ` class="language-${escapeAttribute(block.language)}"`}>${escapeHtml(block.text)}</code></pre>`;
725
+ case "break":
726
+ return "<hr>";
727
+ case "quote":
728
+ return `<blockquote>${renderBlocks(block.blocks, ctx, footnotes, anchors)}</blockquote>`;
729
+ case "table": {
730
+ const header = block.header.map((cell) => `<th>${renderInline(cell, ctx, footnotes)}</th>`).join("");
731
+ const rows = block.rows.map((row) => `<tr>${row.map((cell) => `<td>${renderInline(cell, ctx, footnotes)}</td>`).join("")}</tr>`).join("");
732
+ return `<table><thead><tr>${header}</tr></thead><tbody>${rows}</tbody></table>`;
733
+ }
734
+ case "list": {
735
+ const parts = block.parts.map((part) => {
736
+ switch (part.type) {
737
+ case "open":
738
+ return `<${part.ordered ? "ol" : "ul"}>`;
739
+ case "close":
740
+ return `</${part.ordered ? "ol" : "ul"}>`;
741
+ case "continuation":
742
+ return `
743
+ ${renderInline(part.text, ctx, footnotes)}`;
744
+ case "item":
745
+ return `<li>${part.checked === undefined ? "" : `<input type="checkbox" disabled${part.checked ? " checked" : ""}> `}${renderInline(part.text, ctx, footnotes)}</li>`;
746
+ }
747
+ }).join("");
748
+ return `<${block.ordered ? "ol" : "ul"}>${parts}</${block.ordered ? "ol" : "ul"}>`;
749
+ }
750
+ case "footnote":
751
+ return footnotes.disabled || block.note.invalid ? `<p class="unresolved footnote-definition">${escapeHtml(block.note.authored).replaceAll(`
752
+ `, "<br>")}</p>` : "";
753
+ }
754
+ }).filter((html) => html !== "").join(`
689
755
  `);
690
756
  }
757
+ function renderMarkdownToHtml(content, ctx) {
758
+ const footnotes = { byLabel: new Map, numbered: [], definitions: 0, references: 0, disabled: false };
759
+ const blocks = parseMarkdownBlocks(content, footnotes, true);
760
+ const body = renderBlocks(blocks, ctx, footnotes, new Map);
761
+ if (footnotes.disabled)
762
+ return body;
763
+ for (const note of footnotes.byLabel.values()) {
764
+ if (note !== null && !note.invalid)
765
+ footnoteNumber(note, footnotes);
766
+ }
767
+ if (footnotes.numbered.length === 0)
768
+ return body;
769
+ const notes = footnotes.numbered.map((note) => {
770
+ const number = note.number;
771
+ const backlinks = note.references.map((id, index) => `<a href="#${id}" role="doc-backlink" aria-label="Back to reference ${index + 1} for footnote ${number}">\u21A9 ${index + 1}</a>`).join(" ");
772
+ const text = renderInline(note.body.replaceAll(`
773
+ `, " "), ctx);
774
+ return `<li id="wordcell:footnote:${number}" tabindex="-1">${text}${backlinks === "" ? "" : ` ${backlinks}`}</li>`;
775
+ }).join(`
776
+ `);
777
+ return `${body}${body === "" ? "" : `
778
+ `}<section class="footnotes" aria-label="Footnotes"><ol>
779
+ ${notes}
780
+ </ol></section>`;
781
+ }
782
+ function projectMarkdownText(content, description) {
783
+ const footnotes = { byLabel: new Map, numbered: [], definitions: 0, references: 0, disabled: false, plain: true };
784
+ const blocks = parseMarkdownBlocks(content, footnotes, true);
785
+ const context = {
786
+ source: "",
787
+ resolveNote: () => {
788
+ return;
789
+ },
790
+ resolveAsset: () => {
791
+ return;
792
+ },
793
+ noteHref: () => ""
794
+ };
795
+ const inline = (text2, state) => stripMarkup(renderInline(text2, context, state));
796
+ const parts = (items, state) => items.map((block) => {
797
+ switch (block.type) {
798
+ case "paragraph":
799
+ case "heading":
800
+ return inline(block.text, state);
801
+ case "literal":
802
+ case "code":
803
+ return block.text;
804
+ case "break":
805
+ return "";
806
+ case "quote":
807
+ return parts(block.blocks, state).join(`
808
+
809
+ `);
810
+ case "table":
811
+ return [block.header, ...block.rows].map((row) => row.map((cell) => inline(cell, state)).join(" ")).join(`
812
+ `);
813
+ case "list":
814
+ return block.parts.map((part) => part.type === "item" || part.type === "continuation" ? inline(part.text, state) : "").filter(Boolean).join(`
815
+ `);
816
+ case "footnote":
817
+ return state.disabled || block.note.invalid ? block.note.authored : "";
818
+ }
819
+ });
820
+ const rendered = parts(blocks, footnotes);
821
+ if (!footnotes.disabled)
822
+ for (const note of footnotes.byLabel.values()) {
823
+ if (note !== null && !note.invalid)
824
+ footnoteNumber(note, footnotes);
825
+ }
826
+ const text = [...rendered, ...footnotes.numbered.map((note) => inline(note.body.replaceAll(`
827
+ `, " ")))].filter(Boolean).join(`
828
+
829
+ `);
830
+ let preview = rendered.find((value, index) => blocks[index]?.type === "paragraph" && value.trim() !== "") ?? text;
831
+ if (description !== undefined && description.trim() !== "") {
832
+ const state = {
833
+ byLabel: new Map([...footnotes.byLabel].map(([label, note]) => [label, note === null ? null : { body: note.body, authored: note.authored, invalid: note.invalid, references: [] }])),
834
+ numbered: [],
835
+ definitions: footnotes.definitions,
836
+ references: 0,
837
+ disabled: footnotes.disabled,
838
+ plain: true
839
+ };
840
+ const authored = parts(parseMarkdownBlocks(description, state, false), state).filter(Boolean).join(`
841
+
842
+ `);
843
+ if (authored.trim() !== "")
844
+ preview = authored;
845
+ }
846
+ return { text, preview };
847
+ }
691
848
  function stripMarkup(html) {
692
849
  let stripped = "";
693
850
  let inTag = false;
@@ -702,6 +859,135 @@ function stripMarkup(html) {
702
859
  return stripped.replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&quot;", '"').replaceAll("&#39;", "'").replaceAll("&amp;", "&");
703
860
  }
704
861
 
862
+ // src/publish-index.ts
863
+ var TERM_PATTERN = /[\p{L}\p{N}][\p{L}\p{N}._/-]*/gu;
864
+ function bounded(value, maximumBytes) {
865
+ if (Buffer.byteLength(value, "utf8") <= maximumBytes)
866
+ return { text: value, truncated: false };
867
+ const bytes = Buffer.from(value, "utf8");
868
+ let end = maximumBytes;
869
+ while (end > 0 && ((bytes[end] ?? 0) & 192) === 128)
870
+ end -= 1;
871
+ return { text: bytes.subarray(0, end).toString("utf8"), truncated: true };
872
+ }
873
+ function previewText(note) {
874
+ const display = projectMarkdownText(note.content, note.properties["description"]);
875
+ const collapsed = display.preview.replace(/\s+/gu, " ").trim();
876
+ return bounded(collapsed, WORDCELL_SITE_LIMITS_V1.docPreviewBytes).text;
877
+ }
878
+ function indexableTerms(text) {
879
+ const terms = new Set;
880
+ for (const match of text.matchAll(TERM_PATTERN))
881
+ terms.add(match[0]);
882
+ return [...terms];
883
+ }
884
+ function buildSiteIndex(notes, slugById, options = {}) {
885
+ const indexContent = options.indexContent !== false;
886
+ const fields = [];
887
+ const inlineTexts = [];
888
+ let textTruncated = false;
889
+ let inlineTotal = 0;
890
+ for (const note of notes) {
891
+ const title = bounded(publishNormalize(note.title), WORDCELL_SITE_LIMITS_V1.fieldTextBytes);
892
+ const aliases = bounded(note.aliases.map(publishNormalize).join(`
893
+ `), WORDCELL_SITE_LIMITS_V1.fieldTextBytes);
894
+ const path = bounded(`${publishNormalize(note.path)}
895
+ ${publishNormalize(note.id)}`, WORDCELL_SITE_LIMITS_V1.fieldTextBytes);
896
+ const tags = bounded(note.tags.map(publishNormalize).join(`
897
+ `), WORDCELL_SITE_LIMITS_V1.fieldTextBytes);
898
+ const metadata = bounded(publishNormalize(canonicalJson(note.metadata)), WORDCELL_SITE_LIMITS_V1.fieldTextBytes);
899
+ textTruncated ||= title.truncated || aliases.truncated || path.truncated || tags.truncated || metadata.truncated;
900
+ fields.push({ t: title.text, a: aliases.text, p: path.text, g: tags.text, m: metadata.text });
901
+ if (indexContent) {
902
+ const inline = bounded(publishNormalize(note.searchableText), WORDCELL_SITE_LIMITS_V1.inlineTextBytes);
903
+ textTruncated ||= inline.truncated;
904
+ inlineTexts.push(inline.text);
905
+ inlineTotal += Buffer.byteLength(inline.text, "utf8");
906
+ }
907
+ }
908
+ const mode = !indexContent ? "none" : inlineTotal <= WORDCELL_SITE_LIMITS_V1.inlineTotalBytes ? "inline" : "shards";
909
+ const allTerms = new Set;
910
+ const contentPostings = new Map;
911
+ const docs = [];
912
+ for (const [index, note] of notes.entries()) {
913
+ const slug = slugById.get(note.id);
914
+ if (slug === undefined)
915
+ throw new Error(`Missing published slug for ${note.id}.`);
916
+ const field = fields[index];
917
+ if (field === undefined)
918
+ throw new Error(`Missing indexed fields for ${note.id}.`);
919
+ const inline = mode === "inline" ? inlineTexts[index] : undefined;
920
+ const fieldTerms = indexableTerms(`${field.t}
921
+ ${field.a}
922
+ ${field.p}
923
+ ${field.g}
924
+ ${field.m}`);
925
+ for (const term of fieldTerms)
926
+ allTerms.add(term);
927
+ if (mode !== "none") {
928
+ const content = inlineTexts[index] ?? "";
929
+ for (const term of indexableTerms(content)) {
930
+ allTerms.add(term);
931
+ if (mode === "shards") {
932
+ const list = contentPostings.get(term) ?? [];
933
+ list.push(index);
934
+ contentPostings.set(term, list);
935
+ }
936
+ }
937
+ }
938
+ docs.push({
939
+ i: index,
940
+ s: slug,
941
+ t: note.title,
942
+ p: previewText(note),
943
+ f: field,
944
+ ...inline === undefined ? {} : { x: inline }
945
+ });
946
+ }
947
+ let termsTruncated = false;
948
+ let sortedTerms = [...allTerms].toSorted((left, right) => left.localeCompare(right));
949
+ if (sortedTerms.length > WORDCELL_SITE_LIMITS_V1.indexTerms) {
950
+ sortedTerms = sortedTerms.slice(0, WORDCELL_SITE_LIMITS_V1.indexTerms);
951
+ termsTruncated = true;
952
+ }
953
+ const admitted = new Set(sortedTerms);
954
+ const postings = new Map;
955
+ if (mode === "shards") {
956
+ const grouped = new Map;
957
+ for (const [term, docIds] of [...contentPostings.entries()].toSorted(([a], [b]) => a.localeCompare(b))) {
958
+ if (!admitted.has(term))
959
+ continue;
960
+ const shard = publishShardName(term);
961
+ const bucket = grouped.get(shard) ?? Object.create(null);
962
+ bucket[term] = docIds;
963
+ grouped.set(shard, bucket);
964
+ }
965
+ for (const [shard, bucket] of [...grouped.entries()].toSorted(([a], [b]) => a.localeCompare(b))) {
966
+ postings.set(shard, {
967
+ format: WORDCELL_SITE_POSTINGS_FORMAT_V1,
968
+ shard,
969
+ postings: Object.freeze(bucket)
970
+ });
971
+ }
972
+ }
973
+ return {
974
+ docs: {
975
+ format: WORDCELL_SITE_DOCS_FORMAT_V1,
976
+ content: mode,
977
+ docs
978
+ },
979
+ terms: {
980
+ format: WORDCELL_SITE_TERMS_FORMAT_V1,
981
+ terms: sortedTerms
982
+ },
983
+ postings,
984
+ termsCount: sortedTerms.length,
985
+ termsTruncated,
986
+ textTruncated,
987
+ mode
988
+ };
989
+ }
990
+
705
991
  // src/publish-nav.ts
706
992
  var SITE_NAV_GROUP_LIMIT = 96;
707
993
  var SITE_NAV_NODE_BUDGET = 768;
@@ -1493,7 +1779,7 @@ async function projectVault(snapshot, options, io) {
1493
1779
  let anyTextTruncated = false;
1494
1780
  for (const note of selection.notes) {
1495
1781
  const slug = slugFor(note.id);
1496
- const text = boundedNormalized(note.searchableText, WORDCELL_SITE_LIMITS_V1.noteTextBytes);
1782
+ const text = boundedNormalized(projectMarkdownText(note.content).text, WORDCELL_SITE_LIMITS_V1.noteTextBytes);
1497
1783
  anyTextTruncated ||= text.truncated;
1498
1784
  const payload = {
1499
1785
  format: WORDCELL_SITE_NOTE_FORMAT_V1,