@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/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?.some((child) => child.type === "image") === true;
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 import_ui.UIComponent {
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, import_ui.measureText)("M", this.codeFont));
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 import_ui.RichText(spans, {
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 import_ui.UIComponent {
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.content = new import_ui.Stack({ direction: "vertical", gap: 16 });
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 import_ui.RichText) {
2331
+ if (entity instanceof import_ui2.RichText) {
2012
2332
  entity.setMaxWidth(availableWidth);
2013
2333
  return;
2014
2334
  }
2015
- if (entity instanceof import_ui.Stack) {
2335
+ if (entity instanceof import_ui2.Stack) {
2016
2336
  entity.maxWidth = availableWidth;
2017
2337
  for (const run of entity.children) {
2018
- if (run instanceof import_ui.RichText) run.setMaxWidth(availableWidth);
2019
- else if (run instanceof import_ui.Image) this.refitParagraphImage(run, availableWidth);
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 import_ui.Stack);
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 import_ui.Stack && bqToken.tokens) {
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 import_ui.Stack)) return;
2379
+ if (!(entity instanceof import_ui2.Stack)) return;
2060
2380
  for (const item of entity.children) {
2061
- if (item instanceof import_ui.RichText) item.setMaxWidth(availableWidth);
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 import_ui.Table) entity.setWidth(availableWidth);
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 import_ui.Text) entity.setMaxWidth(availableWidth);
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 import_ui.RichText(this.inlineRunSpans(tokens, t), {
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 import_ui.Image(imgToken.href, {
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 import_ui.RichText(this.tableCellSpans(cell, t), {
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 import_ui.Stack({ direction: "vertical", gap: 4 });
3075
+ const stack = new import_ui2.Stack({ direction: "vertical", gap: 4 });
2688
3076
  const first = children[0];
2689
- const leadChildren = first && (first.type === "text" || first.type === "paragraph") ? [first] : [];
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 import_ui.RichText(this.listItemSpans(token, index), {
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 import_ui.Stack)) return false;
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 import_ui.Stack;
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 import_ui.Stack)) return false;
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 import_ui.RichText)) return false;
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 import_ui.Table)) return false;
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 import_ui.RichText)) return false;
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 import_ui.Stack)) return false;
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 import_ui.RichText(
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 import_ui.Stack({
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 new CodeBlock(codeToken.text, lang, availableWidth, t, this.selectable);
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 import_ui.Stack({ direction: "vertical", gap: 8 });
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 import_ui.Stack({ direction: "vertical", gap: 6 });
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 new import_ui.Table({
3604
- headers,
3605
- rows,
3606
- // `| :--- | :---: | ---: |` already resolves to this on the token; it
3607
- // was previously discarded, so every column rendered left-aligned.
3608
- align: tblToken.align,
3609
- width: availableWidth,
3610
- textColor: t.textColor,
3611
- headerTextColor: t.headingColor,
3612
- font: `${t.fontSize - 2}px ${t.bodyFont}`,
3613
- borderColor: t.hrColor,
3614
- bg: t.tableBgColor,
3615
- headerBg: t.tableHeaderBgColor,
3616
- selectable: this.selectable
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 import_ui.Text(token.text, {
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
  });