@promptctl/rich-js 0.24.0 → 0.25.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/README.md +7 -0
- package/THIRD-PARTY-NOTICES +29 -1
- package/dist/core/anchor.d.ts +5 -0
- package/dist/core/anchor.js +11 -4
- package/dist/core/animation.d.ts +243 -0
- package/dist/core/animation.js +375 -0
- package/dist/core/ansi.js +2 -2
- package/dist/core/box.d.ts +7 -7
- package/dist/core/cell-widths.d.ts +9 -0
- package/dist/core/cell-widths.js +140 -0
- package/dist/core/cells.d.ts +40 -24
- package/dist/core/cells.js +151 -66
- package/dist/core/clock.d.ts +2 -2
- package/dist/core/clock.js +6 -4
- package/dist/core/color.d.ts +27 -10
- package/dist/core/color.js +21 -29
- package/dist/core/console.d.ts +28 -17
- package/dist/core/console.js +48 -43
- package/dist/core/easing.d.ts +140 -10
- package/dist/core/easing.js +328 -50
- package/dist/core/emoji.d.ts +1 -1
- package/dist/core/emoji.js +1 -2
- package/dist/core/export-html.js +2 -1
- package/dist/core/export-lines.js +3 -3
- package/dist/core/highlighter.d.ts +9 -8
- package/dist/core/highlighter.js +3 -2
- package/dist/core/json.js +3 -2
- package/dist/core/markup.js +16 -12
- package/dist/core/measure.js +3 -3
- package/dist/core/memo.d.ts +11 -2
- package/dist/core/memo.js +16 -0
- package/dist/core/oklch.js +17 -6
- package/dist/core/osc8.d.ts +1 -1
- package/dist/core/osc8.js +4 -5
- package/dist/core/paint.d.ts +2 -1
- package/dist/core/paint.js +4 -3
- package/dist/core/pattern.d.ts +21 -0
- package/dist/core/pattern.js +21 -0
- package/dist/core/place.js +2 -1
- package/dist/core/pretty.js +11 -12
- package/dist/core/protocol.d.ts +136 -21
- package/dist/core/protocol.js +203 -16
- package/dist/core/render.d.ts +15 -2
- package/dist/core/render.js +27 -6
- package/dist/core/segment.d.ts +31 -6
- package/dist/core/segment.js +68 -53
- package/dist/core/spinnerData.d.ts +1 -1
- package/dist/core/state.d.ts +53 -0
- package/dist/core/state.js +82 -0
- package/dist/core/strip.d.ts +11 -13
- package/dist/core/strip.js +126 -113
- package/dist/core/style.d.ts +47 -14
- package/dist/core/style.js +74 -34
- package/dist/core/text.d.ts +5 -8
- package/dist/core/text.js +133 -133
- package/dist/core/timeline.d.ts +109 -0
- package/dist/core/timeline.js +180 -0
- package/dist/core/tween.d.ts +49 -0
- package/dist/core/tween.js +71 -0
- package/dist/core/view.d.ts +92 -0
- package/dist/core/view.js +83 -0
- package/dist/host/app.d.ts +49 -21
- package/dist/host/app.js +72 -38
- package/dist/host/input.js +3 -4
- package/dist/index.d.ts +30 -15
- package/dist/index.js +20 -6
- package/dist/node/traceback.js +4 -3
- package/dist/renderables/change.d.ts +42 -0
- package/dist/renderables/change.js +112 -0
- package/dist/renderables/columns.d.ts +1 -1
- package/dist/renderables/columns.js +3 -3
- package/dist/renderables/constrain.js +2 -1
- package/dist/renderables/effect.d.ts +155 -92
- package/dist/renderables/effect.js +203 -136
- package/dist/renderables/effects.d.ts +137 -24
- package/dist/renderables/effects.js +276 -113
- package/dist/renderables/embed.d.ts +1 -1
- package/dist/renderables/embed.js +18 -9
- package/dist/renderables/flexStrip.js +2 -1
- package/dist/renderables/frames.d.ts +45 -43
- package/dist/renderables/frames.js +57 -45
- package/dist/renderables/glyph-effect.d.ts +158 -0
- package/dist/renderables/glyph-effect.js +237 -0
- package/dist/renderables/glyph-effects.d.ts +147 -0
- package/dist/renderables/glyph-effects.js +283 -0
- package/dist/renderables/group.d.ts +1 -1
- package/dist/renderables/group.js +2 -2
- package/dist/renderables/html-entities.d.ts +8 -0
- package/dist/renderables/html-entities.js +386 -0
- package/dist/renderables/layout.d.ts +10 -9
- package/dist/renderables/layout.js +42 -35
- package/dist/renderables/lifecycle.d.ts +77 -0
- package/dist/renderables/lifecycle.js +132 -0
- package/dist/renderables/list-view.d.ts +2 -2
- package/dist/renderables/list-view.js +11 -8
- package/dist/renderables/live.d.ts +36 -21
- package/dist/renderables/live.js +89 -42
- package/dist/renderables/markdown-parse.d.ts +109 -0
- package/dist/renderables/markdown-parse.js +1427 -0
- package/dist/renderables/markdown-url.d.ts +19 -0
- package/dist/renderables/markdown-url.js +314 -0
- package/dist/renderables/markdown.d.ts +5 -7
- package/dist/renderables/markdown.js +356 -683
- package/dist/renderables/padding.d.ts +7 -20
- package/dist/renderables/padding.js +29 -50
- package/dist/renderables/panel.d.ts +30 -32
- package/dist/renderables/panel.js +65 -109
- package/dist/renderables/progress.d.ts +83 -32
- package/dist/renderables/progress.js +186 -90
- package/dist/renderables/progressBar.d.ts +22 -12
- package/dist/renderables/progressBar.js +33 -10
- package/dist/renderables/prompt.js +3 -2
- package/dist/renderables/python-space.d.ts +13 -0
- package/dist/renderables/python-space.js +25 -0
- package/dist/renderables/scroll.d.ts +50 -0
- package/dist/renderables/scroll.js +106 -0
- package/dist/renderables/spinner.d.ts +57 -24
- package/dist/renderables/spinner.js +101 -49
- package/dist/renderables/status.js +1 -1
- package/dist/renderables/syntax.d.ts +10 -9
- package/dist/renderables/syntax.js +5 -3
- package/dist/renderables/table.d.ts +48 -46
- package/dist/renderables/table.js +170 -208
- package/dist/renderables/traceback.d.ts +3 -3
- package/dist/renderables/traceback.js +12 -10
- package/dist/renderables/tree.d.ts +28 -25
- package/dist/renderables/tree.js +108 -58
- package/dist/renderables/viewport.d.ts +77 -38
- package/dist/renderables/viewport.js +156 -72
- package/dist/template-bindings/animation-funcs.d.ts +78 -9
- package/dist/template-bindings/animation-funcs.js +132 -14
- package/dist/template-bindings/index.d.ts +5 -5
- package/dist/template-bindings/index.js +7 -6
- package/dist/themes/ramp.d.ts +62 -4
- package/dist/themes/ramp.js +82 -4
- package/dist/themes/registry.js +5 -7
- package/dist/widgets/button.js +3 -2
- package/dist/widgets/checkbox.js +4 -3
- package/dist/widgets/dropdown.d.ts +3 -1
- package/dist/widgets/dropdown.js +20 -15
- package/dist/widgets/event-router.js +4 -2
- package/dist/widgets/focus-manager.d.ts +23 -14
- package/dist/widgets/focus-manager.js +34 -33
- package/dist/widgets/hit.d.ts +6 -0
- package/dist/widgets/hit.js +8 -1
- package/dist/widgets/index.d.ts +1 -1
- package/dist/widgets/observable.d.ts +23 -0
- package/dist/widgets/observable.js +43 -0
- package/dist/widgets/slider.d.ts +1 -1
- package/dist/widgets/slider.js +12 -3
- package/dist/widgets/static-item.d.ts +1 -1
- package/dist/widgets/static-item.js +6 -7
- package/dist/widgets/text-input.d.ts +53 -47
- package/dist/widgets/text-input.js +456 -399
- package/dist/widgets/themed-widget.js +2 -1
- package/dist/widgets/toggle.js +4 -3
- package/dist/widgets/types.d.ts +10 -4
- package/dist/widgets/types.js +8 -2
- package/dist/widgets/widget-app.d.ts +32 -6
- package/dist/widgets/widget-app.js +74 -27
- package/dist/widgets/widget-base.d.ts +2 -2
- package/dist/widgets/widget-base.js +29 -21
- package/package.json +9 -5
|
@@ -1,666 +1,298 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Markdown — renders Markdown content to the terminal.
|
|
3
|
-
* Uses built-in parsing (no external dependency).
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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
|
|
11
8
|
* followed: HTML, a block of it or a tag inline, is drawn as Rich draws it,
|
|
12
9
|
* not at all.
|
|
13
10
|
*/
|
|
14
|
-
import {
|
|
11
|
+
import { cellCount, cellLen } from "../core/cells.js";
|
|
15
12
|
import { Segment } from "../core/segment.js";
|
|
16
13
|
import { Style } from "../core/style.js";
|
|
17
14
|
import { RichText, resolveStyle } from "../core/text.js";
|
|
18
15
|
import { Rule } from "./rule.js";
|
|
16
|
+
import { Padding } from "./padding.js";
|
|
17
|
+
import { pyRstrip } from "./python-space.js";
|
|
18
|
+
import { Syntax } from "./syntax.js";
|
|
19
19
|
import { Table } from "./table.js";
|
|
20
|
+
import { parseMarkdown } from "./markdown-parse.js";
|
|
20
21
|
import { SIMPLE } from "../core/box.js";
|
|
21
|
-
import { drawable, getStyle, groundedStyle, stackedHeight } from "../core/protocol.js";
|
|
22
|
-
const HEADING = /^(#{1,6})\s+(.+)$/;
|
|
23
|
-
/** A thematic break: three or more of one of `*`, `-` or `_`, spaces and tabs between them allowed. */
|
|
24
|
-
const RULE = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/;
|
|
25
|
-
const SETEXT = /^(=+|-+)[ \t]*$/;
|
|
26
|
-
const FENCE = /^```(\w*)/;
|
|
27
|
-
const QUOTE = /^>/;
|
|
28
|
-
const ITEM_MARKER = /^(?:[*+-]|(\d{1,9})\.)/;
|
|
29
|
-
const LEAD = /^[ \t]*/;
|
|
30
|
-
const HARD_BREAK = /(?: {2,}|\\)$/;
|
|
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 = /^:?-+:?$/;
|
|
22
|
+
import { drawable, getStyle, groundedStyle, render, stackedHeight, withCellWidth } from "../core/protocol.js";
|
|
35
23
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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.
|
|
39
29
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
];
|
|
69
|
-
/** The column `ws` ends at when it starts at column `from`, a tab reaching the next stop of 4 as CommonMark counts it. */
|
|
70
|
-
function advance(from, ws) {
|
|
71
|
-
return [...ws].reduce((col, ch) => (ch === "\t" ? col + 4 - (col % 4) : col + 1), from);
|
|
72
|
-
}
|
|
73
|
-
/** The column `line`'s leading whitespace ends at. */
|
|
74
|
-
function leadEnd(line) {
|
|
75
|
-
return advance(0, LEAD.exec(line.text)[0]);
|
|
76
|
-
}
|
|
77
|
-
/** How far `line` is indented inside its container. */
|
|
78
|
-
function indentOf(line) {
|
|
79
|
-
return Math.max(0, leadEnd(line) - line.margin);
|
|
80
|
-
}
|
|
81
|
-
/** `line` from its first character that is not leading whitespace. */
|
|
82
|
-
function body(line) {
|
|
83
|
-
return line.text.slice(LEAD.exec(line.text)[0].length);
|
|
84
|
-
}
|
|
85
|
-
function isBlank(line) {
|
|
86
|
-
return line.text.trim() === "";
|
|
87
|
-
}
|
|
88
|
-
/** Whether `line` sits less than a code indent into its container and its text matches `re`. */
|
|
89
|
-
function startsWith(line, re) {
|
|
90
|
-
return indentOf(line) < CODE_INDENT && re.test(body(line));
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* `line` from `n` columns into its container on: a block's own indentation
|
|
94
|
-
* cut off, the rest as written. A tab straddling the cut leaves the columns
|
|
95
|
-
* past it as spaces.
|
|
96
|
-
*/
|
|
97
|
-
function dropColumns(line, n) {
|
|
98
|
-
const to = line.margin + n;
|
|
99
|
-
let col = 0;
|
|
100
|
-
let i = 0;
|
|
101
|
-
for (; i < line.text.length && col < to && (line.text[i] === " " || line.text[i] === "\t"); i++) {
|
|
102
|
-
const next = advance(col, line.text[i]);
|
|
103
|
-
if (next > to)
|
|
104
|
-
return " ".repeat(next - to) + line.text.slice(i + 1);
|
|
105
|
-
col = next;
|
|
106
|
-
}
|
|
107
|
-
return line.text.slice(i);
|
|
108
|
-
}
|
|
109
|
-
function itemStart(line) {
|
|
110
|
-
const text = body(line);
|
|
111
|
-
const m = ITEM_MARKER.exec(text);
|
|
112
|
-
if (!m || indentOf(line) >= CODE_INDENT || RULE.test(text))
|
|
113
|
-
return undefined;
|
|
114
|
-
const after = text.slice(m[0].length);
|
|
115
|
-
const spacing = LEAD.exec(after)[0];
|
|
116
|
-
const rest = after.slice(spacing.length);
|
|
117
|
-
if (spacing === "" && rest !== "")
|
|
118
|
-
return undefined;
|
|
119
|
-
const markerEnd = leadEnd(line) + m[0].length;
|
|
120
|
-
const spaced = advance(markerEnd, spacing) - markerEnd;
|
|
121
|
-
const content = rest === "" || spaced > CODE_INDENT ? markerEnd + 1 : markerEnd + spaced;
|
|
122
|
-
const lead = line.text.slice(0, line.text.length - text.length);
|
|
123
|
-
return {
|
|
124
|
-
bullet: m[1] === undefined ? m[0] : undefined,
|
|
125
|
-
start: m[1] === undefined ? 0 : parseInt(m[1], 10),
|
|
126
|
-
content,
|
|
127
|
-
first: rest === "" ? undefined : { text: lead + " ".repeat(m[0].length) + after, margin: content, lazy: false },
|
|
128
|
-
};
|
|
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)}|`;
|
|
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) });
|
|
148
46
|
else
|
|
149
|
-
|
|
47
|
+
yield { kind: "leaf", inline };
|
|
150
48
|
}
|
|
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
49
|
}
|
|
181
|
-
/**
|
|
182
|
-
function
|
|
183
|
-
return
|
|
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"];
|
|
184
53
|
}
|
|
185
54
|
/**
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
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.
|
|
189
66
|
*/
|
|
190
|
-
function
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
* any kind but a lone tag. A line that does not is text, and under an open
|
|
203
|
-
* paragraph it continues that paragraph.
|
|
204
|
-
*/
|
|
205
|
-
function opensBlock(line) {
|
|
206
|
-
return BLOCK_STARTS.some((re) => startsWith(line, re)) || itemStart(line) !== undefined || htmlBlockAt(line)?.interrupts === true;
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Whether `line` ends a paragraph: `opensBlock`, read as CommonMark reads it
|
|
210
|
-
* under a paragraph. Indented code cannot interrupt one; a list item does only
|
|
211
|
-
* when it has text and, numbered, counts from 1, so prose wrapped onto "2024.
|
|
212
|
-
* That year" stays prose; and a line of `=` or `-` is the paragraph's heading
|
|
213
|
-
* underline.
|
|
214
|
-
*/
|
|
215
|
-
function endsParagraph(line) {
|
|
216
|
-
if (line.lazy)
|
|
217
|
-
return false;
|
|
218
|
-
if (underline(line))
|
|
219
|
-
return true;
|
|
220
|
-
const item = itemStart(line);
|
|
221
|
-
return item ? item.first !== undefined && (item.bullet !== undefined || item.start === 1) : opensBlock(line);
|
|
222
|
-
}
|
|
223
|
-
/** The setext underline `line` is — a line of `=` or `-` under a paragraph — if it is one. A lazy line never is. */
|
|
224
|
-
function underline(line) {
|
|
225
|
-
return line && !line.lazy && indentOf(line) < CODE_INDENT ? SETEXT.exec(body(line)) : null;
|
|
226
|
-
}
|
|
227
|
-
/** Whether the last block of `tokens`, followed into the containers it closes, is a paragraph still open. */
|
|
228
|
-
function endsInParagraph(tokens) {
|
|
229
|
-
const last = tokens[tokens.length - 1];
|
|
230
|
-
switch (last?.type) {
|
|
231
|
-
case "paragraph":
|
|
232
|
-
return true;
|
|
233
|
-
case "list_item":
|
|
234
|
-
case "blockquote":
|
|
235
|
-
return endsInParagraph(last.children);
|
|
236
|
-
default:
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Whether `line`, short of a container's marker or indentation, still
|
|
242
|
-
* belongs to it as a lazy continuation: CommonMark's paragraph continuation
|
|
243
|
-
* text, which continues the paragraph open at the end of the container's
|
|
244
|
-
* `inner` lines and opens no block of its own. Outside the container a line
|
|
245
|
-
* is read without the paragraph under it, so any list item — whatever it
|
|
246
|
-
* counts from — ends the container rather than continuing it. A line after a
|
|
247
|
-
* lazy continuation finds that paragraph still open, so `afterLazy` answers
|
|
248
|
-
* without reading `inner` again: a container's lazy lines cost one read of
|
|
249
|
-
* it, not one each.
|
|
250
|
-
*/
|
|
251
|
-
function continuesLazily(inner, line, afterLazy) {
|
|
252
|
-
return !isBlank(line) && !opensBlock(line) && (afterLazy || endsInParagraph(tokenize(inner)));
|
|
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
|
+
};
|
|
253
79
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
* spaces or a backslash breaks hard, and its break is kept.
|
|
260
|
-
*/
|
|
261
|
-
function continuation(lines, start) {
|
|
262
|
-
const parts = [lines[start].text];
|
|
263
|
-
let i = start + 1;
|
|
264
|
-
for (; i < lines.length && !isBlank(lines[i]) && !endsParagraph(lines[i]) && !tableAt(lines, i); i++)
|
|
265
|
-
parts.push(lines[i].text);
|
|
266
|
-
const text = parts
|
|
267
|
-
.map((part, n) => {
|
|
268
|
-
const words = (n === parts.length - 1 ? part : part.replace(HARD_BREAK, "")).trim();
|
|
269
|
-
return n === 0 ? words : (HARD_BREAK.test(parts[n - 1]) ? "\n" : " ") + words;
|
|
270
|
-
})
|
|
271
|
-
.join("");
|
|
272
|
-
return { text, 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}`);
|
|
273
85
|
}
|
|
274
86
|
/**
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
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.
|
|
280
92
|
*/
|
|
281
|
-
function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
children,
|
|
328
|
-
},
|
|
329
|
-
]);
|
|
330
|
-
return { tokens, next };
|
|
331
|
-
}
|
|
332
|
-
function tokenize(lines) {
|
|
333
|
-
const tokens = [];
|
|
334
|
-
let i = 0;
|
|
335
|
-
while (i < lines.length) {
|
|
336
|
-
const line = lines[i];
|
|
337
|
-
// Blank line
|
|
338
|
-
if (isBlank(line)) {
|
|
339
|
-
tokens.push({ type: "blank" });
|
|
340
|
-
i++;
|
|
341
|
-
continue;
|
|
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
|
-
}
|
|
360
|
-
// Indented code: its lines, and the blank lines between them, with the indent cut off
|
|
361
|
-
if (indentOf(line) >= CODE_INDENT) {
|
|
362
|
-
const code = [];
|
|
363
|
-
for (; i < lines.length && (isBlank(lines[i]) || indentOf(lines[i]) >= CODE_INDENT); i++) {
|
|
364
|
-
code.push(dropColumns(lines[i], CODE_INDENT));
|
|
365
|
-
}
|
|
366
|
-
for (; code[code.length - 1].trim() === ""; i--)
|
|
367
|
-
code.pop();
|
|
368
|
-
tokens.push({ type: "code_block", code: code.join("\n") });
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
// Heading
|
|
372
|
-
const headingMatch = HEADING.exec(body(line));
|
|
373
|
-
if (headingMatch) {
|
|
374
|
-
tokens.push({ type: "heading", level: headingMatch[1].length, text: headingMatch[2] });
|
|
375
|
-
i++;
|
|
376
|
-
continue;
|
|
377
|
-
}
|
|
378
|
-
// Horizontal rule
|
|
379
|
-
if (startsWith(line, RULE)) {
|
|
380
|
-
tokens.push({ type: "hr" });
|
|
381
|
-
i++;
|
|
382
|
-
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);
|
|
383
139
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
for (i++; i < lines.length && !startsWith(lines[i], FENCE); i++)
|
|
389
|
-
codeLines.push(dropColumns(lines[i], indent));
|
|
390
|
-
i++; // skip closing ```
|
|
391
|
-
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();
|
|
392
144
|
continue;
|
|
393
145
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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;
|
|
406
171
|
}
|
|
407
|
-
tokens.push({ type: "blockquote", children: tokenize(inner) });
|
|
408
|
-
continue;
|
|
409
|
-
}
|
|
410
|
-
// List: its items, each Markdown of its own, as a quote is
|
|
411
|
-
const item = itemStart(line);
|
|
412
|
-
if (item) {
|
|
413
|
-
const { tokens: items, next } = list(item, lines, i);
|
|
414
|
-
tokens.push(...items);
|
|
415
|
-
i = next;
|
|
416
|
-
continue;
|
|
417
172
|
}
|
|
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
|
-
}
|
|
431
|
-
// Paragraph, or a setext heading when a line of `=` or `-` underlines it
|
|
432
|
-
const { text, next } = continuation(lines, i);
|
|
433
|
-
const setext = underline(lines[next]);
|
|
434
|
-
tokens.push(setext
|
|
435
|
-
? { type: "heading", level: setext[1].startsWith("=") ? 1 : 2, text }
|
|
436
|
-
: { type: "paragraph", text });
|
|
437
|
-
i = setext ? next + 1 : next;
|
|
438
173
|
}
|
|
439
|
-
return tokens;
|
|
440
|
-
}
|
|
441
|
-
const TICKS = /`+/y;
|
|
442
|
-
const ANGLED_DESTINATION = /\(\s*<([^<>\n]*)>/y;
|
|
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");
|
|
456
|
-
/** `re`, a sticky pattern, matched exactly at `i` in `text`. */
|
|
457
|
-
function matchAt(re, text, i) {
|
|
458
|
-
re.lastIndex = i;
|
|
459
|
-
return re.exec(text);
|
|
460
174
|
}
|
|
175
|
+
const EXIT = { kind: "exit", newLine: true };
|
|
461
176
|
/**
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
* than one without.
|
|
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.
|
|
467
181
|
*/
|
|
468
|
-
function
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
+
}
|
|
200
|
+
}
|
|
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 };
|
|
210
|
+
}
|
|
481
211
|
}
|
|
482
|
-
return pairs;
|
|
483
212
|
}
|
|
484
213
|
/**
|
|
485
|
-
* The
|
|
486
|
-
*
|
|
487
|
-
* parentheses balance — so a URL may itself hold `(…)` — then an optional
|
|
488
|
-
* quoted title, then the `)`. The destination, its backslash escapes read, and
|
|
489
|
-
* the index past the `)`.
|
|
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.
|
|
490
216
|
*/
|
|
491
|
-
function
|
|
492
|
-
|
|
493
|
-
return undefined;
|
|
494
|
-
const angled = matchAt(ANGLED_DESTINATION, text, open);
|
|
495
|
-
let i;
|
|
496
|
-
let url;
|
|
497
|
-
if (angled) {
|
|
498
|
-
url = angled[1];
|
|
499
|
-
i = open + angled[0].length;
|
|
500
|
-
}
|
|
501
|
-
else {
|
|
502
|
-
i = open + 1;
|
|
503
|
-
while (text[i] === " ")
|
|
504
|
-
i++;
|
|
505
|
-
const from = i;
|
|
506
|
-
for (let depth = 0; i < text.length && !/\s/.test(text[i]) && !(text[i] === ")" && depth === 0); i++) {
|
|
507
|
-
if (text[i] === "\\")
|
|
508
|
-
i++;
|
|
509
|
-
else if (text[i] === "(")
|
|
510
|
-
depth++;
|
|
511
|
-
else if (text[i] === ")")
|
|
512
|
-
depth--;
|
|
513
|
-
}
|
|
514
|
-
url = text.slice(from, i);
|
|
515
|
-
}
|
|
516
|
-
const tail = matchAt(LINK_TAIL, text, i);
|
|
517
|
-
return tail ? { url: url.replace(ESCAPES, "$1"), end: i + tail[0].length } : undefined;
|
|
217
|
+
function exitStep(token) {
|
|
218
|
+
return { kind: "exit", newLine: token.type !== "hr" };
|
|
518
219
|
}
|
|
519
220
|
/**
|
|
520
|
-
* The
|
|
521
|
-
* `
|
|
522
|
-
*
|
|
523
|
-
* escape, a code span, inline HTML or a link closes nothing.
|
|
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.
|
|
524
224
|
*/
|
|
525
|
-
function
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
i = tighter ? tighter.end : i + 1;
|
|
531
|
-
}
|
|
532
|
-
return undefined;
|
|
533
|
-
}
|
|
534
|
-
/** The inline construct opening at `i` in `text`, if one does; `brackets` is `text`'s `bracketPairs`. */
|
|
535
|
-
function inlineAt(text, i, brackets) {
|
|
536
|
-
switch (text[i]) {
|
|
537
|
-
case "`": {
|
|
538
|
-
const ticks = matchAt(TICKS, text, i)[0];
|
|
539
|
-
const close = new RegExp(`(?<!\`)${ticks}(?!\`)`, "g");
|
|
540
|
-
close.lastIndex = i + ticks.length;
|
|
541
|
-
const found = close.exec(text);
|
|
542
|
-
return found
|
|
543
|
-
? { kind: "code", code: text.slice(i + ticks.length, found.index), end: found.index + ticks.length }
|
|
544
|
-
: { kind: "literal", text: ticks, end: i + ticks.length };
|
|
545
|
-
}
|
|
546
|
-
case "*": {
|
|
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;
|
|
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);
|
|
562
230
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
case "
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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;
|
|
243
|
+
}
|
|
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;
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
case "code_block":
|
|
255
|
+
case "html_block":
|
|
256
|
+
case "hr":
|
|
257
|
+
break;
|
|
582
258
|
}
|
|
583
259
|
}
|
|
584
260
|
/**
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
* draws a nested construct — the inner style on top.
|
|
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.
|
|
588
263
|
*/
|
|
589
|
-
function
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
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);
|
|
269
|
+
}
|
|
593
270
|
}
|
|
594
271
|
/**
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
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.
|
|
599
277
|
*/
|
|
600
|
-
function
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
609
|
-
result.append(text.slice(plain, i));
|
|
610
|
-
switch (found.kind) {
|
|
611
|
-
case "code":
|
|
612
|
-
result.append(found.code, groundedStyle(options, resolveStyle(options, settings.inlineCodeStyle)));
|
|
613
|
-
break;
|
|
614
|
-
case "literal":
|
|
615
|
-
result.append(found.text);
|
|
616
|
-
break;
|
|
617
|
-
case "html":
|
|
618
|
-
// Rich draws nothing for inline HTML.
|
|
619
|
-
break;
|
|
620
|
-
case "emphasis":
|
|
621
|
-
result.append(styled(found.inner, found.style, settings, options));
|
|
622
|
-
break;
|
|
623
|
-
case "link": {
|
|
624
|
-
// As Rich draws it: without hyperlinks the URL is written out, and it
|
|
625
|
-
// is still the link, for a terminal that can click it.
|
|
626
|
-
const link = new RichText("", { style: new Style({ link: found.url }) });
|
|
627
|
-
if (settings.hyperlinks) {
|
|
628
|
-
result.append(link.append(styled(found.inner, "markdown.link_url", settings, options)));
|
|
629
|
-
}
|
|
630
|
-
else {
|
|
631
|
-
result
|
|
632
|
-
.append(styled(found.inner, "markdown.link", settings, options))
|
|
633
|
-
.append(" (")
|
|
634
|
-
.append(link.append(found.url, "markdown.link_url"))
|
|
635
|
-
.append(")");
|
|
636
|
-
}
|
|
637
|
-
break;
|
|
638
|
-
}
|
|
639
|
-
case "image": {
|
|
640
|
-
// Rich's ImageItem: a picture glyph, then the alt text — or, with
|
|
641
|
-
// none, the image's file name — linked to the image, then a space.
|
|
642
|
-
const title = found.alt || (found.src.replace(/\/+$/, "").split("/").pop() ?? "");
|
|
643
|
-
const link = settings.hyperlinks ? new Style({ link: found.src }) : "";
|
|
644
|
-
result.append(drawable(options, "🌆 ", "")).append(styled(title, link, settings, options)).append(" ");
|
|
645
|
-
break;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
i = plain = found.end;
|
|
649
|
-
}
|
|
650
|
-
result.append(text.slice(plain));
|
|
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);
|
|
651
285
|
}
|
|
652
286
|
/**
|
|
653
|
-
* What `
|
|
287
|
+
* What `content` renders in what is left of the width beside a gutter, each of
|
|
654
288
|
* its rows ended. The first row's gutter is `first` and every later row's is
|
|
655
289
|
* `rest`, so a list item's text hangs clear of its bullet and a quote's bar
|
|
656
290
|
* runs its full height. A block with no gutter passes two empty segments:
|
|
657
|
-
* every block is laid out the same way.
|
|
658
|
-
* column
|
|
659
|
-
* 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.
|
|
660
293
|
*/
|
|
661
|
-
function* guttered(
|
|
662
|
-
const
|
|
663
|
-
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) }));
|
|
664
296
|
for (const [row, line] of rows.entries()) {
|
|
665
297
|
yield row === 0 ? first : rest;
|
|
666
298
|
yield* line;
|
|
@@ -674,85 +306,111 @@ function* guttered(draw, options, first, rest, style) {
|
|
|
674
306
|
* and a heading's padding is not underlined.
|
|
675
307
|
*/
|
|
676
308
|
function inline(text, settings, justify, style) {
|
|
677
|
-
return
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
+
},
|
|
683
317
|
};
|
|
684
318
|
}
|
|
685
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
|
+
}
|
|
686
330
|
/**
|
|
687
331
|
* Each block in `tokens`, every row it draws ended, body text placed by
|
|
688
332
|
* `settings.justify`. A heading keeps its own placement whatever `settings`
|
|
689
|
-
* or `options` say — Rich's
|
|
690
|
-
* style stops where its
|
|
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.
|
|
691
336
|
*/
|
|
692
337
|
function* renderTokens(tokens, options, settings) {
|
|
693
|
-
for (const token of tokens)
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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;
|
|
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);
|
|
748
382
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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;
|
|
755
410
|
}
|
|
411
|
+
case "html_block":
|
|
412
|
+
// Rich draws nothing for it.
|
|
413
|
+
break;
|
|
756
414
|
}
|
|
757
415
|
}
|
|
758
416
|
export class Markdown {
|
|
@@ -760,19 +418,34 @@ export class Markdown {
|
|
|
760
418
|
inlineCodeStyle;
|
|
761
419
|
hyperlinks;
|
|
762
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;
|
|
763
425
|
constructor(markdown, options) {
|
|
764
426
|
this.markdown = markdown;
|
|
427
|
+
this.parsed = parseMarkdown(markdown);
|
|
765
428
|
this.inlineCodeStyle = options?.inlineCodeStyle ?? "markdown.code";
|
|
766
429
|
this.hyperlinks = options?.hyperlinks !== false;
|
|
767
430
|
this.justify = options?.justify ?? "left";
|
|
768
431
|
}
|
|
769
432
|
*render(rawOptions) {
|
|
770
|
-
//
|
|
771
|
-
const options = { ...rawOptions, height: stackedHeight(rawOptions.height) };
|
|
772
|
-
|
|
773
|
-
|
|
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
|
+
}
|
|
774
446
|
}
|
|
775
447
|
measure(options) {
|
|
776
|
-
|
|
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 };
|
|
777
450
|
}
|
|
778
451
|
}
|