@vincemakes/kiso-tui-cells 0.46.2 → 0.47.1

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/md.js CHANGED
@@ -36,7 +36,7 @@
36
36
  * `inlineSpans`, and every entry is pinned by a fixture rather than
37
37
  * described twice.
38
38
  */
39
- import { palette } from "./render.js";
39
+ import { headingRule, palette } from "./render.js";
40
40
  import { breakable, charWidth, displayWidth } from "./width.js";
41
41
  import { escapeTerminal } from "./render.js";
42
42
  import { visibleWidth } from "./components.js";
@@ -51,6 +51,14 @@ const HEADING = /^ {0,3}(#{1,6}) +(\S.*)$/;
51
51
  const RULE = /^ {0,3}(?:-{3,}|\*{3,}|_{3,}) *$/;
52
52
  const QUOTE = /^ {0,3}> ?(.*)$/;
53
53
  const TABLE = /^ {0,3}\|/;
54
+ /** A link reference definition: `[label]: url "title"` (the title may be
55
+ * double-, single- or paren-quoted, and may be absent). Graphite R2f
56
+ * (owner, 2026-09-30): it is its own block, one row per definition, dim.
57
+ * DECLARED DEVIATION: CommonMark lets a definition not interrupt a
58
+ * paragraph; here a complete line that is one starts its own block,
59
+ * because the decision is taken line by line and a model writes its
60
+ * definitions after a blank anyway. */
61
+ const REFDEF = /^ {0,3}\[([^\]\n]+)\]:[ \t]*(\S+)(?:[ \t]+("[^"\n]*"|'[^'\n]*'|\([^)\n]*\)))?[ \t]*$/;
54
62
  const ITEM = /^( *)([-*+]|\d{1,9}[.)])[ \t]+(.*)$/;
55
63
  /** MD-1.4 — a SETEXT underline. Only meaningful under an OPEN paragraph,
56
64
  * which is the only place `#line` consults it. Before this, `=` was not a
@@ -106,6 +114,8 @@ function classify(line) {
106
114
  return "quote";
107
115
  if (TABLE.test(line))
108
116
  return "table";
117
+ if (REFDEF.test(line))
118
+ return "refdef";
109
119
  if (ITEM.test(line))
110
120
  return "list";
111
121
  if (CODE.test(line))
@@ -134,6 +144,8 @@ function joins(kind, line) {
134
144
  return c === "table";
135
145
  case "quote":
136
146
  return c === "quote";
147
+ case "refdef":
148
+ return c === "refdef";
137
149
  default:
138
150
  return false;
139
151
  }
@@ -342,16 +354,30 @@ function fenceLang(line) {
342
354
  // ---- rendering ------------------------------------------------------
343
355
  /** The whole message at once — the freeze property's oracle, and the
344
356
  * path a non-streaming caller takes. */
345
- export function renderMarkdown(text, W) {
357
+ export function renderMarkdown(text, W, opts = {}) {
346
358
  const s = new MdStream();
347
359
  s.push(text);
348
360
  s.end();
349
- return s.blocks().flatMap((b) => renderBlock(b, W));
361
+ return s.blocks().flatMap((b) => renderBlock(b, W, opts.hardBreaks === true));
350
362
  }
351
363
  /** One block's screen rows. Pure in (block, W) — this is the whole
352
364
  * freeze guarantee: same source, same width, same bytes, forever. */
353
- export function renderBlock(b, W) {
354
- return withGap(b, blockBody(b, Math.max(1, W), 0));
365
+ export function renderBlock(b, W, hardBreaks = false) {
366
+ return withGap(b, blockBody(b, Math.max(1, W), 0, "", hardBreaks));
367
+ }
368
+ /** Graphite §5 (R2b): a heading block's level, or null — the prose column
369
+ * puts `§` in the mark column beside a `##`, which is outside the block. */
370
+ export function headingLevel(b) {
371
+ return b.kind === "heading" ? headingShape(b.lines).level : null;
372
+ }
373
+ /** Graphite §5 (R2b): the Graphite styles apply where the palette has its
374
+ * colours; elsewhere (the unknown ground, no colour) the mono forms stay,
375
+ * since a marker is the only carrier that survives there (DC-4). */
376
+ const graphite = () => palette().blue !== "";
377
+ /** `####` and deeper are upper case — outside code spans, whose text is
378
+ * literal (an identifier upper-cased is a different identifier). */
379
+ function upperOutsideCode(text) {
380
+ return text.replace(/(`[^`]*`)|([^`]+)/g, (_m, code, rest) => code ?? (rest ?? "").toUpperCase());
355
381
  }
356
382
  /** The markdown rhythm: a block that carries `gap` opens with a blank row.
357
383
  * One helper because there are two callers — this module's entry point and
@@ -366,8 +392,13 @@ function withGap(b, rows) {
366
392
  * recursing on nothing. Cheap insurance: the wrapper already degrades an
367
393
  * over-wide prefix instead of throwing. */
368
394
  const QUOTE_DEPTH = 4;
369
- function blockBody(b, W, depth) {
395
+ /** `base` is the style the text of a paragraph or list starts in and
396
+ * returns to (a quote's italic `ink2`); `hard` keeps every source line of
397
+ * a paragraph a line of its own (the person's own words: a line break
398
+ * they typed is theirs). */
399
+ function blockBody(b, W, depth, base = "", hard = false) {
370
400
  const p = palette();
401
+ const styled = (t) => (base === "" ? inlineSpans(t, "") : `${base}${inlineSpans(t, base)}${p.reset}`);
371
402
  switch (b.kind) {
372
403
  case "heading": {
373
404
  // DC-4: the LEVEL is information and it used to be discarded —
@@ -379,6 +410,19 @@ function blockBody(b, W, depth) {
379
410
  // `**bold**` inside a heading is still a no-op, which is the mono
380
411
  // discipline paying for itself.
381
412
  const { level, text } = headingShape(b.lines);
413
+ if (graphite()) {
414
+ // Graphite §5 (R2b) — DECLARED REVERSAL of DC-4's "levels are
415
+ // NOT differentiated by colour": `#` bold gold over a rule that
416
+ // fades from gold-mark, `##` bold blue (its `§` in the mark
417
+ // column, outside the block), `###` bold ink, `####` and deeper
418
+ // bold dim in upper case. No level prints its own `#`s. The
419
+ // mono forms below stay where there is no colour to carry it.
420
+ const tone = level === 1 ? p.gold : level === 2 ? p.blue : level >= 4 ? p.dim : "";
421
+ const style = `${p.bold}${tone}`;
422
+ const words = level >= 4 ? upperOutsideCode(text) : text;
423
+ const rows = wrap(`${style}${inlineSpans(words, style)}${p.reset}`, W, "", "");
424
+ return level === 1 ? [...rows, headingRule(Math.max(1, Math.min(40, W)))] : rows;
425
+ }
382
426
  const style = level === 1 ? `${p.bold}${p.underline}` : p.bold;
383
427
  const marker = level >= 3 ? `${"#".repeat(level)} ` : "";
384
428
  return wrap(`${style}${marker}${inlineSpans(text, style)}${p.reset}`, W, "", "");
@@ -401,6 +445,12 @@ function blockBody(b, W, depth) {
401
445
  // cannot pay for it, it yields — the same rule `mdWrap` applies to
402
446
  // an over-wide prefix.
403
447
  const inset = W >= 4 ? " " : "";
448
+ // Graphite §5 (R2b) — DECLARED REVERSAL of R3's "the rule is a
449
+ // solid hairline everywhere" for the MODEL's rule only: three
450
+ // spaced dots in `rail`, so a divider in an answer can never be
451
+ // taken for kiso's own chrome (the composer's rules, a panel's).
452
+ if (graphite())
453
+ return [`${inset}${p.rail}${W - inset.length >= 7 ? "\u00b7 \u00b7 \u00b7" : "\u00b7"}${p.fgEnd}`];
404
454
  return [`${inset}${p.dim}${"\u2500".repeat(Math.max(1, W - inset.length))}${p.reset}`];
405
455
  }
406
456
  case "fence-open":
@@ -436,20 +486,44 @@ function blockBody(b, W, depth) {
436
486
  // is verbatim" (E2 replaced the `│` gutter this comment used to
437
487
  // name with them); saying it twice cost legibility and bought
438
488
  // nothing.
439
- return foldLineWidth(src.slice(indent.length), W - visibleWidth(gutter), indent).map((r) => `${gutter}${r}`);
489
+ // Graphite §5 (R2c, owner 2026-09-29: "no ground — keep it blue,
490
+ // as the design has it") — DECLARED REVERSAL of DC-3's "a fenced
491
+ // body carries no colour token": DC-3 removed a 1.54:1 grey, and
492
+ // `blue` reads at the text floor on both grounds. The rails stay
493
+ // (E2: a copied block is still fenced); there is no ground.
494
+ const [on, off] = p.blue !== "" ? [p.blue, p.fgEnd] : ["", ""];
495
+ return foldLineWidth(src.slice(indent.length), W - visibleWidth(gutter), indent).map((r) => `${gutter}${on}${r}${off}`);
440
496
  }
441
497
  case "quote":
442
498
  return quoteRows(b, W, depth);
443
499
  case "list":
444
- return listRows(b, W);
500
+ return listRows(b, W, base);
445
501
  case "table":
446
502
  return tableRows(b, W);
503
+ case "refdef":
504
+ return b.lines.flatMap((l) => refdefRows(l, W));
447
505
  default:
448
506
  // a paragraph's soft line breaks are spaces — the block reflows at
449
507
  // the terminal's width, which is the whole point of rendering it.
450
- return wrap(inlineSpans(b.lines.join(" "), ""), W, "", "");
508
+ // The person's own words keep the breaks they typed (`hard`).
509
+ if (hard)
510
+ return b.lines.flatMap((l) => wrap(styled(l), W, "", ""));
511
+ return wrap(styled(b.lines.join(" ")), W, "", "");
451
512
  }
452
513
  }
514
+ /** Graphite R2f — one reference definition, as a row of its own:
515
+ * `[label] url · title`, dim; the url and the title are data, never
516
+ * styled. A streamed definition renders the moment its line completes,
517
+ * and a reference to it earlier in the text has already frozen — which
518
+ * is why a reference link shows its text and its label, not the url. */
519
+ function refdefRows(line, W) {
520
+ const p = palette();
521
+ const m = REFDEF.exec(line);
522
+ if (m === null)
523
+ return wrap(line, W, "", "");
524
+ const title = m[3] === undefined ? "" : ` \u00b7 ${m[3].slice(1, -1)}`;
525
+ return wrap(`${p.dim}[${m[1]}] ${m[2]}${title}${p.reset}`, W, "", " ");
526
+ }
453
527
  /**
454
528
  * The inline pass — the mono style table applied to one block's text.
455
529
  *
@@ -466,15 +540,24 @@ function blockBody(b, W, depth) {
466
540
  * out, so a nested span can never strand it. Italic is the one span
467
541
  * that closes surgically (SGR 23), because it can.
468
542
  *
543
+ * Graphite R2f (owner, 2026-09-30) adds `***bold italic***`, code spans
544
+ * of any backtick run, a link's title (read and dropped), `<autolinks>`,
545
+ * `![images]` (named: `image` dim, the alt as a link, the url dim),
546
+ * `[reference][links]` (the text as a link, the label dim) and `<br>` (a
547
+ * line break).
548
+ *
469
549
  * Documented deviations from CommonMark: `_` never emphasizes (it is a
470
550
  * character in identifiers far more often than a marker in prose);
471
- * emphasis does not nest across a code span; `~~` is not a construct at
472
- * all — the markers are content (the strict tokenizer both reference
473
- * implementations converged on, taken to its honest conclusion, since
474
- * SGR 9's terminal support is too fragmented to promise).
551
+ * emphasis does not nest across a code span; `~~` makes its text dim
552
+ * (R2b), never SGR 9, whose terminal support is too fragmented to
553
+ * promise — Apple Terminal draws none; a shortcut reference `[label]`
554
+ * stays literal.
475
555
  */
476
556
  export function inlineSpans(text, base) {
477
557
  const p = palette();
558
+ /** a link's words: `blue` and underlined on a known ground (the base
559
+ * style reopened after them), bold off one */
560
+ const linkWords = (t) => (p.blue !== "" ? `${p.blue}${p.underline}${t}${p.underlineEnd}${p.fgEnd}${base}` : `${p.bold}${t}${p.reset}${base}`);
478
561
  let out = "";
479
562
  let i = 0;
480
563
  while (i < text.length) {
@@ -485,20 +568,81 @@ export function inlineSpans(text, base) {
485
568
  continue;
486
569
  }
487
570
  if (ch === "`") {
488
- const end = text.indexOf("`", i + 1);
489
- if (end > i) {
490
- // a code span's content is LITERAL — no markers inside it mean
491
- // anything, which is what makes `x | y` survive a table split
492
- // DC-3: inline code is a SURFACE (`wash`), closed with washEnd
493
- // rather than a reset so the span composes inside a heading's
494
- // or a quote's own style.
495
- out += `${p.wash}${text.slice(i + 1, end)}${p.washEnd}${base}`;
496
- i = end + 1;
571
+ // Graphite R2f: a code span opens with a RUN of backticks and
572
+ // closes at the next run of the SAME length (CommonMark), so
573
+ // ``` `` `x` `` ``` quotes its backticks; one space inside each end
574
+ // is padding and comes off. A run with no closer is literal.
575
+ let n = 1;
576
+ while (text[i + n] === "`")
577
+ n += 1;
578
+ const end = codeCloser(text, i + n, n);
579
+ if (end < 0) {
580
+ out += text.slice(i, i + n);
581
+ i += n;
582
+ continue;
583
+ }
584
+ let body = text.slice(i + n, end);
585
+ if (body.length >= 2 && body.startsWith(" ") && body.endsWith(" ") && body.trim() !== "")
586
+ body = body.slice(1, -1);
587
+ // a code span's content is LITERAL — no markers inside it mean
588
+ // anything, which is what makes `x | y` survive a table split
589
+ // DC-3: inline code is a SURFACE (`wash`), closed with washEnd
590
+ // rather than a reset so the span composes inside a heading's
591
+ // or a quote's own style.
592
+ // Graphite §5 (R2b): `blue`, no ground — like a code block (owner,
593
+ // 2026-09-29, choosing between the prototype's light blue ground
594
+ // and none, side by side in Apple Terminal)
595
+ out += p.blue !== "" ? `${p.blue}${body}${p.fgEnd}${base}` : `${p.wash}${body}${p.washEnd}${base}`;
596
+ i = end + n;
597
+ continue;
598
+ }
599
+ // Graphite R2f (owner, 2026-09-30): `<br>` is a line break wherever it
600
+ // stands — GitHub's reading; in a table cell the cell grows a row. The
601
+ // wrapper breaks at the newline (mdWrap's hard token).
602
+ if (ch === "<") {
603
+ const br = BR.exec(text.slice(i, i + 8));
604
+ if (br !== null) {
605
+ out += "\n";
606
+ i += br[0].length;
607
+ continue;
608
+ }
609
+ // an autolink — `<https://…>`, `<mailto:…>`, `<a@b.c>` — is the
610
+ // address itself, as a link, without its brackets
611
+ const auto = AUTOLINK.exec(text.slice(i));
612
+ if (auto !== null) {
613
+ out += linkWords(auto[1]);
614
+ i += auto[0].length;
615
+ continue;
616
+ }
617
+ }
618
+ // Graphite R2f (owner, 2026-09-30): a terminal cannot draw an image, so
619
+ // it is named — `image` dim, the alt text as a link, the url dim
620
+ if (ch === "!" && text[i + 1] === "[") {
621
+ const img = LINK.exec(text.slice(i + 1));
622
+ const ref = img === null ? REFLINK.exec(text.slice(i + 1)) : null;
623
+ if (img !== null || ref !== null) {
624
+ const alt = (img ?? ref)[1];
625
+ const where = img !== null ? ` (${img[2]})` : ref[2] !== "" ? ` [${ref[2]}]` : "";
626
+ out += `${p.dim}image${p.dim === "" ? "" : `${p.reset}${base}`}${alt === "" ? "" : ` ${linkWords(alt)}`}${where === "" ? "" : `${p.dim}${where}${p.dim === "" ? "" : `${p.reset}${base}`}`}`;
627
+ i += 1 + (img ?? ref)[0].length;
628
+ continue;
629
+ }
630
+ }
631
+ if (text.startsWith("***", i)) {
632
+ // Graphite R2f: `***x***` is bold AND italic
633
+ const end = closerAt(text, i + 3, "***");
634
+ if (end >= 0) {
635
+ out += `${p.bold}${p.italic}${inlineSpans(text.slice(i + 3, end), `${base}${p.bold}${p.italic}`)}${p.italicEnd}${p.reset}${base}`;
636
+ i = end + 3;
497
637
  continue;
498
638
  }
499
639
  }
500
640
  if (text.startsWith("**", i)) {
501
- const end = closerAt(text, i + 2, "**");
641
+ let end = closerAt(text, i + 2, "**");
642
+ // Graphite R2f: the bold closes at the END of an asterisk run, so
643
+ // `**a *b***` closes the italic inside it first (CommonMark's reading)
644
+ while (end >= 0 && text[end + 2] === "*")
645
+ end += 1;
502
646
  if (end >= 0) {
503
647
  out += `${p.bold}${inlineSpans(text.slice(i + 2, end), `${base}${p.bold}`)}${p.reset}${base}`;
504
648
  i = end + 2;
@@ -513,24 +657,73 @@ export function inlineSpans(text, base) {
513
657
  continue;
514
658
  }
515
659
  }
660
+ // Graphite §5 (R2b) — DECLARED REVERSAL of "`~~` is not a construct
661
+ // at all": struck text is `dim`, the prototype's form. Not SGR 9 —
662
+ // Apple Terminal draws none (checked, 2026-09-29) — and only where
663
+ // dim exists: without it the markers are the only carrier.
664
+ if (text.startsWith("~~", i) && p.dim !== "") {
665
+ const end = closerAt(text, i + 2, "~~");
666
+ if (end >= 0) {
667
+ out += `${p.dim}${inlineSpans(text.slice(i + 2, end), `${base}${p.dim}`)}${p.reset}${base}`;
668
+ i = end + 2;
669
+ continue;
670
+ }
671
+ }
516
672
  if (ch === "[") {
517
673
  const link = LINK.exec(text.slice(i));
518
674
  if (link !== null) {
519
675
  // the text bright, the url dim in parentheses. NO OSC 8 this
520
676
  // round: a hyperlink escape is bytes the human cannot see, and
521
677
  // the byte discipline gets to decide that separately.
522
- out += `${p.bold}${link[1]}${p.reset}${p.dim} (${link[2]})${p.reset}${base}`;
678
+ // Graphite §5 (R2b): the text `blue` and underlined, the url dim
679
+ // after it. Still no OSC 8: Apple Terminal draws an OSC 8 link as
680
+ // plain text (checked in the owner's terminal, 2026-09-29).
681
+ out += p.blue !== ""
682
+ ? `${p.blue}${p.underline}${link[1]}${p.underlineEnd}${p.fgEnd}${p.dim} (${link[2]})${p.reset}${base}`
683
+ : `${p.bold}${link[1]}${p.reset}${p.dim} (${link[2]})${p.reset}${base}`;
523
684
  i += link[0].length;
524
685
  continue;
525
686
  }
687
+ // Graphite R2f (owner, 2026-09-30): `[text][label]` — the url is
688
+ // defined elsewhere, often below, and a row once drawn never
689
+ // changes; so the text reads as a link and the label says where its
690
+ // definition is (drawn dim, as its own row, when it arrives)
691
+ const ref = REFLINK.exec(text.slice(i));
692
+ if (ref !== null) {
693
+ out += `${linkWords(ref[1])}${ref[2] === "" ? "" : `${p.dim} [${ref[2]}]${p.dim === "" ? "" : `${p.reset}${base}`}`}`;
694
+ i += ref[0].length;
695
+ continue;
696
+ }
526
697
  }
527
698
  out += ch;
528
699
  i += 1;
529
700
  }
530
701
  return out;
531
702
  }
532
- const ESCAPABLE = /[\\`*_~[\]()#|+.!>-]/;
533
- const LINK = /^\[([^\]\n]*)\]\(([^)\s\n]*)\)/;
703
+ const ESCAPABLE = /[\\`*_~[\]()#|+.!<>-]/;
704
+ /** `[text](url)`, and (R2f) a title after the url — `"…"`, `'…'` or `(…)`
705
+ * — which a terminal has no hover to show, so it is read and dropped. */
706
+ const LINK = /^\[([^\]\n]*)\]\(([^)\s\n]*)(?:[ \t]+(?:"[^"\n]*"|'[^'\n]*'|\([^)\n]*\)))?[ \t]*\)/;
707
+ /** R2f: `[text][label]` and `[text][]`. The shortcut `[label]` alone is
708
+ * left literal — a bracketed word in prose is far more common. */
709
+ const REFLINK = /^\[([^\]\n]+)\]\[([^\]\n]*)\]/;
710
+ /** R2f: `<scheme:…>` and `<a@b.c>`. */
711
+ const AUTOLINK = /^<((?:https?|ftp):\/\/[^\s<>]+|mailto:[^\s<>]+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})>/;
712
+ /** R2f: `<br>`, `<br/>`, `<br />`, any case. */
713
+ const BR = /^<br[ \t]*\/?>/i;
714
+ /** The start of the next run of exactly `n` backticks at or after
715
+ * `from`, or −1 — a longer or shorter run is content. */
716
+ function codeCloser(text, from, n) {
717
+ for (let i = text.indexOf("`", from); i >= 0;) {
718
+ let m = 1;
719
+ while (text[i + m] === "`")
720
+ m += 1;
721
+ if (m === n)
722
+ return i;
723
+ i = text.indexOf("`", i + m);
724
+ }
725
+ return -1;
726
+ }
534
727
  /** The closing delimiter for an emphasis opener at `from`, or −1.
535
728
  * Strict on both edges: an opener followed by a space, or a closer
536
729
  * preceded by one, is arithmetic or prose, not emphasis (`2 * 3 * 4`
@@ -617,8 +810,9 @@ export function tableShape(lines) {
617
810
  /** The list: `•` normalization, numbers kept, 2 spaces per nesting
618
811
  * level, and the HANGING INDENT — a wrapped item's continuation lines
619
812
  * align to the text column, never back to the left margin. */
620
- function listRows(b, W) {
813
+ function listRows(b, W, base = "") {
621
814
  const p = palette();
815
+ const g = graphite();
622
816
  const out = [];
623
817
  let lead = "";
624
818
  let text = "";
@@ -628,7 +822,8 @@ function listRows(b, W) {
628
822
  // the HANGING INDENT: the continuation prefix is the marker's own
629
823
  // visible width, so a wrapped item's later rows align to the text
630
824
  // column instead of returning to the margin.
631
- out.push(...wrap(inlineSpans(text, ""), W, lead, " ".repeat(displayWidth(lead))));
825
+ const body = base === "" ? inlineSpans(text, "") : `${base}${inlineSpans(text, base)}${p.reset}`;
826
+ out.push(...wrap(body, W, lead, " ".repeat(visibleWidth(lead))));
632
827
  };
633
828
  for (const line of b.lines) {
634
829
  const m = ITEM.exec(line);
@@ -643,8 +838,26 @@ function listRows(b, W) {
643
838
  // screen — but the marker is `- ` rather than `•`, so a copied list
644
839
  // is still a list. A numbered list KEEPS its numbers (they are the
645
840
  // author's meaning, not decoration).
841
+ const indent = " ".repeat(depth + 1);
842
+ if (g) {
843
+ // Graphite §5 (R2b) — DECLARED REVERSAL of E1's `- ` (kept so a
844
+ // copied list would still be a list): `–` at the top level and
845
+ // `·` below it, in `dim`, as the prototype draws them; an ordered
846
+ // list keeps its numbers, dim; a task is `✓` (ok) or `○` (dim)
847
+ // in place of the bullet.
848
+ const task = /^\[([ xX])\] +(.*)$/.exec(m[3]);
849
+ if (task !== null && !/^\d/.test(m[2])) {
850
+ const done = task[1] !== " ";
851
+ lead = `${indent}${done ? p.ok : p.dim}${done ? "\u2713" : "\u25cb"}${done ? p.fgEnd : p.reset} `;
852
+ text = task[2];
853
+ continue;
854
+ }
855
+ lead = `${indent}${p.dim}${/^\d/.test(m[2]) ? m[2] : depth === 0 ? "\u2013" : "\u00b7"}${p.reset} `;
856
+ text = m[3];
857
+ continue;
858
+ }
646
859
  const marker = /^\d/.test(m[2]) ? `${m[2]} ` : "- ";
647
- lead = `${" ".repeat(depth + 1)}${marker}`;
860
+ lead = `${indent}${marker}`;
648
861
  text = m[3];
649
862
  }
650
863
  flush();
@@ -674,21 +887,48 @@ function listRows(b, W) {
674
887
  */
675
888
  function quoteRows(b, W, depth) {
676
889
  const p = palette();
677
- const gutter = `${p.dim}\u2502${p.reset} `;
678
- const bar = `${p.dim}\u2502${p.reset}`;
679
- const text = b.lines.map((l) => QUOTE.exec(l)?.[1] ?? l).join("\n");
890
+ const lines = b.lines.map((l) => QUOTE.exec(l)?.[1] ?? l);
891
+ // Graphite §5 (R2b): a GitHub alert — `> [!NOTE]` on the quote's first
892
+ // line — is a quote with a coloured bar and its kind as a label
893
+ const alert = ALERT.exec(lines[0] ?? "");
894
+ const kind = alert === null ? null : alert[1].toUpperCase();
895
+ const text = (kind === null ? lines : lines.slice(1)).join("\n");
896
+ // Graphite §5 (R2b): the bar is one cell of BACKGROUND down every row
897
+ // (§1.5 — a glyph bar seams between rows in Apple Terminal), then a
898
+ // space; where there is no background to paint, the `│` gutter stays
899
+ const tone = kind === null ? p.quoteBar : ALERTS[kind].bar(p);
900
+ const painted = tone !== "";
901
+ const gutter = painted ? `${tone} ${p.washEnd} ` : `${p.dim}\u2502${p.reset} `;
902
+ const bar = painted ? `${tone} ${p.washEnd}` : `${p.dim}\u2502${p.reset}`;
680
903
  const room = Math.max(1, W - visibleWidth(gutter));
904
+ // a plain quote's words are italic `ink2` (the prototype's); an alert's
905
+ // are the answer's own
906
+ const base = kind === null && graphite() ? `${p.italic}${p.ink2}` : "";
907
+ const label = kind === null ? [] : [`${p.bold}${ALERTS[kind].fg(p)}${ALERTS[kind].word}${p.reset}`];
681
908
  if (depth >= QUOTE_DEPTH)
682
- return wrap(inlineSpans(text.split("\n").join(" "), ""), room, "", "").map((r) => `${gutter}${r}`);
909
+ return [...label, ...wrap(inlineSpans(text.split("\n").join(" "), ""), room, "", "")].map((r) => `${gutter}${r}`);
683
910
  const inner = new MdStream();
684
911
  inner.push(text);
685
912
  inner.end();
686
- const rows = inner.blocks().flatMap((blk) => withGap(blk, blockBody(blk, room, depth + 1)));
913
+ const rows = [...label, ...inner.blocks().flatMap((blk) => withGap(blk, blockBody(blk, room, depth + 1, base)))];
687
914
  // a blank row inside a quote takes the bar and no trailing space: the
688
915
  // gutter says "still the quote", the space would be whitespace a human
689
916
  // copies for nothing.
690
917
  return rows.map((r) => (r === "" ? bar : `${gutter}${r}`));
691
918
  }
919
+ /** GitHub's alert marker: the quote's first line, alone. */
920
+ const ALERT = /^\[!(note|tip|important|warning|caution)\]\s*$/i;
921
+ /** Each alert's word, its label colour and its bar (Graphite §5, R2b):
922
+ * the three that inform in `blue`, WARNING in gold, CAUTION in the
923
+ * failure colour. Off a known ground the label is bold and the bar the
924
+ * plain gutter. */
925
+ const ALERTS = {
926
+ NOTE: { word: "Note", fg: (p) => p.blue, bar: (p) => p.noteBar },
927
+ TIP: { word: "Tip", fg: (p) => p.blue, bar: (p) => p.noteBar },
928
+ IMPORTANT: { word: "Important", fg: (p) => p.blue, bar: (p) => p.noteBar },
929
+ WARNING: { word: "Warning", fg: (p) => p.goldMark, bar: (p) => p.warnBar },
930
+ CAUTION: { word: "Caution", fg: (p) => p.fail, bar: (p) => p.cautionBar },
931
+ };
692
932
  /**
693
933
  * The table. Columns are measured at their NATURAL widths, on the
694
934
  * inline-rendered text with the SGR stripped (a bold cell is four
@@ -721,8 +961,10 @@ function tableRows(b, W) {
721
961
  // The rails are hairlines in the R2 hairline colour, and the styling
722
962
  // goes on AFTER the measure, exactly as it does for a cell: a colour
723
963
  // can never move a column.
724
- const rule = (l, m, r) => `${p.dim}${l}${cols.map((w) => "\u2500".repeat(w + 2)).join(m)}${r}${p.reset}`;
725
- const V = `${p.dim}\u2502${p.reset}`;
964
+ // Graphite §5 (R2b): the rails in `edge`, lighter than the words
965
+ const [on, off] = p.edge !== "" ? [p.edge, p.fgEnd] : [p.dim, p.reset];
966
+ const rule = (l, m, r) => `${on}${l}${cols.map((w) => "\u2500".repeat(w + 2)).join(m)}${r}${off}`;
967
+ const V = `${on}\u2502${off}`;
726
968
  const row = (cells, bold) => {
727
969
  const boxes = cells.map((c, i) => cellBox(c, cols[i], t.align[i], bold));
728
970
  const rows = [];
@@ -746,7 +988,8 @@ function tableRows(b, W) {
746
988
  }
747
989
  /** A cell's column count: what a human sees, styling removed. */
748
990
  function cellWidth(cell) {
749
- return visibleWidth(inlineSpans(cell, ""));
991
+ // R2f: a `<br>` makes a cell several rows — its width is the widest
992
+ return Math.max(0, ...inlineSpans(cell, "").split("\n").map(visibleWidth));
750
993
  }
751
994
  /** MD-1.1 — the SHRINK FLOOR: eight columns, four CJK characters. A
752
995
  * column whose natural width is already at or below it never shrinks at
@@ -899,6 +1142,12 @@ function tokens(text) {
899
1142
  const cp = text.codePointAt(i);
900
1143
  const ch = String.fromCodePoint(cp);
901
1144
  i += ch.length;
1145
+ if (ch === "\n") {
1146
+ flush();
1147
+ out.push({ text: "", w: 0, space: false, hard: true });
1148
+ prev = "";
1149
+ continue;
1150
+ }
902
1151
  if (ch === " " || ch === "\t") {
903
1152
  flush();
904
1153
  out.push({ text: " ", w: 1, space: true });
@@ -1009,6 +1258,10 @@ export function mdWrap(text, W, first, hang) {
1009
1258
  pendW = 0;
1010
1259
  };
1011
1260
  for (const t of tokens(text)) {
1261
+ if (t.hard === true) {
1262
+ close();
1263
+ continue;
1264
+ }
1012
1265
  if (t.space) {
1013
1266
  // a space at a break vanishes; inside a row it is held until the
1014
1267
  // next word earns it