@urbicon-ui/blocks 6.35.0 → 6.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Chat/Chat/Chat.svelte +53 -0
- package/dist/components/Chat/Chat/Chat.svelte.d.ts +4 -0
- package/dist/components/Chat/Chat/chat.variants.d.ts +20 -0
- package/dist/components/Chat/Chat/chat.variants.js +15 -0
- package/dist/components/Chat/Chat/index.d.ts +47 -0
- package/dist/components/Chat/Chat/index.js +2 -0
- package/dist/components/Chat/ChatMessage/ChatMessage.svelte +342 -0
- package/dist/components/Chat/ChatMessage/ChatMessage.svelte.d.ts +4 -0
- package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +138 -0
- package/dist/components/Chat/ChatMessage/chat-message.variants.js +118 -0
- package/dist/components/Chat/ChatMessage/index.d.ts +102 -0
- package/dist/components/Chat/ChatMessage/index.js +2 -0
- package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte +301 -0
- package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte.d.ts +6 -0
- package/dist/components/Chat/ChatMessageList/chat-message-list.variants.d.ts +23 -0
- package/dist/components/Chat/ChatMessageList/chat-message-list.variants.js +32 -0
- package/dist/components/Chat/ChatMessageList/chat-scroll.d.ts +45 -0
- package/dist/components/Chat/ChatMessageList/chat-scroll.js +52 -0
- package/dist/components/Chat/ChatMessageList/index.d.ts +93 -0
- package/dist/components/Chat/ChatMessageList/index.js +3 -0
- package/dist/components/Chat/CitationChip/CitationChip.svelte +107 -0
- package/dist/components/Chat/CitationChip/CitationChip.svelte.d.ts +4 -0
- package/dist/components/Chat/CitationChip/citation-chip.variants.d.ts +40 -0
- package/dist/components/Chat/CitationChip/citation-chip.variants.js +42 -0
- package/dist/components/Chat/CitationChip/index.d.ts +69 -0
- package/dist/components/Chat/CitationChip/index.js +2 -0
- package/dist/components/Chat/CodeBlock/CodeBlock.svelte +120 -0
- package/dist/components/Chat/CodeBlock/CodeBlock.svelte.d.ts +4 -0
- package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +40 -0
- package/dist/components/Chat/CodeBlock/code-block.variants.js +51 -0
- package/dist/components/Chat/CodeBlock/index.d.ts +55 -0
- package/dist/components/Chat/CodeBlock/index.js +2 -0
- package/dist/components/Chat/PromptInput/PromptInput.svelte +432 -0
- package/dist/components/Chat/PromptInput/PromptInput.svelte.d.ts +4 -0
- package/dist/components/Chat/PromptInput/index.d.ts +165 -0
- package/dist/components/Chat/PromptInput/index.js +2 -0
- package/dist/components/Chat/PromptInput/prompt-input.variants.d.ts +90 -0
- package/dist/components/Chat/PromptInput/prompt-input.variants.js +102 -0
- package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte +107 -0
- package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte.d.ts +4 -0
- package/dist/components/Chat/ReasoningDisclosure/index.d.ts +85 -0
- package/dist/components/Chat/ReasoningDisclosure/index.js +2 -0
- package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.d.ts +20 -0
- package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.js +31 -0
- package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte +99 -0
- package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte.d.ts +9 -0
- package/dist/components/Chat/StreamingMarkdown/MdInline.svelte +82 -0
- package/dist/components/Chat/StreamingMarkdown/MdInline.svelte.d.ts +9 -0
- package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte +118 -0
- package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte.d.ts +4 -0
- package/dist/components/Chat/StreamingMarkdown/index.d.ts +86 -0
- package/dist/components/Chat/StreamingMarkdown/index.js +2 -0
- package/dist/components/Chat/StreamingMarkdown/md-context.d.ts +58 -0
- package/dist/components/Chat/StreamingMarkdown/md-context.js +1 -0
- package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.d.ts +145 -0
- package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.js +73 -0
- package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +174 -0
- package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte.d.ts +4 -0
- package/dist/components/Chat/ToolCallCard/index.d.ts +84 -0
- package/dist/components/Chat/ToolCallCard/index.js +2 -0
- package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +38 -0
- package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +38 -0
- package/dist/components/Chat/chat.types.d.ts +82 -0
- package/dist/components/Chat/chat.types.js +7 -0
- package/dist/components/Chat/index.d.ts +22 -0
- package/dist/components/Chat/index.js +14 -0
- package/dist/components/Chat/markdown/blocks.d.ts +54 -0
- package/dist/components/Chat/markdown/blocks.js +462 -0
- package/dist/components/Chat/markdown/inline.d.ts +31 -0
- package/dist/components/Chat/markdown/inline.js +446 -0
- package/dist/components/Chat/markdown/repair.d.ts +23 -0
- package/dist/components/Chat/markdown/repair.js +337 -0
- package/dist/components/Chat/markdown/streaming-harness.d.ts +23 -0
- package/dist/components/Chat/markdown/streaming-harness.js +62 -0
- package/dist/components/Chat/markdown/types.d.ts +139 -0
- package/dist/components/Chat/markdown/types.js +13 -0
- package/dist/components/Chat/markdown/url-policy.d.ts +33 -0
- package/dist/components/Chat/markdown/url-policy.js +109 -0
- package/dist/components/FileUpload/FileUpload.svelte +35 -123
- package/dist/components/FileUpload/FileUpload.svelte.d.ts +1 -1
- package/dist/components/FileUpload/index.d.ts +2 -28
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/primitives/Collapsible/Collapsible.svelte +5 -0
- package/dist/primitives/SplitPane/SplitPane.svelte +293 -0
- package/dist/primitives/SplitPane/SplitPane.svelte.d.ts +4 -0
- package/dist/primitives/SplitPane/index.d.ts +88 -0
- package/dist/primitives/SplitPane/index.js +2 -0
- package/dist/primitives/SplitPane/split-pane.utils.d.ts +73 -0
- package/dist/primitives/SplitPane/split-pane.utils.js +94 -0
- package/dist/primitives/SplitPane/split-pane.variants.d.ts +40 -0
- package/dist/primitives/SplitPane/split-pane.variants.js +56 -0
- package/dist/primitives/index.d.ts +2 -0
- package/dist/primitives/index.js +1 -0
- package/dist/utils/file-intake.d.ts +78 -0
- package/dist/utils/file-intake.js +144 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { parseInlines } from './inline.js';
|
|
2
|
+
import { repairMarkdownTail } from './repair.js';
|
|
3
|
+
function splitLines(text) {
|
|
4
|
+
const lines = [];
|
|
5
|
+
let start = 0;
|
|
6
|
+
for (let i = 0; i <= text.length; i += 1) {
|
|
7
|
+
if (i === text.length || text[i] === '\n') {
|
|
8
|
+
lines.push({ text: text.slice(start, i), start });
|
|
9
|
+
start = i + 1;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return lines;
|
|
13
|
+
}
|
|
14
|
+
const FENCE_OPEN = /^ {0,3}(```+)\s*([^\s`]*)\s*$/;
|
|
15
|
+
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)(?:\s+#+\s*)?$/;
|
|
16
|
+
const HR = /^ {0,3}([-*_])(?:\s*\1){2,}\s*$/;
|
|
17
|
+
const QUOTE = /^ {0,3}>\s?/;
|
|
18
|
+
const LIST_MARKER = /^(\s*)([-*+]|\d{1,9}[.)])(\s+|$)/;
|
|
19
|
+
const TABLE_DELIMITER = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;
|
|
20
|
+
const REF_DEF = /^ {0,3}\[([^\]]+)\]:\s*(\S+)(?:\s+["'(](.*)["')])?\s*$/;
|
|
21
|
+
function isBlank(line) {
|
|
22
|
+
return line.trim().length === 0;
|
|
23
|
+
}
|
|
24
|
+
export function scanBlocks(text) {
|
|
25
|
+
const lines = splitLines(text);
|
|
26
|
+
const blocks = [];
|
|
27
|
+
const refs = new Map();
|
|
28
|
+
let unstableRefLabel;
|
|
29
|
+
let i = 0;
|
|
30
|
+
while (i < lines.length) {
|
|
31
|
+
const line = lines[i];
|
|
32
|
+
if (isBlank(line.text)) {
|
|
33
|
+
i += 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const fence = FENCE_OPEN.exec(line.text);
|
|
37
|
+
if (fence) {
|
|
38
|
+
const marker = fence[1];
|
|
39
|
+
const code = [];
|
|
40
|
+
let open = true;
|
|
41
|
+
let j = i + 1;
|
|
42
|
+
for (; j < lines.length; j += 1) {
|
|
43
|
+
const closing = /^ {0,3}(```+)\s*$/.exec(lines[j].text);
|
|
44
|
+
if (closing && closing[1].length >= marker.length) {
|
|
45
|
+
open = false;
|
|
46
|
+
j += 1;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
code.push(lines[j].text);
|
|
50
|
+
}
|
|
51
|
+
blocks.push({ type: 'fence', start: line.start, lang: fence[2] || undefined, code, open });
|
|
52
|
+
i = j;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const heading = HEADING.exec(line.text);
|
|
56
|
+
if (heading) {
|
|
57
|
+
blocks.push({
|
|
58
|
+
type: 'heading',
|
|
59
|
+
start: line.start,
|
|
60
|
+
level: heading[1].length,
|
|
61
|
+
text: heading[2]
|
|
62
|
+
});
|
|
63
|
+
i += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (HR.test(line.text) && !LIST_MARKER.test(line.text)) {
|
|
67
|
+
blocks.push({ type: 'hr', start: line.start });
|
|
68
|
+
i += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (QUOTE.test(line.text)) {
|
|
72
|
+
const quoteLines = [];
|
|
73
|
+
let j = i;
|
|
74
|
+
while (j < lines.length && QUOTE.test(lines[j].text)) {
|
|
75
|
+
quoteLines.push(lines[j].text.replace(QUOTE, ''));
|
|
76
|
+
j += 1;
|
|
77
|
+
}
|
|
78
|
+
blocks.push({ type: 'quote', start: line.start, lines: quoteLines });
|
|
79
|
+
i = j;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const listMatch = LIST_MARKER.exec(line.text);
|
|
83
|
+
if (listMatch && !HR.test(line.text)) {
|
|
84
|
+
const baseIndent = listMatch[1].length;
|
|
85
|
+
const ordered = /\d/.test(listMatch[2]);
|
|
86
|
+
const startNumber = ordered ? Number.parseInt(listMatch[2], 10) : undefined;
|
|
87
|
+
const listLines = [];
|
|
88
|
+
let j = i;
|
|
89
|
+
let pendingBlanks = 0;
|
|
90
|
+
while (j < lines.length) {
|
|
91
|
+
const current = lines[j].text;
|
|
92
|
+
if (isBlank(current)) {
|
|
93
|
+
pendingBlanks += 1;
|
|
94
|
+
j += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const marker = LIST_MARKER.exec(current);
|
|
98
|
+
const indent = /^\s*/.exec(current)?.[0].length ?? 0;
|
|
99
|
+
const isSameKindMarker = marker && marker[1].length <= baseIndent + 1 && /\d/.test(marker[2]) === ordered;
|
|
100
|
+
const isContinuation = indent >= baseIndent + 2;
|
|
101
|
+
const isLazy = pendingBlanks === 0 && listLines.length > 0 && !marker;
|
|
102
|
+
if (isSameKindMarker || isContinuation || (isLazy && indent > 0) || (isLazy && !marker)) {
|
|
103
|
+
for (; pendingBlanks > 0; pendingBlanks -= 1)
|
|
104
|
+
listLines.push('');
|
|
105
|
+
listLines.push(current);
|
|
106
|
+
j += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
blocks.push({ type: 'list', start: line.start, ordered, startNumber, lines: listLines });
|
|
112
|
+
i = j;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const refDef = REF_DEF.exec(line.text);
|
|
116
|
+
if (refDef) {
|
|
117
|
+
const label = refDef[1].trim().toLowerCase();
|
|
118
|
+
// First definition wins (CommonMark) — critical for stream/one-shot parity.
|
|
119
|
+
if (!refs.has(label)) {
|
|
120
|
+
refs.set(label, { href: refDef[2], title: refDef[3] });
|
|
121
|
+
// `splitLines` emits a trailing empty line when the text ends in \n,
|
|
122
|
+
// so index === length-1 means this line is still unterminated.
|
|
123
|
+
if (i === lines.length - 1)
|
|
124
|
+
unstableRefLabel = label;
|
|
125
|
+
}
|
|
126
|
+
i += 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (line.text.includes('|') &&
|
|
130
|
+
i + 1 < lines.length &&
|
|
131
|
+
TABLE_DELIMITER.test(lines[i + 1].text)) {
|
|
132
|
+
const rows = [];
|
|
133
|
+
let j = i + 2;
|
|
134
|
+
while (j < lines.length && !isBlank(lines[j].text) && lines[j].text.includes('|')) {
|
|
135
|
+
rows.push(lines[j].text);
|
|
136
|
+
j += 1;
|
|
137
|
+
}
|
|
138
|
+
blocks.push({
|
|
139
|
+
type: 'table',
|
|
140
|
+
start: line.start,
|
|
141
|
+
header: line.text,
|
|
142
|
+
delimiter: lines[i + 1].text,
|
|
143
|
+
rows
|
|
144
|
+
});
|
|
145
|
+
i = j;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Paragraph: consecutive non-blank lines that start no other block type.
|
|
149
|
+
const paragraphLines = [line.text];
|
|
150
|
+
let j = i + 1;
|
|
151
|
+
while (j < lines.length) {
|
|
152
|
+
const current = lines[j].text;
|
|
153
|
+
if (isBlank(current) ||
|
|
154
|
+
FENCE_OPEN.test(current) ||
|
|
155
|
+
HEADING.test(current) ||
|
|
156
|
+
HR.test(current) ||
|
|
157
|
+
QUOTE.test(current) ||
|
|
158
|
+
LIST_MARKER.exec(current) ||
|
|
159
|
+
(current.includes('|') && j + 1 < lines.length && TABLE_DELIMITER.test(lines[j + 1].text))) {
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
paragraphLines.push(current);
|
|
163
|
+
j += 1;
|
|
164
|
+
}
|
|
165
|
+
blocks.push({ type: 'paragraph', start: line.start, lines: paragraphLines });
|
|
166
|
+
i = j;
|
|
167
|
+
}
|
|
168
|
+
return { blocks, refs, unstableRefLabel };
|
|
169
|
+
}
|
|
170
|
+
function materialize(raw, ctx) {
|
|
171
|
+
switch (raw.type) {
|
|
172
|
+
case 'paragraph':
|
|
173
|
+
return { kind: 'paragraph', children: parseInlines(raw.lines.join('\n'), ctx) };
|
|
174
|
+
case 'heading':
|
|
175
|
+
return { kind: 'heading', level: raw.level, children: parseInlines(raw.text, ctx) };
|
|
176
|
+
case 'fence':
|
|
177
|
+
return {
|
|
178
|
+
kind: 'code-block',
|
|
179
|
+
lang: raw.lang,
|
|
180
|
+
code: raw.code.join('\n'),
|
|
181
|
+
...(raw.open ? { open: true } : {})
|
|
182
|
+
};
|
|
183
|
+
case 'hr':
|
|
184
|
+
return { kind: 'hr' };
|
|
185
|
+
case 'quote': {
|
|
186
|
+
const inner = scanBlocks(raw.lines.join('\n'));
|
|
187
|
+
return { kind: 'blockquote', children: inner.blocks.map((b) => materialize(b, ctx)) };
|
|
188
|
+
}
|
|
189
|
+
case 'table':
|
|
190
|
+
return materializeTable(raw, ctx);
|
|
191
|
+
case 'list':
|
|
192
|
+
return materializeList(raw, ctx);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const TASK_MARKER = /^\[([ xX])\]\s+/;
|
|
196
|
+
function materializeList(raw, ctx) {
|
|
197
|
+
const items = [];
|
|
198
|
+
let currentContent = null;
|
|
199
|
+
let currentChecked;
|
|
200
|
+
let contentIndent = 2;
|
|
201
|
+
const flush = () => {
|
|
202
|
+
if (currentContent === null)
|
|
203
|
+
return;
|
|
204
|
+
const inner = scanBlocks(currentContent.join('\n'));
|
|
205
|
+
items.push({
|
|
206
|
+
children: inner.blocks.map((b) => materialize(b, ctx)),
|
|
207
|
+
...(currentChecked !== undefined ? { checked: currentChecked } : {})
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
for (const line of raw.lines) {
|
|
211
|
+
const marker = LIST_MARKER.exec(line);
|
|
212
|
+
const indent = /^\s*/.exec(line)?.[0].length ?? 0;
|
|
213
|
+
if (marker &&
|
|
214
|
+
indent <= (items.length === 0 && currentContent === null ? indent : contentIndent - 1)) {
|
|
215
|
+
flush();
|
|
216
|
+
contentIndent = marker[1].length + marker[2].length + (marker[3]?.length || 1);
|
|
217
|
+
let rest = line.slice(marker[0].length);
|
|
218
|
+
const task = TASK_MARKER.exec(rest);
|
|
219
|
+
currentChecked = undefined;
|
|
220
|
+
if (task) {
|
|
221
|
+
currentChecked = task[1] !== ' ';
|
|
222
|
+
rest = rest.slice(task[0].length);
|
|
223
|
+
}
|
|
224
|
+
currentContent = [rest];
|
|
225
|
+
}
|
|
226
|
+
else if (currentContent) {
|
|
227
|
+
currentContent.push(line.slice(Math.min(indent, contentIndent)));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
flush();
|
|
231
|
+
return {
|
|
232
|
+
kind: 'list',
|
|
233
|
+
ordered: raw.ordered,
|
|
234
|
+
...(raw.ordered && raw.startNumber !== undefined && raw.startNumber !== 1
|
|
235
|
+
? { start: raw.startNumber }
|
|
236
|
+
: {}),
|
|
237
|
+
items
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function splitTableRow(row) {
|
|
241
|
+
const cells = [];
|
|
242
|
+
let current = '';
|
|
243
|
+
for (let i = 0; i < row.length; i += 1) {
|
|
244
|
+
const ch = row[i];
|
|
245
|
+
if (ch === '\\' && row[i + 1] === '|') {
|
|
246
|
+
current += '|';
|
|
247
|
+
i += 1;
|
|
248
|
+
}
|
|
249
|
+
else if (ch === '|') {
|
|
250
|
+
cells.push(current);
|
|
251
|
+
current = '';
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
current += ch;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
cells.push(current);
|
|
258
|
+
// Leading/trailing pipes produce empty edge cells — drop them.
|
|
259
|
+
if (cells.length > 0 && cells[0].trim() === '')
|
|
260
|
+
cells.shift();
|
|
261
|
+
if (cells.length > 0 && cells[cells.length - 1].trim() === '')
|
|
262
|
+
cells.pop();
|
|
263
|
+
return cells.map((c) => c.trim());
|
|
264
|
+
}
|
|
265
|
+
function materializeTable(raw, ctx) {
|
|
266
|
+
const align = splitTableRow(raw.delimiter).map((cell) => {
|
|
267
|
+
const left = cell.startsWith(':');
|
|
268
|
+
const right = cell.endsWith(':');
|
|
269
|
+
if (left && right)
|
|
270
|
+
return 'center';
|
|
271
|
+
if (right)
|
|
272
|
+
return 'right';
|
|
273
|
+
if (left)
|
|
274
|
+
return 'left';
|
|
275
|
+
return null;
|
|
276
|
+
});
|
|
277
|
+
const header = splitTableRow(raw.header).map((cell) => parseInlines(cell, ctx));
|
|
278
|
+
const rows = raw.rows.map((row) => splitTableRow(row).map((cell) => parseInlines(cell, ctx)));
|
|
279
|
+
return { kind: 'table', align, header, rows };
|
|
280
|
+
}
|
|
281
|
+
// ── Normalization (streaming-safe CRLF handling) ─────────────────────────────
|
|
282
|
+
function normalizeChunk(chunk, prevEndedWithCR) {
|
|
283
|
+
let text = chunk;
|
|
284
|
+
if (prevEndedWithCR && text.startsWith('\n'))
|
|
285
|
+
text = text.slice(1);
|
|
286
|
+
const endsWithCR = text.endsWith('\r');
|
|
287
|
+
text = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
288
|
+
return { text, endsWithCR };
|
|
289
|
+
}
|
|
290
|
+
// ── One-shot parse ───────────────────────────────────────────────────────────
|
|
291
|
+
export function parseMarkdown(text, options = {}) {
|
|
292
|
+
const normalized = normalizeChunk(text, false).text;
|
|
293
|
+
const { blocks: rawBlocks, refs } = scanBlocks(normalized);
|
|
294
|
+
const ctx = { linkRefs: refs, options };
|
|
295
|
+
const blocks = rawBlocks.map((raw, index) => Object.assign(materialize(raw, ctx), { key: index }));
|
|
296
|
+
return { blocks, linkRefs: refs };
|
|
297
|
+
}
|
|
298
|
+
// ── Incremental parser ───────────────────────────────────────────────────────
|
|
299
|
+
/** Blocks kept unsettled at the end of the stream (see module docstring). */
|
|
300
|
+
const TAIL_BUFFER = 2;
|
|
301
|
+
export function createIncrementalParser(options = {}) {
|
|
302
|
+
let raw = '';
|
|
303
|
+
let norm = '';
|
|
304
|
+
let endsWithCR = false;
|
|
305
|
+
let settledUpTo = 0;
|
|
306
|
+
let settled = [];
|
|
307
|
+
let settledRefs = new Map();
|
|
308
|
+
let unresolvedLabels = new Set();
|
|
309
|
+
let nextKey = 0;
|
|
310
|
+
let doc = { blocks: [], linkRefs: settledRefs };
|
|
311
|
+
function mergeRefs(tailRefs) {
|
|
312
|
+
const merged = new Map(settledRefs);
|
|
313
|
+
for (const [label, def] of tailRefs) {
|
|
314
|
+
if (!merged.has(label))
|
|
315
|
+
merged.set(label, def);
|
|
316
|
+
}
|
|
317
|
+
return merged;
|
|
318
|
+
}
|
|
319
|
+
function compute() {
|
|
320
|
+
const tail = norm.slice(settledUpTo);
|
|
321
|
+
const scan = scanBlocks(tail);
|
|
322
|
+
const merged = mergeRefs(scan.refs);
|
|
323
|
+
// Late reference definition that a settled block failed to resolve →
|
|
324
|
+
// re-parse everything, reconciling keys so the UI patches in place. An
|
|
325
|
+
// unstable definition (final unterminated line) does not count as
|
|
326
|
+
// arrived yet — invalidating on it would freeze a half-streamed URL.
|
|
327
|
+
let invalidated = false;
|
|
328
|
+
for (const label of unresolvedLabels) {
|
|
329
|
+
if (scan.refs.has(label) && label !== scan.unstableRefLabel && !settledRefs.has(label)) {
|
|
330
|
+
invalidated = true;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (invalidated) {
|
|
335
|
+
const previous = doc.blocks;
|
|
336
|
+
settledUpTo = 0;
|
|
337
|
+
settled = [];
|
|
338
|
+
settledRefs = new Map();
|
|
339
|
+
unresolvedLabels = new Set();
|
|
340
|
+
nextKey = 0;
|
|
341
|
+
computeFresh(previous);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
computeTail(tail, scan, merged);
|
|
345
|
+
}
|
|
346
|
+
function computeFresh(previousBlocks) {
|
|
347
|
+
const tail = norm;
|
|
348
|
+
const scan = scanBlocks(tail);
|
|
349
|
+
computeTail(tail, scan, mergeRefs(scan.refs));
|
|
350
|
+
// Key reconciliation by position: same-kind blocks keep their old key.
|
|
351
|
+
let maxKey = -1;
|
|
352
|
+
doc.blocks.forEach((block, index) => {
|
|
353
|
+
const old = previousBlocks[index];
|
|
354
|
+
if (old && old.kind === block.kind)
|
|
355
|
+
block.key = old.key;
|
|
356
|
+
maxKey = Math.max(maxKey, block.key);
|
|
357
|
+
});
|
|
358
|
+
nextKey = Math.max(nextKey, maxKey + 1);
|
|
359
|
+
}
|
|
360
|
+
function computeTail(tail, scan, merged) {
|
|
361
|
+
const rawBlocks = scan.blocks;
|
|
362
|
+
// Settle everything before the second-to-last block. Never settle while
|
|
363
|
+
// the raw text ends in a bare CR (its normalization could still change).
|
|
364
|
+
if (rawBlocks.length > TAIL_BUFFER && !endsWithCR) {
|
|
365
|
+
const settleCount = rawBlocks.length - TAIL_BUFFER;
|
|
366
|
+
const boundary = rawBlocks[settleCount].start;
|
|
367
|
+
// Settling must never bake in a half-streamed definition — resolve
|
|
368
|
+
// against stable refs only; the unresolved hook plus the invalidation
|
|
369
|
+
// path above pick the block up once the definition completes.
|
|
370
|
+
const stableRefs = scan.unstableRefLabel === undefined ? merged : new Map(merged);
|
|
371
|
+
if (scan.unstableRefLabel !== undefined && !settledRefs.has(scan.unstableRefLabel)) {
|
|
372
|
+
stableRefs.delete(scan.unstableRefLabel);
|
|
373
|
+
}
|
|
374
|
+
const settleCtx = {
|
|
375
|
+
linkRefs: stableRefs,
|
|
376
|
+
options,
|
|
377
|
+
onUnresolvedRef: (label) => unresolvedLabels.add(label)
|
|
378
|
+
};
|
|
379
|
+
for (let k = 0; k < settleCount; k += 1) {
|
|
380
|
+
// A settling block must keep the key it was *displayed* under, or the
|
|
381
|
+
// keyed {#each} remounts it. In the steady state nextKey coincides
|
|
382
|
+
// with the shown key by construction, but after a computeFresh
|
|
383
|
+
// reconciliation the shown keys lag behind nextKey — so always prefer
|
|
384
|
+
// the previous document's key at this position (same kind).
|
|
385
|
+
const prevShown = doc.blocks[settled.length + k];
|
|
386
|
+
const block = materialize(rawBlocks[k], settleCtx);
|
|
387
|
+
block.key = prevShown && prevShown.kind === block.kind ? prevShown.key : nextKey;
|
|
388
|
+
nextKey = Math.max(nextKey, block.key + 1);
|
|
389
|
+
settled.push(block);
|
|
390
|
+
}
|
|
391
|
+
// Reference definitions inside the settled region become permanent.
|
|
392
|
+
const settledRegion = scanBlocks(tail.slice(0, boundary));
|
|
393
|
+
for (const [label, def] of settledRegion.refs) {
|
|
394
|
+
if (!settledRefs.has(label))
|
|
395
|
+
settledRefs.set(label, def);
|
|
396
|
+
}
|
|
397
|
+
settledUpTo += boundary;
|
|
398
|
+
computeTail(norm.slice(settledUpTo), scanBlocks(norm.slice(settledUpTo)), merged);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
// Display path: repair only the last block's slice, then parse the buffer.
|
|
402
|
+
let displayText = tail;
|
|
403
|
+
if (rawBlocks.length > 0) {
|
|
404
|
+
const lastStart = rawBlocks[rawBlocks.length - 1].start;
|
|
405
|
+
displayText = tail.slice(0, lastStart) + repairMarkdownTail(tail.slice(lastStart));
|
|
406
|
+
}
|
|
407
|
+
const displayScan = scanBlocks(displayText);
|
|
408
|
+
const displayRefs = mergeRefs(displayScan.refs);
|
|
409
|
+
const ctx = { linkRefs: displayRefs, options };
|
|
410
|
+
// Tail keys mirror the settle path: reuse the key each position was last
|
|
411
|
+
// displayed under (kept stable across computeFresh reconciliation); fresh
|
|
412
|
+
// positions draw collision-free keys above nextKey without persisting the
|
|
413
|
+
// counter — the same position deterministically redraws the same key.
|
|
414
|
+
const usedKeys = new Set(settled.map((block) => block.key));
|
|
415
|
+
let alloc = nextKey;
|
|
416
|
+
const takeFresh = () => {
|
|
417
|
+
while (usedKeys.has(alloc))
|
|
418
|
+
alloc += 1;
|
|
419
|
+
const key = alloc;
|
|
420
|
+
alloc += 1;
|
|
421
|
+
return key;
|
|
422
|
+
};
|
|
423
|
+
const tailBlocks = displayScan.blocks.map((rawBlock, index) => {
|
|
424
|
+
const block = materialize(rawBlock, ctx);
|
|
425
|
+
const prevShown = doc.blocks[settled.length + index];
|
|
426
|
+
block.key =
|
|
427
|
+
prevShown && prevShown.kind === block.kind && !usedKeys.has(prevShown.key)
|
|
428
|
+
? prevShown.key
|
|
429
|
+
: takeFresh();
|
|
430
|
+
usedKeys.add(block.key);
|
|
431
|
+
return block;
|
|
432
|
+
});
|
|
433
|
+
doc = { blocks: [...settled, ...tailBlocks], linkRefs: displayRefs };
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
append(chunk) {
|
|
437
|
+
raw += chunk;
|
|
438
|
+
const normalized = normalizeChunk(chunk, endsWithCR);
|
|
439
|
+
norm += normalized.text;
|
|
440
|
+
endsWithCR = normalized.endsWithCR;
|
|
441
|
+
compute();
|
|
442
|
+
return doc;
|
|
443
|
+
},
|
|
444
|
+
get document() {
|
|
445
|
+
return doc;
|
|
446
|
+
},
|
|
447
|
+
get source() {
|
|
448
|
+
return raw;
|
|
449
|
+
},
|
|
450
|
+
reset() {
|
|
451
|
+
raw = '';
|
|
452
|
+
norm = '';
|
|
453
|
+
endsWithCR = false;
|
|
454
|
+
settledUpTo = 0;
|
|
455
|
+
settled = [];
|
|
456
|
+
settledRefs = new Map();
|
|
457
|
+
unresolvedLabels = new Set();
|
|
458
|
+
nextKey = 0;
|
|
459
|
+
doc = { blocks: [], linkRefs: settledRefs };
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { InlineNode, MarkdownParseOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Inline parser for the streaming-markdown subset: code spans, links/images
|
|
4
|
+
* (inline + reference + shortcut), citations, emphasis (`*`/`_`), strong,
|
|
5
|
+
* strikethrough (`~~`), hard breaks, optional autolinks. Raw HTML is not a
|
|
6
|
+
* concept here — angle brackets are plain text (decision A2).
|
|
7
|
+
*
|
|
8
|
+
* Two passes: tokenize into a flat list (code/link/image/citation/break/text
|
|
9
|
+
* and emphasis delimiter runs), then match delimiter runs into a tree. The
|
|
10
|
+
* delimiter rules are CommonMark-lite: `*` needs non-space on the inner side,
|
|
11
|
+
* `_` additionally must not sit inside a word, `~~` only pairs as a double.
|
|
12
|
+
*/
|
|
13
|
+
export interface InlineContext {
|
|
14
|
+
linkRefs: ReadonlyMap<string, {
|
|
15
|
+
href: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
}>;
|
|
18
|
+
options: MarkdownParseOptions;
|
|
19
|
+
/**
|
|
20
|
+
* Fired for `[label][ref]` / `[label]` candidates whose definition is not
|
|
21
|
+
* (yet) known. The incremental parser uses this to know which settled
|
|
22
|
+
* blocks must be invalidated when a late `[ref]: url` definition streams in.
|
|
23
|
+
*/
|
|
24
|
+
onUnresolvedRef?: (label: string) => void;
|
|
25
|
+
}
|
|
26
|
+
export declare function parseInlines(input: string, ctx: InlineContext): InlineNode[];
|
|
27
|
+
/** Parse citation markers of the form 【id】 (some models emit fullwidth brackets). */
|
|
28
|
+
export declare function tryFullwidthCitation(input: string, at: number, ctx: InlineContext): {
|
|
29
|
+
node: InlineNode;
|
|
30
|
+
end: number;
|
|
31
|
+
} | null;
|