@ultimat3/ui 29.4.0 → 30.0.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/CATALOG.md +491 -24
- package/CLAUDE.md +23 -16
- package/README.md +338 -33
- package/package.json +16 -5
- package/src/components/AppShell.module.scss +23 -7
- package/src/components/AppShell.tsx +9 -0
- package/src/components/BarChart.module.scss +26 -0
- package/src/components/BarChart.tsx +242 -93
- package/src/components/Board.module.scss +30 -0
- package/src/components/Board.tsx +47 -0
- package/src/components/BottomNav.module.scss +49 -0
- package/src/components/BottomNav.tsx +57 -0
- package/src/components/Calendar.module.scss +118 -0
- package/src/components/Calendar.tsx +246 -0
- package/src/components/ChartFrame.module.scss +40 -3
- package/src/components/ChartFrame.tsx +84 -28
- package/src/components/CodeBlock.module.scss +101 -0
- package/src/components/CodeBlock.tsx +76 -0
- package/src/components/CommandPalette.module.scss +25 -0
- package/src/components/CommandPalette.tsx +50 -22
- package/src/components/ConfirmDialog.module.scss +36 -0
- package/src/components/ConfirmDialog.tsx +188 -0
- package/src/components/DataTable.tsx +92 -33
- package/src/components/DatePicker.module.scss +37 -0
- package/src/components/DatePicker.tsx +134 -0
- package/src/components/DateRangePicker.module.scss +54 -0
- package/src/components/DateRangePicker.tsx +156 -0
- package/src/components/DescriptionList.module.scss +46 -0
- package/src/components/DescriptionList.tsx +38 -0
- package/src/components/DiffView.module.scss +124 -0
- package/src/components/DiffView.tsx +169 -0
- package/src/components/DonutChart.tsx +13 -4
- package/src/components/Drawer.module.scss +40 -0
- package/src/components/Drawer.tsx +56 -0
- package/src/components/Gauge.module.scss +8 -0
- package/src/components/Gauge.tsx +40 -13
- package/src/components/HeatStrip.module.scss +133 -0
- package/src/components/HeatStrip.tsx +64 -0
- package/src/components/JsonView.module.scss +64 -0
- package/src/components/JsonView.tsx +68 -0
- package/src/components/LineChart.tsx +45 -18
- package/src/components/LiveIndicator.module.scss +44 -0
- package/src/components/LiveIndicator.tsx +45 -0
- package/src/components/LogViewer.module.scss +102 -0
- package/src/components/LogViewer.tsx +123 -0
- package/src/components/Markdown.module.scss +87 -0
- package/src/components/Markdown.tsx +84 -0
- package/src/components/Meter.module.scss +7 -0
- package/src/components/Meter.tsx +16 -0
- package/src/components/NavList.module.scss +66 -0
- package/src/components/NavList.tsx +95 -0
- package/src/components/Pagination.module.scss +6 -2
- package/src/components/Pagination.tsx +57 -13
- package/src/components/Popover.tsx +12 -2
- package/src/components/PullToRefresh.module.scss +41 -0
- package/src/components/PullToRefresh.tsx +108 -0
- package/src/components/Radio.module.scss +51 -0
- package/src/components/Radio.tsx +12 -1
- package/src/components/SearchableSelect.module.scss +216 -0
- package/src/components/SearchableSelect.tsx +331 -0
- package/src/components/SearchableSelectChips.tsx +41 -0
- package/src/components/SearchableSelectNative.tsx +89 -0
- package/src/components/ShortcutSheet.module.scss +33 -0
- package/src/components/ShortcutSheet.tsx +67 -0
- package/src/components/Slider.module.scss +50 -0
- package/src/components/Slider.tsx +68 -0
- package/src/components/SortableList.module.scss +72 -0
- package/src/components/SortableList.tsx +42 -0
- package/src/components/Sparkline.module.scss +10 -2
- package/src/components/Sparkline.tsx +27 -10
- package/src/components/SplitPane.module.scss +39 -0
- package/src/components/SplitPane.tsx +96 -0
- package/src/components/StatTile.module.scss +29 -3
- package/src/components/StatTile.tsx +75 -16
- package/src/components/Stepper.module.scss +78 -0
- package/src/components/Stepper.tsx +73 -0
- package/src/components/Tabs.module.scss +13 -1
- package/src/components/Tabs.tsx +77 -1
- package/src/components/TerminalView.module.scss +68 -0
- package/src/components/TerminalView.tsx +97 -0
- package/src/components/TimeRangePicker.module.scss +85 -0
- package/src/components/TimeRangePicker.tsx +91 -0
- package/src/components/Timeline.module.scss +74 -0
- package/src/components/Timeline.tsx +60 -0
- package/src/components/Tree.module.scss +98 -0
- package/src/components/Tree.tsx +185 -0
- package/src/components/VirtualList.module.scss +60 -0
- package/src/components/VirtualList.tsx +251 -0
- package/src/components/app-shell-view.ts +4 -1
- package/src/components/bar-chart-view.ts +121 -39
- package/src/components/board-view.ts +25 -0
- package/src/components/bottom-nav-view.ts +18 -0
- package/src/components/calendar-view.ts +226 -0
- package/src/components/chart-checks.ts +59 -0
- package/src/components/chart-frame-view.ts +33 -2
- package/src/components/chart-reference-view.ts +41 -0
- package/src/components/command-palette-view.ts +47 -6
- package/src/components/confirm-dialog-view.ts +47 -0
- package/src/components/data-table-rows.ts +93 -0
- package/src/components/date-picker-view.ts +38 -0
- package/src/components/date-range-view.ts +46 -0
- package/src/components/diff-view-view.ts +85 -0
- package/src/components/drawer-gesture-view.ts +33 -0
- package/src/components/gauge-view.ts +29 -0
- package/src/components/heat-strip-view.ts +68 -0
- package/src/components/json-view-view.ts +54 -0
- package/src/components/line-chart-view.ts +87 -25
- package/src/components/log-viewer-view.ts +51 -0
- package/src/components/markdown-nodes.tsx +150 -0
- package/src/components/nav-list-view.ts +37 -0
- package/src/components/pull-refresh-view.ts +93 -0
- package/src/components/rank-options.ts +61 -0
- package/src/components/searchable-select-keys.ts +118 -0
- package/src/components/searchable-select-view.ts +187 -0
- package/src/components/slider-view.ts +47 -0
- package/src/components/sort-pointer.ts +141 -0
- package/src/components/sort-surface.tsx +231 -0
- package/src/components/sparkline-view.ts +74 -14
- package/src/components/split-pane-view.ts +69 -0
- package/src/components/stat-delta.ts +54 -11
- package/src/components/stepper-view.ts +38 -0
- package/src/components/thunk.ts +9 -0
- package/src/components/timeline-view.ts +29 -0
- package/src/components/tree-node.tsx +133 -0
- package/src/components/tree-view.ts +185 -0
- package/src/components/use-command-palette.ts +153 -0
- package/src/components/virtual-list-anchor.ts +35 -0
- package/src/components/virtual-list-observer.ts +58 -0
- package/src/components/virtual-list-view.ts +145 -0
- package/src/content/ansi.ts +144 -0
- package/src/content/heading-slug.ts +32 -0
- package/src/content/highlight.ts +129 -0
- package/src/content/languages/all.ts +21 -0
- package/src/content/languages/diff.ts +14 -0
- package/src/content/languages/html.ts +16 -0
- package/src/content/languages/json.ts +15 -0
- package/src/content/languages/shell.ts +20 -0
- package/src/content/languages/sql.ts +27 -0
- package/src/content/languages/ts.ts +26 -0
- package/src/content/languages/yaml.ts +18 -0
- package/src/content/line-diff.ts +191 -0
- package/src/content/markdown-inline.ts +368 -0
- package/src/content/markdown-parse.ts +267 -0
- package/src/content/markdown-tree.ts +58 -0
- package/src/content/unified-patch.ts +53 -0
- package/src/drag/pointer-drag.ts +185 -0
- package/src/drag/sort-controller.ts +196 -0
- package/src/drag/sort-view.ts +201 -0
- package/src/errors.ts +1 -1
- package/src/form/field-path.ts +7 -33
- package/src/form/form-binding.ts +5 -8
- package/src/form/form-issue.ts +30 -36
- package/src/form/form-values.ts +3 -2
- package/src/hotkeys-view.ts +150 -0
- package/src/i18n-keys.ts +7 -50
- package/src/index.ts +214 -15
- package/src/list-url.ts +123 -0
- package/src/live-window.ts +23 -0
- package/src/roving.ts +2 -0
- package/src/theme/inert-runtime.ts +4 -0
- package/src/theme/runtime-slot.ts +2 -2
- package/src/theme/solid-adapter.ts +11 -5
- package/src/time-range-url.ts +200 -0
- package/src/tokens/_mixins.scss +1 -1
- package/src/tokens/_touch.scss +30 -1
- package/src/tokens/contrast-pairs.ts +12 -0
- package/src/ui-keys.ts +126 -0
- package/src/use-hotkeys.ts +145 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
// Inline markdown in linear time: escapes, code spans, brackets and delimiter runs are each found
|
|
2
|
+
// in ONE pass up front, so a body of 100 KB of `*` and `[` never rescans. A link whose scheme is
|
|
3
|
+
// not http, https, mailto or relative prints as its source text. Raw HTML is text.
|
|
4
|
+
|
|
5
|
+
import { MARKDOWN_MAX_INLINE_DEPTH, MARKDOWN_MAX_URL, type MarkdownInline } from './markdown-tree';
|
|
6
|
+
|
|
7
|
+
const PUNCT = /[!-/:-@[-`{-~]/;
|
|
8
|
+
const SPACE = /\s/;
|
|
9
|
+
const ALNUM = /[\p{L}\p{N}]/u;
|
|
10
|
+
const SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
11
|
+
const SAFE_SCHEMES = new Set(['http:', 'https:', 'mailto:']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A destination a reader may follow: no scheme (relative, `/path`, `#frag`) or one of three. Read
|
|
15
|
+
* off the string with C0 controls and spaces removed, as a browser reads `java\tscript:`.
|
|
16
|
+
*/
|
|
17
|
+
export function safeMarkdownUrl(url: string): boolean {
|
|
18
|
+
const stripped = [...url]
|
|
19
|
+
.filter((char) => (char.codePointAt(0) ?? 0) > 0x20 && char !== '\u007f')
|
|
20
|
+
.join('');
|
|
21
|
+
const scheme = SCHEME.exec(stripped);
|
|
22
|
+
return scheme === null || SAFE_SCHEMES.has(scheme[0].toLowerCase());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Run {
|
|
26
|
+
readonly start: number;
|
|
27
|
+
readonly len: number;
|
|
28
|
+
readonly char: string;
|
|
29
|
+
readonly open: boolean;
|
|
30
|
+
/** The next run, left to right, that can close this one; -1 for none. */
|
|
31
|
+
close: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Scan {
|
|
35
|
+
/** A backslash that escapes the next character: dropped, and its next character is literal. */
|
|
36
|
+
readonly escapes: Uint8Array;
|
|
37
|
+
/** Start of a code span → [content start, content end, span end]. */
|
|
38
|
+
readonly code: Map<number, readonly [number, number, number]>;
|
|
39
|
+
/** `[` → its matching `]`. */
|
|
40
|
+
readonly bracket: Map<number, number>;
|
|
41
|
+
readonly runs: Map<number, Run>;
|
|
42
|
+
readonly runAt: readonly Run[];
|
|
43
|
+
/** Where a bare destination starting here ends: whitespace, or the `)` that unbalances it. */
|
|
44
|
+
readonly destEnd: Int32Array;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Every bare destination's end in one right-to-left pass, so a body of `[](` repeated never
|
|
49
|
+
* rescans: the end is the first whitespace, or the first `)` where the paren depth from `i` drops
|
|
50
|
+
* below zero — the next position whose prefix depth is SMALLER, found with a monotonic stack.
|
|
51
|
+
*/
|
|
52
|
+
function destinationEnds(src: string): Int32Array {
|
|
53
|
+
const n = src.length;
|
|
54
|
+
const depth = new Int32Array(n + 1);
|
|
55
|
+
for (let i = 0; i < n; i += 1)
|
|
56
|
+
depth[i + 1] = (depth[i] ?? 0) + (src[i] === '(' ? 1 : src[i] === ')' ? -1 : 0);
|
|
57
|
+
const end = new Int32Array(n + 1);
|
|
58
|
+
const stack: number[] = [n];
|
|
59
|
+
let space = n;
|
|
60
|
+
end[n] = n;
|
|
61
|
+
for (let i = n - 1; i >= 0; i -= 1) {
|
|
62
|
+
if (SPACE.test(src[i] ?? '')) space = i;
|
|
63
|
+
// `k` is a prefix index; the `)` that drops below depth[i] is the character at k - 1.
|
|
64
|
+
while (stack.length > 0 && (depth[stack[stack.length - 1] ?? 0] ?? 0) >= (depth[i] ?? 0))
|
|
65
|
+
stack.pop();
|
|
66
|
+
const drop = stack.length === 0 ? n + 1 : (stack[stack.length - 1] ?? n + 1);
|
|
67
|
+
end[i] = Math.min(space, drop - 1);
|
|
68
|
+
stack.push(i);
|
|
69
|
+
}
|
|
70
|
+
return end;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const isSpace = (char: string | undefined): boolean => char === undefined || SPACE.test(char);
|
|
74
|
+
const isPunct = (char: string | undefined): boolean => char !== undefined && PUNCT.test(char);
|
|
75
|
+
|
|
76
|
+
function scan(src: string): Scan {
|
|
77
|
+
const n = src.length;
|
|
78
|
+
const escapes = new Uint8Array(n);
|
|
79
|
+
for (let i = 0; i < n - 1; i += 1) {
|
|
80
|
+
if (src[i] === '\\' && escapes[i] === 0 && PUNCT.test(src[i + 1] ?? '')) {
|
|
81
|
+
escapes[i] = 1;
|
|
82
|
+
escapes[i + 1] = 2;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Backtick runs by length, in order; a run closes at the NEXT run of its own length.
|
|
86
|
+
const ticks: { start: number; len: number }[] = [];
|
|
87
|
+
for (let i = 0; i < n; ) {
|
|
88
|
+
if (src[i] === '`' && escapes[i] === 0) {
|
|
89
|
+
let j = i;
|
|
90
|
+
while (src[j] === '`') j += 1;
|
|
91
|
+
ticks.push({ start: i, len: j - i });
|
|
92
|
+
i = j;
|
|
93
|
+
} else i += 1;
|
|
94
|
+
}
|
|
95
|
+
const nextSame = new Map<number, number>();
|
|
96
|
+
const following: number[] = new Array(ticks.length).fill(-1);
|
|
97
|
+
for (let k = ticks.length - 1; k >= 0; k -= 1) {
|
|
98
|
+
const tick = ticks[k];
|
|
99
|
+
if (tick === undefined) continue;
|
|
100
|
+
following[k] = nextSame.get(tick.len) ?? -1;
|
|
101
|
+
nextSame.set(tick.len, k);
|
|
102
|
+
}
|
|
103
|
+
const code = new Map<number, readonly [number, number, number]>();
|
|
104
|
+
const covered = new Uint8Array(n);
|
|
105
|
+
let until = 0;
|
|
106
|
+
ticks.forEach((tick, k) => {
|
|
107
|
+
const closer = ticks[following[k] ?? -1];
|
|
108
|
+
if (tick.start < until || closer === undefined) return;
|
|
109
|
+
const end = closer.start + closer.len;
|
|
110
|
+
code.set(tick.start, [tick.start + tick.len, closer.start, end]);
|
|
111
|
+
covered.fill(1, tick.start, end);
|
|
112
|
+
until = end;
|
|
113
|
+
});
|
|
114
|
+
const usable = (i: number): boolean => covered[i] === 0 && escapes[i] === 0;
|
|
115
|
+
const bracket = new Map<number, number>();
|
|
116
|
+
const stack: number[] = [];
|
|
117
|
+
const runAt: Run[] = [];
|
|
118
|
+
for (let i = 0; i < n; i += 1) {
|
|
119
|
+
const char = src[i];
|
|
120
|
+
if (!usable(i)) continue;
|
|
121
|
+
if (char === '[') stack.push(i);
|
|
122
|
+
else if (char === ']') {
|
|
123
|
+
const open = stack.pop();
|
|
124
|
+
if (open !== undefined) bracket.set(open, i);
|
|
125
|
+
} else if (char === '*' || char === '_' || char === '~') {
|
|
126
|
+
let j = i;
|
|
127
|
+
while (src[j] === char && usable(j)) j += 1;
|
|
128
|
+
const len = j - i;
|
|
129
|
+
const before = src[i - 1];
|
|
130
|
+
const after = src[j];
|
|
131
|
+
const left = !isSpace(after) && (!isPunct(after) || isSpace(before) || isPunct(before));
|
|
132
|
+
const right = !isSpace(before) && (!isPunct(before) || isSpace(after) || isPunct(after));
|
|
133
|
+
const word = char === '_';
|
|
134
|
+
const valid = char === '~' ? len === 2 : len <= 3;
|
|
135
|
+
runAt.push({
|
|
136
|
+
start: i,
|
|
137
|
+
len,
|
|
138
|
+
char,
|
|
139
|
+
open: valid && left && !(word && ALNUM.test(before ?? '')),
|
|
140
|
+
close: valid && right && !(word && ALNUM.test(after ?? '')) ? 1 : 0,
|
|
141
|
+
});
|
|
142
|
+
i = j - 1;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const latest = new Map<string, number>();
|
|
146
|
+
for (let k = runAt.length - 1; k >= 0; k -= 1) {
|
|
147
|
+
const run = runAt[k];
|
|
148
|
+
if (run === undefined) continue;
|
|
149
|
+
const canClose = run.close === 1;
|
|
150
|
+
run.close = latest.get(`${run.char}${run.len}`) ?? -1;
|
|
151
|
+
if (canClose) latest.set(`${run.char}${run.len}`, k);
|
|
152
|
+
}
|
|
153
|
+
const runs = new Map(runAt.map((run) => [run.start, run] as const));
|
|
154
|
+
return { escapes, code, bracket, runs, runAt, destEnd: destinationEnds(src) };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** `indexOf`, but never past `limit`: an unclosed `<` must not rescan the rest of the body. */
|
|
158
|
+
function within(src: string, char: string, from: number, limit: number): number {
|
|
159
|
+
for (let i = from; i < limit; i += 1) if (src[i] === char) return i;
|
|
160
|
+
return -1;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** `(dest "title")` after a `]` at `at`, read in one bounded scan; null when it is not one. */
|
|
164
|
+
function linkTail(
|
|
165
|
+
src: string,
|
|
166
|
+
at: number,
|
|
167
|
+
destEnd: Int32Array,
|
|
168
|
+
): { href: string; title?: string; end: number } | null {
|
|
169
|
+
if (src[at] !== '(') return null;
|
|
170
|
+
let i = at + 1;
|
|
171
|
+
const limit = Math.min(src.length, at + MARKDOWN_MAX_URL + 600);
|
|
172
|
+
while (i < limit && (src[i] === ' ' || src[i] === '\n')) i += 1;
|
|
173
|
+
let href = '';
|
|
174
|
+
if (src[i] === '<') {
|
|
175
|
+
const close = within(src, '>', i, limit);
|
|
176
|
+
if (close === -1 || src.slice(i, close).includes('\n')) return null;
|
|
177
|
+
href = src.slice(i + 1, close);
|
|
178
|
+
i = close + 1;
|
|
179
|
+
} else {
|
|
180
|
+
const from = i;
|
|
181
|
+
i = destEnd[i] ?? i;
|
|
182
|
+
href = src.slice(from, i);
|
|
183
|
+
}
|
|
184
|
+
if (href.length > MARKDOWN_MAX_URL) return null;
|
|
185
|
+
while (i < limit && (src[i] === ' ' || src[i] === '\n')) i += 1;
|
|
186
|
+
let title: string | undefined;
|
|
187
|
+
const quote = src[i];
|
|
188
|
+
if (quote === '"' || quote === "'") {
|
|
189
|
+
const close = within(src, quote, i + 1, limit);
|
|
190
|
+
if (close === -1) return null;
|
|
191
|
+
title = src.slice(i + 1, close);
|
|
192
|
+
i = close + 1;
|
|
193
|
+
while (i < limit && src[i] === ' ') i += 1;
|
|
194
|
+
}
|
|
195
|
+
if (src[i] !== ')') return null;
|
|
196
|
+
return title === undefined ? { href, end: i + 1 } : { href, title, end: i + 1 };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const ANGLE = /<((?:https?:\/\/|mailto:)[^\s<>]{1,2048})>/y;
|
|
200
|
+
const BARE = /(?:https?:\/\/|www\.)[^\s<]{1,2048}/y;
|
|
201
|
+
|
|
202
|
+
/** A bare URL's end: trailing punctuation is the sentence's, and an unbalanced `)` too. */
|
|
203
|
+
function trimBare(url: string): string {
|
|
204
|
+
let end = url.length;
|
|
205
|
+
while (end > 0 && /[.,:;!?"'*_~]/.test(url[end - 1] ?? '')) end -= 1;
|
|
206
|
+
let out = url.slice(0, end);
|
|
207
|
+
while (out.endsWith(')') && (out.match(/\)/g)?.length ?? 0) > (out.match(/\(/g)?.length ?? 0)) {
|
|
208
|
+
out = out.slice(0, -1);
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export const plainText = (nodes: readonly MarkdownInline[]): string =>
|
|
214
|
+
nodes
|
|
215
|
+
.map((node) =>
|
|
216
|
+
node.kind === 'text' || node.kind === 'code'
|
|
217
|
+
? node.text
|
|
218
|
+
: node.kind === 'image'
|
|
219
|
+
? node.alt
|
|
220
|
+
: node.kind === 'break'
|
|
221
|
+
? ' '
|
|
222
|
+
: plainText(node.children),
|
|
223
|
+
)
|
|
224
|
+
.join('');
|
|
225
|
+
|
|
226
|
+
/** The inline tree of one block's text. */
|
|
227
|
+
export function parseInline(src: string): readonly MarkdownInline[] {
|
|
228
|
+
const found = scan(src);
|
|
229
|
+
const walk = (from: number, to: number, depth: number): MarkdownInline[] => {
|
|
230
|
+
const out: MarkdownInline[] = [];
|
|
231
|
+
// Pieces, joined once per flush: a string grown and re-read per line (`endsWith`, `slice`) is
|
|
232
|
+
// a rope flattened per line, and a long paragraph went quadratic on it.
|
|
233
|
+
let pieces: string[] = [];
|
|
234
|
+
const add = (piece: string): void => {
|
|
235
|
+
if (piece !== '') pieces.push(piece);
|
|
236
|
+
};
|
|
237
|
+
const last = (): string => {
|
|
238
|
+
const piece = pieces[pieces.length - 1] ?? '';
|
|
239
|
+
return piece[piece.length - 1] ?? '';
|
|
240
|
+
};
|
|
241
|
+
const dropLast = (): void => {
|
|
242
|
+
const piece = pieces.pop() ?? '';
|
|
243
|
+
if (piece.length > 1) pieces.push(piece.slice(0, -1));
|
|
244
|
+
};
|
|
245
|
+
const flush = (): void => {
|
|
246
|
+
if (pieces.length > 0) out.push({ kind: 'text', text: pieces.join('') });
|
|
247
|
+
pieces = [];
|
|
248
|
+
};
|
|
249
|
+
const nest = depth < MARKDOWN_MAX_INLINE_DEPTH;
|
|
250
|
+
let i = from;
|
|
251
|
+
while (i < to) {
|
|
252
|
+
const char = src[i] ?? '';
|
|
253
|
+
if (found.escapes[i] === 1) {
|
|
254
|
+
add(src[i + 1] ?? '');
|
|
255
|
+
i += 2;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const span = found.code.get(i);
|
|
259
|
+
if (span !== undefined && span[2] <= to) {
|
|
260
|
+
flush();
|
|
261
|
+
let body = src.slice(span[0], span[1]).replace(/\n/g, ' ');
|
|
262
|
+
if (body.length > 1 && body.startsWith(' ') && body.endsWith(' ') && body.trim() !== '')
|
|
263
|
+
body = body.slice(1, -1);
|
|
264
|
+
out.push({ kind: 'code', text: body });
|
|
265
|
+
i = span[2];
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const image = char === '!' && src[i + 1] === '[';
|
|
269
|
+
const open = image ? i + 1 : i;
|
|
270
|
+
const close = src[open] === '[' ? found.bracket.get(open) : undefined;
|
|
271
|
+
if (close !== undefined && close < to && found.escapes[open] === 0) {
|
|
272
|
+
const tail = linkTail(src, close + 1, found.destEnd);
|
|
273
|
+
if (tail !== null && tail.end <= to) {
|
|
274
|
+
flush();
|
|
275
|
+
if (!safeMarkdownUrl(tail.href)) out.push({ kind: 'text', text: src.slice(i, tail.end) });
|
|
276
|
+
else if (image)
|
|
277
|
+
out.push({
|
|
278
|
+
kind: 'image',
|
|
279
|
+
src: tail.href,
|
|
280
|
+
alt: plainText(walk(open + 1, close, depth + 1)),
|
|
281
|
+
});
|
|
282
|
+
else
|
|
283
|
+
out.push({
|
|
284
|
+
kind: 'link',
|
|
285
|
+
href: tail.href,
|
|
286
|
+
...(tail.title === undefined ? {} : { title: tail.title }),
|
|
287
|
+
children: nest
|
|
288
|
+
? walk(open + 1, close, depth + 1)
|
|
289
|
+
: [{ kind: 'text', text: src.slice(open + 1, close) }],
|
|
290
|
+
});
|
|
291
|
+
i = tail.end;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (char === '<') {
|
|
296
|
+
ANGLE.lastIndex = i;
|
|
297
|
+
const angle = ANGLE.exec(src);
|
|
298
|
+
if (angle !== null && i + angle[0].length <= to) {
|
|
299
|
+
flush();
|
|
300
|
+
out.push({
|
|
301
|
+
kind: 'link',
|
|
302
|
+
href: angle[1] ?? '',
|
|
303
|
+
children: [{ kind: 'text', text: angle[1] ?? '' }],
|
|
304
|
+
});
|
|
305
|
+
i += angle[0].length;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if ((char === 'h' || char === 'w') && !ALNUM.test(src[i - 1] ?? ' ')) {
|
|
310
|
+
BARE.lastIndex = i;
|
|
311
|
+
const bare = BARE.exec(src);
|
|
312
|
+
if (bare !== null) {
|
|
313
|
+
const url = trimBare(bare[0].slice(0, to - i));
|
|
314
|
+
if (url.length > 4 && url !== 'www.') {
|
|
315
|
+
flush();
|
|
316
|
+
out.push({
|
|
317
|
+
kind: 'link',
|
|
318
|
+
href: url.startsWith('www.') ? `https://${url}` : url,
|
|
319
|
+
children: [{ kind: 'text', text: url }],
|
|
320
|
+
});
|
|
321
|
+
i += url.length;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const run = found.runs.get(i);
|
|
327
|
+
if (run !== undefined) {
|
|
328
|
+
const closer = found.runAt[run.close];
|
|
329
|
+
if (run.open && nest && closer !== undefined && closer.start + closer.len <= to) {
|
|
330
|
+
flush();
|
|
331
|
+
const inner = walk(i + run.len, closer.start, depth + 1);
|
|
332
|
+
const kind = run.char === '~' ? 'del' : run.len === 1 ? 'em' : 'strong';
|
|
333
|
+
out.push(
|
|
334
|
+
run.len === 3
|
|
335
|
+
? { kind: 'strong', children: [{ kind: 'em', children: inner }] }
|
|
336
|
+
: { kind, children: inner },
|
|
337
|
+
);
|
|
338
|
+
i = closer.start + closer.len;
|
|
339
|
+
} else {
|
|
340
|
+
add(src.slice(i, i + run.len));
|
|
341
|
+
i += run.len;
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (char === '\n') {
|
|
346
|
+
let spaces = 0;
|
|
347
|
+
while (last() === ' ') {
|
|
348
|
+
dropLast();
|
|
349
|
+
spaces += 1;
|
|
350
|
+
}
|
|
351
|
+
const slash = spaces === 0 && last() === '\\';
|
|
352
|
+
if (slash) dropLast();
|
|
353
|
+
const hard = spaces >= 2 || slash;
|
|
354
|
+
if (hard) {
|
|
355
|
+
flush();
|
|
356
|
+
out.push({ kind: 'break' });
|
|
357
|
+
} else add('\n');
|
|
358
|
+
i += 1;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
add(char);
|
|
362
|
+
i += 1;
|
|
363
|
+
}
|
|
364
|
+
flush();
|
|
365
|
+
return out;
|
|
366
|
+
};
|
|
367
|
+
return walk(0, src.length, 0);
|
|
368
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// Markdown blocks, line by line: headings (ATX and setext), fences, indented code, quotes, nested
|
|
2
|
+
// and task lists, pipe tables, rules, paragraphs. The working set an agent writes, not full
|
|
3
|
+
// CommonMark. Every construct is one pass over its lines; caps turn the rest into plain text.
|
|
4
|
+
|
|
5
|
+
import { parseInline } from './markdown-inline';
|
|
6
|
+
import {
|
|
7
|
+
MARKDOWN_MAX_BLOCK_DEPTH,
|
|
8
|
+
MARKDOWN_MAX_CHARS,
|
|
9
|
+
type MarkdownBlock,
|
|
10
|
+
type MarkdownListItem,
|
|
11
|
+
type TableAlign,
|
|
12
|
+
} from './markdown-tree';
|
|
13
|
+
|
|
14
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
15
|
+
const ATX = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/;
|
|
16
|
+
const RULE = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/;
|
|
17
|
+
const QUOTE = /^ {0,3}> ?/;
|
|
18
|
+
const ITEM = /^( {0,3})([-+*]|\d{1,9}[.)])(?:([ \t]+)(.*))?$/;
|
|
19
|
+
const SETEXT = /^ {0,3}(=+|-+)[ \t]*$/;
|
|
20
|
+
const DELIMITER_ROW = /^\|?[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/;
|
|
21
|
+
const TASK = /^\[([ xX])\][ \t]+/;
|
|
22
|
+
const INDENTED = /^(?: {4}|\t)/;
|
|
23
|
+
|
|
24
|
+
const isBlank = (line: string): boolean => line.trim() === '';
|
|
25
|
+
|
|
26
|
+
/** A line that ends a paragraph by starting something else. */
|
|
27
|
+
function interrupts(line: string): boolean {
|
|
28
|
+
if (FENCE.test(line) || ATX.test(line) || RULE.test(line) || QUOTE.test(line)) return true;
|
|
29
|
+
const item = ITEM.exec(line);
|
|
30
|
+
// Only `1.` or a bullet with content interrupts a paragraph, as CommonMark has it.
|
|
31
|
+
return (
|
|
32
|
+
item !== null &&
|
|
33
|
+
item[4] !== undefined &&
|
|
34
|
+
item[4].trim() !== '' &&
|
|
35
|
+
/^(?:[-+*]|1[.)])$/.test(item[2] ?? '')
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Cells of a pipe-table row, split on unescaped pipes, outer pipes dropped. */
|
|
40
|
+
function cells(line: string): string[] {
|
|
41
|
+
let row = line.trim();
|
|
42
|
+
if (row.startsWith('|')) row = row.slice(1);
|
|
43
|
+
if (row.endsWith('|') && !row.endsWith('\\|')) row = row.slice(0, -1);
|
|
44
|
+
const out: string[] = [];
|
|
45
|
+
let cell = '';
|
|
46
|
+
for (let i = 0; i < row.length; i += 1) {
|
|
47
|
+
const char = row[i];
|
|
48
|
+
if (char === '\\' && row[i + 1] === '|') {
|
|
49
|
+
cell += '|';
|
|
50
|
+
i += 1;
|
|
51
|
+
} else if (char === '|') {
|
|
52
|
+
out.push(cell.trim());
|
|
53
|
+
cell = '';
|
|
54
|
+
} else cell += char;
|
|
55
|
+
}
|
|
56
|
+
out.push(cell.trim());
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const alignOf = (cell: string): TableAlign => {
|
|
61
|
+
const start = cell.startsWith(':');
|
|
62
|
+
const end = cell.endsWith(':');
|
|
63
|
+
return start && end ? 'center' : end ? 'end' : start ? 'start' : null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const text = (lines: readonly string[]): MarkdownBlock => ({
|
|
67
|
+
kind: 'text',
|
|
68
|
+
text: lines.join('\n'),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function parseLines(lines: readonly string[], depth: number): MarkdownBlock[] {
|
|
72
|
+
const out: MarkdownBlock[] = [];
|
|
73
|
+
let i = 0;
|
|
74
|
+
while (i < lines.length) {
|
|
75
|
+
const line = lines[i] ?? '';
|
|
76
|
+
if (isBlank(line)) {
|
|
77
|
+
i += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const fence = FENCE.exec(line);
|
|
81
|
+
if (fence !== null && !(fence[1]?.startsWith('`') && fence[2]?.includes('`'))) {
|
|
82
|
+
const mark = fence[1] ?? '```';
|
|
83
|
+
const body: string[] = [];
|
|
84
|
+
let j = i + 1;
|
|
85
|
+
let open = true;
|
|
86
|
+
for (; j < lines.length; j += 1) {
|
|
87
|
+
const closing = lines[j]?.trim() ?? '';
|
|
88
|
+
if (
|
|
89
|
+
closing.startsWith(mark[0] ?? '`') &&
|
|
90
|
+
closing.length >= mark.length &&
|
|
91
|
+
/^(`+|~+)$/.test(closing)
|
|
92
|
+
) {
|
|
93
|
+
open = false;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
body.push(lines[j] ?? '');
|
|
97
|
+
}
|
|
98
|
+
out.push({ kind: 'code', info: (fence[2] ?? '').trim(), text: body.join('\n'), open });
|
|
99
|
+
i = open ? j : j + 1;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const atx = ATX.exec(line);
|
|
103
|
+
if (atx !== null) {
|
|
104
|
+
const level = (atx[1]?.length ?? 1) as 1 | 2 | 3 | 4 | 5 | 6;
|
|
105
|
+
out.push({ kind: 'heading', level, children: parseInline(atx[2] ?? '') });
|
|
106
|
+
i += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (RULE.test(line)) {
|
|
110
|
+
out.push({ kind: 'rule' });
|
|
111
|
+
i += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (INDENTED.test(line)) {
|
|
115
|
+
const body: string[] = [];
|
|
116
|
+
while (i < lines.length && (INDENTED.test(lines[i] ?? '') || isBlank(lines[i] ?? ''))) {
|
|
117
|
+
body.push((lines[i] ?? '').replace(INDENTED, ''));
|
|
118
|
+
i += 1;
|
|
119
|
+
}
|
|
120
|
+
while (body.length > 0 && isBlank(body[body.length - 1] ?? '')) body.pop();
|
|
121
|
+
out.push({ kind: 'code', info: '', text: body.join('\n'), open: false });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (QUOTE.test(line)) {
|
|
125
|
+
const inner: string[] = [];
|
|
126
|
+
while (i < lines.length && QUOTE.test(lines[i] ?? '')) {
|
|
127
|
+
inner.push((lines[i] ?? '').replace(QUOTE, ''));
|
|
128
|
+
i += 1;
|
|
129
|
+
}
|
|
130
|
+
out.push(
|
|
131
|
+
depth + 1 >= MARKDOWN_MAX_BLOCK_DEPTH
|
|
132
|
+
? text(inner)
|
|
133
|
+
: { kind: 'quote', children: parseLines(inner, depth + 1) },
|
|
134
|
+
);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const item = ITEM.exec(line);
|
|
138
|
+
if (item !== null) {
|
|
139
|
+
i = parseList(lines, i, depth, out);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// A table: a header row, then a delimiter row with as many cells.
|
|
143
|
+
const next = lines[i + 1] ?? '';
|
|
144
|
+
if (line.includes('|') && DELIMITER_ROW.test(next) && next.includes('-')) {
|
|
145
|
+
const head = cells(line);
|
|
146
|
+
const delimiters = cells(next);
|
|
147
|
+
if (head.length === delimiters.length) {
|
|
148
|
+
const rows: string[][] = [];
|
|
149
|
+
let j = i + 2;
|
|
150
|
+
while (j < lines.length && !isBlank(lines[j] ?? '') && (lines[j] ?? '').includes('|')) {
|
|
151
|
+
const row = cells(lines[j] ?? '');
|
|
152
|
+
rows.push(head.map((_, c) => row[c] ?? ''));
|
|
153
|
+
j += 1;
|
|
154
|
+
}
|
|
155
|
+
out.push({
|
|
156
|
+
kind: 'table',
|
|
157
|
+
align: delimiters.map(alignOf),
|
|
158
|
+
head: head.map((cell) => parseInline(cell)),
|
|
159
|
+
rows: rows.map((row) => row.map((cell) => parseInline(cell))),
|
|
160
|
+
});
|
|
161
|
+
i = j;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// A paragraph, unless a setext underline turns it into a heading. Every line loses its leading
|
|
166
|
+
// whitespace, and only the LAST its trailing: two spaces before a newline are a hard break.
|
|
167
|
+
const para: string[] = [line.replace(/^[ \t]+/, '')];
|
|
168
|
+
let j = i + 1;
|
|
169
|
+
let level: 1 | 2 | null = null;
|
|
170
|
+
for (; j < lines.length; j += 1) {
|
|
171
|
+
const candidate = lines[j] ?? '';
|
|
172
|
+
if (isBlank(candidate)) break;
|
|
173
|
+
const underline = SETEXT.exec(candidate);
|
|
174
|
+
if (underline !== null) {
|
|
175
|
+
level = underline[1]?.startsWith('=') ? 1 : 2;
|
|
176
|
+
j += 1;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
if (interrupts(candidate)) break;
|
|
180
|
+
para.push(candidate.replace(/^[ \t]+/, ''));
|
|
181
|
+
}
|
|
182
|
+
const children = parseInline(para.join('\n').trimEnd());
|
|
183
|
+
out.push(
|
|
184
|
+
level === null ? { kind: 'paragraph', children } : { kind: 'heading', level, children },
|
|
185
|
+
);
|
|
186
|
+
i = j;
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** One list from `start`, its items and their nested blocks; answers the line after it. */
|
|
192
|
+
function parseList(
|
|
193
|
+
lines: readonly string[],
|
|
194
|
+
start: number,
|
|
195
|
+
depth: number,
|
|
196
|
+
out: MarkdownBlock[],
|
|
197
|
+
): number {
|
|
198
|
+
const first = ITEM.exec(lines[start] ?? '');
|
|
199
|
+
const marker = first?.[2] ?? '-';
|
|
200
|
+
const ordered = /\d/.test(marker);
|
|
201
|
+
const kind = ordered ? marker.slice(-1) : marker;
|
|
202
|
+
const items: MarkdownListItem[] = [];
|
|
203
|
+
const nested = depth + 1 < MARKDOWN_MAX_BLOCK_DEPTH;
|
|
204
|
+
const flat: string[] = [];
|
|
205
|
+
let i = start;
|
|
206
|
+
while (i < lines.length) {
|
|
207
|
+
const head = ITEM.exec(lines[i] ?? '');
|
|
208
|
+
if (head === null) break;
|
|
209
|
+
const sameKind = ordered
|
|
210
|
+
? /\d/.test(head[2] ?? '') && head[2]?.endsWith(kind)
|
|
211
|
+
: head[2] === kind;
|
|
212
|
+
if (!sameKind) break;
|
|
213
|
+
const pad = (head[3] ?? ' ').length;
|
|
214
|
+
const indent = (head[1] ?? '').length + (head[2] ?? '').length + (pad > 4 ? 1 : pad);
|
|
215
|
+
const body: string[] = [head[4] ?? ''];
|
|
216
|
+
let j = i + 1;
|
|
217
|
+
for (; j < lines.length; j += 1) {
|
|
218
|
+
const candidate = lines[j] ?? '';
|
|
219
|
+
const leading = candidate.length - candidate.trimStart().length;
|
|
220
|
+
if (isBlank(candidate)) {
|
|
221
|
+
// A blank line stays in the item only when the next line is still indented into it.
|
|
222
|
+
const after = lines[j + 1] ?? '';
|
|
223
|
+
if (isBlank(after) || after.length - after.trimStart().length < indent) break;
|
|
224
|
+
body.push('');
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (leading >= indent) body.push(candidate.slice(indent));
|
|
228
|
+
else if (
|
|
229
|
+
!interrupts(candidate) &&
|
|
230
|
+
!ITEM.test(candidate) &&
|
|
231
|
+
!isBlank(body[body.length - 1] ?? '')
|
|
232
|
+
) {
|
|
233
|
+
body.push(candidate.trimStart());
|
|
234
|
+
} else break;
|
|
235
|
+
}
|
|
236
|
+
flat.push(...body);
|
|
237
|
+
const task = TASK.exec(body[0] ?? '');
|
|
238
|
+
if (task !== null) body[0] = (body[0] ?? '').slice(task[0].length);
|
|
239
|
+
items.push({
|
|
240
|
+
checked: task === null ? null : task[1] !== ' ',
|
|
241
|
+
children: nested ? parseLines(body, depth + 1) : [text(body)],
|
|
242
|
+
});
|
|
243
|
+
i = j;
|
|
244
|
+
if (isBlank(lines[i] ?? 'x')) {
|
|
245
|
+
const after = ITEM.exec(lines[i + 1] ?? '');
|
|
246
|
+
if (after === null) break;
|
|
247
|
+
i += 1;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
out.push({ kind: 'list', ordered, start: ordered ? Number.parseInt(marker, 10) : 1, items });
|
|
251
|
+
return i;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The block tree of a body. Never throws on content: over `MARKDOWN_MAX_CHARS` the parse stops at
|
|
256
|
+
* the last line break before the cap and the remainder is one plain-text block.
|
|
257
|
+
*/
|
|
258
|
+
export function parseMarkdown(source: string): readonly MarkdownBlock[] {
|
|
259
|
+
const normal = source.replace(/\r\n?/g, '\n');
|
|
260
|
+
if (normal.length <= MARKDOWN_MAX_CHARS) return parseLines(normal.split('\n'), 0);
|
|
261
|
+
const cut = normal.lastIndexOf('\n', MARKDOWN_MAX_CHARS);
|
|
262
|
+
const at = cut <= 0 ? MARKDOWN_MAX_CHARS : cut;
|
|
263
|
+
return [
|
|
264
|
+
...parseLines(normal.slice(0, at).split('\n'), 0),
|
|
265
|
+
{ kind: 'text', text: normal.slice(at).replace(/^\n/, '') },
|
|
266
|
+
];
|
|
267
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// The tree a markdown body parses to, and the caps every construct is held to. Text is always a
|
|
2
|
+
// `text` leaf rendered as a JSX text node, so nothing in a body can become markup.
|
|
3
|
+
|
|
4
|
+
/** A body past this length renders its remainder as one plain-text block, never a thrown page. */
|
|
5
|
+
export const MARKDOWN_MAX_CHARS = 262_144;
|
|
6
|
+
/** Quotes and lists nest at most this deep; deeper lines are paragraph text. */
|
|
7
|
+
export const MARKDOWN_MAX_BLOCK_DEPTH = 8;
|
|
8
|
+
/** Emphasis, links and strikethrough nest at most this deep; deeper delimiters are text. */
|
|
9
|
+
export const MARKDOWN_MAX_INLINE_DEPTH = 12;
|
|
10
|
+
/** A link destination longer than this is not a link: it is read in one bounded scan. */
|
|
11
|
+
export const MARKDOWN_MAX_URL = 2_048;
|
|
12
|
+
|
|
13
|
+
export type MarkdownInline =
|
|
14
|
+
| { readonly kind: 'text'; readonly text: string }
|
|
15
|
+
| { readonly kind: 'code'; readonly text: string }
|
|
16
|
+
| { readonly kind: 'em' | 'strong' | 'del'; readonly children: readonly MarkdownInline[] }
|
|
17
|
+
| {
|
|
18
|
+
readonly kind: 'link';
|
|
19
|
+
readonly href: string;
|
|
20
|
+
readonly title?: string | undefined;
|
|
21
|
+
readonly children: readonly MarkdownInline[];
|
|
22
|
+
}
|
|
23
|
+
| { readonly kind: 'image'; readonly src: string; readonly alt: string }
|
|
24
|
+
| { readonly kind: 'break' };
|
|
25
|
+
|
|
26
|
+
export type TableAlign = 'start' | 'center' | 'end' | null;
|
|
27
|
+
|
|
28
|
+
export interface MarkdownListItem {
|
|
29
|
+
/** `null` for a plain item; a boolean for a task item. */
|
|
30
|
+
readonly checked: boolean | null;
|
|
31
|
+
readonly children: readonly MarkdownBlock[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type MarkdownBlock =
|
|
35
|
+
| {
|
|
36
|
+
readonly kind: 'heading';
|
|
37
|
+
readonly level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
38
|
+
readonly children: readonly MarkdownInline[];
|
|
39
|
+
}
|
|
40
|
+
| { readonly kind: 'paragraph'; readonly children: readonly MarkdownInline[] }
|
|
41
|
+
/** `open`: the fence has not closed yet — a body still streaming in. */
|
|
42
|
+
| { readonly kind: 'code'; readonly info: string; readonly text: string; readonly open: boolean }
|
|
43
|
+
| { readonly kind: 'quote'; readonly children: readonly MarkdownBlock[] }
|
|
44
|
+
| {
|
|
45
|
+
readonly kind: 'list';
|
|
46
|
+
readonly ordered: boolean;
|
|
47
|
+
readonly start: number;
|
|
48
|
+
readonly items: readonly MarkdownListItem[];
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
readonly kind: 'table';
|
|
52
|
+
readonly align: readonly TableAlign[];
|
|
53
|
+
readonly head: readonly (readonly MarkdownInline[])[];
|
|
54
|
+
readonly rows: readonly (readonly (readonly MarkdownInline[])[])[];
|
|
55
|
+
}
|
|
56
|
+
| { readonly kind: 'rule' }
|
|
57
|
+
/** What a cap left unparsed, verbatim. */
|
|
58
|
+
| { readonly kind: 'text'; readonly text: string };
|