@vincemakes/kiso-tui-cells 0.46.1 → 0.47.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/approval-panel.d.ts +149 -62
- package/dist/approval-panel.js +346 -227
- package/dist/components.d.ts +151 -21
- package/dist/components.js +1190 -821
- package/dist/diff.d.ts +40 -3
- package/dist/diff.js +142 -5
- package/dist/graphite.d.ts +123 -0
- package/dist/graphite.js +257 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +6 -3
- package/dist/md.d.ts +18 -7
- package/dist/md.js +290 -37
- package/dist/render.d.ts +136 -27
- package/dist/render.js +381 -137
- package/dist/strings.d.ts +47 -46
- package/dist/strings.js +130 -171
- package/package.json +1 -1
package/dist/md.d.ts
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
/** The block kinds. `fence-open`/`fence-line` are separate kinds on
|
|
40
40
|
* purpose: a fence's rows must be able to freeze ONE AT A TIME, and
|
|
41
41
|
* MD-1.4's `code-line` is the same shape for the same reason. */
|
|
42
|
-
export type MdKind = "para" | "heading" | "list" | "table" | "quote" | "rule" | "fence-open" | "fence-line" | "fence-close" | "code-line";
|
|
42
|
+
export type MdKind = "para" | "heading" | "list" | "table" | "quote" | "rule" | "fence-open" | "fence-line" | "fence-close" | "code-line" | "refdef";
|
|
43
43
|
/** One block: its SOURCE lines, never a rendered form. The render is a
|
|
44
44
|
* pure function of (block, width), which is what makes the freeze
|
|
45
45
|
* property a property of the scanner alone. */
|
|
@@ -70,10 +70,15 @@ export declare class MdStream {
|
|
|
70
70
|
}
|
|
71
71
|
/** The whole message at once — the freeze property's oracle, and the
|
|
72
72
|
* path a non-streaming caller takes. */
|
|
73
|
-
export declare function renderMarkdown(text: string, W: number
|
|
73
|
+
export declare function renderMarkdown(text: string, W: number, opts?: {
|
|
74
|
+
readonly hardBreaks?: boolean;
|
|
75
|
+
}): string[];
|
|
74
76
|
/** One block's screen rows. Pure in (block, W) — this is the whole
|
|
75
77
|
* freeze guarantee: same source, same width, same bytes, forever. */
|
|
76
|
-
export declare function renderBlock(b: MdBlock, W: number): string[];
|
|
78
|
+
export declare function renderBlock(b: MdBlock, W: number, hardBreaks?: boolean): string[];
|
|
79
|
+
/** Graphite §5 (R2b): a heading block's level, or null — the prose column
|
|
80
|
+
* puts `§` in the mark column beside a `##`, which is outside the block. */
|
|
81
|
+
export declare function headingLevel(b: MdBlock): number | null;
|
|
77
82
|
/**
|
|
78
83
|
* The inline pass — the mono style table applied to one block's text.
|
|
79
84
|
*
|
|
@@ -90,12 +95,18 @@ export declare function renderBlock(b: MdBlock, W: number): string[];
|
|
|
90
95
|
* out, so a nested span can never strand it. Italic is the one span
|
|
91
96
|
* that closes surgically (SGR 23), because it can.
|
|
92
97
|
*
|
|
98
|
+
* Graphite R2f (owner, 2026-09-30) adds `***bold italic***`, code spans
|
|
99
|
+
* of any backtick run, a link's title (read and dropped), `<autolinks>`,
|
|
100
|
+
* `![images]` (named: `image` dim, the alt as a link, the url dim),
|
|
101
|
+
* `[reference][links]` (the text as a link, the label dim) and `<br>` (a
|
|
102
|
+
* line break).
|
|
103
|
+
*
|
|
93
104
|
* Documented deviations from CommonMark: `_` never emphasizes (it is a
|
|
94
105
|
* character in identifiers far more often than a marker in prose);
|
|
95
|
-
* emphasis does not nest across a code span; `~~`
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
106
|
+
* emphasis does not nest across a code span; `~~` makes its text dim
|
|
107
|
+
* (R2b), never SGR 9, whose terminal support is too fragmented to
|
|
108
|
+
* promise — Apple Terminal draws none; a shortcut reference `[label]`
|
|
109
|
+
* stays literal.
|
|
99
110
|
*/
|
|
100
111
|
export declare function inlineSpans(text: string, base: string): string;
|
|
101
112
|
/** Split a table line into cells. The `|` walls are found on the RAW
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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; `~~`
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = `${
|
|
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
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
725
|
-
const
|
|
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
|
-
|
|
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
|