@localix-ai/flowdown 0.0.0-stage → 0.5.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/LICENSE +21 -0
- package/README.md +101 -2
- package/dist/browser/file-actions.js +80 -0
- package/dist/browser/file-actions.js.map +1 -0
- package/dist/core/block-facts.js +72 -0
- package/dist/core/block-facts.js.map +1 -0
- package/dist/core/blocks.d.ts +25 -0
- package/dist/core/blocks.js +463 -0
- package/dist/core/blocks.js.map +1 -0
- package/dist/core/bounded-cache.js +42 -0
- package/dist/core/bounded-cache.js.map +1 -0
- package/dist/core/dollars.d.ts +12 -0
- package/dist/core/dollars.js +88 -0
- package/dist/core/dollars.js.map +1 -0
- package/dist/core/fence-scan.js +53 -0
- package/dist/core/fence-scan.js.map +1 -0
- package/dist/core/file-tree-parse.d.ts +26 -0
- package/dist/core/file-tree-parse.js +51 -0
- package/dist/core/file-tree-parse.js.map +1 -0
- package/dist/core/forms-html.js +63 -0
- package/dist/core/forms-html.js.map +1 -0
- package/dist/core/forms.d.ts +28 -0
- package/dist/core/forms.js +23 -0
- package/dist/core/forms.js.map +1 -0
- package/dist/core/headings.d.ts +11 -0
- package/dist/core/headings.js +74 -0
- package/dist/core/headings.js.map +1 -0
- package/dist/core/heal.d.ts +11 -0
- package/dist/core/heal.js +220 -0
- package/dist/core/heal.js.map +1 -0
- package/dist/core/load-once.js +23 -0
- package/dist/core/load-once.js.map +1 -0
- package/dist/core/math-delimiters.js +33 -0
- package/dist/core/math-delimiters.js.map +1 -0
- package/dist/core/math-syntax.js +56 -0
- package/dist/core/math-syntax.js.map +1 -0
- package/dist/core/prose.d.ts +10 -0
- package/dist/core/prose.js +77 -0
- package/dist/core/prose.js.map +1 -0
- package/dist/core/remark-answer-blocks.d.ts +5 -0
- package/dist/core/remark-answer-blocks.js +172 -0
- package/dist/core/remark-answer-blocks.js.map +1 -0
- package/dist/core/remark-answer-containers.js +299 -0
- package/dist/core/remark-answer-containers.js.map +1 -0
- package/dist/core/sanitize-schema.d.ts +18 -0
- package/dist/core/sanitize-schema.js +62 -0
- package/dist/core/sanitize-schema.js.map +1 -0
- package/dist/core/syntax.d.ts +8 -0
- package/dist/core/syntax.js +32 -0
- package/dist/core/syntax.js.map +1 -0
- package/dist/dialect.d.ts +92 -0
- package/dist/dialect.js +176 -0
- package/dist/dialect.js.map +1 -0
- package/dist/document/fence.d.ts +25 -0
- package/dist/document/fence.js +101 -0
- package/dist/document/fence.js.map +1 -0
- package/dist/document/index.d.ts +46 -0
- package/dist/document/index.js +66 -0
- package/dist/document/index.js.map +1 -0
- package/dist/document/table.d.ts +11 -0
- package/dist/document/table.js +35 -0
- package/dist/document/table.js.map +1 -0
- package/dist/dom/index.d.ts +21 -0
- package/dist/dom/index.js +123 -0
- package/dist/dom/index.js.map +1 -0
- package/dist/dom/patch.js +104 -0
- package/dist/dom/patch.js.map +1 -0
- package/dist/element/index.d.ts +29 -0
- package/dist/element/index.js +64 -0
- package/dist/element/index.js.map +1 -0
- package/dist/engine/blocks.js +774 -0
- package/dist/engine/blocks.js.map +1 -0
- package/dist/engine/entities.js +83 -0
- package/dist/engine/entities.js.map +1 -0
- package/dist/engine/entity-core.js +9 -0
- package/dist/engine/entity-core.js.map +1 -0
- package/dist/engine/entity-table.js +9 -0
- package/dist/engine/entity-table.js.map +1 -0
- package/dist/engine/html.d.ts +7 -0
- package/dist/engine/html.js +207 -0
- package/dist/engine/html.js.map +1 -0
- package/dist/engine/index.d.ts +7 -0
- package/dist/engine/index.js +74 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/inlines.js +852 -0
- package/dist/engine/inlines.js.map +1 -0
- package/dist/engine/types.d.ts +109 -0
- package/dist/engine/url.d.ts +11 -0
- package/dist/engine/url.js +16 -0
- package/dist/engine/url.js.map +1 -0
- package/dist/engine/visit.d.ts +10 -0
- package/dist/engine/visit.js +29 -0
- package/dist/engine/visit.js.map +1 -0
- package/dist/highlight/highlight.d.ts +4 -0
- package/dist/highlight/highlight.js +144 -0
- package/dist/highlight/highlight.js.map +1 -0
- package/dist/highlight/index.d.ts +4 -0
- package/dist/highlight/index.js +18 -0
- package/dist/highlight/index.js.map +1 -0
- package/dist/html/index.d.ts +4 -0
- package/dist/html/index.js +544 -0
- package/dist/html/index.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +13 -0
- package/dist/math/index.d.ts +17 -0
- package/dist/math/index.js +20 -0
- package/dist/math/index.js.map +1 -0
- package/dist/math/typeset.d.ts +12 -0
- package/dist/math/typeset.js +74 -0
- package/dist/math/typeset.js.map +1 -0
- package/dist/mermaid/diagram.d.ts +8 -0
- package/dist/mermaid/diagram.js +114 -0
- package/dist/mermaid/diagram.js.map +1 -0
- package/dist/mermaid/drawing.js +139 -0
- package/dist/mermaid/drawing.js.map +1 -0
- package/dist/mermaid/icons.js +16 -0
- package/dist/mermaid/icons.js.map +1 -0
- package/dist/mermaid/index.d.ts +13 -0
- package/dist/mermaid/index.js +3 -0
- package/dist/mermaid/loader.d.ts +5 -0
- package/dist/mermaid/loader.js +18 -0
- package/dist/mermaid/loader.js.map +1 -0
- package/dist/mermaid/look.d.ts +16 -0
- package/dist/mermaid/look.js +167 -0
- package/dist/mermaid/look.js.map +1 -0
- package/dist/mermaid/normalize-svg.js +44 -0
- package/dist/mermaid/normalize-svg.js.map +1 -0
- package/dist/mermaid/renderer.d.ts +22 -0
- package/dist/mermaid/renderer.js +132 -0
- package/dist/mermaid/renderer.js.map +1 -0
- package/dist/mermaid/viewer.js +309 -0
- package/dist/mermaid/viewer.js.map +1 -0
- package/dist/prompt/index.d.ts +19 -0
- package/dist/prompt/index.js +54 -0
- package/dist/prompt/index.js.map +1 -0
- package/dist/react/callout.d.ts +9 -0
- package/dist/react/callout.js +12 -0
- package/dist/react/callout.js.map +1 -0
- package/dist/react/code-block.d.ts +18 -0
- package/dist/react/code-block.js +81 -0
- package/dist/react/code-block.js.map +1 -0
- package/dist/react/context.d.ts +18 -0
- package/dist/react/context.js +17 -0
- package/dist/react/context.js.map +1 -0
- package/dist/react/draw.d.ts +2 -0
- package/dist/react/draw.js +16 -0
- package/dist/react/draw.js.map +1 -0
- package/dist/react/file-tree.d.ts +16 -0
- package/dist/react/file-tree.js +69 -0
- package/dist/react/file-tree.js.map +1 -0
- package/dist/react/growing-code.js +36 -0
- package/dist/react/growing-code.js.map +1 -0
- package/dist/react/helpers.d.ts +6 -0
- package/dist/react/helpers.js +17 -0
- package/dist/react/helpers.js.map +1 -0
- package/dist/react/icons.js +23 -0
- package/dist/react/icons.js.map +1 -0
- package/dist/react/labels.js +9 -0
- package/dist/react/labels.js.map +1 -0
- package/dist/react/markdown.d.ts +78 -0
- package/dist/react/markdown.js +204 -0
- package/dist/react/markdown.js.map +1 -0
- package/dist/react/render.d.ts +8 -0
- package/dist/react/render.js +41 -0
- package/dist/react/render.js.map +1 -0
- package/dist/react/table-rows.js +66 -0
- package/dist/react/table-rows.js.map +1 -0
- package/dist/react/table.js +36 -0
- package/dist/react/table.js.map +1 -0
- package/dist/react/tabs.d.ts +25 -0
- package/dist/react/tabs.js +58 -0
- package/dist/react/tabs.js.map +1 -0
- package/dist/react/use-smooth-text.d.ts +6 -0
- package/dist/react/use-smooth-text.js +156 -0
- package/dist/react/use-smooth-text.js.map +1 -0
- package/dist/react/use-stage.d.ts +9 -0
- package/dist/react/use-stage.js +57 -0
- package/dist/react/use-stage.js.map +1 -0
- package/dist/remark.d.ts +20 -0
- package/dist/remark.js +10 -0
- package/dist/render/factory.d.ts +15 -0
- package/dist/render/labels.d.ts +41 -0
- package/dist/render/labels.js +43 -0
- package/dist/render/labels.js.map +1 -0
- package/dist/render/tree.js +312 -0
- package/dist/render/tree.js.map +1 -0
- package/dist/render/view.d.ts +55 -0
- package/dist/render/view.js +302 -0
- package/dist/render/view.js.map +1 -0
- package/dist/render/views.d.ts +16 -0
- package/dist/render/views.js +261 -0
- package/dist/render/views.js.map +1 -0
- package/dist/render/vnode.d.ts +21 -0
- package/dist/render/vnode.js +194 -0
- package/dist/render/vnode.js.map +1 -0
- package/dist/static/index.d.ts +28 -0
- package/dist/static/index.js +27 -0
- package/dist/static/index.js.map +1 -0
- package/dist/stream.d.ts +12 -0
- package/dist/stream.js +3 -0
- package/dist/styles.css +1351 -0
- package/dist/tui/code.d.ts +5 -0
- package/dist/tui/code.js +240 -0
- package/dist/tui/code.js.map +1 -0
- package/dist/tui/index.d.ts +54 -0
- package/dist/tui/index.js +99 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/math.js +242 -0
- package/dist/tui/math.js.map +1 -0
- package/dist/tui/printable.d.ts +12 -0
- package/dist/tui/printable.js +31 -0
- package/dist/tui/printable.js.map +1 -0
- package/dist/tui/render.d.ts +12 -0
- package/dist/tui/render.js +524 -0
- package/dist/tui/render.js.map +1 -0
- package/dist/tui/style.d.ts +35 -0
- package/dist/tui/style.js +77 -0
- package/dist/tui/style.js.map +1 -0
- package/dist/tui/table.js +73 -0
- package/dist/tui/table.js.map +1 -0
- package/dist/tui/width.d.ts +2 -0
- package/dist/tui/width.js +133 -0
- package/dist/tui/width.js.map +1 -0
- package/dist/tui/wrap.d.ts +8 -0
- package/dist/tui/wrap.js +190 -0
- package/dist/tui/wrap.js.map +1 -0
- package/package.json +127 -4
package/dist/tui/code.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { printable } from "./printable.js";
|
|
2
|
+
import { CODE, DIM, DONE, FAILED, PLAIN, patch } from "./style.js";
|
|
3
|
+
import { span, tabPads, untabbed } from "./wrap.js";
|
|
4
|
+
// src/tui/code.ts
|
|
5
|
+
/**
|
|
6
|
+
* A fence's code as rows, coloured by a highlighter when the terminal was given one.
|
|
7
|
+
*
|
|
8
|
+
* The highlighter is the one a page takes (`@localix-ai/flowdown/highlight`, or any function of that
|
|
9
|
+
* shape): it returns HTML, spans named by highlight.js's classes. A terminal has no HTML, so the
|
|
10
|
+
* spans are read here for what they say, a token's kind, and each kind is drawn in one of the few
|
|
11
|
+
* colours a terminal theme can be trusted with.
|
|
12
|
+
*
|
|
13
|
+
* What a highlighter returns is not trusted with the text. The tokens are drawn only when, put
|
|
14
|
+
* together, they are the code that was given, character for character; otherwise the code is
|
|
15
|
+
* drawn plain, as without a highlighter. So highlighting changes the colours of a row and never
|
|
16
|
+
* its text, its width or where it breaks.
|
|
17
|
+
*
|
|
18
|
+
* A reply that streams draws its last block again on every token, and a highlighter is the dear
|
|
19
|
+
* part of that. So a view keeps a {@link CodeDrawer}: a fence it has coloured is not coloured
|
|
20
|
+
* again while its code is the same, and the fence still being written is coloured in pieces, of
|
|
21
|
+
* which only the last is new.
|
|
22
|
+
*/
|
|
23
|
+
/** How each kind of token is drawn; a kind not listed is drawn as what holds it. */
|
|
24
|
+
const KINDS = {
|
|
25
|
+
comment: DIM,
|
|
26
|
+
quote: DIM,
|
|
27
|
+
keyword: { color: "magenta" },
|
|
28
|
+
"selector-tag": { color: "magenta" },
|
|
29
|
+
doctag: { color: "magenta" },
|
|
30
|
+
name: { color: "magenta" },
|
|
31
|
+
string: { color: "green" },
|
|
32
|
+
regexp: { color: "green" },
|
|
33
|
+
number: { color: "yellow" },
|
|
34
|
+
literal: { color: "yellow" },
|
|
35
|
+
variable: { color: "yellow" },
|
|
36
|
+
"template-variable": { color: "yellow" },
|
|
37
|
+
attr: { color: "yellow" },
|
|
38
|
+
attribute: { color: "yellow" },
|
|
39
|
+
symbol: { color: "yellow" },
|
|
40
|
+
bullet: { color: "yellow" },
|
|
41
|
+
title: { bold: true },
|
|
42
|
+
section: { bold: true },
|
|
43
|
+
type: { color: "cyan" },
|
|
44
|
+
built_in: { color: "cyan" },
|
|
45
|
+
meta: { color: "cyan" },
|
|
46
|
+
"selector-id": { color: "cyan" },
|
|
47
|
+
"selector-class": { color: "cyan" },
|
|
48
|
+
addition: { color: "green" },
|
|
49
|
+
deletion: { color: "red" },
|
|
50
|
+
emphasis: { italic: true },
|
|
51
|
+
strong: { bold: true }
|
|
52
|
+
};
|
|
53
|
+
/** A class's name: `class Queue`, where a function's is only bold. */
|
|
54
|
+
const CLASS_TITLE = { color: "cyan" };
|
|
55
|
+
/** The style of a span with these classes inside text drawn as `outer`; null when none names a kind. */
|
|
56
|
+
function styleOf(classes, outer) {
|
|
57
|
+
const names = classes.split(/\s+/);
|
|
58
|
+
const kind = names.find((name) => name.startsWith("hljs-"))?.slice(5);
|
|
59
|
+
if (kind === void 0) return null;
|
|
60
|
+
if (kind === "subst") return PLAIN;
|
|
61
|
+
if (kind === "title" && names.includes("class_")) return patch(outer, CLASS_TITLE);
|
|
62
|
+
const style = KINDS[kind];
|
|
63
|
+
return style ? patch(outer, style) : null;
|
|
64
|
+
}
|
|
65
|
+
const ENTITIES = {
|
|
66
|
+
amp: "&",
|
|
67
|
+
lt: "<",
|
|
68
|
+
gt: ">",
|
|
69
|
+
quot: "\"",
|
|
70
|
+
apos: "'"
|
|
71
|
+
};
|
|
72
|
+
/** `&` as `&`; an entity that names no character is the text it is. */
|
|
73
|
+
function entity(whole, body) {
|
|
74
|
+
if (body[0] !== "#") return ENTITIES[body] ?? whole;
|
|
75
|
+
const code = /^#x/i.test(body) ? Number.parseInt(body.slice(2), 16) : Number(body.slice(1));
|
|
76
|
+
return Number.isInteger(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
|
|
77
|
+
}
|
|
78
|
+
const MARKUP = /<(\/?)([A-Za-z][^\s/>]*)(?:\s+class\s*=\s*"([^"]*)"|\s[^>]*)?\s*\/?>|&(#[xX][0-9A-Fa-f]+|#[0-9]+|[A-Za-z]+);/g;
|
|
79
|
+
/**
|
|
80
|
+
* A highlighter's HTML as tokens: the text between the tags, each piece with the style of the
|
|
81
|
+
* spans around it. `known` says whether any span named a kind this module draws.
|
|
82
|
+
*/
|
|
83
|
+
function tokens(html) {
|
|
84
|
+
const out = [];
|
|
85
|
+
const open = [PLAIN];
|
|
86
|
+
let known = false;
|
|
87
|
+
let text = "";
|
|
88
|
+
const flush = () => {
|
|
89
|
+
if (text) out.push([text, open.at(-1)]);
|
|
90
|
+
text = "";
|
|
91
|
+
};
|
|
92
|
+
let at = 0;
|
|
93
|
+
for (const match of html.matchAll(MARKUP)) {
|
|
94
|
+
text += html.slice(at, match.index);
|
|
95
|
+
at = match.index + match[0].length;
|
|
96
|
+
const [whole, closes, tag, classes, body] = match;
|
|
97
|
+
if (tag === void 0) {
|
|
98
|
+
text += entity(whole, body);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
flush();
|
|
102
|
+
if (closes) {
|
|
103
|
+
if (open.length > 1) open.pop();
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const style = classes === void 0 ? null : styleOf(classes, open.at(-1));
|
|
107
|
+
if (style) known = true;
|
|
108
|
+
open.push(style ?? open.at(-1));
|
|
109
|
+
}
|
|
110
|
+
text += html.slice(at);
|
|
111
|
+
flush();
|
|
112
|
+
return {
|
|
113
|
+
tokens: out,
|
|
114
|
+
known
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** The languages whose lines say what they are by their first character. */
|
|
118
|
+
const DIFFS = /* @__PURE__ */ new Set(["diff", "patch"]);
|
|
119
|
+
/** What a line of a diff is drawn in: what it adds, what it takes away, where it is. */
|
|
120
|
+
function diffStyle(line) {
|
|
121
|
+
if (line.startsWith("@@")) return DIM;
|
|
122
|
+
if (line.startsWith("+")) return DONE;
|
|
123
|
+
if (line.startsWith("-")) return FAILED;
|
|
124
|
+
return CODE;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* `source` as it is drawn without a highlighter: in the one colour of code, and a diff by its
|
|
128
|
+
* lines, which takes no grammar to read.
|
|
129
|
+
*/
|
|
130
|
+
function plain(source, language) {
|
|
131
|
+
const diff = language != null && DIFFS.has(language.toLowerCase());
|
|
132
|
+
return source.split("\n").map((line) => [span(printable(untabbed(line)), diff ? diffStyle(line) : CODE)]);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The rows of a fence's `source`, not yet cut to a width: one per line of code. With a
|
|
136
|
+
* highlighter that knows `language`, each token in its colour; otherwise all of it as code.
|
|
137
|
+
*/
|
|
138
|
+
function codeRows(source, language, highlight) {
|
|
139
|
+
if (!highlight || !language) return plain(source, language);
|
|
140
|
+
let read;
|
|
141
|
+
try {
|
|
142
|
+
const html = highlight(source, language);
|
|
143
|
+
if (html === null) return plain(source, language);
|
|
144
|
+
read = tokens(html);
|
|
145
|
+
} catch {
|
|
146
|
+
return plain(source, language);
|
|
147
|
+
}
|
|
148
|
+
if (!read.known || read.tokens.map(([text]) => text).join("") !== source) return plain(source, language);
|
|
149
|
+
/** The pads of the tabs in the line of code that starts at `from`. */
|
|
150
|
+
const padsFrom = (from) => {
|
|
151
|
+
const end = source.indexOf("\n", from);
|
|
152
|
+
return tabPads(source.slice(from, end === -1 ? void 0 : end));
|
|
153
|
+
};
|
|
154
|
+
const rows = [[]];
|
|
155
|
+
let pads = padsFrom(0);
|
|
156
|
+
let line = 0;
|
|
157
|
+
let offset = 0;
|
|
158
|
+
for (const [text, style] of read.tokens) {
|
|
159
|
+
let piece = "";
|
|
160
|
+
const put = () => {
|
|
161
|
+
if (piece) rows.at(-1).push(span(printable(piece), style));
|
|
162
|
+
piece = "";
|
|
163
|
+
};
|
|
164
|
+
for (const char of text) {
|
|
165
|
+
offset += char.length;
|
|
166
|
+
if (char === "\n") {
|
|
167
|
+
put();
|
|
168
|
+
rows.push([]);
|
|
169
|
+
pads = padsFrom(offset);
|
|
170
|
+
line = 0;
|
|
171
|
+
} else if (char === " ") piece += " ".repeat(pads[line++]);
|
|
172
|
+
else piece += char;
|
|
173
|
+
}
|
|
174
|
+
put();
|
|
175
|
+
}
|
|
176
|
+
return rows;
|
|
177
|
+
}
|
|
178
|
+
/** The drawer of a view with this highlighter. Without one there is nothing to keep. */
|
|
179
|
+
function createCodeDrawer(highlight) {
|
|
180
|
+
if (!highlight) return {
|
|
181
|
+
rows: (source, language) => plain(source, language),
|
|
182
|
+
pieced: () => false,
|
|
183
|
+
turn() {}
|
|
184
|
+
};
|
|
185
|
+
const pieces = [];
|
|
186
|
+
let tail = null;
|
|
187
|
+
/** The last piece, when it is all of its fence. */
|
|
188
|
+
let only = null;
|
|
189
|
+
let cut = false;
|
|
190
|
+
let now = /* @__PURE__ */ new Map();
|
|
191
|
+
let before = /* @__PURE__ */ new Map();
|
|
192
|
+
const whole = (source, language) => {
|
|
193
|
+
const key = `${language}\u0000${source}`;
|
|
194
|
+
let rows = now.get(key) ?? before.get(key);
|
|
195
|
+
if (rows === void 0 && only?.text === source && only.language === language) rows = only.rows;
|
|
196
|
+
if (rows === void 0) rows = codeRows(source, language, highlight);
|
|
197
|
+
now.set(key, rows);
|
|
198
|
+
return rows;
|
|
199
|
+
};
|
|
200
|
+
const piece = (kept, text, language) => kept && kept.text === text && kept.language === language ? kept : {
|
|
201
|
+
language,
|
|
202
|
+
text,
|
|
203
|
+
rows: codeRows(text, language, highlight)
|
|
204
|
+
};
|
|
205
|
+
const growing = (source, language) => {
|
|
206
|
+
const lines = source.split("\n");
|
|
207
|
+
const out = [];
|
|
208
|
+
let index = 0;
|
|
209
|
+
for (let at = 0; at < lines.length; at += 40, index++) {
|
|
210
|
+
const text = lines.slice(at, at + 40).join("\n");
|
|
211
|
+
const last = at + 40 >= lines.length;
|
|
212
|
+
const made = piece(last ? tail : pieces[index] ?? tail, text, language);
|
|
213
|
+
if (last) tail = made;
|
|
214
|
+
else pieces[index] = made;
|
|
215
|
+
for (const row of made.rows) out.push(row);
|
|
216
|
+
}
|
|
217
|
+
pieces.length = index - 1;
|
|
218
|
+
only = index === 1 ? tail : null;
|
|
219
|
+
if (index > 1) cut = true;
|
|
220
|
+
return out;
|
|
221
|
+
};
|
|
222
|
+
return {
|
|
223
|
+
rows(source, language, open) {
|
|
224
|
+
if (!language) return plain(source);
|
|
225
|
+
return open ? growing(source, language) : whole(source, language);
|
|
226
|
+
},
|
|
227
|
+
pieced() {
|
|
228
|
+
const was = cut;
|
|
229
|
+
cut = false;
|
|
230
|
+
return was;
|
|
231
|
+
},
|
|
232
|
+
turn() {
|
|
233
|
+
before = now;
|
|
234
|
+
now = /* @__PURE__ */ new Map();
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
export { codeRows, createCodeDrawer };
|
|
239
|
+
|
|
240
|
+
//# sourceMappingURL=code.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code.js","names":[],"sources":["../../src/tui/code.ts"],"sourcesContent":["/**\n * A fence's code as rows, coloured by a highlighter when the terminal was given one.\n *\n * The highlighter is the one a page takes (`@localix-ai/flowdown/highlight`, or any function of that\n * shape): it returns HTML, spans named by highlight.js's classes. A terminal has no HTML, so the\n * spans are read here for what they say, a token's kind, and each kind is drawn in one of the few\n * colours a terminal theme can be trusted with.\n *\n * What a highlighter returns is not trusted with the text. The tokens are drawn only when, put\n * together, they are the code that was given, character for character; otherwise the code is\n * drawn plain, as without a highlighter. So highlighting changes the colours of a row and never\n * its text, its width or where it breaks.\n *\n * A reply that streams draws its last block again on every token, and a highlighter is the dear\n * part of that. So a view keeps a {@link CodeDrawer}: a fence it has coloured is not coloured\n * again while its code is the same, and the fence still being written is coloured in pieces, of\n * which only the last is new.\n */\nimport { printable } from './printable'\nimport { CODE, DIM, DONE, FAILED, PLAIN, patch, type Line, type Style } from './style'\nimport { span, tabPads, untabbed } from './wrap'\n\n/**\n * Highlighted HTML for `code`, or null for a language it does not know: the shape `<Markdown\n * highlight>` and the other targets take.\n */\nexport type Highlighter = (code: string, language: string) => string | null\n\n/** How each kind of token is drawn; a kind not listed is drawn as what holds it. */\nconst KINDS: Record<string, Style> = {\n comment: DIM,\n quote: DIM,\n keyword: { color: 'magenta' },\n 'selector-tag': { color: 'magenta' },\n doctag: { color: 'magenta' },\n name: { color: 'magenta' },\n string: { color: 'green' },\n regexp: { color: 'green' },\n number: { color: 'yellow' },\n literal: { color: 'yellow' },\n variable: { color: 'yellow' },\n 'template-variable': { color: 'yellow' },\n attr: { color: 'yellow' },\n attribute: { color: 'yellow' },\n symbol: { color: 'yellow' },\n bullet: { color: 'yellow' },\n title: { bold: true },\n section: { bold: true },\n type: { color: 'cyan' },\n built_in: { color: 'cyan' },\n meta: { color: 'cyan' },\n 'selector-id': { color: 'cyan' },\n 'selector-class': { color: 'cyan' },\n addition: { color: 'green' },\n deletion: { color: 'red' },\n emphasis: { italic: true },\n strong: { bold: true },\n}\n\n/** A class's name: `class Queue`, where a function's is only bold. */\nconst CLASS_TITLE: Style = { color: 'cyan' }\n\n/** The style of a span with these classes inside text drawn as `outer`; null when none names a kind. */\nfunction styleOf(classes: string, outer: Style): Style | null {\n const names = classes.split(/\\s+/)\n const kind = names.find((name) => name.startsWith('hljs-'))?.slice(5)\n if (kind === undefined) return null\n // Code inside a string (`${…}`) is code again, not string.\n if (kind === 'subst') return PLAIN\n if (kind === 'title' && names.includes('class_')) return patch(outer, CLASS_TITLE)\n const style = KINDS[kind]\n return style ? patch(outer, style) : null\n}\n\nconst ENTITIES: Record<string, string> = { amp: '&', lt: '<', gt: '>', quot: '\"', apos: \"'\" }\n\n/** `&` as `&`; an entity that names no character is the text it is. */\nfunction entity(whole: string, body: string): string {\n if (body[0] !== '#') return ENTITIES[body] ?? whole\n const code = /^#x/i.test(body) ? Number.parseInt(body.slice(2), 16) : Number(body.slice(1))\n return Number.isInteger(code) && code >= 0 && code <= 0x10ffff\n ? String.fromCodePoint(code)\n : whole\n}\n\ntype Token = [text: string, style: Style]\n\nconst MARKUP =\n /<(\\/?)([A-Za-z][^\\s/>]*)(?:\\s+class\\s*=\\s*\"([^\"]*)\"|\\s[^>]*)?\\s*\\/?>|&(#[xX][0-9A-Fa-f]+|#[0-9]+|[A-Za-z]+);/g\n\n/**\n * A highlighter's HTML as tokens: the text between the tags, each piece with the style of the\n * spans around it. `known` says whether any span named a kind this module draws.\n */\nfunction tokens(html: string): { tokens: Token[]; known: boolean } {\n const out: Token[] = []\n const open: Style[] = [PLAIN]\n let known = false\n let text = ''\n const flush = (): void => {\n if (text) out.push([text, open.at(-1)!])\n text = ''\n }\n let at = 0\n for (const match of html.matchAll(MARKUP)) {\n text += html.slice(at, match.index)\n at = match.index + match[0].length\n const [whole, closes, tag, classes, body] = match\n if (tag === undefined) {\n text += entity(whole, body!)\n continue\n }\n flush()\n if (closes) {\n if (open.length > 1) open.pop()\n continue\n }\n const style = classes === undefined ? null : styleOf(classes, open.at(-1)!)\n if (style) known = true\n open.push(style ?? open.at(-1)!)\n }\n text += html.slice(at)\n flush()\n return { tokens: out, known }\n}\n\n/** The languages whose lines say what they are by their first character. */\nconst DIFFS = new Set(['diff', 'patch'])\n\n/** What a line of a diff is drawn in: what it adds, what it takes away, where it is. */\nfunction diffStyle(line: string): Style {\n if (line.startsWith('@@')) return DIM\n if (line.startsWith('+')) return DONE\n if (line.startsWith('-')) return FAILED\n return CODE\n}\n\n/**\n * `source` as it is drawn without a highlighter: in the one colour of code, and a diff by its\n * lines, which takes no grammar to read.\n */\nfunction plain(source: string, language?: string | null): Line[] {\n const diff = language != null && DIFFS.has(language.toLowerCase())\n return source\n .split('\\n')\n .map((line) => [span(printable(untabbed(line)), diff ? diffStyle(line) : CODE)])\n}\n\n/**\n * The rows of a fence's `source`, not yet cut to a width: one per line of code. With a\n * highlighter that knows `language`, each token in its colour; otherwise all of it as code.\n */\nexport function codeRows(\n source: string,\n language: string | null | undefined,\n highlight: Highlighter | null,\n): Line[] {\n if (!highlight || !language) return plain(source, language)\n let read: ReturnType<typeof tokens>\n try {\n const html = highlight(source, language)\n if (html === null) return plain(source, language)\n read = tokens(html)\n } catch {\n // A grammar that fails on this code costs the colours, not the reply.\n return plain(source, language)\n }\n // Colours from classes this module does not know (a highlighter with a scheme of its own) are\n // no colours here; and tokens that are not the code are not drawn at all.\n if (!read.known || read.tokens.map(([text]) => text).join('') !== source)\n return plain(source, language)\n\n /** The pads of the tabs in the line of code that starts at `from`. */\n const padsFrom = (from: number): number[] => {\n const end = source.indexOf('\\n', from)\n return tabPads(source.slice(from, end === -1 ? undefined : end))\n }\n const rows: Line[] = [[]]\n let pads = padsFrom(0)\n let line = 0\n let offset = 0\n for (const [text, style] of read.tokens) {\n let piece = ''\n const put = (): void => {\n if (piece) rows.at(-1)!.push(span(printable(piece), style))\n piece = ''\n }\n for (const char of text) {\n offset += char.length\n if (char === '\\n') {\n put()\n rows.push([])\n pads = padsFrom(offset)\n line = 0\n } else if (char === '\\t') {\n piece += ' '.repeat(pads[line++]!)\n } else {\n piece += char\n }\n }\n put()\n }\n return rows\n}\n\n/** The lines of a piece of a fence still being written: what is highlighted at once. */\nexport const PIECE_LINES = 40\n\n/** A fence's rows for one view, which draws the same code many times while a reply streams. */\nexport interface CodeDrawer {\n /**\n * The rows of a fence's `source`, one per line of code. `growing` says the fence may still be\n * written to: it is highlighted in pieces of {@link PIECE_LINES} lines, each on its own, so a\n * token costs the last piece and not the whole fence. A construct that runs over two pieces (a\n * block comment, a template string) may be coloured wrongly until the fence is done; then it\n * is highlighted whole. The pieces are a matter of colour only: the text is the code.\n */\n rows(source: string, language: string | null | undefined, growing: boolean): Line[]\n /** Whether a fence was drawn in more than one piece since this was last asked. */\n pieced(): boolean\n /** A render is over: the fences it did not draw are forgotten. */\n turn(): void\n}\n\ninterface Piece {\n language: string\n text: string\n rows: Line[]\n}\n\n/** The drawer of a view with this highlighter. Without one there is nothing to keep. */\nexport function createCodeDrawer(highlight: Highlighter | null): CodeDrawer {\n if (!highlight)\n return { rows: (source, language) => plain(source, language), pieced: () => false, turn() {} }\n // The pieces of the fence being written that have all their lines, in order; and its last one,\n // which a token that changed nothing in it, or that only began the next, finds as it was.\n const pieces: Piece[] = []\n let tail: Piece | null = null\n /** The last piece, when it is all of its fence. */\n let only: Piece | null = null\n let cut = false\n // Finished fences, by language and code: those of this render, and those of the last.\n let now = new Map<string, Line[]>()\n let before = new Map<string, Line[]>()\n const whole = (source: string, language: string): Line[] => {\n const key = `${language}\\u0000${source}`\n let rows = now.get(key) ?? before.get(key)\n // A fence that was all one piece while it was written is already drawn whole.\n if (rows === undefined && only?.text === source && only.language === language) rows = only.rows\n if (rows === undefined) rows = codeRows(source, language, highlight)\n now.set(key, rows)\n return rows\n }\n const piece = (kept: Piece | null | undefined, text: string, language: string): Piece =>\n kept && kept.text === text && kept.language === language\n ? kept\n : { language, text, rows: codeRows(text, language, highlight) }\n const growing = (source: string, language: string): Line[] => {\n const lines = source.split('\\n')\n const out: Line[] = []\n let index = 0\n for (let at = 0; at < lines.length; at += PIECE_LINES, index++) {\n const text = lines.slice(at, at + PIECE_LINES).join('\\n')\n // The last piece may still gain a line, or the end of its last one.\n const last = at + PIECE_LINES >= lines.length\n const made = piece(last ? tail : (pieces[index] ?? tail), text, language)\n if (last) tail = made\n else pieces[index] = made\n for (const row of made.rows) out.push(row)\n }\n pieces.length = index - 1\n only = index === 1 ? tail : null\n if (index > 1) cut = true\n return out\n }\n return {\n rows(source, language, open) {\n if (!language) return plain(source)\n return open ? growing(source, language) : whole(source, language)\n },\n pieced() {\n const was = cut\n cut = false\n return was\n },\n turn() {\n before = now\n now = new Map()\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Extension, Node } from "../engine/types.js";
|
|
2
|
+
import { Highlighter } from "./code.js";
|
|
3
|
+
import { DEFAULT_LABELS, MIN_WIDTH, TerminalLabels } from "./render.js";
|
|
4
|
+
import { Colour, Line, Span, Style, lineText, sgr, toAnsi } from "./style.js";
|
|
5
|
+
import { printable } from "./printable.js";
|
|
6
|
+
import { width } from "./width.js";
|
|
7
|
+
import { lineWidth } from "./wrap.js";
|
|
8
|
+
/** The words the renderer adds, any of them replaced. */
|
|
9
|
+
export type TerminalLabelOverrides = Partial<Omit<TerminalLabels, 'callouts'>> & {
|
|
10
|
+
callouts?: Partial<TerminalLabels['callouts']>;
|
|
11
|
+
};
|
|
12
|
+
export interface TerminalOptions {
|
|
13
|
+
/** The columns of the terminal; 80 when not given, and never under 8. */
|
|
14
|
+
width?: number;
|
|
15
|
+
/** Your own syntax, after the dialect's (`Extension`). A node the renderer does not know is drawn as its text. */
|
|
16
|
+
extensions?: Extension[];
|
|
17
|
+
/** The words the renderer adds (callout names), for a terminal in another language. */
|
|
18
|
+
labels?: TerminalLabelOverrides;
|
|
19
|
+
/**
|
|
20
|
+
* Colour fenced code: `@localix-ai/flowdown/highlight`, or any function that returns highlight.js's
|
|
21
|
+
* HTML. Off by default; a language it does not know is drawn in the one colour of code.
|
|
22
|
+
*/
|
|
23
|
+
highlight?: Highlighter | null;
|
|
24
|
+
/**
|
|
25
|
+
* Read `$…$`, `$$…$$` and a `math` fence as formulas and write them in Unicode: `x^2` as `x²`,
|
|
26
|
+
* `\frac{a}{b}` as `a/b`. On by default; `false` leaves them as the text they were written as.
|
|
27
|
+
*/
|
|
28
|
+
math?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface RenderOptions {
|
|
31
|
+
/** The text is a reply still arriving: its unfinished end is completed. */
|
|
32
|
+
streaming?: boolean;
|
|
33
|
+
/** The terminal was resized: draw for this width from now on. */
|
|
34
|
+
width?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface TerminalView {
|
|
37
|
+
/**
|
|
38
|
+
* The rows for `text`. The rows of a block that did not change are the same objects as at the
|
|
39
|
+
* last render: a screen that compares by identity repaints only what is new.
|
|
40
|
+
*/
|
|
41
|
+
render(text: string, options?: RenderOptions): Line[];
|
|
42
|
+
}
|
|
43
|
+
/** One text drawn more than once: a reply while it streams. */
|
|
44
|
+
export declare function createTerminalView(options?: TerminalOptions): TerminalView;
|
|
45
|
+
/** `markdown` as terminal rows. */
|
|
46
|
+
export declare function renderToLines(markdown: string, options?: TerminalOptions & RenderOptions): Line[];
|
|
47
|
+
/**
|
|
48
|
+
* `markdown` as text for a terminal, rows parted by `\n`. `color: false` leaves the escape
|
|
49
|
+
* sequences out: for a pipe, a file, a terminal that asked for none (`NO_COLOR`).
|
|
50
|
+
*/
|
|
51
|
+
export declare function renderToAnsi(markdown: string, options?: TerminalOptions & RenderOptions & {
|
|
52
|
+
color?: boolean;
|
|
53
|
+
}): string;
|
|
54
|
+
export { type Colour, DEFAULT_LABELS, type Extension, type Highlighter, type Line, MIN_WIDTH, type Node, type Span, type Style, type TerminalLabels, lineText, lineWidth, printable, sgr, width as textWidth, toAnsi };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { mathSyntax } from "../core/math-syntax.js";
|
|
2
|
+
import { flowdownDialect } from "../core/remark-answer-blocks.js";
|
|
3
|
+
import { createDocument } from "../document/index.js";
|
|
4
|
+
import { parse } from "../engine/index.js";
|
|
5
|
+
import { createCodeDrawer } from "./code.js";
|
|
6
|
+
import { DEFAULT_LABELS, MIN_WIDTH, drawBlock } from "./render.js";
|
|
7
|
+
import { lineText, sgr, toAnsi } from "./style.js";
|
|
8
|
+
import { printable } from "./printable.js";
|
|
9
|
+
import { width } from "./width.js";
|
|
10
|
+
import { lineWidth } from "./wrap.js";
|
|
11
|
+
// src/tui/index.ts
|
|
12
|
+
/**
|
|
13
|
+
* `@localix-ai/flowdown/tui`: Markdown as the rows of a terminal.
|
|
14
|
+
*
|
|
15
|
+
* import { renderToAnsi } from '@localix-ai/flowdown/tui'
|
|
16
|
+
*
|
|
17
|
+
* process.stdout.write(renderToAnsi(reply, { width: process.stdout.columns }) + '\n')
|
|
18
|
+
*
|
|
19
|
+
* The same text every other target reads, parsed by the same parser with the same dialect, and
|
|
20
|
+
* drawn in a terminal's terms: words wrapped to the width by the columns they take, tables in
|
|
21
|
+
* columns, a callout behind a coloured bar, steps down a numbered rail. No row is wider than the
|
|
22
|
+
* width, and nothing a reply holds reaches the terminal as an instruction to it (`printable.ts`).
|
|
23
|
+
*
|
|
24
|
+
* A reply still arriving is drawn with a view, which keeps what it drew: each token costs the
|
|
25
|
+
* block it lands in, and the rows of every block before it are the very same objects.
|
|
26
|
+
*
|
|
27
|
+
* const view = createTerminalView({ width: 80 })
|
|
28
|
+
* for await (const text of reply) draw(view.render(text, { streaming: true }))
|
|
29
|
+
* draw(view.render(text))
|
|
30
|
+
*
|
|
31
|
+
* The rows are plain values (`Line`, a list of `{ text, style }`): `toAnsi` writes them for a
|
|
32
|
+
* terminal, and a TUI library with a screen of its own reads them as they are.
|
|
33
|
+
*/
|
|
34
|
+
const DEFAULT_WIDTH = 80;
|
|
35
|
+
/** One text drawn more than once: a reply while it streams. */
|
|
36
|
+
function createTerminalView(options = {}) {
|
|
37
|
+
const labels = {
|
|
38
|
+
...DEFAULT_LABELS,
|
|
39
|
+
...options.labels,
|
|
40
|
+
callouts: {
|
|
41
|
+
...DEFAULT_LABELS.callouts,
|
|
42
|
+
...options.labels?.callouts
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const math = options.math ?? true;
|
|
46
|
+
const extensions = [
|
|
47
|
+
flowdownDialect,
|
|
48
|
+
...math ? [mathSyntax] : [],
|
|
49
|
+
...options.extensions ?? []
|
|
50
|
+
];
|
|
51
|
+
const code = createCodeDrawer(options.highlight ?? null);
|
|
52
|
+
const doc = createDocument();
|
|
53
|
+
let columns = options.width ?? DEFAULT_WIDTH;
|
|
54
|
+
let drawn = /* @__PURE__ */ new WeakMap();
|
|
55
|
+
return { render(text, { streaming = false, width } = {}) {
|
|
56
|
+
if (width !== void 0 && width !== columns) {
|
|
57
|
+
columns = width;
|
|
58
|
+
drawn = /* @__PURE__ */ new WeakMap();
|
|
59
|
+
}
|
|
60
|
+
const state = doc.update(text, {
|
|
61
|
+
streaming,
|
|
62
|
+
math
|
|
63
|
+
});
|
|
64
|
+
const last = state.blocks.length - 1;
|
|
65
|
+
const out = [];
|
|
66
|
+
state.blocks.forEach((block, index) => {
|
|
67
|
+
const closer = index === last ? state.closer : "";
|
|
68
|
+
const growing = streaming && index === last;
|
|
69
|
+
let made = drawn.get(block);
|
|
70
|
+
if (made === void 0 || made.closer !== closer || made.pieced && !growing) {
|
|
71
|
+
made = {
|
|
72
|
+
closer,
|
|
73
|
+
rows: drawBlock(parse(block.source + closer, { extensions }), columns, labels, code, growing),
|
|
74
|
+
pieced: code.pieced()
|
|
75
|
+
};
|
|
76
|
+
drawn.set(block, made);
|
|
77
|
+
}
|
|
78
|
+
if (made.rows.length === 0) return;
|
|
79
|
+
if (out.length > 0) out.push([]);
|
|
80
|
+
for (const row of made.rows) out.push(row);
|
|
81
|
+
});
|
|
82
|
+
code.turn();
|
|
83
|
+
return out;
|
|
84
|
+
} };
|
|
85
|
+
}
|
|
86
|
+
/** `markdown` as terminal rows. */
|
|
87
|
+
function renderToLines(markdown, options = {}) {
|
|
88
|
+
return createTerminalView(options).render(markdown, { streaming: options.streaming ?? false });
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* `markdown` as text for a terminal, rows parted by `\n`. `color: false` leaves the escape
|
|
92
|
+
* sequences out: for a pipe, a file, a terminal that asked for none (`NO_COLOR`).
|
|
93
|
+
*/
|
|
94
|
+
function renderToAnsi(markdown, options = {}) {
|
|
95
|
+
return toAnsi(renderToLines(markdown, options), options.color ?? true);
|
|
96
|
+
}
|
|
97
|
+
export { DEFAULT_LABELS, MIN_WIDTH, createTerminalView, lineText, lineWidth, printable, renderToAnsi, renderToLines, sgr, width as textWidth, toAnsi };
|
|
98
|
+
|
|
99
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/tui/index.ts"],"sourcesContent":["/**\n * `@localix-ai/flowdown/tui`: Markdown as the rows of a terminal.\n *\n * import { renderToAnsi } from '@localix-ai/flowdown/tui'\n *\n * process.stdout.write(renderToAnsi(reply, { width: process.stdout.columns }) + '\\n')\n *\n * The same text every other target reads, parsed by the same parser with the same dialect, and\n * drawn in a terminal's terms: words wrapped to the width by the columns they take, tables in\n * columns, a callout behind a coloured bar, steps down a numbered rail. No row is wider than the\n * width, and nothing a reply holds reaches the terminal as an instruction to it (`printable.ts`).\n *\n * A reply still arriving is drawn with a view, which keeps what it drew: each token costs the\n * block it lands in, and the rows of every block before it are the very same objects.\n *\n * const view = createTerminalView({ width: 80 })\n * for await (const text of reply) draw(view.render(text, { streaming: true }))\n * draw(view.render(text))\n *\n * The rows are plain values (`Line`, a list of `{ text, style }`): `toAnsi` writes them for a\n * terminal, and a TUI library with a screen of its own reads them as they are.\n */\nimport { mathSyntax } from '../core/math-syntax'\nimport { flowdownDialect } from '../core/remark-answer-blocks'\nimport { createDocument, type MarkdownBlock } from '../document'\nimport { parse } from '../engine'\nimport type { Extension } from '../engine/types'\nimport { createCodeDrawer, type Highlighter } from './code'\nimport { DEFAULT_LABELS, drawBlock, type TerminalLabels } from './render'\nimport { toAnsi, type Line } from './style'\n\nexport { toAnsi, lineText, sgr, type Colour, type Line, type Span, type Style } from './style'\nexport type { Highlighter } from './code'\nexport { printable } from './printable'\nexport { width as textWidth } from './width'\nexport { lineWidth } from './wrap'\nexport { DEFAULT_LABELS, MIN_WIDTH, type TerminalLabels } from './render'\nexport type { Extension, Node } from '../engine/types'\n\n/** The words the renderer adds, any of them replaced. */\nexport type TerminalLabelOverrides = Partial<Omit<TerminalLabels, 'callouts'>> & {\n callouts?: Partial<TerminalLabels['callouts']>\n}\n\nexport interface TerminalOptions {\n /** The columns of the terminal; 80 when not given, and never under 8. */\n width?: number\n /** Your own syntax, after the dialect's (`Extension`). A node the renderer does not know is drawn as its text. */\n extensions?: Extension[]\n /** The words the renderer adds (callout names), for a terminal in another language. */\n labels?: TerminalLabelOverrides\n /**\n * Colour fenced code: `@localix-ai/flowdown/highlight`, or any function that returns highlight.js's\n * HTML. Off by default; a language it does not know is drawn in the one colour of code.\n */\n highlight?: Highlighter | null\n /**\n * Read `$…$`, `$$…$$` and a `math` fence as formulas and write them in Unicode: `x^2` as `x²`,\n * `\\frac{a}{b}` as `a/b`. On by default; `false` leaves them as the text they were written as.\n */\n math?: boolean\n}\n\nexport interface RenderOptions {\n /** The text is a reply still arriving: its unfinished end is completed. */\n streaming?: boolean\n /** The terminal was resized: draw for this width from now on. */\n width?: number\n}\n\nexport interface TerminalView {\n /**\n * The rows for `text`. The rows of a block that did not change are the same objects as at the\n * last render: a screen that compares by identity repaints only what is new.\n */\n render(text: string, options?: RenderOptions): Line[]\n}\n\nconst DEFAULT_WIDTH = 80\n\n/** One text drawn more than once: a reply while it streams. */\nexport function createTerminalView(options: TerminalOptions = {}): TerminalView {\n const labels: TerminalLabels = {\n ...DEFAULT_LABELS,\n ...options.labels,\n callouts: { ...DEFAULT_LABELS.callouts, ...options.labels?.callouts },\n }\n const math = options.math ?? true\n const extensions = [flowdownDialect, ...(math ? [mathSyntax] : []), ...(options.extensions ?? [])]\n const code = createCodeDrawer(options.highlight ?? null)\n const doc = createDocument()\n let columns = options.width ?? DEFAULT_WIDTH\n let drawn = new WeakMap<MarkdownBlock, { closer: string; rows: Line[]; pieced: boolean }>()\n return {\n render(text, { streaming = false, width } = {}) {\n if (width !== undefined && width !== columns) {\n columns = width\n drawn = new WeakMap()\n }\n const state = doc.update(text, { streaming, math })\n const last = state.blocks.length - 1\n const out: Line[] = []\n state.blocks.forEach((block, index) => {\n const closer = index === last ? state.closer : ''\n // The end of a reply still arriving: its last fence may gain code with the next token.\n const growing = streaming && index === last\n let made = drawn.get(block)\n // A fence drawn in pieces is drawn whole once nothing more can come to it.\n if (made === undefined || made.closer !== closer || (made.pieced && !growing)) {\n const rows = drawBlock(\n parse(block.source + closer, { extensions }),\n columns,\n labels,\n code,\n growing,\n )\n made = { closer, rows, pieced: code.pieced() }\n drawn.set(block, made)\n }\n if (made.rows.length === 0) return\n // A blank row between blocks, as between the blocks of one.\n if (out.length > 0) out.push([])\n for (const row of made.rows) out.push(row)\n })\n code.turn()\n return out\n },\n }\n}\n\n/** `markdown` as terminal rows. */\nexport function renderToLines(\n markdown: string,\n options: TerminalOptions & RenderOptions = {},\n): Line[] {\n return createTerminalView(options).render(markdown, { streaming: options.streaming ?? false })\n}\n\n/**\n * `markdown` as text for a terminal, rows parted by `\\n`. `color: false` leaves the escape\n * sequences out: for a pipe, a file, a terminal that asked for none (`NO_COLOR`).\n */\nexport function renderToAnsi(\n markdown: string,\n options: TerminalOptions & RenderOptions & { color?: boolean } = {},\n): string {\n return toAnsi(renderToLines(markdown, options), options.color ?? true)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8EA,MAAM,gBAAgB;;AAGtB,SAAgB,mBAAmB,UAA2B,CAAC,GAAiB;CAC9E,MAAM,SAAyB;EAC7B,GAAG;EACH,GAAG,QAAQ;EACX,UAAU;GAAE,GAAG,eAAe;GAAU,GAAG,QAAQ,QAAQ;EAAS;CACtE;CACA,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,aAAa;EAAC;EAAiB,GAAI,OAAO,CAAC,UAAU,IAAI,CAAC;EAAI,GAAI,QAAQ,cAAc,CAAC;CAAE;CACjG,MAAM,OAAO,iBAAiB,QAAQ,aAAa,IAAI;CACvD,MAAM,MAAM,eAAe;CAC3B,IAAI,UAAU,QAAQ,SAAS;CAC/B,IAAI,wBAAQ,IAAI,QAA0E;CAC1F,OAAO,EACL,OAAO,MAAM,EAAE,YAAY,OAAO,UAAU,CAAC,GAAG;EAC9C,IAAI,UAAU,KAAA,KAAa,UAAU,SAAS;GAC5C,UAAU;GACV,wBAAQ,IAAI,QAAQ;EACtB;EACA,MAAM,QAAQ,IAAI,OAAO,MAAM;GAAE;GAAW;EAAK,CAAC;EAClD,MAAM,OAAO,MAAM,OAAO,SAAS;EACnC,MAAM,MAAc,CAAC;EACrB,MAAM,OAAO,SAAS,OAAO,UAAU;GACrC,MAAM,SAAS,UAAU,OAAO,MAAM,SAAS;GAE/C,MAAM,UAAU,aAAa,UAAU;GACvC,IAAI,OAAO,MAAM,IAAI,KAAK;GAE1B,IAAI,SAAS,KAAA,KAAa,KAAK,WAAW,UAAW,KAAK,UAAU,CAAC,SAAU;IAQ7E,OAAO;KAAE;KAAQ,MAPJ,UACX,MAAM,MAAM,SAAS,QAAQ,EAAE,WAAW,CAAC,GAC3C,SACA,QACA,MACA,OAEkB;KAAG,QAAQ,KAAK,OAAO;IAAE;IAC7C,MAAM,IAAI,OAAO,IAAI;GACvB;GACA,IAAI,KAAK,KAAK,WAAW,GAAG;GAE5B,IAAI,IAAI,SAAS,GAAG,IAAI,KAAK,CAAC,CAAC;GAC/B,KAAK,MAAM,OAAO,KAAK,MAAM,IAAI,KAAK,GAAG;EAC3C,CAAC;EACD,KAAK,KAAK;EACV,OAAO;CACT,EACF;AACF;;AAGA,SAAgB,cACd,UACA,UAA2C,CAAC,GACpC;CACR,OAAO,mBAAmB,OAAO,CAAC,CAAC,OAAO,UAAU,EAAE,WAAW,QAAQ,aAAa,MAAM,CAAC;AAC/F;;;;;AAMA,SAAgB,aACd,UACA,UAAiE,CAAC,GAC1D;CACR,OAAO,OAAO,cAAc,UAAU,OAAO,GAAG,QAAQ,SAAS,IAAI;AACvE"}
|