@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.mjs
CHANGED
|
@@ -423,7 +423,259 @@ function createStreamController(host, options = {}) {
|
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
// src/Markdown.ts
|
|
426
|
-
import {
|
|
426
|
+
import {
|
|
427
|
+
measureText as measureText2,
|
|
428
|
+
RichText,
|
|
429
|
+
Stack,
|
|
430
|
+
Table,
|
|
431
|
+
Text,
|
|
432
|
+
Image,
|
|
433
|
+
UIComponent as UIComponent2
|
|
434
|
+
} from "@vectojs/ui";
|
|
435
|
+
|
|
436
|
+
// src/blockAffordances.ts
|
|
437
|
+
import { Button, measureText, UIComponent } from "@vectojs/ui";
|
|
438
|
+
var LANGUAGE_EXTENSIONS = {
|
|
439
|
+
bash: "sh",
|
|
440
|
+
c: "c",
|
|
441
|
+
cpp: "cpp",
|
|
442
|
+
cs: "cs",
|
|
443
|
+
css: "css",
|
|
444
|
+
diff: "diff",
|
|
445
|
+
dockerfile: "dockerfile",
|
|
446
|
+
go: "go",
|
|
447
|
+
graphql: "graphql",
|
|
448
|
+
haskell: "hs",
|
|
449
|
+
html: "html",
|
|
450
|
+
java: "java",
|
|
451
|
+
javascript: "js",
|
|
452
|
+
js: "js",
|
|
453
|
+
json: "json",
|
|
454
|
+
jsonc: "jsonc",
|
|
455
|
+
jsx: "jsx",
|
|
456
|
+
kotlin: "kt",
|
|
457
|
+
latex: "tex",
|
|
458
|
+
lua: "lua",
|
|
459
|
+
make: "mk",
|
|
460
|
+
markdown: "md",
|
|
461
|
+
md: "md",
|
|
462
|
+
nix: "nix",
|
|
463
|
+
php: "php",
|
|
464
|
+
python: "py",
|
|
465
|
+
py: "py",
|
|
466
|
+
ruby: "rb",
|
|
467
|
+
rust: "rs",
|
|
468
|
+
rs: "rs",
|
|
469
|
+
scss: "scss",
|
|
470
|
+
sh: "sh",
|
|
471
|
+
shell: "sh",
|
|
472
|
+
sql: "sql",
|
|
473
|
+
svelte: "svelte",
|
|
474
|
+
swift: "swift",
|
|
475
|
+
tex: "tex",
|
|
476
|
+
toml: "toml",
|
|
477
|
+
ts: "ts",
|
|
478
|
+
tsx: "tsx",
|
|
479
|
+
typescript: "ts",
|
|
480
|
+
vue: "vue",
|
|
481
|
+
xml: "xml",
|
|
482
|
+
yaml: "yaml",
|
|
483
|
+
yml: "yaml",
|
|
484
|
+
zig: "zig",
|
|
485
|
+
zsh: "sh"
|
|
486
|
+
};
|
|
487
|
+
function extensionForLanguage(lang) {
|
|
488
|
+
const first = lang.trim().toLowerCase().split(/[\s:,{]/)[0] ?? "";
|
|
489
|
+
return LANGUAGE_EXTENSIONS[first] ?? "txt";
|
|
490
|
+
}
|
|
491
|
+
function mimeForLanguage(lang) {
|
|
492
|
+
const ext = extensionForLanguage(lang);
|
|
493
|
+
if (ext === "json" || ext === "jsonc") return "application/json";
|
|
494
|
+
if (ext === "html") return "text/html";
|
|
495
|
+
if (ext === "css") return "text/css";
|
|
496
|
+
if (ext === "xml" || ext === "svelte" || ext === "vue") return "text/plain";
|
|
497
|
+
return "text/plain";
|
|
498
|
+
}
|
|
499
|
+
function escapeCsvField(value) {
|
|
500
|
+
let needsQuoting = false;
|
|
501
|
+
let hasQuote = false;
|
|
502
|
+
for (const char of value) {
|
|
503
|
+
if (char === '"') {
|
|
504
|
+
hasQuote = true;
|
|
505
|
+
needsQuoting = true;
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
if (char === "," || char === "\n" || char === "\r") needsQuoting = true;
|
|
509
|
+
}
|
|
510
|
+
if (!needsQuoting) return value;
|
|
511
|
+
return hasQuote ? `"${value.replace(/"/g, '""')}"` : `"${value}"`;
|
|
512
|
+
}
|
|
513
|
+
function escapeMarkdownTableCell(cell) {
|
|
514
|
+
let needsEscaping = false;
|
|
515
|
+
for (const char of cell) {
|
|
516
|
+
if (char === "\\" || char === "|") {
|
|
517
|
+
needsEscaping = true;
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (!needsEscaping) return cell;
|
|
522
|
+
return cell.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
523
|
+
}
|
|
524
|
+
function tableToCsv(table) {
|
|
525
|
+
const lines = [table.headers.map(escapeCsvField).join(",")];
|
|
526
|
+
for (const row of table.rows) lines.push(row.map(escapeCsvField).join(","));
|
|
527
|
+
return `\uFEFF${lines.join("\r\n")}`;
|
|
528
|
+
}
|
|
529
|
+
function tableToMarkdown(table) {
|
|
530
|
+
const header = `| ${table.headers.map(escapeMarkdownTableCell).join(" | ")} |`;
|
|
531
|
+
const divider = `| ${table.headers.map((_cell, index) => {
|
|
532
|
+
switch (table.align[index]) {
|
|
533
|
+
case "left":
|
|
534
|
+
return ":---";
|
|
535
|
+
case "center":
|
|
536
|
+
return ":---:";
|
|
537
|
+
case "right":
|
|
538
|
+
return "---:";
|
|
539
|
+
default:
|
|
540
|
+
return "---";
|
|
541
|
+
}
|
|
542
|
+
}).join(" | ")} |`;
|
|
543
|
+
const body = table.rows.map(
|
|
544
|
+
(row) => `| ${table.headers.map((_cell, index) => escapeMarkdownTableCell(row[index] ?? "")).join(" | ")} |`
|
|
545
|
+
);
|
|
546
|
+
return [header, divider, ...body].join("\n");
|
|
547
|
+
}
|
|
548
|
+
function defaultWriteClipboard(text) {
|
|
549
|
+
const clipboard = globalThis.navigator?.clipboard;
|
|
550
|
+
clipboard?.writeText?.(text);
|
|
551
|
+
}
|
|
552
|
+
function defaultSaveFile(filename, content, mimeType) {
|
|
553
|
+
const doc = globalThis.document;
|
|
554
|
+
if (!doc?.body) return;
|
|
555
|
+
const blob = new Blob([content], { type: mimeType });
|
|
556
|
+
const url = URL.createObjectURL(blob);
|
|
557
|
+
const anchor = doc.createElement("a");
|
|
558
|
+
anchor.href = url;
|
|
559
|
+
anchor.download = filename;
|
|
560
|
+
doc.body.appendChild(anchor);
|
|
561
|
+
anchor.click();
|
|
562
|
+
doc.body.removeChild(anchor);
|
|
563
|
+
URL.revokeObjectURL(url);
|
|
564
|
+
}
|
|
565
|
+
var BlockAffordanceButton = class _BlockAffordanceButton extends Button {
|
|
566
|
+
constructor(label, successLabel, act, opts = {}) {
|
|
567
|
+
super(label, { ...opts, onClick: () => this.run() });
|
|
568
|
+
this.act = act;
|
|
569
|
+
this.restingLabel = label;
|
|
570
|
+
this.successLabel = successLabel;
|
|
571
|
+
this.width = Math.max(this.width, measureText(successLabel, this.font) + 24);
|
|
572
|
+
}
|
|
573
|
+
act;
|
|
574
|
+
/** How long the confirmation label stays up, in ms. */
|
|
575
|
+
static FEEDBACK_MS = 1600;
|
|
576
|
+
restingLabel;
|
|
577
|
+
successLabel;
|
|
578
|
+
feedbackTimer;
|
|
579
|
+
/**
|
|
580
|
+
* Runs the action, then shows the confirmation.
|
|
581
|
+
*
|
|
582
|
+
* The action runs first and a throw propagates: a clipboard write the browser
|
|
583
|
+
* rejected must not be reported as a success.
|
|
584
|
+
*/
|
|
585
|
+
run() {
|
|
586
|
+
this.act();
|
|
587
|
+
this.setTransientLabel(this.successLabel);
|
|
588
|
+
if (this.feedbackTimer !== void 0) clearTimeout(this.feedbackTimer);
|
|
589
|
+
this.feedbackTimer = setTimeout(() => {
|
|
590
|
+
this.setTransientLabel(this.restingLabel);
|
|
591
|
+
this.feedbackTimer = void 0;
|
|
592
|
+
}, _BlockAffordanceButton.FEEDBACK_MS);
|
|
593
|
+
}
|
|
594
|
+
setTransientLabel(label) {
|
|
595
|
+
this.label = label;
|
|
596
|
+
this.textWidth = measureText(label, this.font);
|
|
597
|
+
this.scene?.markDirty();
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* The label a reader hears is the one they see, transient confirmation
|
|
601
|
+
* included, so an AT user gets the same feedback a sighted user does.
|
|
602
|
+
*/
|
|
603
|
+
getA11yAttributes() {
|
|
604
|
+
return { ...super.getA11yAttributes(), label: this.label };
|
|
605
|
+
}
|
|
606
|
+
/** Clears the pending revert so a destroyed block leaves no timer behind. */
|
|
607
|
+
destroy() {
|
|
608
|
+
if (this.feedbackTimer !== void 0) {
|
|
609
|
+
clearTimeout(this.feedbackTimer);
|
|
610
|
+
this.feedbackTimer = void 0;
|
|
611
|
+
}
|
|
612
|
+
super.destroy();
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
var BlockWithAffordances = class _BlockWithAffordances extends UIComponent {
|
|
616
|
+
constructor(block, controls) {
|
|
617
|
+
super();
|
|
618
|
+
this.block = block;
|
|
619
|
+
this.controls = controls;
|
|
620
|
+
this.add(block);
|
|
621
|
+
for (const control of controls) this.add(control);
|
|
622
|
+
this.layoutAffordances();
|
|
623
|
+
}
|
|
624
|
+
block;
|
|
625
|
+
controls;
|
|
626
|
+
/** Gap between the block's edges and the controls, in px. */
|
|
627
|
+
static INSET = 8;
|
|
628
|
+
/** Gap between adjacent controls, in px. */
|
|
629
|
+
static GAP = 6;
|
|
630
|
+
/**
|
|
631
|
+
* Places the controls right-aligned along the block's top edge.
|
|
632
|
+
*
|
|
633
|
+
* Laid out right-to-left from the block's right edge so the first control in
|
|
634
|
+
* the list ends up leftmost, which keeps DOM order (and therefore tab order and
|
|
635
|
+
* the a11y reading order) matching the visual order.
|
|
636
|
+
*/
|
|
637
|
+
layoutAffordances() {
|
|
638
|
+
this.width = this.block.width;
|
|
639
|
+
this.height = this.block.height;
|
|
640
|
+
let right = this.block.width - _BlockWithAffordances.INSET;
|
|
641
|
+
for (let i = this.controls.length - 1; i >= 0; i--) {
|
|
642
|
+
const control = this.controls[i];
|
|
643
|
+
control.x = right - control.width;
|
|
644
|
+
control.y = _BlockWithAffordances.INSET;
|
|
645
|
+
right = control.x - _BlockWithAffordances.GAP;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Re-places the controls after the block's own box changed.
|
|
650
|
+
*
|
|
651
|
+
* Called by the owner when a block is resized or its content grew; the controls
|
|
652
|
+
* are anchored to the right edge, so a width change moves them.
|
|
653
|
+
*/
|
|
654
|
+
refreshAffordances() {
|
|
655
|
+
this.layoutAffordances();
|
|
656
|
+
this.scene?.markDirty();
|
|
657
|
+
}
|
|
658
|
+
/** The wrapper is a pass-through: its size is the block's size. */
|
|
659
|
+
getLayoutControlledProperties() {
|
|
660
|
+
return ["x", "y"];
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Projected as a group so assistive technology reports one labelled region
|
|
664
|
+
* containing the block and its controls, rather than two unrelated siblings.
|
|
665
|
+
*/
|
|
666
|
+
getA11yAttributes() {
|
|
667
|
+
return { role: "group", pointerEvents: "none" };
|
|
668
|
+
}
|
|
669
|
+
render() {
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
function tableContentOf(token) {
|
|
673
|
+
return {
|
|
674
|
+
headers: token.header.map((cell) => cell.text),
|
|
675
|
+
rows: token.rows.map((row) => row.map((cell) => cell.text)),
|
|
676
|
+
align: token.align
|
|
677
|
+
};
|
|
678
|
+
}
|
|
427
679
|
|
|
428
680
|
// src/frontMatter.ts
|
|
429
681
|
var OPEN_RE = /^---[ \t]*\r?\n/;
|
|
@@ -668,7 +920,59 @@ function isFenceClosed(raw) {
|
|
|
668
920
|
return false;
|
|
669
921
|
}
|
|
670
922
|
function paragraphHasImage(token) {
|
|
671
|
-
return token.tokens
|
|
923
|
+
return containsImage(token.tokens);
|
|
924
|
+
}
|
|
925
|
+
function containsImage(tokens) {
|
|
926
|
+
if (!tokens) return false;
|
|
927
|
+
for (const token of tokens) {
|
|
928
|
+
if (token.type === "image") return true;
|
|
929
|
+
if (containsImage(token.tokens)) return true;
|
|
930
|
+
}
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
function imagesOf(tokens) {
|
|
934
|
+
const images = [];
|
|
935
|
+
for (const token of tokens ?? []) {
|
|
936
|
+
if (token.type === "image") {
|
|
937
|
+
images.push(token);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
images.push(...imagesOf(token.tokens));
|
|
941
|
+
}
|
|
942
|
+
return images;
|
|
943
|
+
}
|
|
944
|
+
function stripImages(token) {
|
|
945
|
+
const children = token.tokens;
|
|
946
|
+
if (!children) return token;
|
|
947
|
+
const kept = [];
|
|
948
|
+
for (const child of children) {
|
|
949
|
+
if (child.type === "image") continue;
|
|
950
|
+
const grandchildren = child.tokens;
|
|
951
|
+
if (grandchildren && containsImage(grandchildren)) {
|
|
952
|
+
const stripped = stripImages(child);
|
|
953
|
+
const remaining = stripped.tokens;
|
|
954
|
+
if (remaining && remaining.length > 0) kept.push(stripped);
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
kept.push(child);
|
|
958
|
+
}
|
|
959
|
+
return { ...token, tokens: kept };
|
|
960
|
+
}
|
|
961
|
+
function liftNestedImages(tokens) {
|
|
962
|
+
const lifted = [];
|
|
963
|
+
for (const token of tokens) {
|
|
964
|
+
if (token.type === "image") {
|
|
965
|
+
lifted.push(token);
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
const children = token.tokens;
|
|
969
|
+
if (children && containsImage(children)) {
|
|
970
|
+
lifted.push(...liftNestedImages(children));
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
lifted.push(token);
|
|
974
|
+
}
|
|
975
|
+
return lifted;
|
|
672
976
|
}
|
|
673
977
|
function lastIndexOfImage(tokens) {
|
|
674
978
|
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
@@ -679,7 +983,7 @@ function lastIndexOfImage(tokens) {
|
|
|
679
983
|
function expectedImageParagraphChildren(tokens) {
|
|
680
984
|
let children = 0;
|
|
681
985
|
let inTextRun = false;
|
|
682
|
-
for (const token of tokens) {
|
|
986
|
+
for (const token of liftNestedImages(tokens)) {
|
|
683
987
|
if (token.type === "image") {
|
|
684
988
|
children++;
|
|
685
989
|
inTextRun = false;
|
|
@@ -1115,7 +1419,7 @@ function highlightLine(line, lang, theme) {
|
|
|
1115
1419
|
flush(theme.codeColor);
|
|
1116
1420
|
return segments;
|
|
1117
1421
|
}
|
|
1118
|
-
var CodeBlock = class extends
|
|
1422
|
+
var CodeBlock = class extends UIComponent2 {
|
|
1119
1423
|
lines;
|
|
1120
1424
|
grid = null;
|
|
1121
1425
|
/** Raw (unhighlighted) lines of the last build, for prefix reuse in buildLines. */
|
|
@@ -1248,7 +1552,7 @@ var CodeBlock = class extends UIComponent {
|
|
|
1248
1552
|
this.height = this.pad * 2 + rawLines.length * this.lineH;
|
|
1249
1553
|
}
|
|
1250
1554
|
ensureGrid() {
|
|
1251
|
-
const cellWidth = this.cellWidth || Math.max(1,
|
|
1555
|
+
const cellWidth = this.cellWidth || Math.max(1, measureText2("M", this.codeFont));
|
|
1252
1556
|
if (!this.grid || this.grid.source !== this.source || this.grid.font !== this.codeFont || this.grid.cellWidth !== cellWidth) {
|
|
1253
1557
|
this.grid = prepareContentGrid(this.source, {
|
|
1254
1558
|
font: this.codeFont,
|
|
@@ -1526,12 +1830,24 @@ function renderInlineToRichText(tokens, fallbackText, font, color, maxWidth, the
|
|
|
1526
1830
|
onLinkClick
|
|
1527
1831
|
});
|
|
1528
1832
|
}
|
|
1529
|
-
var Markdown = class _Markdown extends
|
|
1833
|
+
var Markdown = class _Markdown extends UIComponent2 {
|
|
1530
1834
|
content;
|
|
1531
1835
|
maxWidth;
|
|
1532
1836
|
theme;
|
|
1533
1837
|
onLinkClick;
|
|
1534
1838
|
selectable;
|
|
1839
|
+
/**
|
|
1840
|
+
* Whether code blocks and tables carry copy / download controls.
|
|
1841
|
+
*
|
|
1842
|
+
* Read when a block entity is built, so it affects blocks rendered from here on
|
|
1843
|
+
* rather than retroactively; a document does not rebuild to gain or lose an
|
|
1844
|
+
* affordance.
|
|
1845
|
+
*/
|
|
1846
|
+
blockAffordances;
|
|
1847
|
+
/** Clipboard writer used by the copy controls. */
|
|
1848
|
+
writeClipboard;
|
|
1849
|
+
/** File saver used by the download controls. */
|
|
1850
|
+
saveFile;
|
|
1535
1851
|
activeBlockMetrics = null;
|
|
1536
1852
|
/**
|
|
1537
1853
|
* Called after a streamed append has re-laid-out the document.
|
|
@@ -1756,6 +2072,9 @@ var Markdown = class _Markdown extends UIComponent {
|
|
|
1756
2072
|
this.onLinkClick = opts.onLinkClick;
|
|
1757
2073
|
this.selectable = opts.selectable ?? true;
|
|
1758
2074
|
this._userTiming = opts.userTiming ?? false;
|
|
2075
|
+
this.blockAffordances = opts.blockAffordances ?? false;
|
|
2076
|
+
this.writeClipboard = opts.writeClipboard ?? defaultWriteClipboard;
|
|
2077
|
+
this.saveFile = opts.saveFile ?? defaultSaveFile;
|
|
1759
2078
|
this.content = new Stack({ direction: "vertical", gap: 16 });
|
|
1760
2079
|
this.add(this.content);
|
|
1761
2080
|
this.rawMarkdown = "";
|
|
@@ -2544,6 +2863,73 @@ var Markdown = class _Markdown extends UIComponent {
|
|
|
2544
2863
|
* policy for a zero-dimension source is a separate decision from notifying
|
|
2545
2864
|
* the scene, which is the actual defect here.
|
|
2546
2865
|
*/
|
|
2866
|
+
/**
|
|
2867
|
+
* Wraps a block in its copy / download controls, or returns it untouched.
|
|
2868
|
+
*
|
|
2869
|
+
* The controls are built lazily through `make` so a document with
|
|
2870
|
+
* `blockAffordances` off pays nothing — not the closures, not the measurement
|
|
2871
|
+
* `BlockAffordanceButton` does in its constructor.
|
|
2872
|
+
*/
|
|
2873
|
+
withBlockAffordances(block, make) {
|
|
2874
|
+
if (!this.blockAffordances) return block;
|
|
2875
|
+
const controls = make();
|
|
2876
|
+
return controls.length > 0 ? new BlockWithAffordances(block, controls) : block;
|
|
2877
|
+
}
|
|
2878
|
+
/** Copy and download controls for one fenced code block. */
|
|
2879
|
+
codeBlockAffordances(source, lang) {
|
|
2880
|
+
const opts = this.affordanceButtonOptions();
|
|
2881
|
+
return [
|
|
2882
|
+
new BlockAffordanceButton("Copy code", "Copied", () => this.writeClipboard(source), opts),
|
|
2883
|
+
new BlockAffordanceButton(
|
|
2884
|
+
"Download code",
|
|
2885
|
+
"Saved",
|
|
2886
|
+
() => this.saveFile(`code.${extensionForLanguage(lang)}`, source, mimeForLanguage(lang)),
|
|
2887
|
+
opts
|
|
2888
|
+
)
|
|
2889
|
+
];
|
|
2890
|
+
}
|
|
2891
|
+
/** Copy (as Markdown) and download (as CSV) controls for one table. */
|
|
2892
|
+
tableAffordances(tblToken) {
|
|
2893
|
+
const content = tableContentOf(tblToken);
|
|
2894
|
+
const opts = this.affordanceButtonOptions();
|
|
2895
|
+
return [
|
|
2896
|
+
// Markdown rather than CSV for the clipboard: the reader copied it out of a
|
|
2897
|
+
// Markdown document and the overwhelmingly likely destination is another
|
|
2898
|
+
// one. CSV is what the download is for, where a spreadsheet is the target.
|
|
2899
|
+
new BlockAffordanceButton(
|
|
2900
|
+
"Copy table",
|
|
2901
|
+
"Copied",
|
|
2902
|
+
() => this.writeClipboard(tableToMarkdown(content)),
|
|
2903
|
+
opts
|
|
2904
|
+
),
|
|
2905
|
+
new BlockAffordanceButton(
|
|
2906
|
+
"Download table",
|
|
2907
|
+
"Saved",
|
|
2908
|
+
() => this.saveFile("table.csv", tableToCsv(content), "text/csv;charset=utf-8"),
|
|
2909
|
+
opts
|
|
2910
|
+
)
|
|
2911
|
+
];
|
|
2912
|
+
}
|
|
2913
|
+
/**
|
|
2914
|
+
* Button styling for the affordances, derived from the document theme.
|
|
2915
|
+
*
|
|
2916
|
+
* Themed rather than hardcoded so a light-theme document does not get the dark
|
|
2917
|
+
* default palette. `focusColor` is set explicitly from the theme's accent
|
|
2918
|
+
* because `Button`'s default cyan is tuned for the dark palette and reads as
|
|
2919
|
+
* off-brand elsewhere — while a focus ring is the one affordance a keyboard
|
|
2920
|
+
* user cannot do without.
|
|
2921
|
+
*/
|
|
2922
|
+
affordanceButtonOptions() {
|
|
2923
|
+
return {
|
|
2924
|
+
font: `600 12px ${this.theme.bodyFont}`,
|
|
2925
|
+
padding: 6,
|
|
2926
|
+
radius: 6,
|
|
2927
|
+
bg: this.theme.codeBgColor,
|
|
2928
|
+
hoverBg: this.theme.tableHeaderBgColor,
|
|
2929
|
+
color: this.theme.textColor,
|
|
2930
|
+
focusColor: this.theme.codeColor
|
|
2931
|
+
};
|
|
2932
|
+
}
|
|
2547
2933
|
paragraphImage(imgToken, availableWidth) {
|
|
2548
2934
|
const initialWidth = Math.min(800, availableWidth);
|
|
2549
2935
|
const initialHeight = Math.round(initialWidth * 0.6);
|
|
@@ -2624,6 +3010,7 @@ var Markdown = class _Markdown extends UIComponent {
|
|
|
2624
3010
|
itemIsInlineOnly(item) {
|
|
2625
3011
|
const children = item.tokens;
|
|
2626
3012
|
if (!children || children.length === 0) return true;
|
|
3013
|
+
if (containsImage(children)) return false;
|
|
2627
3014
|
if (children.length === 1 && children[0].type === "paragraph") return true;
|
|
2628
3015
|
return children.every((child) => _Markdown.INLINE_ITEM_TOKENS.has(child.type));
|
|
2629
3016
|
}
|
|
@@ -2653,7 +3040,10 @@ var Markdown = class _Markdown extends UIComponent {
|
|
|
2653
3040
|
const children = item.tokens ?? [];
|
|
2654
3041
|
const stack = new Stack({ direction: "vertical", gap: 4 });
|
|
2655
3042
|
const first = children[0];
|
|
2656
|
-
const
|
|
3043
|
+
const firstIsInline = Boolean(first) && (first.type === "text" || first.type === "paragraph");
|
|
3044
|
+
const leadHasImage = firstIsInline && containsImage(first.tokens);
|
|
3045
|
+
const leadChildren = firstIsInline ? [leadHasImage ? stripImages(first) : first] : [];
|
|
3046
|
+
const leadImages = leadHasImage ? imagesOf(first.tokens) : [];
|
|
2657
3047
|
const leadToken = {
|
|
2658
3048
|
...token,
|
|
2659
3049
|
items: token.items.map((it, i) => i === index ? { ...it, tokens: leadChildren } : it)
|
|
@@ -2666,6 +3056,13 @@ var Markdown = class _Markdown extends UIComponent {
|
|
|
2666
3056
|
indentStart: indent,
|
|
2667
3057
|
availableWidth: Math.max(1, availableWidth - indent)
|
|
2668
3058
|
};
|
|
3059
|
+
for (const image of leadImages) {
|
|
3060
|
+
const el = this.paragraphImage(image, childMetrics.availableWidth);
|
|
3061
|
+
const wrapper = new MarkdownContainer();
|
|
3062
|
+
el.x = indent;
|
|
3063
|
+
wrapper.add(el);
|
|
3064
|
+
stack.add(wrapper);
|
|
3065
|
+
}
|
|
2669
3066
|
for (let i = leadChildren.length; i < children.length; i++) {
|
|
2670
3067
|
const el = this.renderTokenWithMetrics(children[i], childMetrics);
|
|
2671
3068
|
if (!el) continue;
|
|
@@ -3483,7 +3880,7 @@ var Markdown = class _Markdown extends UIComponent {
|
|
|
3483
3880
|
currentTokens = [];
|
|
3484
3881
|
}
|
|
3485
3882
|
};
|
|
3486
|
-
for (const child of pToken.tokens) {
|
|
3883
|
+
for (const child of liftNestedImages(pToken.tokens)) {
|
|
3487
3884
|
if (child.type === "image") {
|
|
3488
3885
|
flushText();
|
|
3489
3886
|
stack.add(this.paragraphImage(child, availableWidth));
|
|
@@ -3511,7 +3908,10 @@ var Markdown = class _Markdown extends UIComponent {
|
|
|
3511
3908
|
const mathBlock = this.renderDisplayMath(codeToken.text, availableWidth);
|
|
3512
3909
|
if (mathBlock) return mathBlock;
|
|
3513
3910
|
}
|
|
3514
|
-
return
|
|
3911
|
+
return this.withBlockAffordances(
|
|
3912
|
+
new CodeBlock(codeToken.text, lang, availableWidth, t, this.selectable),
|
|
3913
|
+
() => this.codeBlockAffordances(codeToken.text, lang)
|
|
3914
|
+
);
|
|
3515
3915
|
}
|
|
3516
3916
|
// ── Blockquotes ──────────────────────────────────────────────────
|
|
3517
3917
|
case "blockquote": {
|
|
@@ -3567,21 +3967,24 @@ var Markdown = class _Markdown extends UIComponent {
|
|
|
3567
3967
|
const rows = tblToken.rows.map(
|
|
3568
3968
|
(row) => row.map((cell) => this.tableCellRichText(cell, false, t))
|
|
3569
3969
|
);
|
|
3570
|
-
return
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3970
|
+
return this.withBlockAffordances(
|
|
3971
|
+
new Table({
|
|
3972
|
+
headers,
|
|
3973
|
+
rows,
|
|
3974
|
+
// `| :--- | :---: | ---: |` already resolves to this on the token; it
|
|
3975
|
+
// was previously discarded, so every column rendered left-aligned.
|
|
3976
|
+
align: tblToken.align,
|
|
3977
|
+
width: availableWidth,
|
|
3978
|
+
textColor: t.textColor,
|
|
3979
|
+
headerTextColor: t.headingColor,
|
|
3980
|
+
font: `${t.fontSize - 2}px ${t.bodyFont}`,
|
|
3981
|
+
borderColor: t.hrColor,
|
|
3982
|
+
bg: t.tableBgColor,
|
|
3983
|
+
headerBg: t.tableHeaderBgColor,
|
|
3984
|
+
selectable: this.selectable
|
|
3985
|
+
}),
|
|
3986
|
+
() => this.tableAffordances(tblToken)
|
|
3987
|
+
);
|
|
3585
3988
|
}
|
|
3586
3989
|
// ── Horizontal rule ──────────────────────────────────────────────
|
|
3587
3990
|
case "hr":
|
|
@@ -3616,13 +4019,22 @@ var Markdown = class _Markdown extends UIComponent {
|
|
|
3616
4019
|
}
|
|
3617
4020
|
};
|
|
3618
4021
|
export {
|
|
4022
|
+
BlockAffordanceButton,
|
|
4023
|
+
BlockWithAffordances,
|
|
3619
4024
|
CodeBlock,
|
|
3620
4025
|
Markdown,
|
|
3621
4026
|
MathBlock,
|
|
3622
4027
|
codeAtlas,
|
|
3623
4028
|
codeAtlasStats,
|
|
4029
|
+
escapeCsvField,
|
|
4030
|
+
escapeMarkdownTableCell,
|
|
4031
|
+
extensionForLanguage,
|
|
3624
4032
|
isMathJaxReady,
|
|
4033
|
+
mimeForLanguage,
|
|
3625
4034
|
parseFrontMatterFields,
|
|
3626
4035
|
preloadMathJax,
|
|
3627
|
-
scanFrontMatter
|
|
4036
|
+
scanFrontMatter,
|
|
4037
|
+
tableContentOf,
|
|
4038
|
+
tableToCsv,
|
|
4039
|
+
tableToMarkdown
|
|
3628
4040
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vectojs/markdown",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"scripts": {
|
|
41
41
|
"build": "(cd ../ui && bun run build) && node scripts/build-worker.js && tsup src/index.ts --format cjs,esm --clean && tsc -p tsconfig.build.json",
|
|
42
42
|
"test": "vitest run",
|
|
43
|
-
"test:e2e": "bun e2e/blockquote-layout.e2e.ts && bun e2e/stream-controller.e2e.ts && bun e2e/lazy-math.e2e.ts && bun e2e/paragraph-image-repaint.e2e.ts && bun e2e/selection-fidelity.e2e.ts && bun e2e/code-atlas-dpr.e2e.ts && bun e2e/set-max-width.e2e.ts"
|
|
43
|
+
"test:e2e": "bun e2e/blockquote-layout.e2e.ts && bun e2e/stream-controller.e2e.ts && bun e2e/lazy-math.e2e.ts && bun e2e/paragraph-image-repaint.e2e.ts && bun e2e/selection-fidelity.e2e.ts && bun e2e/code-atlas-dpr.e2e.ts && bun e2e/set-max-width.e2e.ts && bun e2e/block-affordances.e2e.ts"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"marked": "^18.0.7",
|