@hraness/wordcell 0.22.2 → 0.22.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/cli.js +3 -3
- package/dist/{index-hmw17zaa.js → index-a3j61wz9.js} +317 -101
- package/dist/{index-bw2r37z9.js → index-yx6p1vzc.js} +1 -1
- package/dist/index.js +1 -1
- package/dist/publish.js +1 -1
- package/dist/{support-p452p9c4.js → support-vm8fw4yc.js} +2 -2
- package/package.json +3 -3
- package/skills/wordcell/SKILL.md +1 -1
- package/src/publish-markdown.ts +345 -122
package/README.md
CHANGED
|
@@ -42,14 +42,14 @@ Git evidence, and selective publishing. [Compare the tradeoffs](https://github.c
|
|
|
42
42
|
The CLI and TypeScript SDK run with Bun. Install the versioned GitHub archive:
|
|
43
43
|
|
|
44
44
|
```sh
|
|
45
|
-
bun add --global --ignore-scripts https://github.com/hraness/wordcell/releases/download/v0.22.
|
|
45
|
+
bun add --global --ignore-scripts https://github.com/hraness/wordcell/releases/download/v0.22.3/hraness-wordcell-0.22.3.tgz
|
|
46
46
|
wordcell --help
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
Prefer npm? The same release is [mirrored there](https://www.npmjs.com/package/@hraness/wordcell):
|
|
50
50
|
|
|
51
51
|
```sh
|
|
52
|
-
npm install --global --ignore-scripts @hraness/wordcell@0.22.
|
|
52
|
+
npm install --global --ignore-scripts @hraness/wordcell@0.22.3
|
|
53
53
|
wordcell --help
|
|
54
54
|
```
|
|
55
55
|
|
|
@@ -91,7 +91,7 @@ After trying the CLI, install the public Agent Skill into a compatible agent,
|
|
|
91
91
|
such as Claude Code, Codex, Cursor, or GitHub Copilot:
|
|
92
92
|
|
|
93
93
|
```sh
|
|
94
|
-
bunx skills add hraness/wordcell#v0.22.
|
|
94
|
+
bunx skills add hraness/wordcell#v0.22.3 --skill wordcell
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
Then ask:
|
|
@@ -283,7 +283,7 @@ exist. [Graph queries and proof limits](https://github.com/hraness/wordcell/blob
|
|
|
283
283
|
Add the same immutable release to a Bun project:
|
|
284
284
|
|
|
285
285
|
```sh
|
|
286
|
-
bun add --exact --ignore-scripts https://github.com/hraness/wordcell/releases/download/v0.22.
|
|
286
|
+
bun add --exact --ignore-scripts https://github.com/hraness/wordcell/releases/download/v0.22.3/hraness-wordcell-0.22.3.tgz
|
|
287
287
|
```
|
|
288
288
|
|
|
289
289
|
The SDK provides read-only vault sessions, metadata queries, search, graph
|
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
parseArguments,
|
|
6
6
|
runExecutable,
|
|
7
7
|
usage
|
|
8
|
-
} from "./index-
|
|
8
|
+
} from "./index-yx6p1vzc.js";
|
|
9
9
|
import"./index-bcknqxrq.js";
|
|
10
10
|
import"./index-054mb7d3.js";
|
|
11
11
|
import"./index-bqtqeak3.js";
|
|
@@ -41,7 +41,7 @@ import"./index-tf4wzjew.js";
|
|
|
41
41
|
import"./index-1gwbassd.js";
|
|
42
42
|
import"./index-vnybgywh.js";
|
|
43
43
|
import"./index-1xxnjn0d.js";
|
|
44
|
-
import"./index-
|
|
44
|
+
import"./index-a3j61wz9.js";
|
|
45
45
|
import"./index-233z9wmn.js";
|
|
46
46
|
import"./index-d13v9ckt.js";
|
|
47
47
|
import"./index-48pz4jpc.js";
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
|
|
62
62
|
// src/cli.ts
|
|
63
63
|
if (import.meta.main) {
|
|
64
|
-
const { standaloneSupportEnvironment, isUsefulSupportResult, runProductSupportCommand, showProductSupportInvitation } = await import("./support-
|
|
64
|
+
const { standaloneSupportEnvironment, isUsefulSupportResult, runProductSupportCommand, showProductSupportInvitation } = await import("./support-vm8fw4yc.js");
|
|
65
65
|
const env = standaloneSupportEnvironment();
|
|
66
66
|
const args = process.argv.slice(2);
|
|
67
67
|
if (args[0] === "support") {
|
|
@@ -187,9 +187,15 @@ function escapeAttribute(value) {
|
|
|
187
187
|
}
|
|
188
188
|
function anchorId(text, used) {
|
|
189
189
|
const base = text.normalize("NFC").toLocaleLowerCase("en-US").replace(VOID_SLUG, "-").replace(/^[-._~]+|[-._~]+$/gu, "") || "section";
|
|
190
|
-
|
|
190
|
+
let seen = used.get(base) ?? 0;
|
|
191
|
+
let id = seen === 0 ? base : `${base}-${seen + 1}`;
|
|
192
|
+
while (used.has(id)) {
|
|
193
|
+
seen += 1;
|
|
194
|
+
id = `${base}-${seen + 1}`;
|
|
195
|
+
}
|
|
191
196
|
used.set(base, seen + 1);
|
|
192
|
-
|
|
197
|
+
used.set(id, 1);
|
|
198
|
+
return id;
|
|
193
199
|
}
|
|
194
200
|
function isExternalUrl(value) {
|
|
195
201
|
return /^(?:https?|mailto):/iu.test(value);
|
|
@@ -221,7 +227,39 @@ function wikiDisplay(raw) {
|
|
|
221
227
|
label: alias ?? (fragment === "" ? target : `${target} > ${fragment}`)
|
|
222
228
|
};
|
|
223
229
|
}
|
|
224
|
-
|
|
230
|
+
var FOOTNOTE_LIMITS = {
|
|
231
|
+
labelBytes: 128,
|
|
232
|
+
definitions: 256,
|
|
233
|
+
bodyBytes: 8192,
|
|
234
|
+
bodyLines: 32,
|
|
235
|
+
references: 2048
|
|
236
|
+
};
|
|
237
|
+
function footnoteLabel(raw) {
|
|
238
|
+
const label = raw.normalize("NFC");
|
|
239
|
+
if (label.length > FOOTNOTE_LIMITS.labelBytes || !/^[\p{L}\p{M}\p{N}._:-]+$/u.test(label))
|
|
240
|
+
return;
|
|
241
|
+
return new TextEncoder().encode(label).byteLength <= FOOTNOTE_LIMITS.labelBytes ? label : undefined;
|
|
242
|
+
}
|
|
243
|
+
function footnoteNumber(note, footnotes) {
|
|
244
|
+
if (note.number === undefined) {
|
|
245
|
+
note.number = footnotes.numbered.length + 1;
|
|
246
|
+
footnotes.numbered.push(note);
|
|
247
|
+
}
|
|
248
|
+
return note.number;
|
|
249
|
+
}
|
|
250
|
+
function renderFootnoteReference(raw, label, footnotes) {
|
|
251
|
+
const key = footnoteLabel(label);
|
|
252
|
+
const note = key === undefined ? undefined : footnotes.byLabel.get(key);
|
|
253
|
+
if (footnotes.disabled || note === undefined || note === null || note.invalid || footnotes.references >= FOOTNOTE_LIMITS.references) {
|
|
254
|
+
return `<span class="unresolved footnote-reference" aria-label="Unresolved footnote">${escapeHtml(raw)}</span>`;
|
|
255
|
+
}
|
|
256
|
+
const number = footnoteNumber(note, footnotes);
|
|
257
|
+
const id = `wordcell:footnote-ref:${number}:${note.references.length + 1}`;
|
|
258
|
+
note.references.push(id);
|
|
259
|
+
footnotes.references += 1;
|
|
260
|
+
return `<sup><a id="${id}" href="#wordcell:footnote:${number}" role="doc-noteref" aria-label="Footnote ${number}">${number}</a></sup>`;
|
|
261
|
+
}
|
|
262
|
+
function renderInline(text, ctx, footnotes) {
|
|
225
263
|
let output = "";
|
|
226
264
|
let cursor = 0;
|
|
227
265
|
const length = text.length;
|
|
@@ -236,7 +274,7 @@ function renderInline(text, ctx) {
|
|
|
236
274
|
if (closing !== -1) {
|
|
237
275
|
const code = rest.slice(ticks.length, closing).replace(/\s+/gu, " ");
|
|
238
276
|
output += `<code>${escapeHtml(code)}</code>`;
|
|
239
|
-
cursor +=
|
|
277
|
+
cursor += closing + ticks.length;
|
|
240
278
|
continue;
|
|
241
279
|
}
|
|
242
280
|
pushText("`");
|
|
@@ -348,6 +386,14 @@ function renderInline(text, ctx) {
|
|
|
348
386
|
continue;
|
|
349
387
|
}
|
|
350
388
|
}
|
|
389
|
+
if (footnotes !== undefined && rest.startsWith("[^")) {
|
|
390
|
+
const end = rest.indexOf("]", 2);
|
|
391
|
+
if (end !== -1) {
|
|
392
|
+
output += renderFootnoteReference(rest.slice(0, end + 1), rest.slice(2, end), footnotes);
|
|
393
|
+
cursor += end + 1;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
351
397
|
pushText("[");
|
|
352
398
|
cursor += 1;
|
|
353
399
|
continue;
|
|
@@ -373,7 +419,7 @@ function renderInline(text, ctx) {
|
|
|
373
419
|
if (emphasis !== undefined && emphasis !== "_") {
|
|
374
420
|
const closing = rest.indexOf(emphasis, emphasis.length);
|
|
375
421
|
if (closing > emphasis.length) {
|
|
376
|
-
const inner = renderInline(rest.slice(emphasis.length, closing), ctx);
|
|
422
|
+
const inner = renderInline(rest.slice(emphasis.length, closing), ctx, footnotes);
|
|
377
423
|
const tag = emphasis === "**" || emphasis === "__" ? "strong" : emphasis === "~~" ? "del" : emphasis === "==" ? "mark" : "em";
|
|
378
424
|
output += `<${tag}>${inner}</${tag}>`;
|
|
379
425
|
cursor += closing + emphasis.length;
|
|
@@ -383,7 +429,7 @@ function renderInline(text, ctx) {
|
|
|
383
429
|
if (emphasis === "_") {
|
|
384
430
|
const closing = rest.indexOf("_", 1);
|
|
385
431
|
if (closing > 1) {
|
|
386
|
-
output += `<em>${renderInline(rest.slice(1, closing), ctx)}</em>`;
|
|
432
|
+
output += `<em>${renderInline(rest.slice(1, closing), ctx, footnotes)}</em>`;
|
|
387
433
|
cursor += closing + 1;
|
|
388
434
|
continue;
|
|
389
435
|
}
|
|
@@ -509,44 +555,125 @@ function tableCells(line) {
|
|
|
509
555
|
cells.push(current);
|
|
510
556
|
return cells.map((cell) => cell.trim());
|
|
511
557
|
}
|
|
512
|
-
function
|
|
558
|
+
function withoutComments(line, comments, continuesCode) {
|
|
559
|
+
let text = "";
|
|
560
|
+
let cursor = 0;
|
|
561
|
+
while (cursor < line.length) {
|
|
562
|
+
if (comments.code !== undefined) {
|
|
563
|
+
const end = line.indexOf(comments.code, cursor);
|
|
564
|
+
if (end === -1)
|
|
565
|
+
return text + line.slice(cursor);
|
|
566
|
+
const next = end + comments.code.length;
|
|
567
|
+
text += line.slice(cursor, next);
|
|
568
|
+
cursor = next;
|
|
569
|
+
delete comments.code;
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (comments.closing !== undefined) {
|
|
573
|
+
const end = line.indexOf(comments.closing, cursor);
|
|
574
|
+
if (end === -1)
|
|
575
|
+
return text;
|
|
576
|
+
cursor = end + comments.closing.length;
|
|
577
|
+
delete comments.closing;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const rest = line.slice(cursor);
|
|
581
|
+
if (rest[0] === "\\" && rest.length > 1) {
|
|
582
|
+
text += rest.slice(0, 2);
|
|
583
|
+
cursor += 2;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (rest[0] === "`") {
|
|
587
|
+
const ticks = /^`+/u.exec(rest)?.[0] ?? "`";
|
|
588
|
+
const closing = rest.indexOf(ticks, ticks.length);
|
|
589
|
+
if (closing !== -1) {
|
|
590
|
+
const end = closing + ticks.length;
|
|
591
|
+
text += rest.slice(0, end);
|
|
592
|
+
cursor += end;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (continuesCode?.(ticks) === true) {
|
|
596
|
+
comments.code = ticks;
|
|
597
|
+
return text + rest;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (rest.startsWith("<!--") || rest.startsWith("%%")) {
|
|
601
|
+
comments.closing = rest.startsWith("<!--") ? "-->" : "%%";
|
|
602
|
+
cursor += comments.closing === "-->" ? 4 : 2;
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
text += rest[0] ?? "";
|
|
606
|
+
cursor += 1;
|
|
607
|
+
}
|
|
608
|
+
return text;
|
|
609
|
+
}
|
|
610
|
+
function paragraphBoundary(line, next) {
|
|
611
|
+
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);
|
|
612
|
+
}
|
|
613
|
+
function hasContinuedInlineCode(lines, start, ticks) {
|
|
614
|
+
for (let index = start;index < lines.length; index += 1) {
|
|
615
|
+
const line = lines[index] ?? "";
|
|
616
|
+
if (paragraphBoundary(line, lines[index + 1] ?? ""))
|
|
617
|
+
return false;
|
|
618
|
+
if (line.includes(ticks))
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
function admitFootnote(rawLabel, body, authored, footnotes) {
|
|
624
|
+
const text = body.join(`
|
|
625
|
+
`);
|
|
626
|
+
const label = footnoteLabel(rawLabel);
|
|
627
|
+
const note = {
|
|
628
|
+
body: text,
|
|
629
|
+
authored: authored.join(`
|
|
630
|
+
`),
|
|
631
|
+
invalid: label === undefined || body.length > FOOTNOTE_LIMITS.bodyLines || text.length > FOOTNOTE_LIMITS.bodyBytes || new TextEncoder().encode(text).byteLength > FOOTNOTE_LIMITS.bodyBytes || text.trim() === "",
|
|
632
|
+
references: []
|
|
633
|
+
};
|
|
634
|
+
footnotes.definitions += 1;
|
|
635
|
+
if (footnotes.definitions > FOOTNOTE_LIMITS.definitions) {
|
|
636
|
+
footnotes.disabled = true;
|
|
637
|
+
footnotes.byLabel.clear();
|
|
638
|
+
}
|
|
639
|
+
if (!footnotes.disabled && label !== undefined) {
|
|
640
|
+
if (footnotes.byLabel.has(label)) {
|
|
641
|
+
const prior = footnotes.byLabel.get(label);
|
|
642
|
+
if (prior !== undefined && prior !== null)
|
|
643
|
+
prior.invalid = true;
|
|
644
|
+
note.invalid = true;
|
|
645
|
+
footnotes.byLabel.set(label, null);
|
|
646
|
+
} else {
|
|
647
|
+
footnotes.byLabel.set(label, note.invalid ? null : note);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return note;
|
|
651
|
+
}
|
|
652
|
+
function parseMarkdownBlocks(content, footnotes, topLevel) {
|
|
513
653
|
const lines = content.split(`
|
|
514
654
|
`);
|
|
515
|
-
let
|
|
516
|
-
if (lines[0]?.trim() === "---") {
|
|
517
|
-
for (let
|
|
518
|
-
if (lines[
|
|
519
|
-
|
|
655
|
+
let index = 0;
|
|
656
|
+
if (topLevel && lines[0]?.trim() === "---") {
|
|
657
|
+
for (let cursor = 1;cursor < lines.length; cursor += 1) {
|
|
658
|
+
if (lines[cursor]?.trim() === "---" || lines[cursor]?.trim() === "...") {
|
|
659
|
+
index = cursor + 1;
|
|
520
660
|
break;
|
|
521
661
|
}
|
|
522
662
|
}
|
|
523
663
|
}
|
|
524
|
-
const
|
|
525
|
-
const
|
|
526
|
-
let index = start;
|
|
527
|
-
let inComment = false;
|
|
664
|
+
const blocks = [];
|
|
665
|
+
const comments = {};
|
|
528
666
|
const paragraph = [];
|
|
529
667
|
const flushParagraph = () => {
|
|
530
668
|
if (paragraph.length === 0)
|
|
531
669
|
return;
|
|
532
|
-
|
|
533
|
-
`).replace(/\n/gu, " "), ctx)}</p>`);
|
|
670
|
+
blocks.push({ type: "paragraph", text: paragraph.join(" ") });
|
|
534
671
|
paragraph.length = 0;
|
|
535
672
|
};
|
|
536
673
|
while (index < lines.length) {
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
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);
|
|
674
|
+
const raw = lines[index] ?? "";
|
|
675
|
+
const inCode = comments.code !== undefined;
|
|
676
|
+
const fence = comments.closing === undefined && !inCode ? /^\s{0,3}(`{3,}|~{3,})([^`]*)$/u.exec(raw) : null;
|
|
550
677
|
if (fence !== null) {
|
|
551
678
|
flushParagraph();
|
|
552
679
|
const marker2 = fence[1] ?? "```";
|
|
@@ -556,16 +683,58 @@ function renderMarkdownToHtml(content, ctx) {
|
|
|
556
683
|
while (index < lines.length) {
|
|
557
684
|
const candidate = lines[index] ?? "";
|
|
558
685
|
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)
|
|
686
|
+
if (close !== null && (close[1] ?? "")[0] === marker2[0] && (close[1] ?? "").length >= marker2.length)
|
|
560
687
|
break;
|
|
561
|
-
}
|
|
562
688
|
body.push(candidate);
|
|
563
689
|
index += 1;
|
|
564
690
|
}
|
|
565
691
|
index += 1;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
692
|
+
blocks.push({ type: "code", language, text: body.join(`
|
|
693
|
+
`) });
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (comments.closing === undefined && !inCode && /^(?: {4}|\t)/u.test(raw)) {
|
|
697
|
+
flushParagraph();
|
|
698
|
+
const body = [];
|
|
699
|
+
while (index < lines.length && /^(?: {4}|\t| *$)/u.test(lines[index] ?? "")) {
|
|
700
|
+
body.push((lines[index] ?? "").replace(/^(?: {4}|\t)/u, ""));
|
|
701
|
+
index += 1;
|
|
702
|
+
}
|
|
703
|
+
blocks.push({ type: "code", language: "", text: body.join(`
|
|
704
|
+
`).replace(/\n+$/u, "") });
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const rawDefinition = comments.closing === undefined && !inCode ? /^ {0,3}\[\^([^\]\n]*)\]:/u.exec(raw) : null;
|
|
708
|
+
if (rawDefinition !== null) {
|
|
709
|
+
flushParagraph();
|
|
710
|
+
const authored = [raw];
|
|
711
|
+
index += 1;
|
|
712
|
+
while (index < lines.length) {
|
|
713
|
+
const next = lines[index] ?? "";
|
|
714
|
+
if (/^(?: {4}|\t)/u.test(next) || next.trim() === "" && /^(?: {4}|\t)/u.test(lines[index + 1] ?? "")) {
|
|
715
|
+
authored.push(next);
|
|
716
|
+
index += 1;
|
|
717
|
+
} else {
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const clean = withoutComments(authored.join(`
|
|
722
|
+
`), comments).split(`
|
|
723
|
+
`);
|
|
724
|
+
const definition = /^ {0,3}\[\^([^\]\n]*)\]:[ \t]*(.*)$/u.exec(clean[0] ?? "");
|
|
725
|
+
if (!topLevel || definition === null) {
|
|
726
|
+
blocks.push({ type: "literal", text: clean.join(`
|
|
727
|
+
`) });
|
|
728
|
+
} else {
|
|
729
|
+
const body = [definition[2] ?? "", ...clean.slice(1).map((line2) => line2.replace(/^(?: {4}|\t)/u, ""))];
|
|
730
|
+
blocks.push({ type: "footnote", note: admitFootnote(rawDefinition[1] ?? "", body, clean, footnotes) });
|
|
731
|
+
}
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
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));
|
|
735
|
+
if (inCode) {
|
|
736
|
+
paragraph.push(line);
|
|
737
|
+
index += 1;
|
|
569
738
|
continue;
|
|
570
739
|
}
|
|
571
740
|
if (line.trim() === "") {
|
|
@@ -576,117 +745,164 @@ function renderMarkdownToHtml(content, ctx) {
|
|
|
576
745
|
const heading = /^(#{1,6})\s+(.*)$/u.exec(line);
|
|
577
746
|
if (heading !== null) {
|
|
578
747
|
flushParagraph();
|
|
579
|
-
|
|
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}>`);
|
|
748
|
+
blocks.push({ type: "heading", level: (heading[1] ?? "#").length, text: (heading[2] ?? "").replace(/\s+#+$/u, "") });
|
|
583
749
|
index += 1;
|
|
584
750
|
continue;
|
|
585
751
|
}
|
|
586
752
|
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
753
|
flushParagraph();
|
|
588
|
-
|
|
754
|
+
blocks.push({ type: "break" });
|
|
589
755
|
index += 1;
|
|
590
756
|
continue;
|
|
591
757
|
}
|
|
592
758
|
if (/^\s{0,3}>/u.test(line)) {
|
|
593
759
|
flushParagraph();
|
|
594
|
-
const quote = [];
|
|
760
|
+
const quote = [line.replace(/^\s{0,3}>\s?/u, "")];
|
|
761
|
+
index += 1;
|
|
595
762
|
while (index < lines.length && /^\s{0,3}>/u.test(lines[index] ?? "")) {
|
|
596
763
|
quote.push((lines[index] ?? "").replace(/^\s{0,3}>\s?/u, ""));
|
|
597
764
|
index += 1;
|
|
598
765
|
}
|
|
599
|
-
|
|
600
|
-
`),
|
|
766
|
+
blocks.push({ type: "quote", blocks: parseMarkdownBlocks(quote.join(`
|
|
767
|
+
`), footnotes, false) });
|
|
601
768
|
continue;
|
|
602
769
|
}
|
|
603
770
|
if (line.includes("|") && index + 1 < lines.length && isTableDivider(lines[index + 1] ?? "")) {
|
|
604
771
|
flushParagraph();
|
|
605
772
|
const header = tableCells(line);
|
|
606
|
-
index += 2;
|
|
607
773
|
const rows = [];
|
|
774
|
+
index += 2;
|
|
608
775
|
while (index < lines.length && (lines[index] ?? "").includes("|") && (lines[index] ?? "").trim() !== "") {
|
|
609
|
-
|
|
776
|
+
const row = withoutComments(lines[index] ?? "", comments);
|
|
777
|
+
if (row.trim() !== "")
|
|
778
|
+
rows.push(tableCells(row));
|
|
610
779
|
index += 1;
|
|
611
780
|
}
|
|
612
|
-
|
|
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>`);
|
|
781
|
+
blocks.push({ type: "table", header, rows });
|
|
615
782
|
continue;
|
|
616
783
|
}
|
|
617
784
|
const marker = listMarker(line);
|
|
618
785
|
if (marker !== undefined) {
|
|
619
786
|
flushParagraph();
|
|
620
|
-
const
|
|
621
|
-
let ordered = marker.ordered;
|
|
787
|
+
const parts = [];
|
|
622
788
|
const baseIndent = marker.indent;
|
|
623
789
|
const stack = [];
|
|
790
|
+
let currentLine = line;
|
|
624
791
|
while (index < lines.length) {
|
|
625
|
-
const current = listMarker(
|
|
626
|
-
const raw = lines[index] ?? "";
|
|
792
|
+
const current = listMarker(currentLine);
|
|
627
793
|
if (current === undefined) {
|
|
628
|
-
if (
|
|
794
|
+
if (currentLine.trim() === "") {
|
|
629
795
|
index += 1;
|
|
630
796
|
break;
|
|
631
797
|
}
|
|
632
|
-
const continuationIndent =
|
|
633
|
-
if (
|
|
634
|
-
|
|
635
|
-
${renderInline(raw.trim(), ctx)}`);
|
|
798
|
+
const continuationIndent = currentLine.length - currentLine.trimStart().length;
|
|
799
|
+
if (parts.length > 0 && continuationIndent > baseIndent) {
|
|
800
|
+
parts.push({ type: "continuation", text: currentLine.trim() });
|
|
636
801
|
index += 1;
|
|
637
|
-
|
|
802
|
+
} else {
|
|
803
|
+
break;
|
|
638
804
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
stack.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
items.push(`</${popped?.ordered === true ? "ol" : "ul"}>`);
|
|
805
|
+
} else {
|
|
806
|
+
const body = currentLine.slice(currentLine.search(/\S/u));
|
|
807
|
+
const itemBody = body.replace(/^(?:[-+*]|\d{1,9}[.)])\s+/u, "");
|
|
808
|
+
const task = /^\[([ xX])\]\s+/u.exec(itemBody);
|
|
809
|
+
const text = task === null ? itemBody : itemBody.slice((task[0] ?? "").length);
|
|
810
|
+
if (current.indent > (stack[stack.length - 1]?.indent ?? baseIndent) && stack.length < 8) {
|
|
811
|
+
stack.push({ indent: current.indent, ordered: current.ordered });
|
|
812
|
+
parts.push({ type: "open", ordered: current.ordered });
|
|
813
|
+
}
|
|
814
|
+
while (stack.length > 0 && current.indent < (stack[stack.length - 1]?.indent ?? baseIndent)) {
|
|
815
|
+
parts.push({ type: "close", ordered: stack.pop()?.ordered === true });
|
|
816
|
+
}
|
|
817
|
+
parts.push({ type: "item", text, ...task === null ? {} : { checked: (task[1] ?? " ").toLowerCase() === "x" } });
|
|
818
|
+
index += 1;
|
|
654
819
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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;
|
|
820
|
+
const next = lines[index] ?? "";
|
|
821
|
+
if (listMarker(next) === undefined && next.trim() !== "" && next.length - next.trimStart().length <= baseIndent)
|
|
822
|
+
break;
|
|
823
|
+
currentLine = withoutComments(next, comments);
|
|
679
824
|
}
|
|
680
|
-
|
|
681
|
-
|
|
825
|
+
while (stack.length > 0)
|
|
826
|
+
parts.push({ type: "close", ordered: stack.pop()?.ordered === true });
|
|
827
|
+
blocks.push({ type: "list", ordered: marker.ordered, parts });
|
|
682
828
|
continue;
|
|
683
829
|
}
|
|
684
830
|
paragraph.push(line);
|
|
685
831
|
index += 1;
|
|
686
832
|
}
|
|
687
833
|
flushParagraph();
|
|
688
|
-
return
|
|
834
|
+
return blocks;
|
|
835
|
+
}
|
|
836
|
+
function renderBlocks(blocks, ctx, footnotes, anchors) {
|
|
837
|
+
return blocks.map((block) => {
|
|
838
|
+
switch (block.type) {
|
|
839
|
+
case "paragraph":
|
|
840
|
+
return `<p>${renderInline(block.text, ctx, footnotes)}</p>`;
|
|
841
|
+
case "literal":
|
|
842
|
+
return `<p>${escapeHtml(block.text).replaceAll(`
|
|
843
|
+
`, "<br>")}</p>`;
|
|
844
|
+
case "heading": {
|
|
845
|
+
const body = renderInline(block.text, ctx, footnotes);
|
|
846
|
+
return `<h${block.level} id="${escapeAttribute(anchorId(stripMarkup(body), anchors))}">${body}</h${block.level}>`;
|
|
847
|
+
}
|
|
848
|
+
case "code":
|
|
849
|
+
return `<pre><code${block.language === "" ? "" : ` class="language-${escapeAttribute(block.language)}"`}>${escapeHtml(block.text)}</code></pre>`;
|
|
850
|
+
case "break":
|
|
851
|
+
return "<hr>";
|
|
852
|
+
case "quote":
|
|
853
|
+
return `<blockquote>${renderBlocks(block.blocks, ctx, footnotes, anchors)}</blockquote>`;
|
|
854
|
+
case "table": {
|
|
855
|
+
const header = block.header.map((cell) => `<th>${renderInline(cell, ctx, footnotes)}</th>`).join("");
|
|
856
|
+
const rows = block.rows.map((row) => `<tr>${row.map((cell) => `<td>${renderInline(cell, ctx, footnotes)}</td>`).join("")}</tr>`).join("");
|
|
857
|
+
return `<table><thead><tr>${header}</tr></thead><tbody>${rows}</tbody></table>`;
|
|
858
|
+
}
|
|
859
|
+
case "list": {
|
|
860
|
+
const parts = block.parts.map((part) => {
|
|
861
|
+
switch (part.type) {
|
|
862
|
+
case "open":
|
|
863
|
+
return `<${part.ordered ? "ol" : "ul"}>`;
|
|
864
|
+
case "close":
|
|
865
|
+
return `</${part.ordered ? "ol" : "ul"}>`;
|
|
866
|
+
case "continuation":
|
|
867
|
+
return `
|
|
868
|
+
${renderInline(part.text, ctx, footnotes)}`;
|
|
869
|
+
case "item":
|
|
870
|
+
return `<li>${part.checked === undefined ? "" : `<input type="checkbox" disabled${part.checked ? " checked" : ""}> `}${renderInline(part.text, ctx, footnotes)}</li>`;
|
|
871
|
+
}
|
|
872
|
+
}).join("");
|
|
873
|
+
return `<${block.ordered ? "ol" : "ul"}>${parts}</${block.ordered ? "ol" : "ul"}>`;
|
|
874
|
+
}
|
|
875
|
+
case "footnote":
|
|
876
|
+
return footnotes.disabled || block.note.invalid ? `<p class="unresolved footnote-definition">${escapeHtml(block.note.authored).replaceAll(`
|
|
877
|
+
`, "<br>")}</p>` : "";
|
|
878
|
+
}
|
|
879
|
+
}).filter((html) => html !== "").join(`
|
|
880
|
+
`);
|
|
881
|
+
}
|
|
882
|
+
function renderMarkdownToHtml(content, ctx) {
|
|
883
|
+
const footnotes = { byLabel: new Map, numbered: [], definitions: 0, references: 0, disabled: false };
|
|
884
|
+
const blocks = parseMarkdownBlocks(content, footnotes, true);
|
|
885
|
+
const body = renderBlocks(blocks, ctx, footnotes, new Map);
|
|
886
|
+
if (footnotes.disabled)
|
|
887
|
+
return body;
|
|
888
|
+
for (const note of footnotes.byLabel.values()) {
|
|
889
|
+
if (note !== null && !note.invalid)
|
|
890
|
+
footnoteNumber(note, footnotes);
|
|
891
|
+
}
|
|
892
|
+
if (footnotes.numbered.length === 0)
|
|
893
|
+
return body;
|
|
894
|
+
const notes = footnotes.numbered.map((note) => {
|
|
895
|
+
const number = note.number;
|
|
896
|
+
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(" ");
|
|
897
|
+
const text = renderInline(note.body.replaceAll(`
|
|
898
|
+
`, " "), ctx);
|
|
899
|
+
return `<li id="wordcell:footnote:${number}" tabindex="-1">${text}${backlinks === "" ? "" : ` ${backlinks}`}</li>`;
|
|
900
|
+
}).join(`
|
|
689
901
|
`);
|
|
902
|
+
return `${body}${body === "" ? "" : `
|
|
903
|
+
`}<section class="footnotes" aria-label="Footnotes"><ol>
|
|
904
|
+
${notes}
|
|
905
|
+
</ol></section>`;
|
|
690
906
|
}
|
|
691
907
|
function stripMarkup(html) {
|
|
692
908
|
let stripped = "";
|
package/dist/index.js
CHANGED
package/dist/publish.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
parseArguments as parseArguments2,
|
|
4
4
|
parseUrlMetadataArguments
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-yx6p1vzc.js";
|
|
6
6
|
import"./index-bcknqxrq.js";
|
|
7
7
|
import {
|
|
8
8
|
parsePdfArguments
|
|
@@ -42,7 +42,7 @@ import"./index-tf4wzjew.js";
|
|
|
42
42
|
import"./index-1gwbassd.js";
|
|
43
43
|
import"./index-vnybgywh.js";
|
|
44
44
|
import"./index-1xxnjn0d.js";
|
|
45
|
-
import"./index-
|
|
45
|
+
import"./index-a3j61wz9.js";
|
|
46
46
|
import"./index-233z9wmn.js";
|
|
47
47
|
import"./index-d13v9ckt.js";
|
|
48
48
|
import"./index-48pz4jpc.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hraness/wordcell",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.3",
|
|
4
4
|
"description": "A local Markdown knowledge base with superpowers. Save decisions, sources, and plans as files you own, then recover them through exact and semantic search, typed relationships, backlinks, and Git history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -479,8 +479,8 @@
|
|
|
479
479
|
"check": "bun run check:worker && bun run check:effect && bun run check:workflows && bun run check:portfolio-inventory && bun run check:installed-command-docs && bun run typecheck && bun run build && bun run test:package && bun run test && bun run url-metadata:tool:check",
|
|
480
480
|
"prepack": "bun run check",
|
|
481
481
|
"check:effect": "bun run scripts/check-effect-policy.ts",
|
|
482
|
-
"kb:search": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.22.
|
|
483
|
-
"kb:search:local": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.22.
|
|
482
|
+
"kb:search": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.22.2/hraness-wordcell-0.22.2.tgz wordcell search --root kb --mode exact --rerank typesafe --rerank-limit 25",
|
|
483
|
+
"kb:search:local": "bunx --bun --package https://github.com/hraness/wordcell/releases/download/v0.22.2/hraness-wordcell-0.22.2.tgz wordcell search --root kb --mode exact",
|
|
484
484
|
"check:worker": "tsc -p worker/tsconfig.json --noEmit"
|
|
485
485
|
},
|
|
486
486
|
"dependencies": {
|
package/skills/wordcell/SKILL.md
CHANGED
|
@@ -56,7 +56,7 @@ missing:
|
|
|
56
56
|
```sh
|
|
57
57
|
command -v wordcell >/dev/null 2>&1 || {
|
|
58
58
|
command -v bun >/dev/null 2>&1 || exit 1
|
|
59
|
-
bun add --global --ignore-scripts https://github.com/hraness/wordcell/releases/download/v0.22.
|
|
59
|
+
bun add --global --ignore-scripts https://github.com/hraness/wordcell/releases/download/v0.22.3/hraness-wordcell-0.22.3.tgz
|
|
60
60
|
}
|
|
61
61
|
wordcell --help
|
|
62
62
|
```
|
package/src/publish-markdown.ts
CHANGED
|
@@ -41,9 +41,15 @@ function anchorId(text: string, used: Map<string, number>): string {
|
|
|
41
41
|
.toLocaleLowerCase("en-US")
|
|
42
42
|
.replace(VOID_SLUG, "-")
|
|
43
43
|
.replace(/^[-._~]+|[-._~]+$/gu, "") || "section";
|
|
44
|
-
|
|
44
|
+
let seen = used.get(base) ?? 0;
|
|
45
|
+
let id = seen === 0 ? base : `${base}-${seen + 1}`;
|
|
46
|
+
while (used.has(id)) {
|
|
47
|
+
seen += 1;
|
|
48
|
+
id = `${base}-${seen + 1}`;
|
|
49
|
+
}
|
|
45
50
|
used.set(base, seen + 1);
|
|
46
|
-
|
|
51
|
+
used.set(id, 1);
|
|
52
|
+
return id;
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
function isExternalUrl(value: string): boolean {
|
|
@@ -80,7 +86,60 @@ function wikiDisplay(raw: string): { target: string; fragment: string; label: st
|
|
|
80
86
|
};
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
|
|
89
|
+
const FOOTNOTE_LIMITS = {
|
|
90
|
+
labelBytes: 128,
|
|
91
|
+
definitions: 256,
|
|
92
|
+
bodyBytes: 8_192,
|
|
93
|
+
bodyLines: 32,
|
|
94
|
+
references: 2_048,
|
|
95
|
+
} as const;
|
|
96
|
+
|
|
97
|
+
type Footnote = {
|
|
98
|
+
readonly body: string;
|
|
99
|
+
readonly authored: string;
|
|
100
|
+
invalid: boolean;
|
|
101
|
+
number?: number;
|
|
102
|
+
readonly references: string[];
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
type Footnotes = {
|
|
106
|
+
readonly byLabel: Map<string, Footnote | null>;
|
|
107
|
+
readonly numbered: Footnote[];
|
|
108
|
+
definitions: number;
|
|
109
|
+
references: number;
|
|
110
|
+
disabled: boolean;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function footnoteLabel(raw: string): string | undefined {
|
|
114
|
+
const label = raw.normalize("NFC");
|
|
115
|
+
if (label.length > FOOTNOTE_LIMITS.labelBytes || !/^[\p{L}\p{M}\p{N}._:-]+$/u.test(label)) return undefined;
|
|
116
|
+
return new TextEncoder().encode(label).byteLength <= FOOTNOTE_LIMITS.labelBytes ? label : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function footnoteNumber(note: Footnote, footnotes: Footnotes): number {
|
|
120
|
+
if (note.number === undefined) {
|
|
121
|
+
note.number = footnotes.numbered.length + 1;
|
|
122
|
+
footnotes.numbered.push(note);
|
|
123
|
+
}
|
|
124
|
+
return note.number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderFootnoteReference(raw: string, label: string, footnotes: Footnotes): string {
|
|
128
|
+
const key = footnoteLabel(label);
|
|
129
|
+
const note = key === undefined ? undefined : footnotes.byLabel.get(key);
|
|
130
|
+
if (footnotes.disabled || note === undefined || note === null || note.invalid || footnotes.references >= FOOTNOTE_LIMITS.references) {
|
|
131
|
+
return `<span class="unresolved footnote-reference" aria-label="Unresolved footnote">${escapeHtml(raw)}</span>`;
|
|
132
|
+
}
|
|
133
|
+
const number = footnoteNumber(note, footnotes);
|
|
134
|
+
// Colons cannot occur in heading slugs, so authored headings cannot capture
|
|
135
|
+
// these targets. Raw labels never enter an HTML id or fragment.
|
|
136
|
+
const id = `wordcell:footnote-ref:${number}:${note.references.length + 1}`;
|
|
137
|
+
note.references.push(id);
|
|
138
|
+
footnotes.references += 1;
|
|
139
|
+
return `<sup><a id="${id}" href="#wordcell:footnote:${number}" role="doc-noteref" aria-label="Footnote ${number}">${number}</a></sup>`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function renderInline(text: string, ctx: PublishRenderContext, footnotes?: Footnotes): string {
|
|
84
143
|
let output = "";
|
|
85
144
|
let cursor = 0;
|
|
86
145
|
const length = text.length;
|
|
@@ -99,7 +158,7 @@ function renderInline(text: string, ctx: PublishRenderContext): string {
|
|
|
99
158
|
if (closing !== -1) {
|
|
100
159
|
const code = rest.slice(ticks.length, closing).replace(/\s+/gu, " ");
|
|
101
160
|
output += `<code>${escapeHtml(code)}</code>`;
|
|
102
|
-
cursor +=
|
|
161
|
+
cursor += closing + ticks.length;
|
|
103
162
|
continue;
|
|
104
163
|
}
|
|
105
164
|
pushText("`");
|
|
@@ -198,6 +257,7 @@ function renderInline(text: string, ctx: PublishRenderContext): string {
|
|
|
198
257
|
if (closing !== -1 && rest[closing + 1] === "(") {
|
|
199
258
|
const destination = readDestination(rest, closing + 2);
|
|
200
259
|
if (destination !== null) {
|
|
260
|
+
// Supported explicit links take precedence over footnote markers.
|
|
201
261
|
const label = renderInline(rest.slice(1, closing), ctx);
|
|
202
262
|
const target = destination.target;
|
|
203
263
|
const asset = ctx.resolveAsset(target, ctx.source);
|
|
@@ -219,6 +279,14 @@ function renderInline(text: string, ctx: PublishRenderContext): string {
|
|
|
219
279
|
continue;
|
|
220
280
|
}
|
|
221
281
|
}
|
|
282
|
+
if (footnotes !== undefined && rest.startsWith("[^")) {
|
|
283
|
+
const end = rest.indexOf("]", 2);
|
|
284
|
+
if (end !== -1) {
|
|
285
|
+
output += renderFootnoteReference(rest.slice(0, end + 1), rest.slice(2, end), footnotes);
|
|
286
|
+
cursor += end + 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
222
290
|
pushText("[");
|
|
223
291
|
cursor += 1;
|
|
224
292
|
continue;
|
|
@@ -249,7 +317,7 @@ function renderInline(text: string, ctx: PublishRenderContext): string {
|
|
|
249
317
|
if (emphasis !== undefined && emphasis !== "_") {
|
|
250
318
|
const closing = rest.indexOf(emphasis, emphasis.length);
|
|
251
319
|
if (closing > emphasis.length) {
|
|
252
|
-
const inner = renderInline(rest.slice(emphasis.length, closing), ctx);
|
|
320
|
+
const inner = renderInline(rest.slice(emphasis.length, closing), ctx, footnotes);
|
|
253
321
|
const tag = emphasis === "**" || emphasis === "__"
|
|
254
322
|
? "strong"
|
|
255
323
|
: emphasis === "~~"
|
|
@@ -265,7 +333,7 @@ function renderInline(text: string, ctx: PublishRenderContext): string {
|
|
|
265
333
|
if (emphasis === "_") {
|
|
266
334
|
const closing = rest.indexOf("_", 1);
|
|
267
335
|
if (closing > 1) {
|
|
268
|
-
output += `<em>${renderInline(rest.slice(1, closing), ctx)}</em>`;
|
|
336
|
+
output += `<em>${renderInline(rest.slice(1, closing), ctx, footnotes)}</em>`;
|
|
269
337
|
cursor += closing + 1;
|
|
270
338
|
continue;
|
|
271
339
|
}
|
|
@@ -398,49 +466,145 @@ function tableCells(line: string): string[] {
|
|
|
398
466
|
return cells.map((cell) => cell.trim());
|
|
399
467
|
}
|
|
400
468
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
469
|
+
type ListPart =
|
|
470
|
+
| { readonly type: "open" | "close"; readonly ordered: boolean }
|
|
471
|
+
| { readonly type: "item"; readonly text: string; readonly checked?: boolean }
|
|
472
|
+
| { readonly type: "continuation"; readonly text: string };
|
|
473
|
+
|
|
474
|
+
type MarkdownBlock =
|
|
475
|
+
| { readonly type: "paragraph"; readonly text: string }
|
|
476
|
+
| { readonly type: "heading"; readonly level: number; readonly text: string }
|
|
477
|
+
| { readonly type: "code"; readonly language: string; readonly text: string }
|
|
478
|
+
| { readonly type: "break" }
|
|
479
|
+
| { readonly type: "quote"; readonly blocks: readonly MarkdownBlock[] }
|
|
480
|
+
| { readonly type: "table"; readonly header: readonly string[]; readonly rows: readonly string[][] }
|
|
481
|
+
| { readonly type: "list"; readonly ordered: boolean; readonly parts: readonly ListPart[] }
|
|
482
|
+
| { readonly type: "literal"; readonly text: string }
|
|
483
|
+
| { readonly type: "footnote"; readonly note: Footnote };
|
|
484
|
+
|
|
485
|
+
type Comments = { closing?: "-->" | "%%"; code?: string };
|
|
486
|
+
|
|
487
|
+
/** Strip comments only in prose; code blocks never pass through this scanner. */
|
|
488
|
+
function withoutComments(line: string, comments: Comments, continuesCode?: (ticks: string) => boolean): string {
|
|
489
|
+
let text = "";
|
|
490
|
+
let cursor = 0;
|
|
491
|
+
while (cursor < line.length) {
|
|
492
|
+
if (comments.code !== undefined) {
|
|
493
|
+
const end = line.indexOf(comments.code, cursor);
|
|
494
|
+
if (end === -1) return text + line.slice(cursor);
|
|
495
|
+
const next = end + comments.code.length;
|
|
496
|
+
text += line.slice(cursor, next);
|
|
497
|
+
cursor = next;
|
|
498
|
+
delete comments.code;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (comments.closing !== undefined) {
|
|
502
|
+
const end = line.indexOf(comments.closing, cursor);
|
|
503
|
+
if (end === -1) return text;
|
|
504
|
+
cursor = end + comments.closing.length;
|
|
505
|
+
delete comments.closing;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
const rest = line.slice(cursor);
|
|
509
|
+
if (rest[0] === "\\" && rest.length > 1) {
|
|
510
|
+
text += rest.slice(0, 2);
|
|
511
|
+
cursor += 2;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
if (rest[0] === "`") {
|
|
515
|
+
const ticks = /^`+/u.exec(rest)?.[0] ?? "`";
|
|
516
|
+
const closing = rest.indexOf(ticks, ticks.length);
|
|
517
|
+
if (closing !== -1) {
|
|
518
|
+
const end = closing + ticks.length;
|
|
519
|
+
text += rest.slice(0, end);
|
|
520
|
+
cursor += end;
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
if (continuesCode?.(ticks) === true) {
|
|
524
|
+
comments.code = ticks;
|
|
525
|
+
return text + rest;
|
|
411
526
|
}
|
|
412
527
|
}
|
|
528
|
+
if (rest.startsWith("<!--") || rest.startsWith("%%")) {
|
|
529
|
+
comments.closing = rest.startsWith("<!--") ? "-->" : "%%";
|
|
530
|
+
cursor += comments.closing === "-->" ? 4 : 2;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
text += rest[0] ?? "";
|
|
534
|
+
cursor += 1;
|
|
535
|
+
}
|
|
536
|
+
return text;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function paragraphBoundary(line: string, next: string): boolean {
|
|
540
|
+
return line.trim() === "" || /^(?: {4}|\t|#{1,6}\s|\s{0,3}>|\s{0,3}(?:`{3,}|~{3,}))/u.test(line)
|
|
541
|
+
|| listMarker(line) !== undefined || (line.includes("|") && isTableDivider(next))
|
|
542
|
+
|| /^\s{0,3}(?:(?:\*\s*){3,}|(?:-\s*){3,}|(?:_\s*){3,})$/u.test(line);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function hasContinuedInlineCode(lines: readonly string[], start: number, ticks: string): boolean {
|
|
546
|
+
for (let index = start; index < lines.length; index += 1) {
|
|
547
|
+
const line = lines[index] ?? "";
|
|
548
|
+
if (paragraphBoundary(line, lines[index + 1] ?? "")) return false;
|
|
549
|
+
if (line.includes(ticks)) return true;
|
|
413
550
|
}
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
414
553
|
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
554
|
+
function admitFootnote(rawLabel: string, body: readonly string[], authored: readonly string[], footnotes: Footnotes): Footnote {
|
|
555
|
+
const text = body.join("\n");
|
|
556
|
+
const label = footnoteLabel(rawLabel);
|
|
557
|
+
const note: Footnote = {
|
|
558
|
+
body: text,
|
|
559
|
+
authored: authored.join("\n"),
|
|
560
|
+
invalid: label === undefined || body.length > FOOTNOTE_LIMITS.bodyLines || text.length > FOOTNOTE_LIMITS.bodyBytes
|
|
561
|
+
|| new TextEncoder().encode(text).byteLength > FOOTNOTE_LIMITS.bodyBytes || text.trim() === "",
|
|
562
|
+
references: [],
|
|
563
|
+
};
|
|
564
|
+
footnotes.definitions += 1;
|
|
565
|
+
if (footnotes.definitions > FOOTNOTE_LIMITS.definitions) {
|
|
566
|
+
footnotes.disabled = true;
|
|
567
|
+
footnotes.byLabel.clear();
|
|
568
|
+
}
|
|
569
|
+
if (!footnotes.disabled && label !== undefined) {
|
|
570
|
+
if (footnotes.byLabel.has(label)) {
|
|
571
|
+
const prior = footnotes.byLabel.get(label);
|
|
572
|
+
if (prior !== undefined && prior !== null) prior.invalid = true;
|
|
573
|
+
note.invalid = true;
|
|
574
|
+
footnotes.byLabel.set(label, null);
|
|
575
|
+
} else {
|
|
576
|
+
footnotes.byLabel.set(label, note.invalid ? null : note);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return note;
|
|
580
|
+
}
|
|
419
581
|
|
|
582
|
+
/** One block scan owns both rendering precedence and definition admission. */
|
|
583
|
+
function parseMarkdownBlocks(content: string, footnotes: Footnotes, topLevel: boolean): MarkdownBlock[] {
|
|
584
|
+
const lines = content.split("\n");
|
|
585
|
+
let index = 0;
|
|
586
|
+
if (topLevel && lines[0]?.trim() === "---") {
|
|
587
|
+
for (let cursor = 1; cursor < lines.length; cursor += 1) {
|
|
588
|
+
if (lines[cursor]?.trim() === "---" || lines[cursor]?.trim() === "...") {
|
|
589
|
+
index = cursor + 1;
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
const blocks: MarkdownBlock[] = [];
|
|
595
|
+
const comments: Comments = {};
|
|
420
596
|
const paragraph: string[] = [];
|
|
421
597
|
const flushParagraph = (): void => {
|
|
422
598
|
if (paragraph.length === 0) return;
|
|
423
|
-
|
|
599
|
+
blocks.push({ type: "paragraph", text: paragraph.join(" ") });
|
|
424
600
|
paragraph.length = 0;
|
|
425
601
|
};
|
|
426
602
|
|
|
427
603
|
while (index < lines.length) {
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const close = line.indexOf("-->");
|
|
433
|
-
index += 1;
|
|
434
|
-
if (close !== -1) inComment = false;
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
if (line.trim().startsWith("%%") && line.trim().endsWith("%%") && line.trim().length > 3) {
|
|
438
|
-
index += 1;
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Fenced code.
|
|
443
|
-
const fence = /^\s{0,3}(`{3,}|~{3,})([^`]*)$/u.exec(line);
|
|
604
|
+
const raw = lines[index] ?? "";
|
|
605
|
+
// Code has precedence over prose comments and footnote definitions.
|
|
606
|
+
const inCode = comments.code !== undefined;
|
|
607
|
+
const fence = comments.closing === undefined && !inCode ? /^\s{0,3}(`{3,}|~{3,})([^`]*)$/u.exec(raw) : null;
|
|
444
608
|
if (fence !== null) {
|
|
445
609
|
flushParagraph();
|
|
446
610
|
const marker = fence[1] ?? "```";
|
|
@@ -450,151 +614,210 @@ export function renderMarkdownToHtml(content: string, ctx: PublishRenderContext)
|
|
|
450
614
|
while (index < lines.length) {
|
|
451
615
|
const candidate = lines[index] ?? "";
|
|
452
616
|
const close = /^\s{0,3}(`{3,}|~{3,})\s*$/u.exec(candidate);
|
|
453
|
-
if (close !== null && (close[1] ?? "")[0] === marker[0] && (close[1] ?? "").length >= marker.length)
|
|
454
|
-
break;
|
|
455
|
-
}
|
|
617
|
+
if (close !== null && (close[1] ?? "")[0] === marker[0] && (close[1] ?? "").length >= marker.length) break;
|
|
456
618
|
body.push(candidate);
|
|
457
619
|
index += 1;
|
|
458
620
|
}
|
|
459
621
|
index += 1;
|
|
460
|
-
|
|
461
|
-
|
|
622
|
+
blocks.push({ type: "code", language, text: body.join("\n") });
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (comments.closing === undefined && !inCode && /^(?: {4}|\t)/u.test(raw)) {
|
|
626
|
+
flushParagraph();
|
|
627
|
+
const body: string[] = [];
|
|
628
|
+
while (index < lines.length && /^(?: {4}|\t| *$)/u.test(lines[index] ?? "")) {
|
|
629
|
+
body.push((lines[index] ?? "").replace(/^(?: {4}|\t)/u, ""));
|
|
630
|
+
index += 1;
|
|
631
|
+
}
|
|
632
|
+
blocks.push({ type: "code", language: "", text: body.join("\n").replace(/\n+$/u, "") });
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const rawDefinition = comments.closing === undefined && !inCode ? /^ {0,3}\[\^([^\]\n]*)\]:/u.exec(raw) : null;
|
|
636
|
+
if (rawDefinition !== null) {
|
|
637
|
+
flushParagraph();
|
|
638
|
+
const authored = [raw];
|
|
639
|
+
index += 1;
|
|
640
|
+
while (index < lines.length) {
|
|
641
|
+
const next = lines[index] ?? "";
|
|
642
|
+
if (/^(?: {4}|\t)/u.test(next) || (next.trim() === "" && /^(?: {4}|\t)/u.test(lines[index + 1] ?? ""))) {
|
|
643
|
+
authored.push(next);
|
|
644
|
+
index += 1;
|
|
645
|
+
} else {
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// Scan the full inline body together so comments inside a code span
|
|
650
|
+
// crossing continuation lines remain code, rather than disappearing.
|
|
651
|
+
const clean = withoutComments(authored.join("\n"), comments).split("\n");
|
|
652
|
+
const definition = /^ {0,3}\[\^([^\]\n]*)\]:[ \t]*(.*)$/u.exec(clean[0] ?? "");
|
|
653
|
+
if (!topLevel || definition === null) {
|
|
654
|
+
blocks.push({ type: "literal", text: clean.join("\n") });
|
|
655
|
+
} else {
|
|
656
|
+
const body = [definition[2] ?? "", ...clean.slice(1).map((line) => line.replace(/^(?: {4}|\t)/u, ""))];
|
|
657
|
+
blocks.push({ type: "footnote", note: admitFootnote(rawDefinition[1] ?? "", body, clean, footnotes) });
|
|
658
|
+
}
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
// Quoted blocks own their comment/code scope after removing the marker.
|
|
662
|
+
const line = comments.closing === undefined && !inCode && /^\s{0,3}>/u.test(raw)
|
|
663
|
+
? raw
|
|
664
|
+
: withoutComments(raw, comments, paragraphBoundary(raw, lines[index + 1] ?? "")
|
|
665
|
+
? undefined
|
|
666
|
+
: (ticks) => hasContinuedInlineCode(lines, index + 1, ticks));
|
|
667
|
+
if (inCode) {
|
|
668
|
+
paragraph.push(line);
|
|
669
|
+
index += 1;
|
|
462
670
|
continue;
|
|
463
671
|
}
|
|
464
|
-
|
|
465
|
-
// Blank line.
|
|
466
672
|
if (line.trim() === "") {
|
|
467
673
|
flushParagraph();
|
|
468
674
|
index += 1;
|
|
469
675
|
continue;
|
|
470
676
|
}
|
|
471
|
-
|
|
472
|
-
// ATX heading.
|
|
473
677
|
const heading = /^(#{1,6})\s+(.*)$/u.exec(line);
|
|
474
678
|
if (heading !== null) {
|
|
475
679
|
flushParagraph();
|
|
476
|
-
|
|
477
|
-
const body = renderInline((heading[2] ?? "").replace(/\s+#+$/u, ""), ctx);
|
|
478
|
-
const id = anchorId(stripMarkup(body), anchors);
|
|
479
|
-
html.push(`<h${level} id="${escapeAttribute(id)}">${body}</h${level}>`);
|
|
680
|
+
blocks.push({ type: "heading", level: (heading[1] ?? "#").length, text: (heading[2] ?? "").replace(/\s+#+$/u, "") });
|
|
480
681
|
index += 1;
|
|
481
682
|
continue;
|
|
482
683
|
}
|
|
483
|
-
|
|
484
|
-
// Thematic break.
|
|
485
684
|
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)) {
|
|
486
685
|
flushParagraph();
|
|
487
|
-
|
|
686
|
+
blocks.push({ type: "break" });
|
|
488
687
|
index += 1;
|
|
489
688
|
continue;
|
|
490
689
|
}
|
|
491
|
-
|
|
492
|
-
// Blockquote (one level per pass; nesting via > > accumulation).
|
|
493
690
|
if (/^\s{0,3}>/u.test(line)) {
|
|
494
691
|
flushParagraph();
|
|
495
|
-
const quote
|
|
692
|
+
const quote = [line.replace(/^\s{0,3}>\s?/u, "")];
|
|
693
|
+
index += 1;
|
|
496
694
|
while (index < lines.length && /^\s{0,3}>/u.test(lines[index] ?? "")) {
|
|
497
695
|
quote.push((lines[index] ?? "").replace(/^\s{0,3}>\s?/u, ""));
|
|
498
696
|
index += 1;
|
|
499
697
|
}
|
|
500
|
-
|
|
698
|
+
blocks.push({ type: "quote", blocks: parseMarkdownBlocks(quote.join("\n"), footnotes, false) });
|
|
501
699
|
continue;
|
|
502
700
|
}
|
|
503
|
-
|
|
504
|
-
// Tables: header row, divider, body rows.
|
|
505
701
|
if (line.includes("|") && index + 1 < lines.length && isTableDivider(lines[index + 1] ?? "")) {
|
|
506
702
|
flushParagraph();
|
|
507
703
|
const header = tableCells(line);
|
|
704
|
+
const rows: string[][] = [];
|
|
508
705
|
index += 2;
|
|
509
|
-
const rows: string[] = [];
|
|
510
706
|
while (index < lines.length && (lines[index] ?? "").includes("|") && (lines[index] ?? "").trim() !== "") {
|
|
511
|
-
|
|
707
|
+
const row = withoutComments(lines[index] ?? "", comments);
|
|
708
|
+
if (row.trim() !== "") rows.push(tableCells(row));
|
|
512
709
|
index += 1;
|
|
513
710
|
}
|
|
514
|
-
|
|
515
|
-
const bodyRows = rows.map((row) =>
|
|
516
|
-
`<tr>${tableCells(row).map((cell) => `<td>${renderInline(cell, ctx)}</td>`).join("")}</tr>`).join("");
|
|
517
|
-
html.push(`<table><thead><tr>${head}</tr></thead><tbody>${bodyRows}</tbody></table>`);
|
|
711
|
+
blocks.push({ type: "table", header, rows });
|
|
518
712
|
continue;
|
|
519
713
|
}
|
|
520
|
-
|
|
521
|
-
// Lists: flat scan with indent-based nesting.
|
|
522
714
|
const marker = listMarker(line);
|
|
523
715
|
if (marker !== undefined) {
|
|
524
716
|
flushParagraph();
|
|
525
|
-
const
|
|
526
|
-
let ordered = marker.ordered;
|
|
717
|
+
const parts: ListPart[] = [];
|
|
527
718
|
const baseIndent = marker.indent;
|
|
528
719
|
const stack: { readonly indent: number; readonly ordered: boolean }[] = [];
|
|
720
|
+
let currentLine = line;
|
|
529
721
|
while (index < lines.length) {
|
|
530
|
-
const current = listMarker(
|
|
531
|
-
const raw = lines[index] ?? "";
|
|
722
|
+
const current = listMarker(currentLine);
|
|
532
723
|
if (current === undefined) {
|
|
533
|
-
if (
|
|
724
|
+
if (currentLine.trim() === "") {
|
|
534
725
|
index += 1;
|
|
535
726
|
break;
|
|
536
727
|
}
|
|
537
|
-
const continuationIndent =
|
|
538
|
-
if (
|
|
539
|
-
|
|
728
|
+
const continuationIndent = currentLine.length - currentLine.trimStart().length;
|
|
729
|
+
if (parts.length > 0 && continuationIndent > baseIndent) {
|
|
730
|
+
parts.push({ type: "continuation", text: currentLine.trim() });
|
|
540
731
|
index += 1;
|
|
541
|
-
|
|
732
|
+
} else {
|
|
733
|
+
break;
|
|
542
734
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
while (stack.length > 0 && current.indent < (stack[stack.length - 1]?.indent ?? baseIndent)) {
|
|
558
|
-
const popped = stack.pop();
|
|
559
|
-
items.push(`</${popped?.ordered === true ? "ol" : "ul"}>`);
|
|
735
|
+
} else {
|
|
736
|
+
const body = currentLine.slice(currentLine.search(/\S/u));
|
|
737
|
+
const itemBody = body.replace(/^(?:[-+*]|\d{1,9}[.)])\s+/u, "");
|
|
738
|
+
const task = /^\[([ xX])\]\s+/u.exec(itemBody);
|
|
739
|
+
const text = task === null ? itemBody : itemBody.slice((task[0] ?? "").length);
|
|
740
|
+
if (current.indent > (stack[stack.length - 1]?.indent ?? baseIndent) && stack.length < 8) {
|
|
741
|
+
stack.push({ indent: current.indent, ordered: current.ordered });
|
|
742
|
+
parts.push({ type: "open", ordered: current.ordered });
|
|
743
|
+
}
|
|
744
|
+
while (stack.length > 0 && current.indent < (stack[stack.length - 1]?.indent ?? baseIndent)) {
|
|
745
|
+
parts.push({ type: "close", ordered: stack.pop()?.ordered === true });
|
|
746
|
+
}
|
|
747
|
+
parts.push({ type: "item", text, ...(task === null ? {} : { checked: (task[1] ?? " ").toLowerCase() === "x" }) });
|
|
748
|
+
index += 1;
|
|
560
749
|
}
|
|
561
|
-
|
|
562
|
-
index
|
|
750
|
+
// Do not consume comments on a line that belongs to the next block.
|
|
751
|
+
const next = lines[index] ?? "";
|
|
752
|
+
if (listMarker(next) === undefined && next.trim() !== "" && next.length - next.trimStart().length <= baseIndent) break;
|
|
753
|
+
currentLine = withoutComments(next, comments);
|
|
563
754
|
}
|
|
564
|
-
while (stack.length > 0) {
|
|
565
|
-
|
|
566
|
-
items.push(`</${popped?.ordered === true ? "ol" : "ul"}>`);
|
|
567
|
-
}
|
|
568
|
-
html.push(`<${ordered ? "ol" : "ul"}>${items.join("")}</${ordered ? "ol" : "ul"}>`);
|
|
569
|
-
continue;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// HTML comment open.
|
|
573
|
-
const commentStart = line.indexOf("<!--");
|
|
574
|
-
if (commentStart !== -1) {
|
|
575
|
-
const close = line.indexOf("-->", commentStart + 4);
|
|
576
|
-
if (close === -1) inComment = true;
|
|
577
|
-
index += 1;
|
|
755
|
+
while (stack.length > 0) parts.push({ type: "close", ordered: stack.pop()?.ordered === true });
|
|
756
|
+
blocks.push({ type: "list", ordered: marker.ordered, parts });
|
|
578
757
|
continue;
|
|
579
758
|
}
|
|
759
|
+
paragraph.push(line);
|
|
760
|
+
index += 1;
|
|
761
|
+
}
|
|
762
|
+
flushParagraph();
|
|
763
|
+
return blocks;
|
|
764
|
+
}
|
|
580
765
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
766
|
+
function renderBlocks(blocks: readonly MarkdownBlock[], ctx: PublishRenderContext, footnotes: Footnotes, anchors: Map<string, number>): string {
|
|
767
|
+
return blocks.map((block): string => {
|
|
768
|
+
switch (block.type) {
|
|
769
|
+
case "paragraph": return `<p>${renderInline(block.text, ctx, footnotes)}</p>`;
|
|
770
|
+
case "literal": return `<p>${escapeHtml(block.text).replaceAll("\n", "<br>")}</p>`;
|
|
771
|
+
case "heading": {
|
|
772
|
+
const body = renderInline(block.text, ctx, footnotes);
|
|
773
|
+
return `<h${block.level} id="${escapeAttribute(anchorId(stripMarkup(body), anchors))}">${body}</h${block.level}>`;
|
|
588
774
|
}
|
|
589
|
-
|
|
590
|
-
|
|
775
|
+
case "code": return `<pre><code${block.language === "" ? "" : ` class="language-${escapeAttribute(block.language)}"`}>${escapeHtml(block.text)}</code></pre>`;
|
|
776
|
+
case "break": return "<hr>";
|
|
777
|
+
case "quote": return `<blockquote>${renderBlocks(block.blocks, ctx, footnotes, anchors)}</blockquote>`;
|
|
778
|
+
case "table": {
|
|
779
|
+
const header = block.header.map((cell) => `<th>${renderInline(cell, ctx, footnotes)}</th>`).join("");
|
|
780
|
+
const rows = block.rows.map((row) => `<tr>${row.map((cell) => `<td>${renderInline(cell, ctx, footnotes)}</td>`).join("")}</tr>`).join("");
|
|
781
|
+
return `<table><thead><tr>${header}</tr></thead><tbody>${rows}</tbody></table>`;
|
|
782
|
+
}
|
|
783
|
+
case "list": {
|
|
784
|
+
const parts = block.parts.map((part) => {
|
|
785
|
+
switch (part.type) {
|
|
786
|
+
case "open": return `<${part.ordered ? "ol" : "ul"}>`;
|
|
787
|
+
case "close": return `</${part.ordered ? "ol" : "ul"}>`;
|
|
788
|
+
case "continuation": return `\n${renderInline(part.text, ctx, footnotes)}`;
|
|
789
|
+
case "item": return `<li>${part.checked === undefined ? "" : `<input type="checkbox" disabled${part.checked ? " checked" : ""}> `}${renderInline(part.text, ctx, footnotes)}</li>`;
|
|
790
|
+
}
|
|
791
|
+
}).join("");
|
|
792
|
+
return `<${block.ordered ? "ol" : "ul"}>${parts}</${block.ordered ? "ol" : "ul"}>`;
|
|
793
|
+
}
|
|
794
|
+
case "footnote": return footnotes.disabled || block.note.invalid
|
|
795
|
+
? `<p class="unresolved footnote-definition">${escapeHtml(block.note.authored).replaceAll("\n", "<br>")}</p>`
|
|
796
|
+
: "";
|
|
591
797
|
}
|
|
798
|
+
}).filter((html) => html !== "").join("\n");
|
|
799
|
+
}
|
|
592
800
|
|
|
593
|
-
|
|
594
|
-
|
|
801
|
+
/** Render authored Markdown to a sanitized HTML fragment. */
|
|
802
|
+
export function renderMarkdownToHtml(content: string, ctx: PublishRenderContext): string {
|
|
803
|
+
const footnotes: Footnotes = { byLabel: new Map(), numbered: [], definitions: 0, references: 0, disabled: false };
|
|
804
|
+
const blocks = parseMarkdownBlocks(content, footnotes, true);
|
|
805
|
+
const body = renderBlocks(blocks, ctx, footnotes, new Map());
|
|
806
|
+
if (footnotes.disabled) return body;
|
|
807
|
+
// Keep unreferenced authored definitions visible after referenced notes.
|
|
808
|
+
for (const note of footnotes.byLabel.values()) {
|
|
809
|
+
if (note !== null && !note.invalid) footnoteNumber(note, footnotes);
|
|
595
810
|
}
|
|
596
|
-
|
|
597
|
-
|
|
811
|
+
if (footnotes.numbered.length === 0) return body;
|
|
812
|
+
const notes = footnotes.numbered.map((note) => {
|
|
813
|
+
const number = note.number;
|
|
814
|
+
const backlinks = note.references.map((id, index) => `<a href="#${id}" role="doc-backlink" aria-label="Back to reference ${index + 1} for footnote ${number}">↩ ${index + 1}</a>`).join(" ");
|
|
815
|
+
// Definitions are inline prose. Footnote syntax inside them stays literal,
|
|
816
|
+
// so cycles cannot expand and every backlink corresponds to authored prose.
|
|
817
|
+
const text = renderInline(note.body.replaceAll("\n", " "), ctx);
|
|
818
|
+
return `<li id="wordcell:footnote:${number}" tabindex="-1">${text}${backlinks === "" ? "" : ` ${backlinks}`}</li>`;
|
|
819
|
+
}).join("\n");
|
|
820
|
+
return `${body}${body === "" ? "" : "\n"}<section class="footnotes" aria-label="Footnotes"><ol>\n${notes}\n</ol></section>`;
|
|
598
821
|
}
|
|
599
822
|
|
|
600
823
|
export function stripMarkup(html: string): string {
|