@urbicon-ui/blocks 6.35.0 → 6.37.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/PinInput/PinInput.svelte +321 -0
- package/dist/components/PinInput/PinInput.svelte.d.ts +4 -0
- package/dist/components/PinInput/index.d.ts +98 -0
- package/dist/components/PinInput/index.js +2 -0
- package/dist/components/PinInput/pin-input.variants.d.ts +96 -0
- package/dist/components/PinInput/pin-input.variants.js +111 -0
- package/dist/components/QRCode/QRCode.svelte +107 -0
- package/dist/components/QRCode/QRCode.svelte.d.ts +4 -0
- package/dist/components/QRCode/index.d.ts +74 -0
- package/dist/components/QRCode/index.js +3 -0
- package/dist/components/QRCode/qr-code.variants.d.ts +25 -0
- package/dist/components/QRCode/qr-code.variants.js +24 -0
- package/dist/components/QRCode/qr-encode.d.ts +22 -0
- package/dist/components/QRCode/qr-encode.js +528 -0
- package/dist/components/TimeInput/TimeInput.svelte +521 -0
- package/dist/components/TimeInput/TimeInput.svelte.d.ts +4 -0
- package/dist/components/TimeInput/index.d.ts +93 -0
- package/dist/components/TimeInput/index.js +2 -0
- package/dist/components/TimeInput/time-input.variants.d.ts +131 -0
- package/dist/components/TimeInput/time-input.variants.js +120 -0
- package/dist/components/index.d.ts +7 -0
- package/dist/components/index.js +4 -0
- package/dist/i18n/index.d.ts +18 -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/translations/de.d.ts +6 -0
- package/dist/translations/de.js +6 -0
- package/dist/translations/en.d.ts +6 -0
- package/dist/translations/en.js +6 -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,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming tail repair — the "remend" step for the incremental markdown engine.
|
|
3
|
+
*
|
|
4
|
+
* The incremental parser calls {@link repairMarkdownTail} on the *unsettled tail*
|
|
5
|
+
* of the stream before parsing it, so half-arrived syntax renders as its settled
|
|
6
|
+
* form instead of flickering (a dangling `**bold` shows as bold immediately, not
|
|
7
|
+
* as two literal asterisks that vanish on the next chunk).
|
|
8
|
+
*
|
|
9
|
+
* Hard invariant: on text that already ends in complete syntax the function is an
|
|
10
|
+
* exact no-op (returns the input unchanged). That is what keeps the streamed end
|
|
11
|
+
* state identical to a one-shot parse of the full source. The function is also
|
|
12
|
+
* idempotent — `repairMarkdownTail(repairMarkdownTail(x)) === repairMarkdownTail(x)`.
|
|
13
|
+
*
|
|
14
|
+
* Scope: this operates on the CommonMark/GFM subset the engine renders. Raw HTML
|
|
15
|
+
* is plain text here (no tag balancing), and `$$` math is ordinary text — neither
|
|
16
|
+
* is repaired. Open fenced code blocks are left entirely untouched so the block
|
|
17
|
+
* parser can keep rendering them as an open code block.
|
|
18
|
+
*/
|
|
19
|
+
// ── Character classes ────────────────────────────────────────────────────────
|
|
20
|
+
function isWs(ch) {
|
|
21
|
+
// Undefined = start/end of string, which flanking rules treat as whitespace.
|
|
22
|
+
return (ch === undefined ||
|
|
23
|
+
ch === ' ' ||
|
|
24
|
+
ch === '\t' ||
|
|
25
|
+
ch === '\n' ||
|
|
26
|
+
ch === '\r' ||
|
|
27
|
+
ch === '\f' ||
|
|
28
|
+
ch === '\v');
|
|
29
|
+
}
|
|
30
|
+
/** ASCII punctuation (the CommonMark flanking set; sufficient for this subset). */
|
|
31
|
+
function isPunct(ch) {
|
|
32
|
+
if (ch === undefined)
|
|
33
|
+
return false;
|
|
34
|
+
const c = ch.charCodeAt(0);
|
|
35
|
+
return ((c >= 0x21 && c <= 0x2f) ||
|
|
36
|
+
(c >= 0x3a && c <= 0x40) ||
|
|
37
|
+
(c >= 0x5b && c <= 0x60) ||
|
|
38
|
+
(c >= 0x7b && c <= 0x7e));
|
|
39
|
+
}
|
|
40
|
+
function runLength(s, i, ch) {
|
|
41
|
+
let n = 0;
|
|
42
|
+
while (s[i + n] === ch)
|
|
43
|
+
n++;
|
|
44
|
+
return n;
|
|
45
|
+
}
|
|
46
|
+
// ── Fenced code blocks ───────────────────────────────────────────────────────
|
|
47
|
+
const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
|
|
48
|
+
const FENCE_CLOSE = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the text ends inside an open fenced code block. When it does, the tail
|
|
51
|
+
* is left verbatim: no marker repair inside the fence, and the fence is not
|
|
52
|
+
* closed (the block parser renders it as an open code block until the closing
|
|
53
|
+
* fence streams in).
|
|
54
|
+
*/
|
|
55
|
+
function endsInsideOpenFence(text) {
|
|
56
|
+
const lines = text.split('\n');
|
|
57
|
+
let open = null;
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
if (open === null) {
|
|
60
|
+
const m = FENCE_OPEN.exec(line);
|
|
61
|
+
if (m)
|
|
62
|
+
open = { char: m[1][0], len: m[1].length };
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const m = FENCE_CLOSE.exec(line);
|
|
66
|
+
if (m && m[1][0] === open.char && m[1].length >= open.len)
|
|
67
|
+
open = null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return open !== null;
|
|
71
|
+
}
|
|
72
|
+
// ── Inline code spans ────────────────────────────────────────────────────────
|
|
73
|
+
/** Index of the next backtick run of *exactly* `len` (a code-span closer), or -1. */
|
|
74
|
+
function findClosingBacktick(s, from, len) {
|
|
75
|
+
let i = from;
|
|
76
|
+
while (i < s.length) {
|
|
77
|
+
if (s[i] === '`') {
|
|
78
|
+
const run = runLength(s, i, '`');
|
|
79
|
+
if (run === len)
|
|
80
|
+
return i;
|
|
81
|
+
i += run;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
i++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return -1;
|
|
88
|
+
}
|
|
89
|
+
// ── Link / image detection ───────────────────────────────────────────────────
|
|
90
|
+
/** Match the `(` at `open` against its closing `)`, honouring nested parens. */
|
|
91
|
+
function findMatchingParen(s, open) {
|
|
92
|
+
let depth = 0;
|
|
93
|
+
for (let i = open; i < s.length; i++) {
|
|
94
|
+
const c = s[i];
|
|
95
|
+
if (c === '\\') {
|
|
96
|
+
i++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (c === '(')
|
|
100
|
+
depth++;
|
|
101
|
+
else if (c === ')') {
|
|
102
|
+
depth--;
|
|
103
|
+
if (depth === 0)
|
|
104
|
+
return i;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return -1;
|
|
108
|
+
}
|
|
109
|
+
/** Match the `[` at `open` against its closing `]`, honouring nested brackets. */
|
|
110
|
+
function findClosingBracket(s, open) {
|
|
111
|
+
let depth = 0;
|
|
112
|
+
for (let i = open; i < s.length; i++) {
|
|
113
|
+
const c = s[i];
|
|
114
|
+
if (c === '\\') {
|
|
115
|
+
i++;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (c === '[')
|
|
119
|
+
depth++;
|
|
120
|
+
else if (c === ']') {
|
|
121
|
+
depth--;
|
|
122
|
+
if (depth === 0)
|
|
123
|
+
return i;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return -1;
|
|
127
|
+
}
|
|
128
|
+
function makeDangling(s, bracket) {
|
|
129
|
+
// A blank line inside the candidate means it is settled literal text (a link
|
|
130
|
+
// label cannot span a paragraph break), not a half-streamed link.
|
|
131
|
+
if (s.indexOf('\n\n', bracket) !== -1)
|
|
132
|
+
return null;
|
|
133
|
+
const isImage = bracket >= 1 && s[bracket - 1] === '!' && !(bracket >= 2 && s[bracket - 2] === '\\');
|
|
134
|
+
return { bracket, isImage, markStart: isImage ? bracket - 1 : bracket };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Find an incomplete link/image whose syntax runs to the end of the text. A
|
|
138
|
+
* construct is complete once its `](url)` or `][ref]` (or a bare closed `[...]`)
|
|
139
|
+
* has arrived; anything short of that is dangling.
|
|
140
|
+
*/
|
|
141
|
+
function findDanglingConstruct(s) {
|
|
142
|
+
let i = 0;
|
|
143
|
+
let lastOpen = -1;
|
|
144
|
+
const n = s.length;
|
|
145
|
+
while (i < n) {
|
|
146
|
+
const c = s[i];
|
|
147
|
+
if (c === '\\') {
|
|
148
|
+
i += 2;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (c === '`') {
|
|
152
|
+
const run = runLength(s, i, '`');
|
|
153
|
+
const close = findClosingBacktick(s, i + run, run);
|
|
154
|
+
if (close === -1)
|
|
155
|
+
break; // open code span owns the rest of the string
|
|
156
|
+
i = close + run;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (c === '[') {
|
|
160
|
+
lastOpen = i;
|
|
161
|
+
i++;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (c === ']' && lastOpen !== -1) {
|
|
165
|
+
const j = i + 1;
|
|
166
|
+
const next = s[j];
|
|
167
|
+
if (next === '(') {
|
|
168
|
+
const cp = findMatchingParen(s, j);
|
|
169
|
+
if (cp === -1)
|
|
170
|
+
return makeDangling(s, lastOpen); // `[label](url…`
|
|
171
|
+
i = cp + 1;
|
|
172
|
+
lastOpen = -1;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (next === '[') {
|
|
176
|
+
const cb = findClosingBracket(s, j);
|
|
177
|
+
if (cb === -1)
|
|
178
|
+
return makeDangling(s, lastOpen); // `[label][ref…`
|
|
179
|
+
i = cb + 1;
|
|
180
|
+
lastOpen = -1;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
// Closed bracket with no link/ref continuation → complete/plain `[...]`.
|
|
184
|
+
lastOpen = -1;
|
|
185
|
+
i = j;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
i++;
|
|
189
|
+
}
|
|
190
|
+
return lastOpen === -1 ? null : makeDangling(s, lastOpen);
|
|
191
|
+
}
|
|
192
|
+
function extractLabel(s, bracket) {
|
|
193
|
+
const close = findClosingBracket(s, bracket);
|
|
194
|
+
return close === -1 ? s.slice(bracket + 1) : s.slice(bracket + 1, close);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Strip a trailing incomplete link/image. Links keep their label text (which is
|
|
198
|
+
* then marker-repaired downstream); images are removed whole, since a broken
|
|
199
|
+
* image placeholder is worse than nothing.
|
|
200
|
+
*/
|
|
201
|
+
function stripDanglingLinkOrImage(text) {
|
|
202
|
+
const found = findDanglingConstruct(text);
|
|
203
|
+
if (found === null)
|
|
204
|
+
return text;
|
|
205
|
+
if (found.isImage)
|
|
206
|
+
return text.slice(0, found.markStart);
|
|
207
|
+
return text.slice(0, found.bracket) + extractLabel(text, found.bracket);
|
|
208
|
+
}
|
|
209
|
+
function flanking(char, before, after) {
|
|
210
|
+
const leftFlanking = !isWs(after) && (!isPunct(after) || isWs(before) || isPunct(before));
|
|
211
|
+
const rightFlanking = !isWs(before) && (!isPunct(before) || isWs(after) || isPunct(after));
|
|
212
|
+
if (char === '_') {
|
|
213
|
+
// Underscore cannot open/close intra-word (so `snake_case` stays literal).
|
|
214
|
+
return {
|
|
215
|
+
canOpen: leftFlanking && (!rightFlanking || isPunct(before)),
|
|
216
|
+
canClose: rightFlanking && (!leftFlanking || isPunct(after))
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return { canOpen: leftFlanking, canClose: rightFlanking };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Tokenize inline markers, skipping escapes and closed code spans. `openCode` is
|
|
223
|
+
* the length of a trailing unclosed backtick run (0 if none); when set, scanning
|
|
224
|
+
* stops there — everything after an unclosed run is code, not markup.
|
|
225
|
+
*/
|
|
226
|
+
function scanInline(s) {
|
|
227
|
+
const tokens = [];
|
|
228
|
+
let openCode = 0;
|
|
229
|
+
let i = 0;
|
|
230
|
+
const n = s.length;
|
|
231
|
+
while (i < n) {
|
|
232
|
+
const c = s[i];
|
|
233
|
+
if (c === '\\') {
|
|
234
|
+
i += 2; // escaped char is literal — the marker after `\` does not count
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (c === '`') {
|
|
238
|
+
const run = runLength(s, i, '`');
|
|
239
|
+
const close = findClosingBacktick(s, i + run, run);
|
|
240
|
+
if (close === -1) {
|
|
241
|
+
// Only close a backtick that plausibly opens a still-streaming span:
|
|
242
|
+
// its content is non-empty and on the same line (a lone trailing
|
|
243
|
+
// backtick, or one with a full prose line after it, is literal in
|
|
244
|
+
// CommonMark — closing it would break the no-op invariant). Content
|
|
245
|
+
// ending in a backtick is skipped too, so the appended closer cannot
|
|
246
|
+
// merge into the run and defeat idempotency.
|
|
247
|
+
const content = s.slice(i + run);
|
|
248
|
+
if (content.length > 0 && !content.includes('\n') && content[content.length - 1] !== '`') {
|
|
249
|
+
openCode = run;
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
i = close + run;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (c === '*' || c === '_' || c === '~') {
|
|
257
|
+
const run = runLength(s, i, c);
|
|
258
|
+
// A lone `~` is not a marker — only `~~` is strikethrough.
|
|
259
|
+
if (c === '~' && run < 2) {
|
|
260
|
+
i += run;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const { canOpen, canClose } = flanking(c, s[i - 1], s[i + run]);
|
|
264
|
+
// Normalize `~` runs to a single strike delimiter (length 2).
|
|
265
|
+
tokens.push({ char: c, len: c === '~' ? 2 : run, canOpen, canClose });
|
|
266
|
+
i += run;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
i++;
|
|
270
|
+
}
|
|
271
|
+
return { tokens, openCode };
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Reduce the delimiter run list to the openers still unmatched at the end
|
|
275
|
+
* (bottom-of-stack first). Closers without a matching opener are left as literal
|
|
276
|
+
* text; only genuinely open markers are reported.
|
|
277
|
+
*/
|
|
278
|
+
function resolveOpenMarkers(tokens) {
|
|
279
|
+
const stack = [];
|
|
280
|
+
for (const tok of tokens) {
|
|
281
|
+
let remaining = tok.len;
|
|
282
|
+
if (tok.canClose) {
|
|
283
|
+
while (remaining > 0) {
|
|
284
|
+
let idx = -1;
|
|
285
|
+
for (let k = stack.length - 1; k >= 0; k--) {
|
|
286
|
+
if (stack[k].char === tok.char) {
|
|
287
|
+
idx = k;
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (idx === -1)
|
|
292
|
+
break;
|
|
293
|
+
// Openers stacked above the match cannot pair with this closer.
|
|
294
|
+
stack.length = idx + 1;
|
|
295
|
+
const opener = stack[idx];
|
|
296
|
+
let use = tok.char === '~' ? 2 : opener.remaining >= 2 && remaining >= 2 ? 2 : 1;
|
|
297
|
+
use = Math.min(use, opener.remaining, remaining);
|
|
298
|
+
opener.remaining -= use;
|
|
299
|
+
remaining -= use;
|
|
300
|
+
if (opener.remaining === 0)
|
|
301
|
+
stack.pop();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (tok.canOpen && remaining > 0)
|
|
305
|
+
stack.push({ char: tok.char, remaining });
|
|
306
|
+
}
|
|
307
|
+
return stack;
|
|
308
|
+
}
|
|
309
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
310
|
+
function repairInline(text) {
|
|
311
|
+
const stripped = stripDanglingLinkOrImage(text);
|
|
312
|
+
const { tokens, openCode } = scanInline(stripped);
|
|
313
|
+
const openMarkers = resolveOpenMarkers(tokens);
|
|
314
|
+
if (openCode === 0 && openMarkers.length === 0)
|
|
315
|
+
return stripped;
|
|
316
|
+
let out = stripped;
|
|
317
|
+
// Innermost first: an open code span sits inside any open emphasis, and the
|
|
318
|
+
// emphasis stack is innermost-at-top.
|
|
319
|
+
if (openCode > 0)
|
|
320
|
+
out += '`'.repeat(openCode);
|
|
321
|
+
for (let k = openMarkers.length - 1; k >= 0; k--) {
|
|
322
|
+
out += openMarkers[k].char.repeat(openMarkers[k].remaining);
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Repair the unsettled tail of a streamed markdown source: close unpaired inline
|
|
328
|
+
* markers and drop half-arrived links/images. A no-op on complete syntax.
|
|
329
|
+
*/
|
|
330
|
+
export function repairMarkdownTail(text) {
|
|
331
|
+
if (text.length === 0)
|
|
332
|
+
return text;
|
|
333
|
+
// Inside an open fence, everything is code — leave it exactly as-is.
|
|
334
|
+
if (endsInsideOpenFence(text))
|
|
335
|
+
return text;
|
|
336
|
+
return repairInline(text);
|
|
337
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chunking strategies for the streaming-parser invariant suite.
|
|
3
|
+
*
|
|
4
|
+
* Each strategy slices the *same* source text into an append-only chunk
|
|
5
|
+
* sequence. The union of all strategies is designed to stress the parser's
|
|
6
|
+
* boundary handling: `per-code-unit` deliberately splits UTF-16 surrogate pairs
|
|
7
|
+
* (the hardest case — a chunk boundary can land inside a single emoji), while
|
|
8
|
+
* the seeded strategies exercise arbitrary mid-token cuts reproducibly.
|
|
9
|
+
*
|
|
10
|
+
* Determinism is a hard requirement: no `Math.random`, no `Date.now`. The
|
|
11
|
+
* seeded strategies use a tiny inline LCG so every run produces byte-identical
|
|
12
|
+
* chunk boundaries.
|
|
13
|
+
*/
|
|
14
|
+
export interface Chunking {
|
|
15
|
+
name: string;
|
|
16
|
+
chunks: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Produce every chunking of `text`. Each strategy self-checks the fundamental
|
|
20
|
+
* invariant `chunks.join('') === text` before it is returned, so a bug in the
|
|
21
|
+
* harness surfaces here rather than as a confusing parser mismatch downstream.
|
|
22
|
+
*/
|
|
23
|
+
export declare function chunkings(text: string): Chunking[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chunking strategies for the streaming-parser invariant suite.
|
|
3
|
+
*
|
|
4
|
+
* Each strategy slices the *same* source text into an append-only chunk
|
|
5
|
+
* sequence. The union of all strategies is designed to stress the parser's
|
|
6
|
+
* boundary handling: `per-code-unit` deliberately splits UTF-16 surrogate pairs
|
|
7
|
+
* (the hardest case — a chunk boundary can land inside a single emoji), while
|
|
8
|
+
* the seeded strategies exercise arbitrary mid-token cuts reproducibly.
|
|
9
|
+
*
|
|
10
|
+
* Determinism is a hard requirement: no `Math.random`, no `Date.now`. The
|
|
11
|
+
* seeded strategies use a tiny inline LCG so every run produces byte-identical
|
|
12
|
+
* chunk boundaries.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Numerical-Recipes LCG. Returns a float in [0, 1). Pure and seedable, so the
|
|
16
|
+
* "random" chunk sizes are fixed across runs and machines.
|
|
17
|
+
*/
|
|
18
|
+
function makeRng(seed) {
|
|
19
|
+
let state = seed >>> 0;
|
|
20
|
+
return () => {
|
|
21
|
+
state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
|
|
22
|
+
return state / 0x100000000;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Split into fixed-size code-unit windows sized by `rng` within [min, max]. */
|
|
26
|
+
function seededChunks(text, min, max, seed) {
|
|
27
|
+
const rng = makeRng(seed);
|
|
28
|
+
const chunks = [];
|
|
29
|
+
let i = 0;
|
|
30
|
+
const span = max - min + 1;
|
|
31
|
+
while (i < text.length) {
|
|
32
|
+
const size = min + Math.floor(rng() * span);
|
|
33
|
+
chunks.push(text.slice(i, i + size));
|
|
34
|
+
i += size;
|
|
35
|
+
}
|
|
36
|
+
return chunks;
|
|
37
|
+
}
|
|
38
|
+
/** Split at `\n` boundaries, keeping each newline attached to its line. */
|
|
39
|
+
function perLine(text) {
|
|
40
|
+
const parts = text.split('\n');
|
|
41
|
+
return parts.map((part, idx) => (idx < parts.length - 1 ? `${part}\n` : part));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Produce every chunking of `text`. Each strategy self-checks the fundamental
|
|
45
|
+
* invariant `chunks.join('') === text` before it is returned, so a bug in the
|
|
46
|
+
* harness surfaces here rather than as a confusing parser mismatch downstream.
|
|
47
|
+
*/
|
|
48
|
+
export function chunkings(text) {
|
|
49
|
+
const strategies = [
|
|
50
|
+
{ name: 'per-code-unit', chunks: text.split('') },
|
|
51
|
+
{ name: 'tiny-seeded', chunks: seededChunks(text, 2, 8, 0x9e3779b9) },
|
|
52
|
+
{ name: 'medium-seeded', chunks: seededChunks(text, 16, 64, 0x85ebca6b) },
|
|
53
|
+
{ name: 'per-line', chunks: perLine(text) },
|
|
54
|
+
{ name: 'single', chunks: [text] }
|
|
55
|
+
];
|
|
56
|
+
for (const strategy of strategies) {
|
|
57
|
+
if (strategy.chunks.join('') !== text) {
|
|
58
|
+
throw new Error(`chunking "${strategy.name}" is not lossless — join !== source`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return strategies;
|
|
62
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared contracts for the streaming markdown engine (AI-Kit P0).
|
|
3
|
+
*
|
|
4
|
+
* The engine is a zero-dependency CommonMark/GFM *subset* parser that renders
|
|
5
|
+
* to a component tree — never to an HTML string. Raw HTML in the source is
|
|
6
|
+
* treated as plain text by design (see AI-KIT plan, decision A2).
|
|
7
|
+
*
|
|
8
|
+
* Streaming model: `createIncrementalParser()` accepts append-only chunks.
|
|
9
|
+
* Settled blocks keep object identity across appends (Svelte's keyed `{#each}`
|
|
10
|
+
* then skips re-rendering them); only the unsettled tail is re-parsed per
|
|
11
|
+
* append, after running `repairMarkdownTail` over it.
|
|
12
|
+
*/
|
|
13
|
+
export type InlineNode = {
|
|
14
|
+
kind: 'text';
|
|
15
|
+
text: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'strong';
|
|
18
|
+
children: InlineNode[];
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'em';
|
|
21
|
+
children: InlineNode[];
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'strike';
|
|
24
|
+
children: InlineNode[];
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'code';
|
|
27
|
+
text: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'link';
|
|
30
|
+
/** Empty string when `blocked` is true — the original URL never reaches the DOM. */
|
|
31
|
+
href: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
children: InlineNode[];
|
|
34
|
+
/** Set when the URL failed the `MarkdownUrlPolicy`; render as inert text. */
|
|
35
|
+
blocked?: boolean;
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'image';
|
|
38
|
+
/** Empty string when `blocked` is true. */
|
|
39
|
+
src: string;
|
|
40
|
+
alt: string;
|
|
41
|
+
title?: string;
|
|
42
|
+
blocked?: boolean;
|
|
43
|
+
} | {
|
|
44
|
+
/** `[id]` / `【id】` citation marker; emitted only for ids in `MarkdownParseOptions.citationIds`. */
|
|
45
|
+
kind: 'citation';
|
|
46
|
+
id: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'break';
|
|
49
|
+
};
|
|
50
|
+
export interface ListItem {
|
|
51
|
+
children: BlockNode[];
|
|
52
|
+
/** Task-list state; undefined for ordinary items. */
|
|
53
|
+
checked?: boolean;
|
|
54
|
+
}
|
|
55
|
+
export type BlockNode = {
|
|
56
|
+
kind: 'paragraph';
|
|
57
|
+
children: InlineNode[];
|
|
58
|
+
} | {
|
|
59
|
+
kind: 'heading';
|
|
60
|
+
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
61
|
+
children: InlineNode[];
|
|
62
|
+
} | {
|
|
63
|
+
kind: 'code-block';
|
|
64
|
+
lang?: string;
|
|
65
|
+
code: string;
|
|
66
|
+
/** True while the closing fence has not streamed in yet. */
|
|
67
|
+
open?: boolean;
|
|
68
|
+
} | {
|
|
69
|
+
kind: 'list';
|
|
70
|
+
ordered: boolean;
|
|
71
|
+
start?: number;
|
|
72
|
+
items: ListItem[];
|
|
73
|
+
} | {
|
|
74
|
+
kind: 'blockquote';
|
|
75
|
+
children: BlockNode[];
|
|
76
|
+
} | {
|
|
77
|
+
kind: 'table';
|
|
78
|
+
align: ('left' | 'center' | 'right' | null)[];
|
|
79
|
+
header: InlineNode[][];
|
|
80
|
+
rows: InlineNode[][][];
|
|
81
|
+
} | {
|
|
82
|
+
kind: 'hr';
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Top-level block with a stable identity for keyed `{#each}`. Settled blocks
|
|
86
|
+
* additionally keep referential identity (`===`) across `append()` calls.
|
|
87
|
+
*/
|
|
88
|
+
export type MarkdownBlock = BlockNode & {
|
|
89
|
+
key: number;
|
|
90
|
+
};
|
|
91
|
+
export interface MarkdownDocument {
|
|
92
|
+
blocks: MarkdownBlock[];
|
|
93
|
+
/** Link-reference definitions (`[label]: url "title"`), keys lowercased. */
|
|
94
|
+
linkRefs: ReadonlyMap<string, {
|
|
95
|
+
href: string;
|
|
96
|
+
title?: string;
|
|
97
|
+
}>;
|
|
98
|
+
}
|
|
99
|
+
export interface MarkdownUrlPolicy {
|
|
100
|
+
/**
|
|
101
|
+
* Allowed link protocols (with trailing colon). Relative URLs are always
|
|
102
|
+
* allowed for links. @default ['http:', 'https:', 'mailto:', 'tel:']
|
|
103
|
+
*/
|
|
104
|
+
allowedLinkProtocols?: string[];
|
|
105
|
+
/**
|
|
106
|
+
* Prefix allowlist for image sources, matched against the normalized
|
|
107
|
+
* absolute URL. Empty (the default) blocks every external image; relative
|
|
108
|
+
* sources are allowed. Prefer deep, specific prefixes — broad ones are
|
|
109
|
+
* open-redirect bait. `data:` images are blocked unless a `data:` prefix
|
|
110
|
+
* is listed explicitly.
|
|
111
|
+
* @default []
|
|
112
|
+
*/
|
|
113
|
+
allowedImagePrefixes?: string[];
|
|
114
|
+
/** Observability hook — fires once per blocked URL occurrence. */
|
|
115
|
+
onBlocked?: (kind: 'link' | 'image', url: string) => void;
|
|
116
|
+
}
|
|
117
|
+
export interface MarkdownParseOptions {
|
|
118
|
+
urlPolicy?: MarkdownUrlPolicy;
|
|
119
|
+
/**
|
|
120
|
+
* Ids for which `[id]` / `【id】` markers become `citation` nodes. Markers
|
|
121
|
+
* with unknown ids stay plain text (so `[1]` in prose is not mangled).
|
|
122
|
+
*/
|
|
123
|
+
citationIds?: ReadonlySet<string>;
|
|
124
|
+
/** Recognize bare `https://…` autolinks (GFM-style). @default false */
|
|
125
|
+
autolink?: boolean;
|
|
126
|
+
}
|
|
127
|
+
export interface IncrementalMarkdownParser {
|
|
128
|
+
/**
|
|
129
|
+
* Append a chunk and get the current document. Settled blocks are the same
|
|
130
|
+
* objects as in the previous call; the tail is freshly parsed from
|
|
131
|
+
* `repairMarkdownTail(tail)`.
|
|
132
|
+
*/
|
|
133
|
+
append(chunk: string): MarkdownDocument;
|
|
134
|
+
/** Current document (same value the last `append` returned). */
|
|
135
|
+
readonly document: MarkdownDocument;
|
|
136
|
+
/** Full text received so far. */
|
|
137
|
+
readonly source: string;
|
|
138
|
+
reset(): void;
|
|
139
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared contracts for the streaming markdown engine (AI-Kit P0).
|
|
3
|
+
*
|
|
4
|
+
* The engine is a zero-dependency CommonMark/GFM *subset* parser that renders
|
|
5
|
+
* to a component tree — never to an HTML string. Raw HTML in the source is
|
|
6
|
+
* treated as plain text by design (see AI-KIT plan, decision A2).
|
|
7
|
+
*
|
|
8
|
+
* Streaming model: `createIncrementalParser()` accepts append-only chunks.
|
|
9
|
+
* Settled blocks keep object identity across appends (Svelte's keyed `{#each}`
|
|
10
|
+
* then skips re-rendering them); only the unsettled tail is re-parsed per
|
|
11
|
+
* append, after running `repairMarkdownTail` over it.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { MarkdownUrlPolicy } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* URL policy enforcement (decision A4 — strict by default).
|
|
4
|
+
*
|
|
5
|
+
* LLM output is untrusted input: prompt-injected markdown links/images are a
|
|
6
|
+
* proven data-exfiltration channel (query-param payloads to attacker hosts)
|
|
7
|
+
* even without any script execution. Policy therefore runs on every URL the
|
|
8
|
+
* parser emits, and blocked URLs never reach the produced node tree — a
|
|
9
|
+
* blocked link/image carries `blocked: true` and an empty href/src.
|
|
10
|
+
*
|
|
11
|
+
* Checks run against the *normalized* absolute URL (WHATWG `URL`), so scheme
|
|
12
|
+
* tricks (`JaVaScRiPt:`, embedded tabs/newlines, `https:\\host`, `/../`
|
|
13
|
+
* path escapes against a prefix) are neutralized before matching.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEFAULT_LINK_PROTOCOLS: string[];
|
|
16
|
+
export type UrlCheck = {
|
|
17
|
+
ok: true;
|
|
18
|
+
href: string;
|
|
19
|
+
} | {
|
|
20
|
+
ok: false;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Check a link destination. Relative references are always allowed; absolute
|
|
24
|
+
* URLs must carry an allowed protocol.
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkLinkUrl(raw: string, policy: MarkdownUrlPolicy | undefined): UrlCheck;
|
|
27
|
+
/**
|
|
28
|
+
* Check an image source. Relative references are allowed (same-origin is not
|
|
29
|
+
* an exfiltration sink); absolute URLs must match an allowlisted prefix on
|
|
30
|
+
* the normalized href. The default empty allowlist blocks every external
|
|
31
|
+
* image — the CamoLeak lesson, and the posture ChatGPT/Copilot converged on.
|
|
32
|
+
*/
|
|
33
|
+
export declare function checkImageUrl(raw: string, policy: MarkdownUrlPolicy | undefined): UrlCheck;
|