@promptctl/rich-js 0.22.0 → 0.24.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/core/clock.d.ts +9 -1
- package/dist/core/clock.js +22 -2
- package/dist/core/color.d.ts +10 -0
- package/dist/core/color.js +36 -1
- package/dist/core/easing.d.ts +31 -0
- package/dist/core/easing.js +106 -1
- package/dist/core/segment.d.ts +10 -0
- package/dist/core/segment.js +15 -0
- package/dist/core/strip.d.ts +108 -31
- package/dist/core/strip.js +240 -131
- package/dist/core/style.d.ts +18 -1
- package/dist/core/style.js +32 -3
- package/dist/core/text.js +4 -1
- package/dist/host/app.js +2 -4
- package/dist/host/index.d.ts +1 -1
- package/dist/host/input.d.ts +37 -13
- package/dist/host/input.js +146 -56
- package/dist/host/terminal-host.d.ts +16 -0
- package/dist/host/terminal-host.js +1 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.js +5 -4
- package/dist/node/terminal-host.d.ts +8 -1
- package/dist/node/terminal-host.js +2 -0
- package/dist/renderables/effect.d.ts +91 -18
- package/dist/renderables/effect.js +158 -46
- package/dist/renderables/effects.d.ts +73 -26
- package/dist/renderables/effects.js +412 -92
- package/dist/renderables/embed.d.ts +10 -0
- package/dist/renderables/embed.js +15 -7
- package/dist/renderables/frames.d.ts +95 -0
- package/dist/renderables/frames.js +106 -0
- package/dist/renderables/live.d.ts +23 -7
- package/dist/renderables/live.js +68 -34
- package/dist/renderables/markdown.d.ts +5 -2
- package/dist/renderables/markdown.js +210 -19
- package/dist/renderables/progress.d.ts +1 -1
- package/dist/renderables/progress.js +3 -1
- package/dist/renderables/progressBar.js +3 -3
- package/dist/renderables/spinner.d.ts +62 -28
- package/dist/renderables/spinner.js +99 -52
- package/dist/renderables/status.d.ts +8 -3
- package/dist/renderables/status.js +8 -5
- package/dist/template-bindings/animation-funcs.d.ts +37 -0
- package/dist/template-bindings/animation-funcs.js +83 -0
- package/dist/template-bindings/color-funcs.d.ts +13 -0
- package/dist/template-bindings/color-funcs.js +1 -1
- package/dist/template-bindings/helpers.d.ts +6 -0
- package/dist/template-bindings/helpers.js +12 -3
- package/dist/template-bindings/index.d.ts +6 -2
- package/dist/template-bindings/index.js +6 -2
- package/package.json +1 -1
|
@@ -5,14 +5,19 @@
|
|
|
5
5
|
* Block structure is read as CommonMark reads it: a blockquote and a list
|
|
6
6
|
* item are both containers, their lines tokenized as Markdown of their own
|
|
7
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.
|
|
9
|
-
*
|
|
8
|
+
* gutter — the quote's bar, the item's marker then its hang. A table, which
|
|
9
|
+
* CommonMark has none of, and an HTML block are read as markdown-it — Rich's
|
|
10
|
+
* parser — reads them. Where CommonMark leaves the drawing open, Rich's is
|
|
11
|
+
* followed: HTML, a block of it or a tag inline, is drawn as Rich draws it,
|
|
12
|
+
* not at all.
|
|
10
13
|
*/
|
|
11
14
|
import { cellLen, expandTabs, parseTabSize } from "../core/cells.js";
|
|
12
15
|
import { Segment } from "../core/segment.js";
|
|
13
16
|
import { Style } from "../core/style.js";
|
|
14
17
|
import { RichText, resolveStyle } from "../core/text.js";
|
|
15
18
|
import { Rule } from "./rule.js";
|
|
19
|
+
import { Table } from "./table.js";
|
|
20
|
+
import { SIMPLE } from "../core/box.js";
|
|
16
21
|
import { drawable, getStyle, groundedStyle, stackedHeight } from "../core/protocol.js";
|
|
17
22
|
const HEADING = /^(#{1,6})\s+(.+)$/;
|
|
18
23
|
/** A thematic break: three or more of one of `*`, `-` or `_`, spaces and tabs between them allowed. */
|
|
@@ -24,11 +29,43 @@ const ITEM_MARKER = /^(?:[*+-]|(\d{1,9})\.)/;
|
|
|
24
29
|
const LEAD = /^[ \t]*/;
|
|
25
30
|
const HARD_BREAK = /(?: {2,}|\\)$/;
|
|
26
31
|
const BLOCK_STARTS = [HEADING, FENCE, QUOTE, RULE];
|
|
32
|
+
/** A line that may be a table's delimiter row, screened as markdown-it screens one: pipes, dashes, colons and space only, and not a dash then a space, which opens a list item. */
|
|
33
|
+
const DELIMITER_ROW = /^(?!-[ \t])[-:|][-:| \t]+$/;
|
|
34
|
+
const DELIMITER_CELL = /^:?-+:?$/;
|
|
35
|
+
/**
|
|
36
|
+
* How many empty cells a table's short rows may be filled out with before the
|
|
37
|
+
* table ends, as markdown-it caps them: a header of many columns over many
|
|
38
|
+
* one-cell rows would otherwise grow a few kilobytes of source into millions of cells.
|
|
39
|
+
*/
|
|
40
|
+
const MAX_AUTOCOMPLETED_CELLS = 0x10000;
|
|
27
41
|
/** Indentation that makes a line indented code when it opens a block, as CommonMark counts it. */
|
|
28
42
|
const CODE_INDENT = 4;
|
|
29
43
|
/** The tab stop code is drawn with, as Rich's `Syntax` draws it. */
|
|
30
44
|
const CODE_TAB_SIZE = parseTabSize(8);
|
|
31
45
|
const BLANK_LINE = { text: "", margin: 0, lazy: false };
|
|
46
|
+
/** The block-level element names an HTML block of the sixth kind opens with, as markdown-it-py lists them. */
|
|
47
|
+
const HTML_BLOCK_NAMES = "address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|" +
|
|
48
|
+
"fieldset|figcaption|figure|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hr|html|iframe|legend|li|link|" +
|
|
49
|
+
"main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|" +
|
|
50
|
+
"title|tr|track|ul";
|
|
51
|
+
// The tag grammar of markdown-it-py's common/html_re, shared by the HTML block a lone tag opens and the inline HTML it reads in text.
|
|
52
|
+
const HTML_ATTRIBUTE = String.raw `(?:\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\s*=\s*(?:[^"'=<>\`\x00-\x20]+|'[^']*'|"[^"]*"))?)`;
|
|
53
|
+
const HTML_OPEN_TAG = String.raw `<[A-Za-z][A-Za-z0-9-]*${HTML_ATTRIBUTE}*\s*\/?>`;
|
|
54
|
+
const HTML_CLOSE_TAG = String.raw `<\/[A-Za-z][A-Za-z0-9-]*\s*>`;
|
|
55
|
+
const HTML_BLOCKS = [
|
|
56
|
+
{ start: /^<(?:script|pre|style|textarea)(?=\s|>|$)/i, end: /<\/(?:script|pre|style|textarea)>/i, interrupts: true },
|
|
57
|
+
{ start: /^<!--/, end: /-->/, interrupts: true },
|
|
58
|
+
{ start: /^<\?/, end: /\?>/, interrupts: true },
|
|
59
|
+
// Uppercase only, as markdown-it-py has it, where markdown-it in JS and CommonMark 0.31 take `<!doctype` too.
|
|
60
|
+
{ start: /^<![A-Z]/, end: />/, interrupts: true },
|
|
61
|
+
{ start: /^<!\[CDATA\[/, end: /\]\]>/, interrupts: true },
|
|
62
|
+
{ start: new RegExp(String.raw `^<\/?(?:${HTML_BLOCK_NAMES})(?=\s|\/?>|$)`, "i"), end: "blank", interrupts: true },
|
|
63
|
+
{
|
|
64
|
+
start: new RegExp(String.raw `^(?:${HTML_OPEN_TAG}|${HTML_CLOSE_TAG})\s*$`),
|
|
65
|
+
end: "blank",
|
|
66
|
+
interrupts: false,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
32
69
|
/** The column `ws` ends at when it starts at column `from`, a tab reaching the next stop of 4 as CommonMark counts it. */
|
|
33
70
|
function advance(from, ws) {
|
|
34
71
|
return [...ws].reduce((col, ch) => (ch === "\t" ? col + 4 - (col % 4) : col + 1), from);
|
|
@@ -90,6 +127,57 @@ function itemStart(line) {
|
|
|
90
127
|
first: rest === "" ? undefined : { text: lead + " ".repeat(m[0].length) + after, margin: content, lazy: false },
|
|
91
128
|
};
|
|
92
129
|
}
|
|
130
|
+
/** Whether `line` may be a table's row: a line of its container's own, neither lazy paragraph text nor indented code. */
|
|
131
|
+
function tableLine(line) {
|
|
132
|
+
return !line.lazy && indentOf(line) < CODE_INDENT;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A table row's cells, as markdown-it splits one: at every `|` no backslash escapes,
|
|
136
|
+
* a pipe at either end of the row closing it rather than opening an empty
|
|
137
|
+
* cell, and each cell trimmed. An escaped `\|` is a `|` in its cell, inside a
|
|
138
|
+
* code span too.
|
|
139
|
+
*/
|
|
140
|
+
function rowCells(row) {
|
|
141
|
+
const cells = [""];
|
|
142
|
+
for (const ch of row.trim()) {
|
|
143
|
+
const last = cells.length - 1;
|
|
144
|
+
if (ch !== "|")
|
|
145
|
+
cells[last] += ch;
|
|
146
|
+
else if (cells[last].endsWith("\\"))
|
|
147
|
+
cells[last] = `${cells[last].slice(0, -1)}|`;
|
|
148
|
+
else
|
|
149
|
+
cells.push("");
|
|
150
|
+
}
|
|
151
|
+
const from = cells[0] === "" ? 1 : 0;
|
|
152
|
+
const to = cells.length > from && cells[cells.length - 1] === "" ? -1 : undefined;
|
|
153
|
+
return cells.slice(from, to).map((cell) => cell.trim());
|
|
154
|
+
}
|
|
155
|
+
/** The alignment of each column `line` declares, if it is a table's delimiter row: a run of dashes per column, a colon on the side its cells sit. */
|
|
156
|
+
function alignments(line) {
|
|
157
|
+
const text = body(line);
|
|
158
|
+
if (!tableLine(line) || !DELIMITER_ROW.test(text))
|
|
159
|
+
return undefined;
|
|
160
|
+
const cells = rowCells(text);
|
|
161
|
+
if (!cells.every((cell) => DELIMITER_CELL.test(cell)))
|
|
162
|
+
return undefined;
|
|
163
|
+
return cells.map((cell) => (cell.endsWith(":") ? (cell.startsWith(":") ? "center" : "right") : cell.startsWith(":") ? "left" : undefined));
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The columns of the table opening at `start`, if one does, as markdown-it
|
|
167
|
+
* reads one: a header row with a pipe in it, then a delimiter row naming as
|
|
168
|
+
* many columns.
|
|
169
|
+
*/
|
|
170
|
+
function tableAt(lines, start) {
|
|
171
|
+
const head = lines[start];
|
|
172
|
+
const delimiter = lines[start + 1];
|
|
173
|
+
if (delimiter === undefined || !tableLine(head) || !body(head).includes("|"))
|
|
174
|
+
return undefined;
|
|
175
|
+
const aligns = alignments(delimiter);
|
|
176
|
+
if (!aligns)
|
|
177
|
+
return undefined;
|
|
178
|
+
const headers = rowCells(body(head));
|
|
179
|
+
return headers.length === aligns.length ? headers.map((header, n) => ({ header, align: aligns[n] })) : undefined;
|
|
180
|
+
}
|
|
93
181
|
/** Whether two item starts belong to one list: the same bullet character, or both numbered. */
|
|
94
182
|
function sameList(a, b) {
|
|
95
183
|
return a.bullet === b.bullet;
|
|
@@ -104,13 +192,18 @@ function quoted(line) {
|
|
|
104
192
|
const spaced = /[ \t]/.test(line.text[at + 1] ?? "");
|
|
105
193
|
return { text: `${line.text.slice(0, at)} ${line.text.slice(at + 1)}`, margin: leadEnd(line) + 1 + (spaced ? 1 : 0), lazy: false };
|
|
106
194
|
}
|
|
195
|
+
/** The kind of HTML block `line` opens, if it opens one: the first kind whose opening it matches, as markdown-it tries them in order. */
|
|
196
|
+
function htmlBlockAt(line) {
|
|
197
|
+
return HTML_BLOCKS.find((kind) => startsWith(line, kind.start));
|
|
198
|
+
}
|
|
107
199
|
/**
|
|
108
|
-
* Whether `line` opens a block of its own
|
|
109
|
-
*
|
|
200
|
+
* Whether `line` opens a block of its own that may cut short the block before
|
|
201
|
+
* it — a heading, a fence, a quote, a rule, a list item or an HTML block of
|
|
202
|
+
* any kind but a lone tag. A line that does not is text, and under an open
|
|
110
203
|
* paragraph it continues that paragraph.
|
|
111
204
|
*/
|
|
112
205
|
function opensBlock(line) {
|
|
113
|
-
return BLOCK_STARTS.some((re) => startsWith(line, re)) || itemStart(line) !== undefined;
|
|
206
|
+
return BLOCK_STARTS.some((re) => startsWith(line, re)) || itemStart(line) !== undefined || htmlBlockAt(line)?.interrupts === true;
|
|
114
207
|
}
|
|
115
208
|
/**
|
|
116
209
|
* Whether `line` ends a paragraph: `opensBlock`, read as CommonMark reads it
|
|
@@ -160,7 +253,7 @@ function continuesLazily(inner, line, afterLazy) {
|
|
|
160
253
|
}
|
|
161
254
|
/**
|
|
162
255
|
* The text of the paragraph begun at `start`, every line after it up to a
|
|
163
|
-
* blank
|
|
256
|
+
* blank, a line that ends a paragraph or a table's header row, and the index of the line that ended
|
|
164
257
|
* it. Soft-broken lines are joined by one space, as a Markdown renderer
|
|
165
258
|
* reflows them, with their own indentation dropped; a line ending in two
|
|
166
259
|
* spaces or a backslash breaks hard, and its break is kept.
|
|
@@ -168,7 +261,7 @@ function continuesLazily(inner, line, afterLazy) {
|
|
|
168
261
|
function continuation(lines, start) {
|
|
169
262
|
const parts = [lines[start].text];
|
|
170
263
|
let i = start + 1;
|
|
171
|
-
for (; i < lines.length && !isBlank(lines[i]) && !endsParagraph(lines[i]); i++)
|
|
264
|
+
for (; i < lines.length && !isBlank(lines[i]) && !endsParagraph(lines[i]) && !tableAt(lines, i); i++)
|
|
172
265
|
parts.push(lines[i].text);
|
|
173
266
|
const text = parts
|
|
174
267
|
.map((part, n) => {
|
|
@@ -247,6 +340,23 @@ function tokenize(lines) {
|
|
|
247
340
|
i++;
|
|
248
341
|
continue;
|
|
249
342
|
}
|
|
343
|
+
// Table: before every other block, as markdown-it tries it, so a header
|
|
344
|
+
// row that reads as a heading or a list item is still a header. Its body
|
|
345
|
+
// rows run to a blank line or a line that opens a block.
|
|
346
|
+
const columns = tableAt(lines, i);
|
|
347
|
+
if (columns) {
|
|
348
|
+
const rows = [];
|
|
349
|
+
let autocompleted = 0;
|
|
350
|
+
for (i += 2; i < lines.length && !isBlank(lines[i]) && tableLine(lines[i]) && !opensBlock(lines[i]); i++) {
|
|
351
|
+
const cells = rowCells(body(lines[i]));
|
|
352
|
+
autocompleted += columns.length - cells.length;
|
|
353
|
+
if (autocompleted > MAX_AUTOCOMPLETED_CELLS)
|
|
354
|
+
break;
|
|
355
|
+
rows.push(columns.map((_, n) => cells[n] ?? ""));
|
|
356
|
+
}
|
|
357
|
+
tokens.push({ type: "table", columns, rows });
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
250
360
|
// Indented code: its lines, and the blank lines between them, with the indent cut off
|
|
251
361
|
if (indentOf(line) >= CODE_INDENT) {
|
|
252
362
|
const code = [];
|
|
@@ -305,6 +415,19 @@ function tokenize(lines) {
|
|
|
305
415
|
i = next;
|
|
306
416
|
continue;
|
|
307
417
|
}
|
|
418
|
+
// HTML block: to the line that closes its kind, or to the end of its
|
|
419
|
+
// container when none does; a closing blank line is left to what follows
|
|
420
|
+
const html = htmlBlockAt(line);
|
|
421
|
+
if (html) {
|
|
422
|
+
const { end } = html;
|
|
423
|
+
const closes = end === "blank" ? isBlank : (closing) => end.test(body(closing));
|
|
424
|
+
while (i < lines.length && !closes(lines[i]))
|
|
425
|
+
i++;
|
|
426
|
+
if (i < lines.length && end !== "blank")
|
|
427
|
+
i++;
|
|
428
|
+
tokens.push({ type: "html_block" });
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
308
431
|
// Paragraph, or a setext heading when a line of `=` or `-` underlines it
|
|
309
432
|
const { text, next } = continuation(lines, i);
|
|
310
433
|
const setext = underline(lines[next]);
|
|
@@ -316,10 +439,20 @@ function tokenize(lines) {
|
|
|
316
439
|
return tokens;
|
|
317
440
|
}
|
|
318
441
|
const TICKS = /`+/y;
|
|
319
|
-
const STRONG = /\*\*(.+?)\*\*/y;
|
|
320
|
-
const EMPHASIS = /\*(.+?)\*/y;
|
|
321
442
|
const ANGLED_DESTINATION = /\(\s*<([^<>\n]*)>/y;
|
|
322
443
|
const LINK_TAIL = /\s*(?:"[^"]*"|'[^']*'|\([^()]*\))?\s*\)/y;
|
|
444
|
+
/** A backslash escape, as markdown-it reads one: the ASCII punctuation character after it, standing for itself. */
|
|
445
|
+
const ESCAPE = /\\([!-\/:-@[-`{-~])/y;
|
|
446
|
+
const ESCAPES = new RegExp(ESCAPE.source, "g");
|
|
447
|
+
/** Inline HTML, as markdown-it-py's HTML_TAG_RE reads it: a tag, a comment, a processing instruction, a declaration or a CDATA section. */
|
|
448
|
+
const HTML_TAG = new RegExp([
|
|
449
|
+
HTML_OPEN_TAG,
|
|
450
|
+
HTML_CLOSE_TAG,
|
|
451
|
+
String.raw `<!---?>|<!--(?:[^-]|-[^-]|--[^>])*-->`,
|
|
452
|
+
String.raw `<[?][\s\S]*?[?]>`,
|
|
453
|
+
String.raw `<![A-Za-z][^>]*>`,
|
|
454
|
+
String.raw `<!\[CDATA\[[\s\S]*?\]\]>`,
|
|
455
|
+
].join("|"), "y");
|
|
323
456
|
/** `re`, a sticky pattern, matched exactly at `i` in `text`. */
|
|
324
457
|
function matchAt(re, text, i) {
|
|
325
458
|
re.lastIndex = i;
|
|
@@ -327,8 +460,10 @@ function matchAt(re, text, i) {
|
|
|
327
460
|
}
|
|
328
461
|
/**
|
|
329
462
|
* Each `[` in `text` that a `]` closes, to the index of that `]`: brackets
|
|
330
|
-
* nest,
|
|
331
|
-
*
|
|
463
|
+
* nest, a backslash escapes the character after it, and a bracket inside a
|
|
464
|
+
* code span or inline HTML, which bind tighter than a link, is not one. One
|
|
465
|
+
* pass, so a run of text full of brackets that close nothing costs no more
|
|
466
|
+
* than one without.
|
|
332
467
|
*/
|
|
333
468
|
function bracketPairs(text) {
|
|
334
469
|
const pairs = new Map();
|
|
@@ -340,6 +475,9 @@ function bracketPairs(text) {
|
|
|
340
475
|
open.push(i);
|
|
341
476
|
else if (text[i] === "]" && open.length > 0)
|
|
342
477
|
pairs.set(open.pop(), i);
|
|
478
|
+
// Neither construct reads the brackets, so the pairs found so far serve.
|
|
479
|
+
else if (text[i] === "`" || text[i] === "<")
|
|
480
|
+
i = (inlineAt(text, i, pairs)?.end ?? i + 1) - 1;
|
|
343
481
|
}
|
|
344
482
|
return pairs;
|
|
345
483
|
}
|
|
@@ -347,7 +485,8 @@ function bracketPairs(text) {
|
|
|
347
485
|
* The `(destination "title")` of a link opening at `open`, as CommonMark reads
|
|
348
486
|
* it: a destination in angle brackets, or a run of non-space characters whose
|
|
349
487
|
* parentheses balance — so a URL may itself hold `(…)` — then an optional
|
|
350
|
-
* quoted title, then the `)`. The destination
|
|
488
|
+
* quoted title, then the `)`. The destination, its backslash escapes read, and
|
|
489
|
+
* the index past the `)`.
|
|
351
490
|
*/
|
|
352
491
|
function linkDestination(text, open) {
|
|
353
492
|
if (text[open] !== "(")
|
|
@@ -375,7 +514,22 @@ function linkDestination(text, open) {
|
|
|
375
514
|
url = text.slice(from, i);
|
|
376
515
|
}
|
|
377
516
|
const tail = matchAt(LINK_TAIL, text, i);
|
|
378
|
-
return tail ? { url, end: i + tail[0].length } : undefined;
|
|
517
|
+
return tail ? { url: url.replace(ESCAPES, "$1"), end: i + tail[0].length } : undefined;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* The index of the `delimiter` that closes emphasis whose text starts at
|
|
521
|
+
* `from`, after at least one character of it. Every other inline construct
|
|
522
|
+
* binds tighter than emphasis, as CommonMark has it, so a delimiter inside an
|
|
523
|
+
* escape, a code span, inline HTML or a link closes nothing.
|
|
524
|
+
*/
|
|
525
|
+
function emphasisClose(text, from, delimiter, brackets) {
|
|
526
|
+
for (let i = from; i < text.length;) {
|
|
527
|
+
if (i > from && text.startsWith(delimiter, i))
|
|
528
|
+
return i;
|
|
529
|
+
const tighter = text[i] === "*" ? undefined : inlineAt(text, i, brackets);
|
|
530
|
+
i = tighter ? tighter.end : i + 1;
|
|
531
|
+
}
|
|
532
|
+
return undefined;
|
|
379
533
|
}
|
|
380
534
|
/** The inline construct opening at `i` in `text`, if one does; `brackets` is `text`'s `bracketPairs`. */
|
|
381
535
|
function inlineAt(text, i, brackets) {
|
|
@@ -390,11 +544,25 @@ function inlineAt(text, i, brackets) {
|
|
|
390
544
|
: { kind: "literal", text: ticks, end: i + ticks.length };
|
|
391
545
|
}
|
|
392
546
|
case "*": {
|
|
393
|
-
const strong =
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
547
|
+
const strong = text.startsWith("**", i) ? emphasisClose(text, i + 2, "**", brackets) : undefined;
|
|
548
|
+
const delimiter = strong === undefined ? "*" : "**";
|
|
549
|
+
const close = strong ?? emphasisClose(text, i + 1, "*", brackets);
|
|
550
|
+
return close === undefined
|
|
551
|
+
? undefined
|
|
552
|
+
: {
|
|
553
|
+
kind: "emphasis",
|
|
554
|
+
style: strong === undefined ? "italic" : "bold",
|
|
555
|
+
inner: text.slice(i + delimiter.length, close),
|
|
556
|
+
end: close + delimiter.length,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
case "\\": {
|
|
560
|
+
const escape = matchAt(ESCAPE, text, i);
|
|
561
|
+
return escape ? { kind: "literal", text: escape[1], end: i + escape[0].length } : undefined;
|
|
562
|
+
}
|
|
563
|
+
case "<": {
|
|
564
|
+
const tag = matchAt(HTML_TAG, text, i);
|
|
565
|
+
return tag ? { kind: "html", end: i + tag[0].length } : undefined;
|
|
398
566
|
}
|
|
399
567
|
case "!":
|
|
400
568
|
case "[": {
|
|
@@ -426,7 +594,8 @@ function styled(text, style, settings, options) {
|
|
|
426
594
|
/**
|
|
427
595
|
* `text` as inline Markdown appended to `result`: code spans, emphasis, links
|
|
428
596
|
* and images, the text inside emphasis, links and images parsed the same way,
|
|
429
|
-
*
|
|
597
|
+
* a backslash escape as the character it escapes, inline HTML as nothing, and
|
|
598
|
+
* everything else as it is.
|
|
430
599
|
*/
|
|
431
600
|
function appendInline(result, text, settings, options) {
|
|
432
601
|
const brackets = bracketPairs(text);
|
|
@@ -445,6 +614,9 @@ function appendInline(result, text, settings, options) {
|
|
|
445
614
|
case "literal":
|
|
446
615
|
result.append(found.text);
|
|
447
616
|
break;
|
|
617
|
+
case "html":
|
|
618
|
+
// Rich draws nothing for inline HTML.
|
|
619
|
+
break;
|
|
448
620
|
case "emphasis":
|
|
449
621
|
result.append(styled(found.inner, found.style, settings, options));
|
|
450
622
|
break;
|
|
@@ -558,6 +730,25 @@ function* renderTokens(tokens, options, settings) {
|
|
|
558
730
|
yield* guttered(body, options, bar, bar, Style.parse("dim italic"));
|
|
559
731
|
break;
|
|
560
732
|
}
|
|
733
|
+
case "table": {
|
|
734
|
+
// Rich's TableElement: each cell inline Markdown placed by its
|
|
735
|
+
// column's alignment, the header's in `markdown.table.header` on top.
|
|
736
|
+
const cell = (text, justify) => {
|
|
737
|
+
const content = new RichText("", { justify });
|
|
738
|
+
appendInline(content, text, settings, options);
|
|
739
|
+
return content;
|
|
740
|
+
};
|
|
741
|
+
const table = new Table({ box: SIMPLE, padEdge: false, style: "markdown.table.border", showEdge: true, collapsePadding: true });
|
|
742
|
+
for (const { header, align } of token.columns)
|
|
743
|
+
table.addColumn(cell(header, align).stylize("markdown.table.header"));
|
|
744
|
+
for (const row of token.rows)
|
|
745
|
+
table.addRow(...row.map((text, n) => cell(text, token.columns[n].align)));
|
|
746
|
+
yield* table.render(options);
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
case "html_block":
|
|
750
|
+
// Rich draws nothing for one.
|
|
751
|
+
break;
|
|
561
752
|
case "blank":
|
|
562
753
|
yield Segment.line();
|
|
563
754
|
break;
|
|
@@ -124,7 +124,7 @@ export interface SpinnerColumnOptions {
|
|
|
124
124
|
}
|
|
125
125
|
export declare class SpinnerColumn implements ProgressColumn {
|
|
126
126
|
readonly tableColumn: ColumnOptions;
|
|
127
|
-
private _spinner;
|
|
127
|
+
private readonly _spinner;
|
|
128
128
|
private _finishedText;
|
|
129
129
|
constructor(spinnerName?: string, options?: SpinnerColumnOptions);
|
|
130
130
|
render(task: Task): Renderable;
|
|
@@ -173,8 +173,10 @@ export class SpinnerColumn {
|
|
|
173
173
|
const finishedText = options?.finishedText ?? " ";
|
|
174
174
|
this._finishedText = typeof finishedText === "string" ? readStr(finishedText, true) : finishedText.copy();
|
|
175
175
|
}
|
|
176
|
+
// Rich's `self.spinner.render(task.get_time())`: the frame for the time the
|
|
177
|
+
// task is drawn at, which `Progress` read off its clock once for the frame.
|
|
176
178
|
render(task) {
|
|
177
|
-
return finished(task) ? this._finishedText : this._spinner;
|
|
179
|
+
return finished(task) ? this._finishedText : this._spinner.at(task.time);
|
|
178
180
|
}
|
|
179
181
|
}
|
|
180
182
|
/**
|
|
@@ -59,17 +59,17 @@ export class ProgressBar {
|
|
|
59
59
|
// [LAW:no-ambient-temporal-coupling] The moment is the caller's; the bar
|
|
60
60
|
// only says what it looks like then. An empty track, lit by the shimmer.
|
|
61
61
|
const track = { render: (o) => this.track(o, width, 0, back) };
|
|
62
|
-
yield* new Effected(track, this.pulseEffect(options, back,
|
|
62
|
+
yield* new Effected(track, this.pulseEffect(options, back, depth, theme), { t: this.pulse.t, key: "progress-bar", theme }).render(options);
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* The shimmer over this bar's back colour as `Effected` sees it drawn at
|
|
66
66
|
* `depth`, toward `bar.pulse`. A bar has no words to keep legible, so its
|
|
67
67
|
* back takes the whole of the light.
|
|
68
68
|
*/
|
|
69
|
-
pulseEffect(options, back,
|
|
69
|
+
pulseEffect(options, back, depth, theme) {
|
|
70
70
|
const ink = back.drawnColors(depth).color ?? ColorSpec.default();
|
|
71
71
|
const glow = getStyle(options, "bar.pulse").drawnColors(depth).color ?? ColorSpec.default();
|
|
72
|
-
const loop = shimmer(PULSE_CURVE,
|
|
72
|
+
const loop = shimmer(PULSE_CURVE, PULSE_REACH, glow.getTruecolor(theme, true), 0);
|
|
73
73
|
const whole = new Map([[ink.getTruecolor(theme, true).hex, 1]]);
|
|
74
74
|
return atShares(loop, () => whole);
|
|
75
75
|
}
|
|
@@ -1,15 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Spinner —
|
|
2
|
+
* Spinner — a named spinner, asked for its frame at a time, with an optional
|
|
3
|
+
* label after it.
|
|
4
|
+
*
|
|
5
|
+
* A spinner is a `FrameSequence` (`./frames.ts`) over one entry of the named spinner
|
|
6
|
+
* data (`core/spinnerData.ts`): each frame held for the spinner's interval,
|
|
7
|
+
* looping, the frame in progress at `t` drawn. The data names the frames and
|
|
8
|
+
* the interval; the beat, the width every frame draws at and which frame `t`
|
|
9
|
+
* lands on are the frames primitive's, so a spinner is one more sequence on
|
|
10
|
+
* it and not a second way of turning frames. Custom frames are a `FrameSequence`
|
|
11
|
+
* drawn through `Frames` directly. [LAW:one-source-of-truth]
|
|
12
|
+
*
|
|
13
|
+
* It is built once and asked for each frame, as Rich's `Spinner` is built
|
|
14
|
+
* once and has `render(time)` called on it: the name is parsed and the
|
|
15
|
+
* styled frames are prepared where the spinner is given, and `at(t)` is what
|
|
16
|
+
* draws, so a `Status` or `SpinnerColumn` holding one pays for the name and
|
|
17
|
+
* the styling once, and a bad name fails at the call that gave it.
|
|
18
|
+
* [LAW:parse-dont-validate] What a `FrameSequence` makes of the frames —
|
|
19
|
+
* the beat, the width, each frame padded to it — is made there too, so a
|
|
20
|
+
* drawn frame costs only finding which one `t` lands on.
|
|
21
|
+
*
|
|
22
|
+
* [LAW:no-ambient-temporal-coupling] A Spinner draws nothing on its own:
|
|
23
|
+
* only `at(t)` is a renderable, so a spinner cannot be drawn without being
|
|
24
|
+
* handed its time, and nothing here reads a clock. Whoever draws frames
|
|
25
|
+
* hands each one its time (`core/clock.ts`) — `SpinnerColumn` the task's
|
|
26
|
+
* `time`, a `Live` the frame's time on its clock — as Rich's `Spinner.render`
|
|
27
|
+
* is handed `console.get_time()` or `task.get_time()`. `t` is the clock's
|
|
28
|
+
* time, as `App` and `Task.time` hand it, where Rich's spinner counts from
|
|
29
|
+
* its first draw: here nothing is kept between frames, so the same `t` draws
|
|
30
|
+
* the same bytes at one redraw a second or sixty, two spinners on one clock
|
|
31
|
+
* agree at every `t`, a `Progress` on a hand-moved clock turns its spinners
|
|
32
|
+
* on that clock, and a draw costs the same at `t = 0` and `t = 10⁶`.
|
|
33
|
+
*
|
|
34
|
+
* `speed` scales the holds, not the time: at speed 2 each frame is held half
|
|
35
|
+
* the interval, and a negative speed is the frames in reverse order, each held
|
|
36
|
+
* the interval over the speed's size. [LAW:one-source-of-truth] Rich scales
|
|
37
|
+
* the time instead, `t × speed` into the beat. At a positive speed the frame
|
|
38
|
+
* drawn is the same; but when the frame next changes (`Style.nextChange`) is
|
|
39
|
+
* a time on the host's timeline, and a beat over scaled time answers on the
|
|
40
|
+
* scaled one — so the sequence is scaled where it is built, and the
|
|
41
|
+
* `FrameSequence` stamps host time with nothing to map back. Backwards, this is the plain
|
|
42
|
+
* reversed sequence from `t = 0`, where Rich's `int()` truncation holds
|
|
43
|
+
* frame 0 for a first interval and then counts down: a departure, made so a
|
|
44
|
+
* negative speed is one more sequence on the beat and not a second rule
|
|
45
|
+
* about time. A speed of zero would hold a frame forever, which is not a
|
|
46
|
+
* beat, and is refused where it is given.
|
|
3
47
|
*/
|
|
4
|
-
import { Segment } from "../core/segment.js";
|
|
5
48
|
import { Style } from "../core/style.js";
|
|
6
49
|
import { RichText } from "../core/text.js";
|
|
7
|
-
import type { Renderable, Measurable
|
|
50
|
+
import type { Renderable, Measurable } from "../core/protocol.js";
|
|
8
51
|
export interface SpinnerOptions {
|
|
52
|
+
/** Multiplies the spinner's rate; negative runs it backwards; zero is refused. Defaults to 1. */
|
|
9
53
|
speed?: number;
|
|
10
54
|
style?: string | Style;
|
|
11
55
|
}
|
|
12
|
-
export declare class Spinner
|
|
56
|
+
export declare class Spinner {
|
|
13
57
|
readonly name: string;
|
|
14
58
|
readonly speed: number;
|
|
15
59
|
readonly style: string | Style;
|
|
@@ -22,36 +66,26 @@ export declare class Spinner implements Renderable, Measurable {
|
|
|
22
66
|
*/
|
|
23
67
|
text: string | RichText;
|
|
24
68
|
private readonly _data;
|
|
25
|
-
private
|
|
26
|
-
private
|
|
69
|
+
private readonly _named;
|
|
70
|
+
private readonly _ascii;
|
|
27
71
|
constructor(name?: string, text?: string | RichText, options?: SpinnerOptions);
|
|
28
72
|
get frames(): readonly string[];
|
|
73
|
+
/** Milliseconds each frame is held, as the spinner data gives it. */
|
|
29
74
|
get interval(): number;
|
|
30
|
-
/** Advance the frame count by the time elapsed, and return it. */
|
|
31
|
-
private _advance;
|
|
32
|
-
/** The frames this output can draw: the `line` spinner stands in for any other on an ASCII-only one. */
|
|
33
|
-
private _drawnFrames;
|
|
34
75
|
/**
|
|
35
|
-
* The
|
|
36
|
-
* ended, as the reference's `Text` is —
|
|
37
|
-
* laid out in a `Progress` cell alike.
|
|
76
|
+
* The frame in progress at `t` seconds and the label, as one renderable:
|
|
77
|
+
* the line wrapped to the width and ended, as the reference's `Text` is —
|
|
78
|
+
* printed, stacked in a `Group`, or laid out in a `Progress` cell alike.
|
|
79
|
+
* The frames this output can draw are chosen where the output is known.
|
|
38
80
|
*/
|
|
39
|
-
|
|
40
|
-
private
|
|
81
|
+
at(t: number): Renderable & Measurable;
|
|
82
|
+
private _render;
|
|
41
83
|
/**
|
|
42
|
-
*
|
|
43
|
-
* text)`: a long label wraps as one line with its
|
|
44
|
-
* nothing — the default `""`, or markup that
|
|
45
|
-
* not a trailing space, as an empty `Text` is
|
|
84
|
+
* The frame in progress and the label as one text, as Rich's
|
|
85
|
+
* `Text.assemble(frame, " ", text)`: a long label wraps as one line with its
|
|
86
|
+
* frame. A label that draws nothing — the default `""`, or markup that
|
|
87
|
+
* styles nothing — is no label, not a trailing space, as an empty `Text` is
|
|
88
|
+
* falsy there.
|
|
46
89
|
*/
|
|
47
90
|
private _line;
|
|
48
|
-
/**
|
|
49
|
-
* Measured at its widest frame, so a column holding it never narrows as it
|
|
50
|
-
* turns, and never offered less than that frame: a frame is one picture,
|
|
51
|
-
* which the reference's `Text` measure would let wrap apart at its spaces.
|
|
52
|
-
*/
|
|
53
|
-
measure(options: RenderOptions): {
|
|
54
|
-
minimum: number;
|
|
55
|
-
maximum: number;
|
|
56
|
-
};
|
|
57
91
|
}
|