panelui-native 0.30.0 → 0.34.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 +4 -2
- package/lib/module/components/button/index.js +106 -11
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/calendar/index.js +107 -24
- package/lib/module/components/calendar/index.js.map +1 -1
- package/lib/module/components/drawer/index.js +477 -0
- package/lib/module/components/drawer/index.js.map +1 -0
- package/lib/module/components/heatmap-chart/index.js +9 -8
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/map/index.js +50 -4
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/panelside/index.js +1024 -0
- package/lib/module/components/panelside/index.js.map +1 -0
- package/lib/module/components/plan/index.js +307 -21
- package/lib/module/components/plan/index.js.map +1 -1
- package/lib/module/components/post/index.js +992 -0
- package/lib/module/components/post/index.js.map +1 -0
- package/lib/module/components/response/index.js +296 -0
- package/lib/module/components/response/index.js.map +1 -0
- package/lib/module/components/response/markdown.js +463 -0
- package/lib/module/components/response/markdown.js.map +1 -0
- package/lib/module/components/select/index.js +111 -18
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/table/index.js +632 -0
- package/lib/module/components/table/index.js.map +1 -0
- package/lib/module/icons/index.js +220 -0
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +6 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js +30 -0
- package/lib/module/native/index.js.map +1 -1
- package/lib/module/utils/date.js +129 -22
- package/lib/module/utils/date.js.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +22 -1
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/calendar/index.d.ts +12 -3
- package/lib/typescript/src/components/calendar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/drawer/index.d.ts +146 -0
- package/lib/typescript/src/components/drawer/index.d.ts.map +1 -0
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +8 -1
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/panelside/index.d.ts +354 -0
- package/lib/typescript/src/components/panelside/index.d.ts.map +1 -0
- package/lib/typescript/src/components/plan/index.d.ts +121 -10
- package/lib/typescript/src/components/plan/index.d.ts.map +1 -1
- package/lib/typescript/src/components/post/index.d.ts +358 -0
- package/lib/typescript/src/components/post/index.d.ts.map +1 -0
- package/lib/typescript/src/components/response/index.d.ts +91 -0
- package/lib/typescript/src/components/response/index.d.ts.map +1 -0
- package/lib/typescript/src/components/response/markdown.d.ts +88 -0
- package/lib/typescript/src/components/response/markdown.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts +27 -2
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/table/index.d.ts +274 -0
- package/lib/typescript/src/components/table/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +41 -1
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +7 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +20 -0
- package/lib/typescript/src/native/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/date.d.ts +49 -5
- package/lib/typescript/src/utils/date.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/button/index.tsx +140 -8
- package/src/components/calendar/index.tsx +136 -23
- package/src/components/drawer/index.tsx +662 -0
- package/src/components/heatmap-chart/index.tsx +9 -9
- package/src/components/map/index.tsx +53 -4
- package/src/components/panelside/index.tsx +1358 -0
- package/src/components/plan/index.tsx +344 -21
- package/src/components/post/index.tsx +1116 -0
- package/src/components/response/index.tsx +390 -0
- package/src/components/response/markdown.ts +489 -0
- package/src/components/select/index.tsx +155 -15
- package/src/components/table/index.tsx +747 -0
- package/src/icons/index.tsx +176 -1
- package/src/index.ts +93 -0
- package/src/native/index.ts +51 -0
- package/src/utils/date.ts +146 -22
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small Markdown reader, written for text that is still arriving.
|
|
3
|
+
*
|
|
4
|
+
* No parser library. What a chat answer actually contains is headings,
|
|
5
|
+
* paragraphs, lists, fenced code, blockquotes, tables, rules and a handful of
|
|
6
|
+
* inline marks — and every general-purpose parser worth using assumes it has
|
|
7
|
+
* been handed the whole document. That assumption is the one thing that is
|
|
8
|
+
* never true here.
|
|
9
|
+
*
|
|
10
|
+
* ## Half-arrived markdown
|
|
11
|
+
*
|
|
12
|
+
* A token stream shows you every prefix of the final text, so the reader sees
|
|
13
|
+
* `**bo`, then `**bol`, then `**bold**`. Treated naively that is three
|
|
14
|
+
* documents: two with literal asterisks in them and one with a bolded word. The
|
|
15
|
+
* text would flash between styles on nearly every frame, which is worse than no
|
|
16
|
+
* formatting at all — the eye tracks the flicker instead of the words.
|
|
17
|
+
*
|
|
18
|
+
* So an unterminated construct at the very end of the input is *completed
|
|
19
|
+
* speculatively* rather than escaped: an open fence becomes a code block that
|
|
20
|
+
* is still filling, an open `**` becomes bold text that is still being written.
|
|
21
|
+
* Anything unterminated anywhere else is literal, because a document that has
|
|
22
|
+
* stopped arriving means what it says.
|
|
23
|
+
*
|
|
24
|
+
* The two rules that follow from this:
|
|
25
|
+
*
|
|
26
|
+
* - **Only the tail is speculative.** An asterisk in the middle of a finished
|
|
27
|
+
* paragraph is an asterisk.
|
|
28
|
+
* - **A speculative construct never removes visible text.** The characters that
|
|
29
|
+
* opened it are hidden, and everything after them stays on screen. Otherwise
|
|
30
|
+
* the last word of the answer disappears and reappears as its delimiter
|
|
31
|
+
* arrives.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export type Align = 'left' | 'center' | 'right';
|
|
35
|
+
|
|
36
|
+
export interface InlineToken {
|
|
37
|
+
kind: 'text' | 'code' | 'link' | 'image';
|
|
38
|
+
value: string;
|
|
39
|
+
/** For links and images. */
|
|
40
|
+
href?: string;
|
|
41
|
+
bold?: boolean;
|
|
42
|
+
italic?: boolean;
|
|
43
|
+
strike?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type Block =
|
|
47
|
+
| { type: 'heading'; level: 1 | 2 | 3 | 4 | 5 | 6; inline: InlineToken[] }
|
|
48
|
+
| { type: 'paragraph'; inline: InlineToken[] }
|
|
49
|
+
| { type: 'code'; code: string; language?: string; open: boolean }
|
|
50
|
+
| { type: 'quote'; blocks: Block[] }
|
|
51
|
+
| { type: 'list'; ordered: boolean; start: number; items: Block[][] }
|
|
52
|
+
| { type: 'table'; head: InlineToken[][]; rows: InlineToken[][][]; align: (Align | null)[] }
|
|
53
|
+
| { type: 'rule' };
|
|
54
|
+
|
|
55
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([^`\s]*)/;
|
|
56
|
+
const HEADING = /^ {0,3}(#{1,6})\s+(.*)$/;
|
|
57
|
+
const RULE = /^ {0,3}((\*\s*){3,}|(-\s*){3,}|(_\s*){3,})$/;
|
|
58
|
+
const QUOTE = /^ {0,3}> ?(.*)$/;
|
|
59
|
+
const BULLET = /^(\s*)([-*+])\s+(.*)$/;
|
|
60
|
+
const ORDERED = /^(\s*)(\d{1,9})[.)]\s+(.*)$/;
|
|
61
|
+
const TABLE_DIVIDER = /^\s*\|?\s*:?-{1,}:?\s*(\|\s*:?-{1,}:?\s*)*\|?\s*$/;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Splits a document into blocks.
|
|
65
|
+
*
|
|
66
|
+
* `streaming` is what turns speculative completion on. With it off — a stored
|
|
67
|
+
* answer, a message that has finished — an unterminated fence is just a
|
|
68
|
+
* paragraph starting with three backticks, which is what it literally is.
|
|
69
|
+
*/
|
|
70
|
+
export function parseMarkdown(source: string, streaming = false): Block[] {
|
|
71
|
+
const lines = source.replace(/\r\n?/g, '\n').split('\n');
|
|
72
|
+
return parseBlocks(lines, streaming);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseBlocks(lines: string[], streaming: boolean): Block[] {
|
|
76
|
+
const blocks: Block[] = [];
|
|
77
|
+
let index = 0;
|
|
78
|
+
|
|
79
|
+
while (index < lines.length) {
|
|
80
|
+
const line = lines[index]!;
|
|
81
|
+
|
|
82
|
+
if (!line.trim()) {
|
|
83
|
+
index += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const fence = FENCE.exec(line);
|
|
88
|
+
if (fence) {
|
|
89
|
+
const marker = fence[1]!;
|
|
90
|
+
const language = fence[2] || undefined;
|
|
91
|
+
const body: string[] = [];
|
|
92
|
+
let closed = false;
|
|
93
|
+
index += 1;
|
|
94
|
+
|
|
95
|
+
while (index < lines.length) {
|
|
96
|
+
const current = lines[index]!;
|
|
97
|
+
// Closed by a run of the same character at least as long as the opener,
|
|
98
|
+
// and nothing else on the line.
|
|
99
|
+
if (current.trimStart().startsWith(marker[0]!.repeat(marker.length)) && !current.trim().slice(marker.length).trim()) {
|
|
100
|
+
closed = true;
|
|
101
|
+
index += 1;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
body.push(current);
|
|
105
|
+
index += 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// An unclosed fence in a finished document is not a code block: it is a
|
|
109
|
+
// paragraph that begins with three backticks, and pretending otherwise
|
|
110
|
+
// swallows the rest of the answer.
|
|
111
|
+
if (!closed && !streaming) {
|
|
112
|
+
blocks.push({ type: 'paragraph', inline: parseInline(line, false) });
|
|
113
|
+
for (const text of body) {
|
|
114
|
+
if (text.trim()) blocks.push({ type: 'paragraph', inline: parseInline(text, false) });
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
blocks.push({ type: 'code', code: body.join('\n'), language, open: !closed });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (RULE.test(line)) {
|
|
124
|
+
blocks.push({ type: 'rule' });
|
|
125
|
+
index += 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const heading = HEADING.exec(line);
|
|
130
|
+
if (heading) {
|
|
131
|
+
const level = heading[1]!.length as 1 | 2 | 3 | 4 | 5 | 6;
|
|
132
|
+
blocks.push({
|
|
133
|
+
type: 'heading',
|
|
134
|
+
level,
|
|
135
|
+
// Trailing hashes are a closing marker, not content.
|
|
136
|
+
inline: parseInline(heading[2]!.replace(/\s+#+\s*$/, ''), streaming && index === lines.length - 1),
|
|
137
|
+
});
|
|
138
|
+
index += 1;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (QUOTE.test(line)) {
|
|
143
|
+
const body: string[] = [];
|
|
144
|
+
while (index < lines.length) {
|
|
145
|
+
const match = QUOTE.exec(lines[index]!);
|
|
146
|
+
if (!match) break;
|
|
147
|
+
body.push(match[1]!);
|
|
148
|
+
index += 1;
|
|
149
|
+
}
|
|
150
|
+
blocks.push({ type: 'quote', blocks: parseBlocks(body, streaming) });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const table = parseTable(lines, index, streaming);
|
|
155
|
+
if (table) {
|
|
156
|
+
blocks.push(table.block);
|
|
157
|
+
index = table.next;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (BULLET.test(line) || ORDERED.test(line)) {
|
|
162
|
+
const list = parseList(lines, index, streaming);
|
|
163
|
+
blocks.push(list.block);
|
|
164
|
+
index = list.next;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// A paragraph runs to the first blank line or the first line that starts
|
|
169
|
+
// something else.
|
|
170
|
+
const body: string[] = [];
|
|
171
|
+
while (index < lines.length) {
|
|
172
|
+
const current = lines[index]!;
|
|
173
|
+
if (!current.trim()) break;
|
|
174
|
+
if (
|
|
175
|
+
index > 0 &&
|
|
176
|
+
(FENCE.test(current) ||
|
|
177
|
+
HEADING.test(current) ||
|
|
178
|
+
RULE.test(current) ||
|
|
179
|
+
QUOTE.test(current) ||
|
|
180
|
+
BULLET.test(current) ||
|
|
181
|
+
ORDERED.test(current)) &&
|
|
182
|
+
body.length
|
|
183
|
+
) {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
body.push(current.trim());
|
|
187
|
+
index += 1;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
blocks.push({
|
|
191
|
+
type: 'paragraph',
|
|
192
|
+
inline: parseInline(body.join(' '), streaming && index >= lines.length),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return blocks;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A GFM table, or nothing.
|
|
201
|
+
*
|
|
202
|
+
* The divider row is what makes a table a table — a run of pipes on its own is
|
|
203
|
+
* just a paragraph with pipes in it — so a header line is only a header once
|
|
204
|
+
* the line under it has arrived. Mid-stream that means a table appears a beat
|
|
205
|
+
* after its first row, which is the right beat: it is not a table yet.
|
|
206
|
+
*/
|
|
207
|
+
function parseTable(
|
|
208
|
+
lines: string[],
|
|
209
|
+
start: number,
|
|
210
|
+
streaming: boolean
|
|
211
|
+
): { block: Block; next: number } | null {
|
|
212
|
+
const header = lines[start];
|
|
213
|
+
const divider = lines[start + 1];
|
|
214
|
+
if (!header || !divider) return null;
|
|
215
|
+
if (!header.includes('|')) return null;
|
|
216
|
+
if (!TABLE_DIVIDER.test(divider)) return null;
|
|
217
|
+
|
|
218
|
+
const head = splitRow(header).map((cell) => parseInline(cell, false));
|
|
219
|
+
const align = splitRow(divider).map((cell): Align | null => {
|
|
220
|
+
const left = cell.startsWith(':');
|
|
221
|
+
const right = cell.endsWith(':');
|
|
222
|
+
if (left && right) return 'center';
|
|
223
|
+
if (right) return 'right';
|
|
224
|
+
if (left) return 'left';
|
|
225
|
+
return null;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const rows: InlineToken[][][] = [];
|
|
229
|
+
let index = start + 2;
|
|
230
|
+
while (index < lines.length) {
|
|
231
|
+
const line = lines[index]!;
|
|
232
|
+
if (!line.trim() || !line.includes('|')) break;
|
|
233
|
+
// The last line of a stream is very likely half-typed. Holding it back for
|
|
234
|
+
// one token is better than a row that grows a cell at a time.
|
|
235
|
+
const partial = streaming && index === lines.length - 1;
|
|
236
|
+
if (partial) {
|
|
237
|
+
index += 1;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
rows.push(splitRow(line).map((cell) => parseInline(cell, false)));
|
|
241
|
+
index += 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return { block: { type: 'table', head, rows, align }, next: index };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function splitRow(row: string): string[] {
|
|
248
|
+
return row
|
|
249
|
+
.trim()
|
|
250
|
+
.replace(/^\|/, '')
|
|
251
|
+
.replace(/\|$/, '')
|
|
252
|
+
.split('|')
|
|
253
|
+
.map((cell) => cell.trim());
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* A list, and everything indented under it.
|
|
258
|
+
*
|
|
259
|
+
* Nesting is by indent, and a nested list becomes a block inside its parent
|
|
260
|
+
* item rather than a sibling — which is what lets an item hold a paragraph, a
|
|
261
|
+
* sub-list and a code block at once.
|
|
262
|
+
*/
|
|
263
|
+
function parseList(
|
|
264
|
+
lines: string[],
|
|
265
|
+
start: number,
|
|
266
|
+
streaming: boolean
|
|
267
|
+
): { block: Block; next: number } {
|
|
268
|
+
const first = BULLET.exec(lines[start]!) ?? ORDERED.exec(lines[start]!)!;
|
|
269
|
+
const ordered = !BULLET.test(lines[start]!);
|
|
270
|
+
const baseIndent = first[1]!.length;
|
|
271
|
+
const startNumber = ordered ? Number(first[2]) : 1;
|
|
272
|
+
|
|
273
|
+
const items: string[][] = [];
|
|
274
|
+
let current: string[] | null = null;
|
|
275
|
+
let index = start;
|
|
276
|
+
|
|
277
|
+
while (index < lines.length) {
|
|
278
|
+
const line = lines[index]!;
|
|
279
|
+
|
|
280
|
+
if (!line.trim()) {
|
|
281
|
+
// A blank line inside a list is only the end of it if what follows is not
|
|
282
|
+
// indented under an item.
|
|
283
|
+
const next = lines[index + 1];
|
|
284
|
+
if (next && next.trim() && leadingSpaces(next) <= baseIndent && !isItem(next)) break;
|
|
285
|
+
if (current) current.push('');
|
|
286
|
+
index += 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const item = BULLET.exec(line) ?? ORDERED.exec(line);
|
|
291
|
+
if (item && item[1]!.length <= baseIndent) {
|
|
292
|
+
if (ordered !== !BULLET.test(line)) break;
|
|
293
|
+
current = [item[3]!];
|
|
294
|
+
items.push(current);
|
|
295
|
+
index += 1;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (leadingSpaces(line) > baseIndent && current) {
|
|
300
|
+
current.push(line.slice(baseIndent + 1));
|
|
301
|
+
index += 1;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
block: {
|
|
310
|
+
type: 'list',
|
|
311
|
+
ordered,
|
|
312
|
+
start: Number.isFinite(startNumber) ? startNumber : 1,
|
|
313
|
+
items: items.map((body, position) =>
|
|
314
|
+
parseBlocks(body, streaming && position === items.length - 1)
|
|
315
|
+
),
|
|
316
|
+
},
|
|
317
|
+
next: index,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isItem(line: string): boolean {
|
|
322
|
+
return BULLET.test(line) || ORDERED.test(line);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function leadingSpaces(line: string): number {
|
|
326
|
+
return line.length - line.trimStart().length;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* -------------------------------------------------------------------------- */
|
|
330
|
+
/* Inline */
|
|
331
|
+
/* -------------------------------------------------------------------------- */
|
|
332
|
+
|
|
333
|
+
interface Marks {
|
|
334
|
+
bold?: boolean;
|
|
335
|
+
italic?: boolean;
|
|
336
|
+
strike?: boolean;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Splits a run of text into styled spans.
|
|
341
|
+
*
|
|
342
|
+
* `tail` says this run is the end of a stream, which is what licenses closing
|
|
343
|
+
* an open delimiter speculatively. It is deliberately narrow: only the *last*
|
|
344
|
+
* unterminated opener on the line is completed, and only when nothing after it
|
|
345
|
+
* could still turn out to be the closer.
|
|
346
|
+
*/
|
|
347
|
+
export function parseInline(source: string, tail: boolean): InlineToken[] {
|
|
348
|
+
const tokens: InlineToken[] = [];
|
|
349
|
+
let buffer = '';
|
|
350
|
+
let index = 0;
|
|
351
|
+
const marks: Marks = {};
|
|
352
|
+
|
|
353
|
+
const flush = () => {
|
|
354
|
+
if (!buffer) return;
|
|
355
|
+
tokens.push({ kind: 'text', value: buffer, ...marks });
|
|
356
|
+
buffer = '';
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
while (index < source.length) {
|
|
360
|
+
const rest = source.slice(index);
|
|
361
|
+
|
|
362
|
+
// Escapes come first, or `\*` would open emphasis.
|
|
363
|
+
if (rest[0] === '\\' && rest.length > 1) {
|
|
364
|
+
buffer += rest[1];
|
|
365
|
+
index += 2;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Inline code wins over emphasis: asterisks inside backticks are literal.
|
|
370
|
+
if (rest[0] === '`') {
|
|
371
|
+
const run = /^(`+)/.exec(rest)![1]!;
|
|
372
|
+
const closer = source.indexOf(run, index + run.length);
|
|
373
|
+
if (closer !== -1) {
|
|
374
|
+
flush();
|
|
375
|
+
tokens.push({ kind: 'code', value: source.slice(index + run.length, closer), ...marks });
|
|
376
|
+
index = closer + run.length;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (tail) {
|
|
380
|
+
// Still being typed. Everything after the backtick is code so far.
|
|
381
|
+
flush();
|
|
382
|
+
const value = source.slice(index + run.length);
|
|
383
|
+
if (value) tokens.push({ kind: 'code', value, ...marks });
|
|
384
|
+
index = source.length;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
buffer += run;
|
|
388
|
+
index += run.length;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const image = /^!\[([^\]]*)\]\(([^)\s]*)[^)]*\)/.exec(rest);
|
|
393
|
+
if (image) {
|
|
394
|
+
flush();
|
|
395
|
+
tokens.push({ kind: 'image', value: image[1]!, href: image[2]! });
|
|
396
|
+
index += image[0].length;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const link = /^\[([^\]]*)\]\(([^)\s]*)[^)]*\)/.exec(rest);
|
|
401
|
+
if (link) {
|
|
402
|
+
flush();
|
|
403
|
+
tokens.push({ kind: 'link', value: link[1]!, href: link[2]!, ...marks });
|
|
404
|
+
index += link[0].length;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// A link whose closing paren has not arrived. Show the label and nothing
|
|
409
|
+
// else — the brackets and the half-typed URL are scaffolding, and dropping
|
|
410
|
+
// them now means the text does not jump when the rest lands.
|
|
411
|
+
if (tail) {
|
|
412
|
+
const partialLink = /^\[([^\]]*)\](\([^)]*)?$/.exec(rest);
|
|
413
|
+
if (partialLink) {
|
|
414
|
+
flush();
|
|
415
|
+
if (partialLink[1]) tokens.push({ kind: 'text', value: partialLink[1], ...marks });
|
|
416
|
+
index = source.length;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const emphasis = /^(\*\*\*|___|\*\*|__|~~|\*|_)/.exec(rest);
|
|
422
|
+
if (emphasis) {
|
|
423
|
+
const marker = emphasis[1]!;
|
|
424
|
+
const applied = markFor(marker);
|
|
425
|
+
const open = Object.keys(applied).every((key) => marks[key as keyof Marks]);
|
|
426
|
+
|
|
427
|
+
// Closing whichever of these is currently open.
|
|
428
|
+
if (open) {
|
|
429
|
+
flush();
|
|
430
|
+
for (const key of Object.keys(applied)) delete marks[key as keyof Marks];
|
|
431
|
+
index += marker.length;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// An opener has to lean on the word it opens. `2 * 3` is multiplication,
|
|
436
|
+
// and `a * b` is not the start of anything — without this the asterisk
|
|
437
|
+
// disappears and the reader is left with two spaces where an operator was.
|
|
438
|
+
const after = source[index + marker.length];
|
|
439
|
+
if (after === undefined || /\s/.test(after)) {
|
|
440
|
+
buffer += marker;
|
|
441
|
+
index += marker.length;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const closer = source.indexOf(marker, index + marker.length);
|
|
446
|
+
if (closer !== -1) {
|
|
447
|
+
flush();
|
|
448
|
+
Object.assign(marks, applied);
|
|
449
|
+
index += marker.length;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (tail && index + marker.length < source.length) {
|
|
454
|
+
// Unterminated at the end of a stream: assume it will close, so the
|
|
455
|
+
// words that have arrived are styled now rather than restyled later.
|
|
456
|
+
flush();
|
|
457
|
+
Object.assign(marks, applied);
|
|
458
|
+
index += marker.length;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Unterminated in finished text, or nothing after it yet. Literal.
|
|
463
|
+
buffer += marker;
|
|
464
|
+
index += marker.length;
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
buffer += rest[0];
|
|
469
|
+
index += 1;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
flush();
|
|
473
|
+
return tokens;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function markFor(marker: string): Marks {
|
|
477
|
+
switch (marker) {
|
|
478
|
+
case '***':
|
|
479
|
+
case '___':
|
|
480
|
+
return { bold: true, italic: true };
|
|
481
|
+
case '**':
|
|
482
|
+
case '__':
|
|
483
|
+
return { bold: true };
|
|
484
|
+
case '~~':
|
|
485
|
+
return { strike: true };
|
|
486
|
+
default:
|
|
487
|
+
return { italic: true };
|
|
488
|
+
}
|
|
489
|
+
}
|