@promptctl/rich-js 0.23.0 → 0.24.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.
Files changed (117) hide show
  1. package/THIRD-PARTY-NOTICES +29 -1
  2. package/dist/core/animation.d.ts +78 -0
  3. package/dist/core/animation.js +107 -0
  4. package/dist/core/cell-widths.d.ts +9 -0
  5. package/dist/core/cell-widths.js +140 -0
  6. package/dist/core/cells.d.ts +17 -11
  7. package/dist/core/cells.js +100 -25
  8. package/dist/core/clock.d.ts +9 -1
  9. package/dist/core/clock.js +22 -2
  10. package/dist/core/color.d.ts +10 -0
  11. package/dist/core/color.js +36 -1
  12. package/dist/core/console.js +5 -5
  13. package/dist/core/easing.d.ts +138 -7
  14. package/dist/core/easing.js +355 -17
  15. package/dist/core/json.js +2 -1
  16. package/dist/core/measure.js +2 -2
  17. package/dist/core/place.js +2 -1
  18. package/dist/core/pretty.js +2 -2
  19. package/dist/core/protocol.d.ts +73 -19
  20. package/dist/core/protocol.js +56 -15
  21. package/dist/core/render.js +2 -1
  22. package/dist/core/segment.d.ts +28 -1
  23. package/dist/core/segment.js +60 -44
  24. package/dist/core/strip.d.ts +107 -29
  25. package/dist/core/strip.js +242 -131
  26. package/dist/core/style.d.ts +18 -1
  27. package/dist/core/style.js +49 -9
  28. package/dist/core/text.js +26 -15
  29. package/dist/core/timeline.d.ts +84 -0
  30. package/dist/core/timeline.js +144 -0
  31. package/dist/core/tween.d.ts +49 -0
  32. package/dist/core/tween.js +69 -0
  33. package/dist/host/app.js +4 -6
  34. package/dist/host/index.d.ts +1 -1
  35. package/dist/host/input.d.ts +37 -13
  36. package/dist/host/input.js +146 -56
  37. package/dist/host/terminal-host.d.ts +16 -0
  38. package/dist/host/terminal-host.js +1 -0
  39. package/dist/index.d.ts +16 -11
  40. package/dist/index.js +15 -5
  41. package/dist/node/terminal-host.d.ts +8 -1
  42. package/dist/node/terminal-host.js +2 -0
  43. package/dist/renderables/align.d.ts +3 -2
  44. package/dist/renderables/align.js +3 -0
  45. package/dist/renderables/columns.d.ts +3 -2
  46. package/dist/renderables/columns.js +7 -2
  47. package/dist/renderables/constrain.d.ts +3 -2
  48. package/dist/renderables/constrain.js +5 -1
  49. package/dist/renderables/effect.d.ts +83 -30
  50. package/dist/renderables/effect.js +155 -57
  51. package/dist/renderables/effects.d.ts +88 -39
  52. package/dist/renderables/effects.js +444 -105
  53. package/dist/renderables/embed.d.ts +10 -0
  54. package/dist/renderables/embed.js +18 -9
  55. package/dist/renderables/flexStrip.d.ts +3 -2
  56. package/dist/renderables/flexStrip.js +6 -1
  57. package/dist/renderables/frames.d.ts +116 -0
  58. package/dist/renderables/frames.js +133 -0
  59. package/dist/renderables/group.d.ts +3 -2
  60. package/dist/renderables/group.js +5 -2
  61. package/dist/renderables/html-entities.d.ts +8 -0
  62. package/dist/renderables/html-entities.js +386 -0
  63. package/dist/renderables/layout.d.ts +3 -2
  64. package/dist/renderables/layout.js +15 -2
  65. package/dist/renderables/list-view.d.ts +3 -2
  66. package/dist/renderables/list-view.js +12 -5
  67. package/dist/renderables/live.d.ts +23 -7
  68. package/dist/renderables/live.js +69 -35
  69. package/dist/renderables/markdown-parse.d.ts +109 -0
  70. package/dist/renderables/markdown-parse.js +1421 -0
  71. package/dist/renderables/markdown-url.d.ts +19 -0
  72. package/dist/renderables/markdown-url.js +314 -0
  73. package/dist/renderables/markdown.d.ts +7 -6
  74. package/dist/renderables/markdown.js +363 -499
  75. package/dist/renderables/padding.d.ts +8 -20
  76. package/dist/renderables/padding.js +32 -50
  77. package/dist/renderables/panel.d.ts +26 -26
  78. package/dist/renderables/panel.js +67 -103
  79. package/dist/renderables/progress.d.ts +5 -3
  80. package/dist/renderables/progress.js +14 -3
  81. package/dist/renderables/progressBar.js +4 -4
  82. package/dist/renderables/python-space.d.ts +13 -0
  83. package/dist/renderables/python-space.js +24 -0
  84. package/dist/renderables/spinner.d.ts +73 -27
  85. package/dist/renderables/spinner.js +110 -51
  86. package/dist/renderables/status.d.ts +8 -3
  87. package/dist/renderables/status.js +8 -5
  88. package/dist/renderables/syntax.js +2 -1
  89. package/dist/renderables/table.d.ts +19 -11
  90. package/dist/renderables/table.js +140 -156
  91. package/dist/renderables/traceback.d.ts +1 -1
  92. package/dist/renderables/traceback.js +6 -5
  93. package/dist/renderables/tree.d.ts +6 -5
  94. package/dist/renderables/tree.js +11 -7
  95. package/dist/renderables/viewport.d.ts +13 -5
  96. package/dist/renderables/viewport.js +41 -20
  97. package/dist/template-bindings/animation-funcs.d.ts +37 -0
  98. package/dist/template-bindings/animation-funcs.js +83 -0
  99. package/dist/template-bindings/color-funcs.d.ts +13 -0
  100. package/dist/template-bindings/color-funcs.js +1 -1
  101. package/dist/template-bindings/helpers.d.ts +6 -0
  102. package/dist/template-bindings/helpers.js +12 -3
  103. package/dist/template-bindings/index.d.ts +6 -2
  104. package/dist/template-bindings/index.js +8 -3
  105. package/dist/themes/ramp.d.ts +18 -3
  106. package/dist/themes/ramp.js +23 -2
  107. package/dist/widgets/focus-manager.d.ts +21 -14
  108. package/dist/widgets/focus-manager.js +28 -29
  109. package/dist/widgets/index.d.ts +1 -1
  110. package/dist/widgets/static-item.d.ts +4 -3
  111. package/dist/widgets/static-item.js +9 -7
  112. package/dist/widgets/text-input.d.ts +28 -18
  113. package/dist/widgets/text-input.js +144 -101
  114. package/dist/widgets/widget-app.d.ts +19 -3
  115. package/dist/widgets/widget-app.js +39 -21
  116. package/dist/widgets/widget-base.js +0 -3
  117. package/package.json +2 -5
@@ -1,494 +1,298 @@
1
1
  /**
2
2
  * Markdown — renders Markdown content to the terminal.
3
- * Uses built-in parsing (no external dependency).
4
3
  *
5
- * Block structure is read as CommonMark reads it: a blockquote and a list
6
- * item are both containers, their lines tokenized as Markdown of their own
7
- * once the container's marker or indentation is cut off, and drawn beside a
8
- * gutter — the quote's bar, the item's marker then its hang. Where CommonMark
9
- * leaves the drawing open, Rich's is followed.
4
+ * `markdown-parse.ts` reads the source, once, when a `Markdown` is made; this
5
+ * module draws what it read. A blockquote and a list item are drawn beside a
6
+ * gutter — the quote's bar, the item's marker then its hang — and a table as
7
+ * Rich draws one. Where CommonMark leaves the drawing open, Rich's is
8
+ * followed: HTML, a block of it or a tag inline, is drawn as Rich draws it,
9
+ * not at all.
10
10
  */
11
- import { cellLen, expandTabs, parseTabSize } from "../core/cells.js";
11
+ import { cellCount, cellLen } from "../core/cells.js";
12
12
  import { Segment } from "../core/segment.js";
13
13
  import { Style } from "../core/style.js";
14
14
  import { RichText, resolveStyle } from "../core/text.js";
15
15
  import { Rule } from "./rule.js";
16
- import { drawable, getStyle, groundedStyle, stackedHeight } from "../core/protocol.js";
17
- const HEADING = /^(#{1,6})\s+(.+)$/;
18
- /** A thematic break: three or more of one of `*`, `-` or `_`, spaces and tabs between them allowed. */
19
- const RULE = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/;
20
- const SETEXT = /^(=+|-+)[ \t]*$/;
21
- const FENCE = /^```(\w*)/;
22
- const QUOTE = /^>/;
23
- const ITEM_MARKER = /^(?:[*+-]|(\d{1,9})\.)/;
24
- const LEAD = /^[ \t]*/;
25
- const HARD_BREAK = /(?: {2,}|\\)$/;
26
- const BLOCK_STARTS = [HEADING, FENCE, QUOTE, RULE];
27
- /** Indentation that makes a line indented code when it opens a block, as CommonMark counts it. */
28
- const CODE_INDENT = 4;
29
- /** The tab stop code is drawn with, as Rich's `Syntax` draws it. */
30
- const CODE_TAB_SIZE = parseTabSize(8);
31
- const BLANK_LINE = { text: "", margin: 0, lazy: false };
32
- /** The column `ws` ends at when it starts at column `from`, a tab reaching the next stop of 4 as CommonMark counts it. */
33
- function advance(from, ws) {
34
- return [...ws].reduce((col, ch) => (ch === "\t" ? col + 4 - (col % 4) : col + 1), from);
35
- }
36
- /** The column `line`'s leading whitespace ends at. */
37
- function leadEnd(line) {
38
- return advance(0, LEAD.exec(line.text)[0]);
39
- }
40
- /** How far `line` is indented inside its container. */
41
- function indentOf(line) {
42
- return Math.max(0, leadEnd(line) - line.margin);
43
- }
44
- /** `line` from its first character that is not leading whitespace. */
45
- function body(line) {
46
- return line.text.slice(LEAD.exec(line.text)[0].length);
47
- }
48
- function isBlank(line) {
49
- return line.text.trim() === "";
50
- }
51
- /** Whether `line` sits less than a code indent into its container and its text matches `re`. */
52
- function startsWith(line, re) {
53
- return indentOf(line) < CODE_INDENT && re.test(body(line));
54
- }
16
+ import { Padding } from "./padding.js";
17
+ import { pyRstrip } from "./python-space.js";
18
+ import { Syntax } from "./syntax.js";
19
+ import { Table } from "./table.js";
20
+ import { parseMarkdown } from "./markdown-parse.js";
21
+ import { SIMPLE } from "../core/box.js";
22
+ import { drawable, getStyle, groundedStyle, render, stackedHeight, withCellWidth } from "../core/protocol.js";
55
23
  /**
56
- * `line` from `n` columns into its container on: a block's own indentation
57
- * cut off, the rest as written. A tab straddling the cut leaves the columns
58
- * past it as spaces.
24
+ * Every inline in `inlines` holding none, in document order, and each
25
+ * container's `leave` after its last, carrying the frame `open` made for it
26
+ * as `walk` reached it. A loop over a stack, not recursion: emphasis nests as
27
+ * deep as its source does, and Rich, walking markdown-it's flat token stream,
28
+ * draws ten thousand levels of it.
59
29
  */
60
- function dropColumns(line, n) {
61
- const to = line.margin + n;
62
- let col = 0;
63
- let i = 0;
64
- for (; i < line.text.length && col < to && (line.text[i] === " " || line.text[i] === "\t"); i++) {
65
- const next = advance(col, line.text[i]);
66
- if (next > to)
67
- return " ".repeat(next - to) + line.text.slice(i + 1);
68
- col = next;
69
- }
70
- return line.text.slice(i);
71
- }
72
- function itemStart(line) {
73
- const text = body(line);
74
- const m = ITEM_MARKER.exec(text);
75
- if (!m || indentOf(line) >= CODE_INDENT || RULE.test(text))
76
- return undefined;
77
- const after = text.slice(m[0].length);
78
- const spacing = LEAD.exec(after)[0];
79
- const rest = after.slice(spacing.length);
80
- if (spacing === "" && rest !== "")
81
- return undefined;
82
- const markerEnd = leadEnd(line) + m[0].length;
83
- const spaced = advance(markerEnd, spacing) - markerEnd;
84
- const content = rest === "" || spaced > CODE_INDENT ? markerEnd + 1 : markerEnd + spaced;
85
- const lead = line.text.slice(0, line.text.length - text.length);
86
- return {
87
- bullet: m[1] === undefined ? m[0] : undefined,
88
- start: m[1] === undefined ? 0 : parseInt(m[1], 10),
89
- content,
90
- first: rest === "" ? undefined : { text: lead + " ".repeat(m[0].length) + after, margin: content, lazy: false },
91
- };
92
- }
93
- /** Whether two item starts belong to one list: the same bullet character, or both numbered. */
94
- function sameList(a, b) {
95
- return a.bullet === b.bullet;
96
- }
97
- /**
98
- * A `>` line as a line of its quote: the marker blanked, and the quote's
99
- * content starting past it and the one column of space that may follow it —
100
- * one column of a tab, when a tab follows it.
101
- */
102
- function quoted(line) {
103
- const at = line.text.length - body(line).length;
104
- const spaced = /[ \t]/.test(line.text[at + 1] ?? "");
105
- return { text: `${line.text.slice(0, at)} ${line.text.slice(at + 1)}`, margin: leadEnd(line) + 1 + (spaced ? 1 : 0), lazy: false };
106
- }
107
- /**
108
- * Whether `line` opens a block of its own — a heading, a fence, a quote, a
109
- * rule or a list item. A line that does not is text, and under an open
110
- * paragraph it continues that paragraph.
111
- */
112
- function opensBlock(line) {
113
- return BLOCK_STARTS.some((re) => startsWith(line, re)) || itemStart(line) !== undefined;
114
- }
115
- /**
116
- * Whether `line` ends a paragraph: `opensBlock`, read as CommonMark reads it
117
- * under a paragraph. Indented code cannot interrupt one; a list item does only
118
- * when it has text and, numbered, counts from 1, so prose wrapped onto "2024.
119
- * That year" stays prose; and a line of `=` or `-` is the paragraph's heading
120
- * underline.
121
- */
122
- function endsParagraph(line) {
123
- if (line.lazy)
124
- return false;
125
- if (underline(line))
126
- return true;
127
- const item = itemStart(line);
128
- return item ? item.first !== undefined && (item.bullet !== undefined || item.start === 1) : opensBlock(line);
129
- }
130
- /** The setext underline `line` is — a line of `=` or `-` under a paragraph — if it is one. A lazy line never is. */
131
- function underline(line) {
132
- return line && !line.lazy && indentOf(line) < CODE_INDENT ? SETEXT.exec(body(line)) : null;
133
- }
134
- /** Whether the last block of `tokens`, followed into the containers it closes, is a paragraph still open. */
135
- function endsInParagraph(tokens) {
136
- const last = tokens[tokens.length - 1];
137
- switch (last?.type) {
138
- case "paragraph":
139
- return true;
140
- case "list_item":
141
- case "blockquote":
142
- return endsInParagraph(last.children);
143
- default:
144
- return false;
30
+ function* walk(inlines, open) {
31
+ // The containers open here, innermost last; `outside` is the level outside them all.
32
+ const outside = { inlines, next: 0 };
33
+ const stack = [];
34
+ for (;;) {
35
+ const top = stack[stack.length - 1] ?? outside;
36
+ const inline = top.inlines[top.next++];
37
+ if (inline === undefined) {
38
+ const left = stack.pop();
39
+ if (left === undefined)
40
+ return;
41
+ yield { kind: "leave", frame: left.frame };
42
+ continue;
43
+ }
44
+ if ("children" in inline)
45
+ stack.push({ inlines: inline.children, next: 0, frame: open(inline) });
46
+ else
47
+ yield { kind: "leaf", inline };
145
48
  }
146
49
  }
147
- /**
148
- * Whether `line`, short of a container's marker or indentation, still
149
- * belongs to it as a lazy continuation: CommonMark's paragraph continuation
150
- * text, which continues the paragraph open at the end of the container's
151
- * `inner` lines and opens no block of its own. Outside the container a line
152
- * is read without the paragraph under it, so any list item — whatever it
153
- * counts from — ends the container rather than continuing it. A line after a
154
- * lazy continuation finds that paragraph still open, so `afterLazy` answers
155
- * without reading `inner` again: a container's lazy lines cost one read of
156
- * it, not one each.
157
- */
158
- function continuesLazily(inner, line, afterLazy) {
159
- return !isBlank(line) && !opensBlock(line) && (afterLazy || endsInParagraph(tokenize(inner)));
50
+ /** The styles Rich enters for a link with hyperlinks on, outermost first. */
51
+ function linkStyles(url) {
52
+ return [new Style({ link: url }), "markdown.link_url"];
160
53
  }
161
54
  /**
162
- * The text of the paragraph begun at `start`, every line after it up to a
163
- * blank or a line that ends a paragraph, and the index of the line that ended
164
- * it. Soft-broken lines are joined by one space, as a Markdown renderer
165
- * reflows them, with their own indentation dropped; a line ending in two
166
- * spaces or a backslash breaks hard, and its break is kept.
55
+ * Rich's one inline HTML tag that draws: `<kbd>` exactly as written pushes
56
+ * `entry()` onto `stack`, and `</kbd>` pops the top of it — whatever is
57
+ * there, as Rich's style stack pops, so a close that crosses an emphasis
58
+ * ends that emphasis's style and the emphasis ends the key's. Every other tag
59
+ * draws nothing. `stack` is one block's, or one table cell's: a key left open
60
+ * ends with it, and a close with no key open in it pops nothing. Rich's one
61
+ * stack spans the document, so there a key left open styles every block after
62
+ * it, a block's own close pops the key's entry and leaves the block's style
63
+ * behind for the rest, and a stray close pops whatever is left or, with
64
+ * nothing left, raises. An HTML parser closes an inline element where its
65
+ * block ends, and that is the reading kept here.
167
66
  */
168
- function continuation(lines, start) {
169
- const parts = [lines[start].text];
170
- let i = start + 1;
171
- for (; i < lines.length && !isBlank(lines[i]) && !endsParagraph(lines[i]); i++)
172
- parts.push(lines[i].text);
173
- const text = parts
174
- .map((part, n) => {
175
- const words = (n === parts.length - 1 ? part : part.replace(HARD_BREAK, "")).trim();
176
- return n === 0 ? words : (HARD_BREAK.test(parts[n - 1]) ? "\n" : " ") + words;
177
- })
178
- .join("");
179
- return { text, next: i };
67
+ function kbdTags(stack, entry) {
68
+ let open = 0;
69
+ return (tag) => {
70
+ if (tag === "<kbd>") {
71
+ open++;
72
+ stack.push(entry());
73
+ }
74
+ else if (tag === "</kbd>" && open > 0) {
75
+ open--;
76
+ stack.pop();
77
+ }
78
+ };
180
79
  }
181
- /**
182
- * The lines of the list item opened at `start` — every line after it that is
183
- * blank, indented to the content column, or a lazy continuation — and the
184
- * index of the line that ended it. Blank lines trailing the item are left to
185
- * whatever follows it, and an item that opens with a blank line ends at the
186
- * next one: it is empty.
187
- */
188
- function itemLines(item, lines, start) {
189
- const inner = item.first === undefined ? [] : [item.first];
190
- let lazy = false;
191
- let i = start + 1;
192
- for (; i < lines.length; i++) {
193
- const line = lines[i];
194
- const blank = isBlank(line);
195
- if (blank && inner.length === 0)
196
- break;
197
- const held = blank || leadEnd(line) >= item.content;
198
- lazy = !held && continuesLazily(inner, line, lazy);
199
- if (!held && !lazy)
200
- break;
201
- inner.push(held ? { text: line.text, margin: item.content, lazy: false } : { ...line, lazy: true });
202
- }
203
- for (; inner.length > 0 && isBlank(inner[inner.length - 1]); i--)
204
- inner.pop();
205
- return { inner, next: i };
80
+ const KBD_STYLE = "markdown.kbd";
81
+ /** The style a quote draws its bar in and lays under everything it holds. */
82
+ const QUOTE_STYLE = "markdown.block_quote";
83
+ function headingStyle(options, level) {
84
+ return getStyle(options, `markdown.h${level}`);
206
85
  }
207
86
  /**
208
- * The list opened at `start`: each item as a token holding its blocks, and
209
- * between items the blank lines that separated them in the source. Numbers
210
- * count on from the first item's, as Rich draws them, right-aligned to the
211
- * widest so every item hangs at one column.
87
+ * `inlines` appended to `result` as Rich draws each: emphasis, code and
88
+ * `<kbd>` in their styles, other inline HTML as nothing, an image as nothing
89
+ * where it sits — `drawImage` draws it, ahead of the block — and text as it is. A link's text
90
+ * is the link; or, without hyperlinks, nothing is, and its text is written
91
+ * plain with the URL after it, for a reader who cannot click it.
212
92
  */
213
- function list(first, lines, start) {
214
- const items = [];
215
- let i = start;
216
- let gap = 0;
217
- for (let item = first; item && sameList(first, item); item = i < lines.length ? itemStart(lines[i]) : undefined) {
218
- const { inner, next } = itemLines(item, lines, i);
219
- items.push({ gap, children: tokenize(inner.length === 0 ? [BLANK_LINE] : inner) });
220
- let after = next;
221
- while (after < lines.length && isBlank(lines[after]))
222
- after++;
223
- gap = after - next;
224
- i = after;
225
- }
226
- // The blank lines after the last item belong to whatever follows the list.
227
- const next = i - gap;
228
- const width = String(first.start + items.length - 1).length + 1;
229
- const tokens = items.flatMap(({ gap: blanks, children }, n) => [
230
- ...Array.from({ length: blanks }, () => ({ type: "blank" })),
231
- {
232
- type: "list_item",
233
- marker: first.bullet === undefined ? { kind: "number", label: `${first.start + n}.`.padStart(width) } : { kind: "bullet" },
234
- children,
235
- },
236
- ]);
237
- return { tokens, next };
238
- }
239
- function tokenize(lines) {
240
- const tokens = [];
241
- let i = 0;
242
- while (i < lines.length) {
243
- const line = lines[i];
244
- // Blank line
245
- if (isBlank(line)) {
246
- tokens.push({ type: "blank" });
247
- i++;
248
- continue;
249
- }
250
- // Indented code: its lines, and the blank lines between them, with the indent cut off
251
- if (indentOf(line) >= CODE_INDENT) {
252
- const code = [];
253
- for (; i < lines.length && (isBlank(lines[i]) || indentOf(lines[i]) >= CODE_INDENT); i++) {
254
- code.push(dropColumns(lines[i], CODE_INDENT));
255
- }
256
- for (; code[code.length - 1].trim() === ""; i--)
257
- code.pop();
258
- tokens.push({ type: "code_block", code: code.join("\n") });
259
- continue;
260
- }
261
- // Heading
262
- const headingMatch = HEADING.exec(body(line));
263
- if (headingMatch) {
264
- tokens.push({ type: "heading", level: headingMatch[1].length, text: headingMatch[2] });
265
- i++;
266
- continue;
267
- }
268
- // Horizontal rule
269
- if (startsWith(line, RULE)) {
270
- tokens.push({ type: "hr" });
271
- i++;
272
- continue;
93
+ function appendInline(result, inlines, settings, options) {
94
+ // Rich's style stack: each open construct's style laid over the styles it
95
+ // sits in, so every piece of text takes one span, the style on top, and a
96
+ // nest ten thousand deep costs one combination a level.
97
+ const stack = [Style.null()];
98
+ // Inside a link drawn without hyperlinks, whose text is styled whole as it closes, nothing is styled.
99
+ let plain = false;
100
+ // The style on top with `styles` laid over it.
101
+ const over = (styles) => styles.reduce((under, style) => under.add(resolveStyle(options, style)), stack[stack.length - 1]);
102
+ const kbd = kbdTags(stack, () => over([KBD_STYLE]));
103
+ // The text from `start` to the end in the style on top, with `styles` over it.
104
+ const paint = (start, styles = []) => {
105
+ if (!plain)
106
+ result.stylize(over(styles), start);
107
+ };
108
+ const write = (text, styles) => {
109
+ const start = result.length;
110
+ result.append(text);
111
+ paint(start, styles);
112
+ };
113
+ const enter = (styles) => {
114
+ stack.push(over(styles));
115
+ return () => stack.pop();
116
+ };
117
+ // A link to `url` opened here: its text is the link, or without hyperlinks
118
+ // is written plain, in `markdown.link`, with the URL after it when it closes.
119
+ const openLink = (url) => {
120
+ if (settings.hyperlinks)
121
+ return enter(linkStyles(url));
122
+ const [start, outer] = [result.length, plain];
123
+ plain = true;
124
+ return () => {
125
+ plain = outer;
126
+ paint(start, ["markdown.link"]);
127
+ write(" (");
128
+ write(url, ["markdown.link_url"]);
129
+ write(")");
130
+ };
131
+ };
132
+ // Each construct's frame is what closing it does.
133
+ const open = (container) => {
134
+ switch (container.kind) {
135
+ case "emphasis":
136
+ return enter([`markdown.${container.tag}`]);
137
+ case "link":
138
+ return openLink(container.url);
273
139
  }
274
- // Fenced code block: its lines with the fence's own indentation cut off
275
- if (startsWith(line, FENCE)) {
276
- const indent = indentOf(line);
277
- const codeLines = [];
278
- for (i++; i < lines.length && !startsWith(lines[i], FENCE); i++)
279
- codeLines.push(dropColumns(lines[i], indent));
280
- i++; // skip closing ```
281
- tokens.push({ type: "code_block", code: codeLines.join("\n") });
140
+ };
141
+ for (const step of walk(inlines, open)) {
142
+ if (step.kind === "leave") {
143
+ step.frame();
282
144
  continue;
283
145
  }
284
- // Blockquote: Markdown of its own — its `>` lines with the marker blanked,
285
- // and any unmarked line lazily continuing the paragraph open inside it
286
- if (startsWith(line, QUOTE)) {
287
- const inner = [];
288
- let lazy = false;
289
- for (; i < lines.length; i++) {
290
- const next = lines[i];
291
- const marked = startsWith(next, QUOTE);
292
- lazy = !marked && continuesLazily(inner, next, lazy);
293
- if (!marked && !lazy)
294
- break;
295
- inner.push(marked ? quoted(next) : { ...next, lazy: true });
146
+ const inline = step.inline;
147
+ switch (inline.kind) {
148
+ case "text":
149
+ write(inline.text);
150
+ break;
151
+ // Rich writes a soft break as a space and a hard one as a newline.
152
+ case "softbreak":
153
+ write(" ");
154
+ break;
155
+ case "hardbreak":
156
+ write("\n");
157
+ break;
158
+ case "code":
159
+ write(inline.code, [groundedStyle(options, resolveStyle(options, settings.inlineCodeStyle))]);
160
+ break;
161
+ case "html":
162
+ kbd(inline.content);
163
+ break;
164
+ case "image":
165
+ break;
166
+ case "autolink": {
167
+ const close = openLink(inline.url);
168
+ write(inline.text);
169
+ close();
170
+ break;
296
171
  }
297
- tokens.push({ type: "blockquote", children: tokenize(inner) });
298
- continue;
299
- }
300
- // List: its items, each Markdown of its own, as a quote is
301
- const item = itemStart(line);
302
- if (item) {
303
- const { tokens: items, next } = list(item, lines, i);
304
- tokens.push(...items);
305
- i = next;
306
- continue;
307
172
  }
308
- // Paragraph, or a setext heading when a line of `=` or `-` underlines it
309
- const { text, next } = continuation(lines, i);
310
- const setext = underline(lines[next]);
311
- tokens.push(setext
312
- ? { type: "heading", level: setext[1].startsWith("=") ? 1 : 2, text }
313
- : { type: "paragraph", text });
314
- i = setext ? next + 1 : next;
315
173
  }
316
- return tokens;
317
- }
318
- const TICKS = /`+/y;
319
- const STRONG = /\*\*(.+?)\*\*/y;
320
- const EMPHASIS = /\*(.+?)\*/y;
321
- const ANGLED_DESTINATION = /\(\s*<([^<>\n]*)>/y;
322
- const LINK_TAIL = /\s*(?:"[^"]*"|'[^']*'|\([^()]*\))?\s*\)/y;
323
- /** `re`, a sticky pattern, matched exactly at `i` in `text`. */
324
- function matchAt(re, text, i) {
325
- re.lastIndex = i;
326
- return re.exec(text);
327
- }
328
- /**
329
- * Each `[` in `text` that a `]` closes, to the index of that `]`: brackets
330
- * nest, and a backslash escapes the character after it. One pass, so a run of
331
- * text full of brackets that close nothing costs no more than one without.
332
- */
333
- function bracketPairs(text) {
334
- const pairs = new Map();
335
- const open = [];
336
- for (let i = 0; i < text.length; i++) {
337
- if (text[i] === "\\")
338
- i++;
339
- else if (text[i] === "[")
340
- open.push(i);
341
- else if (text[i] === "]" && open.length > 0)
342
- pairs.set(open.pop(), i);
343
- }
344
- return pairs;
345
174
  }
175
+ const EXIT = { kind: "exit", newLine: true };
346
176
  /**
347
- * The `(destination "title")` of a link opening at `open`, as CommonMark reads
348
- * it: a destination in angle brackets, or a run of non-space characters whose
349
- * parentheses balance — so a URL may itself hold `(…)` — then an optional
350
- * quoted title, then the `)`. The destination and the index past the `)`.
177
+ * The steps in a block's text. An image is an element, Rich's `ImageItem`,
178
+ * which sets no `new_line`. Text with no inlines is one too: markdown-it
179
+ * leaves an empty inline token unflattened, and Rich exits it as an element
180
+ * it draws nothing for.
351
181
  */
352
- function linkDestination(text, open) {
353
- if (text[open] !== "(")
354
- return undefined;
355
- const angled = matchAt(ANGLED_DESTINATION, text, open);
356
- let i;
357
- let url;
358
- if (angled) {
359
- url = angled[1];
360
- i = open + angled[0].length;
361
- }
362
- else {
363
- i = open + 1;
364
- while (text[i] === " ")
365
- i++;
366
- const from = i;
367
- for (let depth = 0; i < text.length && !/\s/.test(text[i]) && !(text[i] === ")" && depth === 0); i++) {
368
- if (text[i] === "\\")
369
- i++;
370
- else if (text[i] === "(")
371
- depth++;
372
- else if (text[i] === ")")
373
- depth--;
374
- }
375
- url = text.slice(from, i);
376
- }
377
- const tail = matchAt(LINK_TAIL, text, i);
378
- return tail ? { url, end: i + tail[0].length } : undefined;
379
- }
380
- /** The inline construct opening at `i` in `text`, if one does; `brackets` is `text`'s `bracketPairs`. */
381
- function inlineAt(text, i, brackets) {
382
- switch (text[i]) {
383
- case "`": {
384
- const ticks = matchAt(TICKS, text, i)[0];
385
- const close = new RegExp(`(?<!\`)${ticks}(?!\`)`, "g");
386
- close.lastIndex = i + ticks.length;
387
- const found = close.exec(text);
388
- return found
389
- ? { kind: "code", code: text.slice(i + ticks.length, found.index), end: found.index + ticks.length }
390
- : { kind: "literal", text: ticks, end: i + ticks.length };
391
- }
392
- case "*": {
393
- const strong = matchAt(STRONG, text, i);
394
- const emphasis = strong ?? matchAt(EMPHASIS, text, i);
395
- return emphasis
396
- ? { kind: "emphasis", style: strong ? "bold" : "italic", inner: emphasis[1], end: i + emphasis[0].length }
397
- : undefined;
182
+ function* inlineSteps(inlines, within, settings) {
183
+ if (inlines.length === 0)
184
+ yield EXIT;
185
+ // Rich's style stack: what each open construct entered, innermost last, one entry each as Rich pushes one.
186
+ const stack = [within];
187
+ const kbd = kbdTags(stack, () => [KBD_STYLE]);
188
+ // Each construct's frame is how many entries it pushed.
189
+ const open = (container) => {
190
+ switch (container.kind) {
191
+ case "emphasis":
192
+ stack.push([`markdown.${container.tag}`]);
193
+ return 1;
194
+ case "link": {
195
+ // Without hyperlinks Rich enters no style for a link.
196
+ const entered = settings.hyperlinks ? [linkStyles(container.url)] : [];
197
+ stack.push(...entered);
198
+ return entered.length;
199
+ }
398
200
  }
399
- case "!":
400
- case "[": {
401
- const image = text[i] === "!";
402
- const open = image ? i + 1 : i;
403
- const close = brackets.get(open);
404
- const destination = close === undefined ? undefined : linkDestination(text, close + 1);
405
- if (!destination)
406
- return undefined;
407
- const label = text.slice(open + 1, close);
408
- return image
409
- ? { kind: "image", alt: label, src: destination.url, end: destination.end }
410
- : { kind: "link", inner: label, url: destination.url, end: destination.end };
201
+ };
202
+ for (const step of walk(inlines, open)) {
203
+ if (step.kind === "leave")
204
+ stack.length -= step.frame;
205
+ else if (step.inline.kind === "html")
206
+ kbd(step.inline.content);
207
+ else if (step.inline.kind === "image") {
208
+ yield { kind: "image", image: { alt: step.inline.alt, src: step.inline.src, styles: stack.flat() } };
209
+ yield { kind: "exit", newLine: false };
411
210
  }
412
- default:
413
- return undefined;
414
211
  }
415
212
  }
416
213
  /**
417
- * `text` as inline Markdown in `style`: a text of its own whose style sits
418
- * under every style inside it, so appended where it goes it draws as Rich
419
- * draws a nested construct — the inner style on top.
214
+ * The step Rich takes exiting `token`'s element: every element sets
215
+ * `new_line` but a rule, which draws its own blank row after it.
420
216
  */
421
- function styled(text, style, settings, options) {
422
- const result = new RichText("", { style });
423
- appendInline(result, text, settings, options);
424
- return result;
217
+ function exitStep(token) {
218
+ return { kind: "exit", newLine: token.type !== "hr" };
425
219
  }
426
220
  /**
427
- * `text` as inline Markdown appended to `result`: code spans, emphasis, links
428
- * and images, the text inside emphasis, links and images parsed the same way,
429
- * and everything else as it is.
221
+ * The steps inside `token`, up to and not including its own exit: Rich
222
+ * checks `new_line` ahead of drawing a block before it exits it. A table's
223
+ * cells are elements of their own, the header's first.
430
224
  */
431
- function appendInline(result, text, settings, options) {
432
- const brackets = bracketPairs(text);
433
- let plain = 0;
434
- for (let i = 0; i < text.length;) {
435
- const found = inlineAt(text, i, brackets);
436
- if (!found) {
437
- i++;
438
- continue;
225
+ function* stepsInside(token, within, options, settings) {
226
+ const children = function* (blocks, inner) {
227
+ for (const child of blocks) {
228
+ yield* stepsInside(child, inner, options, settings);
229
+ yield exitStep(child);
439
230
  }
440
- result.append(text.slice(plain, i));
441
- switch (found.kind) {
442
- case "code":
443
- result.append(found.code, groundedStyle(options, resolveStyle(options, settings.inlineCodeStyle)));
444
- break;
445
- case "literal":
446
- result.append(found.text);
447
- break;
448
- case "emphasis":
449
- result.append(styled(found.inner, found.style, settings, options));
450
- break;
451
- case "link": {
452
- // As Rich draws it: without hyperlinks the URL is written out, and it
453
- // is still the link, for a terminal that can click it.
454
- const link = new RichText("", { style: new Style({ link: found.url }) });
455
- if (settings.hyperlinks) {
456
- result.append(link.append(styled(found.inner, "markdown.link_url", settings, options)));
457
- }
458
- else {
459
- result
460
- .append(styled(found.inner, "markdown.link", settings, options))
461
- .append(" (")
462
- .append(link.append(found.url, "markdown.link_url"))
463
- .append(")");
464
- }
465
- break;
231
+ };
232
+ switch (token.type) {
233
+ case "paragraph":
234
+ yield* inlineSteps(token.text, within, settings);
235
+ break;
236
+ case "heading":
237
+ yield* inlineSteps(token.text, [...within, headingStyle(options, token.level)], settings);
238
+ break;
239
+ case "list":
240
+ for (const item of token.items) {
241
+ yield* children(item, within);
242
+ yield EXIT;
466
243
  }
467
- case "image": {
468
- // Rich's ImageItem: a picture glyph, then the alt text — or, with
469
- // none, the image's file name — linked to the image, then a space.
470
- const title = found.alt || (found.src.replace(/\/+$/, "").split("/").pop() ?? "");
471
- const link = settings.hyperlinks ? new Style({ link: found.src }) : "";
472
- result.append(drawable(options, "🌆 ", "")).append(styled(title, link, settings, options)).append(" ");
473
- break;
244
+ break;
245
+ case "blockquote":
246
+ yield* children(token.children, [...within, QUOTE_STYLE]);
247
+ break;
248
+ case "table":
249
+ for (const cell of [...token.columns.map(({ header }) => header), ...token.rows.flat()]) {
250
+ yield* inlineSteps(cell, within, settings);
251
+ yield EXIT;
474
252
  }
475
- }
476
- i = plain = found.end;
253
+ break;
254
+ case "code_block":
255
+ case "html_block":
256
+ case "hr":
257
+ break;
258
+ }
259
+ }
260
+ /**
261
+ * The steps of the whole document. Rich draws a block at the top of it as it
262
+ * exits, so a block is drawn where its exit is.
263
+ */
264
+ function* documentSteps(tokens, options, settings) {
265
+ for (const token of tokens) {
266
+ yield* stepsInside(token, [], options, settings);
267
+ yield { kind: "block", token };
268
+ yield exitStep(token);
477
269
  }
478
- result.append(text.slice(plain));
479
270
  }
480
271
  /**
481
- * What `draw` renders in what is left of the width beside a gutter, each of
272
+ * An image drawn as Rich's `ImageItem` draws one: a picture glyph, then the
273
+ * alt text in the styles it sits in — or, with none, the image's file name,
274
+ * unstyled — linked to the link those styles carry or else to the image, then a
275
+ * space, at the full width, ending no row — so whatever is drawn next carries
276
+ * on after it.
277
+ */
278
+ function drawImage(image, options, settings) {
279
+ // Rich reads the link off its style stack, so a link a crossed `</kbd>` left open still holds.
280
+ const style = image.styles.reduce((under, over) => under.add(resolveStyle(options, over)), Style.null());
281
+ const title = image.alt ? new RichText(image.alt).stylize(style) : new RichText(image.src.replace(/\/+$/, "").split("/").pop() ?? "");
282
+ if (settings.hyperlinks)
283
+ title.stylize(new Style({ link: style.link || image.src || undefined }));
284
+ return render(new RichText("", { end: "" }).append(drawable(options, "🌆 ", "")).append(title).append(" "), options);
285
+ }
286
+ /**
287
+ * What `content` renders in what is left of the width beside a gutter, each of
482
288
  * its rows ended. The first row's gutter is `first` and every later row's is
483
289
  * `rest`, so a list item's text hangs clear of its bullet and a quote's bar
484
290
  * runs its full height. A block with no gutter passes two empty segments:
485
- * every block is laid out the same way. What is drawn keeps at least one
486
- * column however wide the gutter, so a row may overrun its width but no gutter
487
- * swallows what it introduces.
291
+ * every block is laid out the same way. A gutter that leaves its content no
292
+ * column draws nothing beside it, since that content draws no rows.
488
293
  */
489
- function* guttered(draw, options, first, rest, style) {
490
- const width = Math.max(1, options.maxWidth - cellLen(first.text));
491
- const rows = Segment.splitLines(Segment.applyStyle([...draw({ ...options, maxWidth: width })], style));
294
+ function* guttered(content, options, first, rest) {
295
+ const rows = Segment.splitLines(render(content, { ...options, maxWidth: options.maxWidth - cellLen(first.text) }));
492
296
  for (const [row, line] of rows.entries()) {
493
297
  yield row === 0 ? first : rest;
494
298
  yield* line;
@@ -502,66 +306,111 @@ function* guttered(draw, options, first, rest, style) {
502
306
  * and a heading's padding is not underlined.
503
307
  */
504
308
  function inline(text, settings, justify, style) {
505
- return function* (options) {
506
- const body = new RichText("", { end: "", style });
507
- appendInline(body, text, settings, options);
508
- // `append` lays the body's base style down as the first span.
509
- yield* new RichText("", { end: "", justify }).append(body).render(options);
510
- yield Segment.line();
309
+ return {
310
+ *render(options) {
311
+ const body = new RichText("", { end: "", style });
312
+ appendInline(body, text, settings, options);
313
+ // `append` lays the body's base style down as the first span.
314
+ yield* render(new RichText("", { end: "", justify }).append(body), options);
315
+ yield Segment.line();
316
+ },
511
317
  };
512
318
  }
513
319
  const NO_GUTTER = new Segment("");
320
+ /**
321
+ * `tokens` drawn as Rich's `render_lines` draws a quote's or an item's
322
+ * content: in `style`, every row padded to the width in it and ended.
323
+ */
324
+ function* renderLines(tokens, options, settings, style) {
325
+ for (const line of Segment.splitLines(Segment.applyStyle(renderTokens(tokens, options, settings), style))) {
326
+ yield* Segment.adjustLineLength(line, options.maxWidth, style);
327
+ yield Segment.line();
328
+ }
329
+ }
514
330
  /**
515
331
  * Each block in `tokens`, every row it draws ended, body text placed by
516
332
  * `settings.justify`. A heading keeps its own placement whatever `settings`
517
- * or `options` say — Rich's: an h1 centred, every other level left — and its
518
- * style stops where its text does.
333
+ * or `options` say — Rich's `Heading.LEVEL_ALIGN`: an h1 centred, every other
334
+ * level left, each row padded to the width — and its style stops where its
335
+ * text does.
519
336
  */
520
337
  function* renderTokens(tokens, options, settings) {
521
- for (const token of tokens) {
522
- switch (token.type) {
523
- case "heading": {
524
- const style = getStyle(options, `markdown.h${Math.min(token.level, 4)}`);
525
- const draw = inline(token.text, settings, token.level === 1 ? "center" : undefined, style);
526
- yield* guttered(draw, { ...options, justify: undefined }, NO_GUTTER, NO_GUTTER);
527
- break;
528
- }
529
- case "paragraph": {
530
- yield* guttered(inline(token.text, settings, settings.justify), options, NO_GUTTER, NO_GUTTER);
531
- break;
532
- }
533
- case "code_block": {
534
- const codeStyle = groundedStyle(options, getStyle(options, "markdown.code"));
535
- // Tabs expanded here, as `Syntax` expands them, since a terminal would
536
- // count a raw one from its own column 0 and not the code's.
537
- for (const line of expandTabs(token.code, CODE_TAB_SIZE).text.split("\n")) {
538
- yield new Segment(line, codeStyle);
539
- yield Segment.line();
540
- }
541
- break;
542
- }
543
- case "hr": {
544
- const rule = new Rule(undefined, { style: "markdown.hr" });
545
- yield* rule.render(options);
546
- break;
547
- }
548
- case "list_item": {
549
- const marker = token.marker.kind === "number" ? `${token.marker.label} ` : drawable(options, " • ", " * ");
550
- const hang = new Segment(" ".repeat(cellLen(marker)));
551
- const body = (inner) => renderTokens(token.children, inner, settings);
552
- yield* guttered(body, options, new Segment(marker), hang);
553
- break;
554
- }
555
- case "blockquote": {
556
- const bar = new Segment(drawable(options, "▎ ", "| "), getStyle(options, "markdown.hr"));
557
- const body = (inner) => renderTokens(token.children, inner, settings);
558
- yield* guttered(body, options, bar, bar, Style.parse("dim italic"));
559
- break;
338
+ for (const token of tokens)
339
+ yield* renderBlock(token, options, settings);
340
+ }
341
+ function* renderBlock(token, options, settings) {
342
+ switch (token.type) {
343
+ case "heading": {
344
+ const draw = inline(token.text, settings, token.level === 1 ? "center" : "left", headingStyle(options, token.level));
345
+ yield* guttered(draw, options, NO_GUTTER, NO_GUTTER);
346
+ break;
347
+ }
348
+ case "paragraph": {
349
+ yield* guttered(inline(token.text, settings, settings.justify), options, NO_GUTTER, NO_GUTTER);
350
+ break;
351
+ }
352
+ case "code_block": {
353
+ // Rich's CodeBlock: the code, its trailing whitespace dropped, handed to
354
+ // `Syntax` word-wrapped inside a padding of one cell all round — every
355
+ // cell of the block, padding and all, in the code's style.
356
+ const style = groundedStyle(options, getStyle(options, "markdown.code"));
357
+ const syntax = new Syntax(pyRstrip(token.code), "text", { wordWrap: true });
358
+ yield* render(new Padding(syntax, 1, { style }), options);
359
+ break;
360
+ }
361
+ case "hr": {
362
+ // Rich's HorizontalRule: the rule, drawn in `-` rather than Rule's own
363
+ // glyph, then a blank row of its own.
364
+ const rule = new Rule(undefined, { characters: "-", style: "markdown.hr" });
365
+ yield* render(rule, options);
366
+ yield Segment.line();
367
+ break;
368
+ }
369
+ case "list": {
370
+ // Rich's ListItem: a bullet, or the number without its `.` or `)`,
371
+ // right-aligned in a column sized for the number one past the last
372
+ // item's — Rich's `last_number` — and the hang under it in the marker's
373
+ // style.
374
+ const { start } = token;
375
+ const markerStyle = getStyle(options, start === undefined ? "markdown.item.bullet" : "markdown.item.number");
376
+ const width = start === undefined ? 3 : String(start + token.items.length).length + 2;
377
+ const hang = new Segment(" ".repeat(width), markerStyle);
378
+ for (const [n, item] of token.items.entries()) {
379
+ const marker = start === undefined ? drawable(options, " • ", " * ") : `${String(start + n).padStart(width - 1)} `;
380
+ const body = { render: (inner) => renderLines(item, inner, settings, getStyle(options, "markdown.item")) };
381
+ yield* guttered(body, options, new Segment(marker, markerStyle), hang);
560
382
  }
561
- case "blank":
562
- yield Segment.line();
563
- break;
383
+ break;
384
+ }
385
+ case "blockquote": {
386
+ // Rich's BlockQuote: the bar and everything the quote holds in the one
387
+ // style, the content laid out four cells short of the width beside a
388
+ // bar of two, so every row of a quote ends two cells short.
389
+ const style = getStyle(options, QUOTE_STYLE);
390
+ const bar = new Segment(drawable(options, "▌ ", "| "), style);
391
+ const body = { render: (inner) => renderLines(token.children, inner, settings, style) };
392
+ yield* guttered(body, { ...options, maxWidth: cellCount(options.maxWidth - 2) }, bar, bar);
393
+ break;
394
+ }
395
+ case "table": {
396
+ // Rich's TableElement: each cell inline Markdown placed by its
397
+ // column's alignment, the header's in `markdown.table.header` on top.
398
+ const cell = (text, justify) => {
399
+ const content = new RichText("", { justify });
400
+ appendInline(content, text, settings, options);
401
+ return content;
402
+ };
403
+ const table = new Table({ box: SIMPLE, padEdge: false, style: "markdown.table.border", showEdge: true, collapsePadding: true });
404
+ for (const { header, align } of token.columns)
405
+ table.addColumn(cell(header, align).stylize("markdown.table.header"));
406
+ for (const row of token.rows)
407
+ table.addRow(...row.map((text, n) => cell(text, token.columns[n].align)));
408
+ yield* render(table, options);
409
+ break;
564
410
  }
411
+ case "html_block":
412
+ // Rich draws nothing for it.
413
+ break;
565
414
  }
566
415
  }
567
416
  export class Markdown {
@@ -569,19 +418,34 @@ export class Markdown {
569
418
  inlineCodeStyle;
570
419
  hyperlinks;
571
420
  justify;
421
+ // Read once, as Rich's Markdown parses its markup when it is made: nothing a
422
+ // render is asked for changes how the source reads, and a Live redraws it
423
+ // every frame.
424
+ parsed;
572
425
  constructor(markdown, options) {
573
426
  this.markdown = markdown;
427
+ this.parsed = parseMarkdown(markdown);
574
428
  this.inlineCodeStyle = options?.inlineCodeStyle ?? "markdown.code";
575
429
  this.hyperlinks = options?.hyperlinks !== false;
576
430
  this.justify = options?.justify ?? "left";
577
431
  }
578
432
  *render(rawOptions) {
579
- // Every block below is one of a stack.
580
- const options = { ...rawOptions, height: stackedHeight(rawOptions.height) };
581
- const lines = this.markdown.split(/\r?\n/).map((text) => ({ text, margin: 0, lazy: false }));
582
- yield* renderTokens(tokenize(lines), options, this);
433
+ // The width parsed once, for every block below, each one of a stack.
434
+ const options = { ...withCellWidth(rawOptions), height: stackedHeight(rawOptions.height) };
435
+ // Rich's `new_line`: set by each element as it exits, read before anything is drawn.
436
+ let newLine = false;
437
+ for (const step of documentSteps(this.parsed, options, this)) {
438
+ if (step.kind === "exit") {
439
+ newLine = step.newLine;
440
+ continue;
441
+ }
442
+ if (newLine)
443
+ yield Segment.line();
444
+ yield* step.kind === "image" ? drawImage(step.image, options, this) : renderBlock(step.token, options, this);
445
+ }
583
446
  }
584
447
  measure(options) {
585
- return { minimum: 1, maximum: options.maxWidth };
448
+ // Rich's Markdown has no `__rich_measure__`, so Rich measures it as nothing up to the width.
449
+ return { minimum: 0, maximum: options.maxWidth };
586
450
  }
587
451
  }