@vectojs/markdown 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Markdown.d.ts +62 -0
- package/dist/blockAffordances.d.ts +179 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +467 -54
- package/dist/index.mjs +437 -25
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -30,15 +30,24 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
BlockAffordanceButton: () => BlockAffordanceButton,
|
|
34
|
+
BlockWithAffordances: () => BlockWithAffordances,
|
|
33
35
|
CodeBlock: () => CodeBlock,
|
|
34
36
|
Markdown: () => Markdown,
|
|
35
37
|
MathBlock: () => MathBlock,
|
|
36
38
|
codeAtlas: () => codeAtlas,
|
|
37
39
|
codeAtlasStats: () => codeAtlasStats,
|
|
40
|
+
escapeCsvField: () => escapeCsvField,
|
|
41
|
+
escapeMarkdownTableCell: () => escapeMarkdownTableCell,
|
|
42
|
+
extensionForLanguage: () => extensionForLanguage,
|
|
38
43
|
isMathJaxReady: () => isMathJaxReady,
|
|
44
|
+
mimeForLanguage: () => mimeForLanguage,
|
|
39
45
|
parseFrontMatterFields: () => parseFrontMatterFields,
|
|
40
46
|
preloadMathJax: () => preloadMathJax,
|
|
41
|
-
scanFrontMatter: () => scanFrontMatter
|
|
47
|
+
scanFrontMatter: () => scanFrontMatter,
|
|
48
|
+
tableContentOf: () => tableContentOf,
|
|
49
|
+
tableToCsv: () => tableToCsv,
|
|
50
|
+
tableToMarkdown: () => tableToMarkdown
|
|
42
51
|
});
|
|
43
52
|
module.exports = __toCommonJS(index_exports);
|
|
44
53
|
|
|
@@ -456,7 +465,251 @@ function createStreamController(host, options = {}) {
|
|
|
456
465
|
}
|
|
457
466
|
|
|
458
467
|
// src/Markdown.ts
|
|
468
|
+
var import_ui2 = require("@vectojs/ui");
|
|
469
|
+
|
|
470
|
+
// src/blockAffordances.ts
|
|
459
471
|
var import_ui = require("@vectojs/ui");
|
|
472
|
+
var LANGUAGE_EXTENSIONS = {
|
|
473
|
+
bash: "sh",
|
|
474
|
+
c: "c",
|
|
475
|
+
cpp: "cpp",
|
|
476
|
+
cs: "cs",
|
|
477
|
+
css: "css",
|
|
478
|
+
diff: "diff",
|
|
479
|
+
dockerfile: "dockerfile",
|
|
480
|
+
go: "go",
|
|
481
|
+
graphql: "graphql",
|
|
482
|
+
haskell: "hs",
|
|
483
|
+
html: "html",
|
|
484
|
+
java: "java",
|
|
485
|
+
javascript: "js",
|
|
486
|
+
js: "js",
|
|
487
|
+
json: "json",
|
|
488
|
+
jsonc: "jsonc",
|
|
489
|
+
jsx: "jsx",
|
|
490
|
+
kotlin: "kt",
|
|
491
|
+
latex: "tex",
|
|
492
|
+
lua: "lua",
|
|
493
|
+
make: "mk",
|
|
494
|
+
markdown: "md",
|
|
495
|
+
md: "md",
|
|
496
|
+
nix: "nix",
|
|
497
|
+
php: "php",
|
|
498
|
+
python: "py",
|
|
499
|
+
py: "py",
|
|
500
|
+
ruby: "rb",
|
|
501
|
+
rust: "rs",
|
|
502
|
+
rs: "rs",
|
|
503
|
+
scss: "scss",
|
|
504
|
+
sh: "sh",
|
|
505
|
+
shell: "sh",
|
|
506
|
+
sql: "sql",
|
|
507
|
+
svelte: "svelte",
|
|
508
|
+
swift: "swift",
|
|
509
|
+
tex: "tex",
|
|
510
|
+
toml: "toml",
|
|
511
|
+
ts: "ts",
|
|
512
|
+
tsx: "tsx",
|
|
513
|
+
typescript: "ts",
|
|
514
|
+
vue: "vue",
|
|
515
|
+
xml: "xml",
|
|
516
|
+
yaml: "yaml",
|
|
517
|
+
yml: "yaml",
|
|
518
|
+
zig: "zig",
|
|
519
|
+
zsh: "sh"
|
|
520
|
+
};
|
|
521
|
+
function extensionForLanguage(lang) {
|
|
522
|
+
const first = lang.trim().toLowerCase().split(/[\s:,{]/)[0] ?? "";
|
|
523
|
+
return LANGUAGE_EXTENSIONS[first] ?? "txt";
|
|
524
|
+
}
|
|
525
|
+
function mimeForLanguage(lang) {
|
|
526
|
+
const ext = extensionForLanguage(lang);
|
|
527
|
+
if (ext === "json" || ext === "jsonc") return "application/json";
|
|
528
|
+
if (ext === "html") return "text/html";
|
|
529
|
+
if (ext === "css") return "text/css";
|
|
530
|
+
if (ext === "xml" || ext === "svelte" || ext === "vue") return "text/plain";
|
|
531
|
+
return "text/plain";
|
|
532
|
+
}
|
|
533
|
+
function escapeCsvField(value) {
|
|
534
|
+
let needsQuoting = false;
|
|
535
|
+
let hasQuote = false;
|
|
536
|
+
for (const char of value) {
|
|
537
|
+
if (char === '"') {
|
|
538
|
+
hasQuote = true;
|
|
539
|
+
needsQuoting = true;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
if (char === "," || char === "\n" || char === "\r") needsQuoting = true;
|
|
543
|
+
}
|
|
544
|
+
if (!needsQuoting) return value;
|
|
545
|
+
return hasQuote ? `"${value.replace(/"/g, '""')}"` : `"${value}"`;
|
|
546
|
+
}
|
|
547
|
+
function escapeMarkdownTableCell(cell) {
|
|
548
|
+
let needsEscaping = false;
|
|
549
|
+
for (const char of cell) {
|
|
550
|
+
if (char === "\\" || char === "|") {
|
|
551
|
+
needsEscaping = true;
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (!needsEscaping) return cell;
|
|
556
|
+
return cell.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
557
|
+
}
|
|
558
|
+
function tableToCsv(table) {
|
|
559
|
+
const lines = [table.headers.map(escapeCsvField).join(",")];
|
|
560
|
+
for (const row of table.rows) lines.push(row.map(escapeCsvField).join(","));
|
|
561
|
+
return `\uFEFF${lines.join("\r\n")}`;
|
|
562
|
+
}
|
|
563
|
+
function tableToMarkdown(table) {
|
|
564
|
+
const header = `| ${table.headers.map(escapeMarkdownTableCell).join(" | ")} |`;
|
|
565
|
+
const divider = `| ${table.headers.map((_cell, index) => {
|
|
566
|
+
switch (table.align[index]) {
|
|
567
|
+
case "left":
|
|
568
|
+
return ":---";
|
|
569
|
+
case "center":
|
|
570
|
+
return ":---:";
|
|
571
|
+
case "right":
|
|
572
|
+
return "---:";
|
|
573
|
+
default:
|
|
574
|
+
return "---";
|
|
575
|
+
}
|
|
576
|
+
}).join(" | ")} |`;
|
|
577
|
+
const body = table.rows.map(
|
|
578
|
+
(row) => `| ${table.headers.map((_cell, index) => escapeMarkdownTableCell(row[index] ?? "")).join(" | ")} |`
|
|
579
|
+
);
|
|
580
|
+
return [header, divider, ...body].join("\n");
|
|
581
|
+
}
|
|
582
|
+
function defaultWriteClipboard(text) {
|
|
583
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
584
|
+
clipboard?.writeText?.(text);
|
|
585
|
+
}
|
|
586
|
+
function defaultSaveFile(filename, content, mimeType) {
|
|
587
|
+
const doc = globalThis.document;
|
|
588
|
+
if (!doc?.body) return;
|
|
589
|
+
const blob = new Blob([content], { type: mimeType });
|
|
590
|
+
const url = URL.createObjectURL(blob);
|
|
591
|
+
const anchor = doc.createElement("a");
|
|
592
|
+
anchor.href = url;
|
|
593
|
+
anchor.download = filename;
|
|
594
|
+
doc.body.appendChild(anchor);
|
|
595
|
+
anchor.click();
|
|
596
|
+
doc.body.removeChild(anchor);
|
|
597
|
+
URL.revokeObjectURL(url);
|
|
598
|
+
}
|
|
599
|
+
var BlockAffordanceButton = class _BlockAffordanceButton extends import_ui.Button {
|
|
600
|
+
constructor(label, successLabel, act, opts = {}) {
|
|
601
|
+
super(label, { ...opts, onClick: () => this.run() });
|
|
602
|
+
this.act = act;
|
|
603
|
+
this.restingLabel = label;
|
|
604
|
+
this.successLabel = successLabel;
|
|
605
|
+
this.width = Math.max(this.width, (0, import_ui.measureText)(successLabel, this.font) + 24);
|
|
606
|
+
}
|
|
607
|
+
act;
|
|
608
|
+
/** How long the confirmation label stays up, in ms. */
|
|
609
|
+
static FEEDBACK_MS = 1600;
|
|
610
|
+
restingLabel;
|
|
611
|
+
successLabel;
|
|
612
|
+
feedbackTimer;
|
|
613
|
+
/**
|
|
614
|
+
* Runs the action, then shows the confirmation.
|
|
615
|
+
*
|
|
616
|
+
* The action runs first and a throw propagates: a clipboard write the browser
|
|
617
|
+
* rejected must not be reported as a success.
|
|
618
|
+
*/
|
|
619
|
+
run() {
|
|
620
|
+
this.act();
|
|
621
|
+
this.setTransientLabel(this.successLabel);
|
|
622
|
+
if (this.feedbackTimer !== void 0) clearTimeout(this.feedbackTimer);
|
|
623
|
+
this.feedbackTimer = setTimeout(() => {
|
|
624
|
+
this.setTransientLabel(this.restingLabel);
|
|
625
|
+
this.feedbackTimer = void 0;
|
|
626
|
+
}, _BlockAffordanceButton.FEEDBACK_MS);
|
|
627
|
+
}
|
|
628
|
+
setTransientLabel(label) {
|
|
629
|
+
this.label = label;
|
|
630
|
+
this.textWidth = (0, import_ui.measureText)(label, this.font);
|
|
631
|
+
this.scene?.markDirty();
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* The label a reader hears is the one they see, transient confirmation
|
|
635
|
+
* included, so an AT user gets the same feedback a sighted user does.
|
|
636
|
+
*/
|
|
637
|
+
getA11yAttributes() {
|
|
638
|
+
return { ...super.getA11yAttributes(), label: this.label };
|
|
639
|
+
}
|
|
640
|
+
/** Clears the pending revert so a destroyed block leaves no timer behind. */
|
|
641
|
+
destroy() {
|
|
642
|
+
if (this.feedbackTimer !== void 0) {
|
|
643
|
+
clearTimeout(this.feedbackTimer);
|
|
644
|
+
this.feedbackTimer = void 0;
|
|
645
|
+
}
|
|
646
|
+
super.destroy();
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
var BlockWithAffordances = class _BlockWithAffordances extends import_ui.UIComponent {
|
|
650
|
+
constructor(block, controls) {
|
|
651
|
+
super();
|
|
652
|
+
this.block = block;
|
|
653
|
+
this.controls = controls;
|
|
654
|
+
this.add(block);
|
|
655
|
+
for (const control of controls) this.add(control);
|
|
656
|
+
this.layoutAffordances();
|
|
657
|
+
}
|
|
658
|
+
block;
|
|
659
|
+
controls;
|
|
660
|
+
/** Gap between the block's edges and the controls, in px. */
|
|
661
|
+
static INSET = 8;
|
|
662
|
+
/** Gap between adjacent controls, in px. */
|
|
663
|
+
static GAP = 6;
|
|
664
|
+
/**
|
|
665
|
+
* Places the controls right-aligned along the block's top edge.
|
|
666
|
+
*
|
|
667
|
+
* Laid out right-to-left from the block's right edge so the first control in
|
|
668
|
+
* the list ends up leftmost, which keeps DOM order (and therefore tab order and
|
|
669
|
+
* the a11y reading order) matching the visual order.
|
|
670
|
+
*/
|
|
671
|
+
layoutAffordances() {
|
|
672
|
+
this.width = this.block.width;
|
|
673
|
+
this.height = this.block.height;
|
|
674
|
+
let right = this.block.width - _BlockWithAffordances.INSET;
|
|
675
|
+
for (let i = this.controls.length - 1; i >= 0; i--) {
|
|
676
|
+
const control = this.controls[i];
|
|
677
|
+
control.x = right - control.width;
|
|
678
|
+
control.y = _BlockWithAffordances.INSET;
|
|
679
|
+
right = control.x - _BlockWithAffordances.GAP;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Re-places the controls after the block's own box changed.
|
|
684
|
+
*
|
|
685
|
+
* Called by the owner when a block is resized or its content grew; the controls
|
|
686
|
+
* are anchored to the right edge, so a width change moves them.
|
|
687
|
+
*/
|
|
688
|
+
refreshAffordances() {
|
|
689
|
+
this.layoutAffordances();
|
|
690
|
+
this.scene?.markDirty();
|
|
691
|
+
}
|
|
692
|
+
/** The wrapper is a pass-through: its size is the block's size. */
|
|
693
|
+
getLayoutControlledProperties() {
|
|
694
|
+
return ["x", "y"];
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Projected as a group so assistive technology reports one labelled region
|
|
698
|
+
* containing the block and its controls, rather than two unrelated siblings.
|
|
699
|
+
*/
|
|
700
|
+
getA11yAttributes() {
|
|
701
|
+
return { role: "group", pointerEvents: "none" };
|
|
702
|
+
}
|
|
703
|
+
render() {
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
function tableContentOf(token) {
|
|
707
|
+
return {
|
|
708
|
+
headers: token.header.map((cell) => cell.text),
|
|
709
|
+
rows: token.rows.map((row) => row.map((cell) => cell.text)),
|
|
710
|
+
align: token.align
|
|
711
|
+
};
|
|
712
|
+
}
|
|
460
713
|
|
|
461
714
|
// src/frontMatter.ts
|
|
462
715
|
var OPEN_RE = /^---[ \t]*\r?\n/;
|
|
@@ -701,7 +954,59 @@ function isFenceClosed(raw) {
|
|
|
701
954
|
return false;
|
|
702
955
|
}
|
|
703
956
|
function paragraphHasImage(token) {
|
|
704
|
-
return token.tokens
|
|
957
|
+
return containsImage(token.tokens);
|
|
958
|
+
}
|
|
959
|
+
function containsImage(tokens) {
|
|
960
|
+
if (!tokens) return false;
|
|
961
|
+
for (const token of tokens) {
|
|
962
|
+
if (token.type === "image") return true;
|
|
963
|
+
if (containsImage(token.tokens)) return true;
|
|
964
|
+
}
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
function imagesOf(tokens) {
|
|
968
|
+
const images = [];
|
|
969
|
+
for (const token of tokens ?? []) {
|
|
970
|
+
if (token.type === "image") {
|
|
971
|
+
images.push(token);
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
images.push(...imagesOf(token.tokens));
|
|
975
|
+
}
|
|
976
|
+
return images;
|
|
977
|
+
}
|
|
978
|
+
function stripImages(token) {
|
|
979
|
+
const children = token.tokens;
|
|
980
|
+
if (!children) return token;
|
|
981
|
+
const kept = [];
|
|
982
|
+
for (const child of children) {
|
|
983
|
+
if (child.type === "image") continue;
|
|
984
|
+
const grandchildren = child.tokens;
|
|
985
|
+
if (grandchildren && containsImage(grandchildren)) {
|
|
986
|
+
const stripped = stripImages(child);
|
|
987
|
+
const remaining = stripped.tokens;
|
|
988
|
+
if (remaining && remaining.length > 0) kept.push(stripped);
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
kept.push(child);
|
|
992
|
+
}
|
|
993
|
+
return { ...token, tokens: kept };
|
|
994
|
+
}
|
|
995
|
+
function liftNestedImages(tokens) {
|
|
996
|
+
const lifted = [];
|
|
997
|
+
for (const token of tokens) {
|
|
998
|
+
if (token.type === "image") {
|
|
999
|
+
lifted.push(token);
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
const children = token.tokens;
|
|
1003
|
+
if (children && containsImage(children)) {
|
|
1004
|
+
lifted.push(...liftNestedImages(children));
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
lifted.push(token);
|
|
1008
|
+
}
|
|
1009
|
+
return lifted;
|
|
705
1010
|
}
|
|
706
1011
|
function lastIndexOfImage(tokens) {
|
|
707
1012
|
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
@@ -712,7 +1017,7 @@ function lastIndexOfImage(tokens) {
|
|
|
712
1017
|
function expectedImageParagraphChildren(tokens) {
|
|
713
1018
|
let children = 0;
|
|
714
1019
|
let inTextRun = false;
|
|
715
|
-
for (const token of tokens) {
|
|
1020
|
+
for (const token of liftNestedImages(tokens)) {
|
|
716
1021
|
if (token.type === "image") {
|
|
717
1022
|
children++;
|
|
718
1023
|
inTextRun = false;
|
|
@@ -1148,7 +1453,7 @@ function highlightLine(line, lang, theme) {
|
|
|
1148
1453
|
flush(theme.codeColor);
|
|
1149
1454
|
return segments;
|
|
1150
1455
|
}
|
|
1151
|
-
var CodeBlock = class extends
|
|
1456
|
+
var CodeBlock = class extends import_ui2.UIComponent {
|
|
1152
1457
|
lines;
|
|
1153
1458
|
grid = null;
|
|
1154
1459
|
/** Raw (unhighlighted) lines of the last build, for prefix reuse in buildLines. */
|
|
@@ -1281,7 +1586,7 @@ var CodeBlock = class extends import_ui.UIComponent {
|
|
|
1281
1586
|
this.height = this.pad * 2 + rawLines.length * this.lineH;
|
|
1282
1587
|
}
|
|
1283
1588
|
ensureGrid() {
|
|
1284
|
-
const cellWidth = this.cellWidth || Math.max(1, (0,
|
|
1589
|
+
const cellWidth = this.cellWidth || Math.max(1, (0, import_ui2.measureText)("M", this.codeFont));
|
|
1285
1590
|
if (!this.grid || this.grid.source !== this.source || this.grid.font !== this.codeFont || this.grid.cellWidth !== cellWidth) {
|
|
1286
1591
|
this.grid = (0, import_core.prepareContentGrid)(this.source, {
|
|
1287
1592
|
font: this.codeFont,
|
|
@@ -1550,7 +1855,7 @@ function renderInlineToRichText(tokens, fallbackText, font, color, maxWidth, the
|
|
|
1550
1855
|
if (spans.length === 0) {
|
|
1551
1856
|
spans.push({ text: decodeEntities(fallbackText) });
|
|
1552
1857
|
}
|
|
1553
|
-
return new
|
|
1858
|
+
return new import_ui2.RichText(spans, {
|
|
1554
1859
|
font,
|
|
1555
1860
|
color,
|
|
1556
1861
|
maxWidth,
|
|
@@ -1559,12 +1864,24 @@ function renderInlineToRichText(tokens, fallbackText, font, color, maxWidth, the
|
|
|
1559
1864
|
onLinkClick
|
|
1560
1865
|
});
|
|
1561
1866
|
}
|
|
1562
|
-
var Markdown = class _Markdown extends
|
|
1867
|
+
var Markdown = class _Markdown extends import_ui2.UIComponent {
|
|
1563
1868
|
content;
|
|
1564
1869
|
maxWidth;
|
|
1565
1870
|
theme;
|
|
1566
1871
|
onLinkClick;
|
|
1567
1872
|
selectable;
|
|
1873
|
+
/**
|
|
1874
|
+
* Whether code blocks and tables carry copy / download controls.
|
|
1875
|
+
*
|
|
1876
|
+
* Read when a block entity is built, so it affects blocks rendered from here on
|
|
1877
|
+
* rather than retroactively; a document does not rebuild to gain or lose an
|
|
1878
|
+
* affordance.
|
|
1879
|
+
*/
|
|
1880
|
+
blockAffordances;
|
|
1881
|
+
/** Clipboard writer used by the copy controls. */
|
|
1882
|
+
writeClipboard;
|
|
1883
|
+
/** File saver used by the download controls. */
|
|
1884
|
+
saveFile;
|
|
1568
1885
|
activeBlockMetrics = null;
|
|
1569
1886
|
/**
|
|
1570
1887
|
* Called after a streamed append has re-laid-out the document.
|
|
@@ -1789,7 +2106,10 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
1789
2106
|
this.onLinkClick = opts.onLinkClick;
|
|
1790
2107
|
this.selectable = opts.selectable ?? true;
|
|
1791
2108
|
this._userTiming = opts.userTiming ?? false;
|
|
1792
|
-
this.
|
|
2109
|
+
this.blockAffordances = opts.blockAffordances ?? false;
|
|
2110
|
+
this.writeClipboard = opts.writeClipboard ?? defaultWriteClipboard;
|
|
2111
|
+
this.saveFile = opts.saveFile ?? defaultSaveFile;
|
|
2112
|
+
this.content = new import_ui2.Stack({ direction: "vertical", gap: 16 });
|
|
1793
2113
|
this.add(this.content);
|
|
1794
2114
|
this.rawMarkdown = "";
|
|
1795
2115
|
this.setTokens([]);
|
|
@@ -2008,15 +2328,15 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2008
2328
|
switch (token.type) {
|
|
2009
2329
|
case "heading":
|
|
2010
2330
|
case "paragraph": {
|
|
2011
|
-
if (entity instanceof
|
|
2331
|
+
if (entity instanceof import_ui2.RichText) {
|
|
2012
2332
|
entity.setMaxWidth(availableWidth);
|
|
2013
2333
|
return;
|
|
2014
2334
|
}
|
|
2015
|
-
if (entity instanceof
|
|
2335
|
+
if (entity instanceof import_ui2.Stack) {
|
|
2016
2336
|
entity.maxWidth = availableWidth;
|
|
2017
2337
|
for (const run of entity.children) {
|
|
2018
|
-
if (run instanceof
|
|
2019
|
-
else if (run instanceof
|
|
2338
|
+
if (run instanceof import_ui2.RichText) run.setMaxWidth(availableWidth);
|
|
2339
|
+
else if (run instanceof import_ui2.Image) this.refitParagraphImage(run, availableWidth);
|
|
2020
2340
|
}
|
|
2021
2341
|
entity.layout();
|
|
2022
2342
|
}
|
|
@@ -2029,11 +2349,11 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2029
2349
|
}
|
|
2030
2350
|
case "blockquote": {
|
|
2031
2351
|
const bqToken = token;
|
|
2032
|
-
const innerStack = entity.children.find((c) => c instanceof
|
|
2352
|
+
const innerStack = entity.children.find((c) => c instanceof import_ui2.Stack);
|
|
2033
2353
|
const border = entity.children.find((c) => c instanceof QuoteBorder);
|
|
2034
2354
|
const indentStart = Math.min(16, availableWidth);
|
|
2035
2355
|
const childWidth = Math.max(0, availableWidth - indentStart);
|
|
2036
|
-
if (innerStack instanceof
|
|
2356
|
+
if (innerStack instanceof import_ui2.Stack && bqToken.tokens) {
|
|
2037
2357
|
let index = 0;
|
|
2038
2358
|
for (const inner of bqToken.tokens) {
|
|
2039
2359
|
if (!this.producesEntity(inner)) continue;
|
|
@@ -2056,15 +2376,15 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2056
2376
|
return;
|
|
2057
2377
|
}
|
|
2058
2378
|
case "list": {
|
|
2059
|
-
if (!(entity instanceof
|
|
2379
|
+
if (!(entity instanceof import_ui2.Stack)) return;
|
|
2060
2380
|
for (const item of entity.children) {
|
|
2061
|
-
if (item instanceof
|
|
2381
|
+
if (item instanceof import_ui2.RichText) item.setMaxWidth(availableWidth);
|
|
2062
2382
|
}
|
|
2063
2383
|
entity.layout();
|
|
2064
2384
|
return;
|
|
2065
2385
|
}
|
|
2066
2386
|
case "table": {
|
|
2067
|
-
if (entity instanceof
|
|
2387
|
+
if (entity instanceof import_ui2.Table) entity.setWidth(availableWidth);
|
|
2068
2388
|
return;
|
|
2069
2389
|
}
|
|
2070
2390
|
case "hr": {
|
|
@@ -2072,7 +2392,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2072
2392
|
return;
|
|
2073
2393
|
}
|
|
2074
2394
|
default: {
|
|
2075
|
-
if (entity instanceof
|
|
2395
|
+
if (entity instanceof import_ui2.Text) entity.setMaxWidth(availableWidth);
|
|
2076
2396
|
return;
|
|
2077
2397
|
}
|
|
2078
2398
|
}
|
|
@@ -2543,7 +2863,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2543
2863
|
}
|
|
2544
2864
|
/** One text run of an image-bearing paragraph, as both paths build it. */
|
|
2545
2865
|
inlineRunRichText(tokens, availableWidth, t) {
|
|
2546
|
-
return new
|
|
2866
|
+
return new import_ui2.RichText(this.inlineRunSpans(tokens, t), {
|
|
2547
2867
|
font: `${t.fontSize}px ${t.bodyFont}`,
|
|
2548
2868
|
color: t.textColor,
|
|
2549
2869
|
maxWidth: availableWidth,
|
|
@@ -2577,10 +2897,77 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2577
2897
|
* policy for a zero-dimension source is a separate decision from notifying
|
|
2578
2898
|
* the scene, which is the actual defect here.
|
|
2579
2899
|
*/
|
|
2900
|
+
/**
|
|
2901
|
+
* Wraps a block in its copy / download controls, or returns it untouched.
|
|
2902
|
+
*
|
|
2903
|
+
* The controls are built lazily through `make` so a document with
|
|
2904
|
+
* `blockAffordances` off pays nothing — not the closures, not the measurement
|
|
2905
|
+
* `BlockAffordanceButton` does in its constructor.
|
|
2906
|
+
*/
|
|
2907
|
+
withBlockAffordances(block, make) {
|
|
2908
|
+
if (!this.blockAffordances) return block;
|
|
2909
|
+
const controls = make();
|
|
2910
|
+
return controls.length > 0 ? new BlockWithAffordances(block, controls) : block;
|
|
2911
|
+
}
|
|
2912
|
+
/** Copy and download controls for one fenced code block. */
|
|
2913
|
+
codeBlockAffordances(source, lang) {
|
|
2914
|
+
const opts = this.affordanceButtonOptions();
|
|
2915
|
+
return [
|
|
2916
|
+
new BlockAffordanceButton("Copy code", "Copied", () => this.writeClipboard(source), opts),
|
|
2917
|
+
new BlockAffordanceButton(
|
|
2918
|
+
"Download code",
|
|
2919
|
+
"Saved",
|
|
2920
|
+
() => this.saveFile(`code.${extensionForLanguage(lang)}`, source, mimeForLanguage(lang)),
|
|
2921
|
+
opts
|
|
2922
|
+
)
|
|
2923
|
+
];
|
|
2924
|
+
}
|
|
2925
|
+
/** Copy (as Markdown) and download (as CSV) controls for one table. */
|
|
2926
|
+
tableAffordances(tblToken) {
|
|
2927
|
+
const content = tableContentOf(tblToken);
|
|
2928
|
+
const opts = this.affordanceButtonOptions();
|
|
2929
|
+
return [
|
|
2930
|
+
// Markdown rather than CSV for the clipboard: the reader copied it out of a
|
|
2931
|
+
// Markdown document and the overwhelmingly likely destination is another
|
|
2932
|
+
// one. CSV is what the download is for, where a spreadsheet is the target.
|
|
2933
|
+
new BlockAffordanceButton(
|
|
2934
|
+
"Copy table",
|
|
2935
|
+
"Copied",
|
|
2936
|
+
() => this.writeClipboard(tableToMarkdown(content)),
|
|
2937
|
+
opts
|
|
2938
|
+
),
|
|
2939
|
+
new BlockAffordanceButton(
|
|
2940
|
+
"Download table",
|
|
2941
|
+
"Saved",
|
|
2942
|
+
() => this.saveFile("table.csv", tableToCsv(content), "text/csv;charset=utf-8"),
|
|
2943
|
+
opts
|
|
2944
|
+
)
|
|
2945
|
+
];
|
|
2946
|
+
}
|
|
2947
|
+
/**
|
|
2948
|
+
* Button styling for the affordances, derived from the document theme.
|
|
2949
|
+
*
|
|
2950
|
+
* Themed rather than hardcoded so a light-theme document does not get the dark
|
|
2951
|
+
* default palette. `focusColor` is set explicitly from the theme's accent
|
|
2952
|
+
* because `Button`'s default cyan is tuned for the dark palette and reads as
|
|
2953
|
+
* off-brand elsewhere — while a focus ring is the one affordance a keyboard
|
|
2954
|
+
* user cannot do without.
|
|
2955
|
+
*/
|
|
2956
|
+
affordanceButtonOptions() {
|
|
2957
|
+
return {
|
|
2958
|
+
font: `600 12px ${this.theme.bodyFont}`,
|
|
2959
|
+
padding: 6,
|
|
2960
|
+
radius: 6,
|
|
2961
|
+
bg: this.theme.codeBgColor,
|
|
2962
|
+
hoverBg: this.theme.tableHeaderBgColor,
|
|
2963
|
+
color: this.theme.textColor,
|
|
2964
|
+
focusColor: this.theme.codeColor
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2580
2967
|
paragraphImage(imgToken, availableWidth) {
|
|
2581
2968
|
const initialWidth = Math.min(800, availableWidth);
|
|
2582
2969
|
const initialHeight = Math.round(initialWidth * 0.6);
|
|
2583
|
-
const img = new
|
|
2970
|
+
const img = new import_ui2.Image(imgToken.href, {
|
|
2584
2971
|
width: initialWidth,
|
|
2585
2972
|
height: initialHeight,
|
|
2586
2973
|
alt: imgToken.text,
|
|
@@ -2599,7 +2986,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2599
2986
|
}
|
|
2600
2987
|
/** One table cell entity, shared by the render arm and the streamed-table path. */
|
|
2601
2988
|
tableCellRichText(cell, header, t) {
|
|
2602
|
-
return new
|
|
2989
|
+
return new import_ui2.RichText(this.tableCellSpans(cell, t), {
|
|
2603
2990
|
font: `${t.fontSize - 2}px ${t.bodyFont}`,
|
|
2604
2991
|
color: header ? t.headingColor : t.textColor,
|
|
2605
2992
|
baseStyle: header ? { bold: true } : void 0,
|
|
@@ -2657,6 +3044,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2657
3044
|
itemIsInlineOnly(item) {
|
|
2658
3045
|
const children = item.tokens;
|
|
2659
3046
|
if (!children || children.length === 0) return true;
|
|
3047
|
+
if (containsImage(children)) return false;
|
|
2660
3048
|
if (children.length === 1 && children[0].type === "paragraph") return true;
|
|
2661
3049
|
return children.every((child) => _Markdown.INLINE_ITEM_TOKENS.has(child.type));
|
|
2662
3050
|
}
|
|
@@ -2684,9 +3072,12 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2684
3072
|
listItemBlockStack(token, index, availableWidth, t) {
|
|
2685
3073
|
const item = token.items[index];
|
|
2686
3074
|
const children = item.tokens ?? [];
|
|
2687
|
-
const stack = new
|
|
3075
|
+
const stack = new import_ui2.Stack({ direction: "vertical", gap: 4 });
|
|
2688
3076
|
const first = children[0];
|
|
2689
|
-
const
|
|
3077
|
+
const firstIsInline = Boolean(first) && (first.type === "text" || first.type === "paragraph");
|
|
3078
|
+
const leadHasImage = firstIsInline && containsImage(first.tokens);
|
|
3079
|
+
const leadChildren = firstIsInline ? [leadHasImage ? stripImages(first) : first] : [];
|
|
3080
|
+
const leadImages = leadHasImage ? imagesOf(first.tokens) : [];
|
|
2690
3081
|
const leadToken = {
|
|
2691
3082
|
...token,
|
|
2692
3083
|
items: token.items.map((it, i) => i === index ? { ...it, tokens: leadChildren } : it)
|
|
@@ -2699,6 +3090,13 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2699
3090
|
indentStart: indent,
|
|
2700
3091
|
availableWidth: Math.max(1, availableWidth - indent)
|
|
2701
3092
|
};
|
|
3093
|
+
for (const image of leadImages) {
|
|
3094
|
+
const el = this.paragraphImage(image, childMetrics.availableWidth);
|
|
3095
|
+
const wrapper = new MarkdownContainer();
|
|
3096
|
+
el.x = indent;
|
|
3097
|
+
wrapper.add(el);
|
|
3098
|
+
stack.add(wrapper);
|
|
3099
|
+
}
|
|
2702
3100
|
for (let i = leadChildren.length; i < children.length; i++) {
|
|
2703
3101
|
const el = this.renderTokenWithMetrics(children[i], childMetrics);
|
|
2704
3102
|
if (!el) continue;
|
|
@@ -2746,7 +3144,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2746
3144
|
}
|
|
2747
3145
|
/** Construct the `RichText` for one list item. */
|
|
2748
3146
|
listItemRichText(token, index, availableWidth, t) {
|
|
2749
|
-
return new
|
|
3147
|
+
return new import_ui2.RichText(this.listItemSpans(token, index), {
|
|
2750
3148
|
font: `${t.fontSize}px ${t.bodyFont}`,
|
|
2751
3149
|
color: t.textColor,
|
|
2752
3150
|
maxWidth: availableWidth,
|
|
@@ -2781,7 +3179,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2781
3179
|
* and keep stale spans. Bail when `loose` flips.
|
|
2782
3180
|
*/
|
|
2783
3181
|
updateStreamedList(stack, oldToken, newToken) {
|
|
2784
|
-
if (!(stack instanceof
|
|
3182
|
+
if (!(stack instanceof import_ui2.Stack)) return false;
|
|
2785
3183
|
if (newToken.items.length < oldToken.items.length || oldToken.items.length === 0) return false;
|
|
2786
3184
|
if (oldToken.ordered !== newToken.ordered) return false;
|
|
2787
3185
|
if ((oldToken.start ?? 1) !== (newToken.start ?? 1)) return false;
|
|
@@ -2790,7 +3188,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2790
3188
|
const lastRetained = oldToken.items.length - 1;
|
|
2791
3189
|
for (let i = 0; i < lastRetained; i++) {
|
|
2792
3190
|
if (oldToken.items[i].text !== newToken.items[i].text) return false;
|
|
2793
|
-
const isStack = stack.children[i] instanceof
|
|
3191
|
+
const isStack = stack.children[i] instanceof import_ui2.Stack;
|
|
2794
3192
|
if (isStack !== !this.itemIsInlineOnly(newToken.items[i])) return false;
|
|
2795
3193
|
}
|
|
2796
3194
|
const availableWidth = this.activeBlockMetrics?.availableWidth ?? this.maxWidth;
|
|
@@ -2849,7 +3247,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2849
3247
|
* *token runs* split at the last image, never token index against child index.
|
|
2850
3248
|
*/
|
|
2851
3249
|
updateImageParagraph(entity, oldToken, newToken) {
|
|
2852
|
-
if (!(entity instanceof
|
|
3250
|
+
if (!(entity instanceof import_ui2.Stack)) return false;
|
|
2853
3251
|
const oldTokens = oldToken.tokens;
|
|
2854
3252
|
const newTokens = newToken.tokens;
|
|
2855
3253
|
if (!oldTokens || !newTokens) return false;
|
|
@@ -2874,7 +3272,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2874
3272
|
entity.add(this.inlineRunRichText(newTail, availableWidth, t));
|
|
2875
3273
|
} else {
|
|
2876
3274
|
const tailEntity = entity.children[entity.children.length - 1];
|
|
2877
|
-
if (!(tailEntity instanceof
|
|
3275
|
+
if (!(tailEntity instanceof import_ui2.RichText)) return false;
|
|
2878
3276
|
tailEntity.setSpans(this.inlineRunSpans(newTail, t));
|
|
2879
3277
|
}
|
|
2880
3278
|
const last = entity.children[entity.children.length - 1];
|
|
@@ -2906,7 +3304,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2906
3304
|
* (its keys are `text`/`tokens`/`header`/`align`).
|
|
2907
3305
|
*/
|
|
2908
3306
|
updateStreamedTable(entity, oldToken, newToken) {
|
|
2909
|
-
if (!(entity instanceof
|
|
3307
|
+
if (!(entity instanceof import_ui2.Table)) return false;
|
|
2910
3308
|
if (oldToken.header.length !== newToken.header.length) return false;
|
|
2911
3309
|
for (let c = 0; c < oldToken.header.length; c++) {
|
|
2912
3310
|
if (oldToken.header[c].text !== newToken.header[c].text) return false;
|
|
@@ -2927,7 +3325,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2927
3325
|
if (lastRetained >= 0) {
|
|
2928
3326
|
for (let c = 0; c < oldToken.header.length; c++) {
|
|
2929
3327
|
const cell = entity.rows[lastRetained]?.[c];
|
|
2930
|
-
if (!(cell instanceof
|
|
3328
|
+
if (!(cell instanceof import_ui2.RichText)) return false;
|
|
2931
3329
|
}
|
|
2932
3330
|
}
|
|
2933
3331
|
const t = this.theme;
|
|
@@ -2960,7 +3358,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
2960
3358
|
const newTail = newInner[tail];
|
|
2961
3359
|
if (oldTail.type !== newTail.type) return false;
|
|
2962
3360
|
const innerStack = container.children[1];
|
|
2963
|
-
if (!(innerStack instanceof
|
|
3361
|
+
if (!(innerStack instanceof import_ui2.Stack)) return false;
|
|
2964
3362
|
const wrapper = innerStack.children.at(-1);
|
|
2965
3363
|
if (!wrapper || wrapper.children.length !== 1) return false;
|
|
2966
3364
|
const entity = wrapper.children[0];
|
|
@@ -3425,7 +3823,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
3425
3823
|
const width = intrinsicW * scale;
|
|
3426
3824
|
const height = intrinsicH * scale;
|
|
3427
3825
|
const uri = mathData.uri;
|
|
3428
|
-
const math = new
|
|
3826
|
+
const math = new import_ui2.RichText(
|
|
3429
3827
|
[
|
|
3430
3828
|
{
|
|
3431
3829
|
text: import_core.OBJECT_REPLACEMENT,
|
|
@@ -3504,7 +3902,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
3504
3902
|
this.onLinkClick
|
|
3505
3903
|
);
|
|
3506
3904
|
}
|
|
3507
|
-
const stack = new
|
|
3905
|
+
const stack = new import_ui2.Stack({
|
|
3508
3906
|
direction: "vertical",
|
|
3509
3907
|
gap: 16,
|
|
3510
3908
|
maxWidth: availableWidth
|
|
@@ -3516,7 +3914,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
3516
3914
|
currentTokens = [];
|
|
3517
3915
|
}
|
|
3518
3916
|
};
|
|
3519
|
-
for (const child of pToken.tokens) {
|
|
3917
|
+
for (const child of liftNestedImages(pToken.tokens)) {
|
|
3520
3918
|
if (child.type === "image") {
|
|
3521
3919
|
flushText();
|
|
3522
3920
|
stack.add(this.paragraphImage(child, availableWidth));
|
|
@@ -3544,12 +3942,15 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
3544
3942
|
const mathBlock = this.renderDisplayMath(codeToken.text, availableWidth);
|
|
3545
3943
|
if (mathBlock) return mathBlock;
|
|
3546
3944
|
}
|
|
3547
|
-
return
|
|
3945
|
+
return this.withBlockAffordances(
|
|
3946
|
+
new CodeBlock(codeToken.text, lang, availableWidth, t, this.selectable),
|
|
3947
|
+
() => this.codeBlockAffordances(codeToken.text, lang)
|
|
3948
|
+
);
|
|
3548
3949
|
}
|
|
3549
3950
|
// ── Blockquotes ──────────────────────────────────────────────────
|
|
3550
3951
|
case "blockquote": {
|
|
3551
3952
|
const bqToken = token;
|
|
3552
|
-
const innerStack = new
|
|
3953
|
+
const innerStack = new import_ui2.Stack({ direction: "vertical", gap: 8 });
|
|
3553
3954
|
const indentStart = Math.min(16, availableWidth);
|
|
3554
3955
|
const childMetrics = {
|
|
3555
3956
|
marginBefore: 0,
|
|
@@ -3585,7 +3986,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
3585
3986
|
// ── Lists ────────────────────────────────────────────────
|
|
3586
3987
|
case "list": {
|
|
3587
3988
|
const listToken = token;
|
|
3588
|
-
const listStack = new
|
|
3989
|
+
const listStack = new import_ui2.Stack({ direction: "vertical", gap: 6 });
|
|
3589
3990
|
for (let i = 0; i < listToken.items.length; i++) {
|
|
3590
3991
|
listStack.add(
|
|
3591
3992
|
this.itemIsInlineOnly(listToken.items[i]) ? this.listItemRichText(listToken, i, availableWidth, t) : this.listItemBlockStack(listToken, i, availableWidth, t)
|
|
@@ -3600,21 +4001,24 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
3600
4001
|
const rows = tblToken.rows.map(
|
|
3601
4002
|
(row) => row.map((cell) => this.tableCellRichText(cell, false, t))
|
|
3602
4003
|
);
|
|
3603
|
-
return
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
4004
|
+
return this.withBlockAffordances(
|
|
4005
|
+
new import_ui2.Table({
|
|
4006
|
+
headers,
|
|
4007
|
+
rows,
|
|
4008
|
+
// `| :--- | :---: | ---: |` already resolves to this on the token; it
|
|
4009
|
+
// was previously discarded, so every column rendered left-aligned.
|
|
4010
|
+
align: tblToken.align,
|
|
4011
|
+
width: availableWidth,
|
|
4012
|
+
textColor: t.textColor,
|
|
4013
|
+
headerTextColor: t.headingColor,
|
|
4014
|
+
font: `${t.fontSize - 2}px ${t.bodyFont}`,
|
|
4015
|
+
borderColor: t.hrColor,
|
|
4016
|
+
bg: t.tableBgColor,
|
|
4017
|
+
headerBg: t.tableHeaderBgColor,
|
|
4018
|
+
selectable: this.selectable
|
|
4019
|
+
}),
|
|
4020
|
+
() => this.tableAffordances(tblToken)
|
|
4021
|
+
);
|
|
3618
4022
|
}
|
|
3619
4023
|
// ── Horizontal rule ──────────────────────────────────────────────
|
|
3620
4024
|
case "hr":
|
|
@@ -3633,7 +4037,7 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
3633
4037
|
// ── Fallback ─────────────────────────────────────────────────────
|
|
3634
4038
|
default:
|
|
3635
4039
|
if ("text" in token) {
|
|
3636
|
-
return new
|
|
4040
|
+
return new import_ui2.Text(token.text, {
|
|
3637
4041
|
font: bodyFont,
|
|
3638
4042
|
color: t.textColor,
|
|
3639
4043
|
maxWidth: availableWidth,
|
|
@@ -3650,13 +4054,22 @@ var Markdown = class _Markdown extends import_ui.UIComponent {
|
|
|
3650
4054
|
};
|
|
3651
4055
|
// Annotate the CommonJS export names for ESM import in node:
|
|
3652
4056
|
0 && (module.exports = {
|
|
4057
|
+
BlockAffordanceButton,
|
|
4058
|
+
BlockWithAffordances,
|
|
3653
4059
|
CodeBlock,
|
|
3654
4060
|
Markdown,
|
|
3655
4061
|
MathBlock,
|
|
3656
4062
|
codeAtlas,
|
|
3657
4063
|
codeAtlasStats,
|
|
4064
|
+
escapeCsvField,
|
|
4065
|
+
escapeMarkdownTableCell,
|
|
4066
|
+
extensionForLanguage,
|
|
3658
4067
|
isMathJaxReady,
|
|
4068
|
+
mimeForLanguage,
|
|
3659
4069
|
parseFrontMatterFields,
|
|
3660
4070
|
preloadMathJax,
|
|
3661
|
-
scanFrontMatter
|
|
4071
|
+
scanFrontMatter,
|
|
4072
|
+
tableContentOf,
|
|
4073
|
+
tableToCsv,
|
|
4074
|
+
tableToMarkdown
|
|
3662
4075
|
});
|