@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.
Files changed (124) hide show
  1. package/dist/components/Chat/Chat/Chat.svelte +53 -0
  2. package/dist/components/Chat/Chat/Chat.svelte.d.ts +4 -0
  3. package/dist/components/Chat/Chat/chat.variants.d.ts +20 -0
  4. package/dist/components/Chat/Chat/chat.variants.js +15 -0
  5. package/dist/components/Chat/Chat/index.d.ts +47 -0
  6. package/dist/components/Chat/Chat/index.js +2 -0
  7. package/dist/components/Chat/ChatMessage/ChatMessage.svelte +342 -0
  8. package/dist/components/Chat/ChatMessage/ChatMessage.svelte.d.ts +4 -0
  9. package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +138 -0
  10. package/dist/components/Chat/ChatMessage/chat-message.variants.js +118 -0
  11. package/dist/components/Chat/ChatMessage/index.d.ts +102 -0
  12. package/dist/components/Chat/ChatMessage/index.js +2 -0
  13. package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte +301 -0
  14. package/dist/components/Chat/ChatMessageList/ChatMessageList.svelte.d.ts +6 -0
  15. package/dist/components/Chat/ChatMessageList/chat-message-list.variants.d.ts +23 -0
  16. package/dist/components/Chat/ChatMessageList/chat-message-list.variants.js +32 -0
  17. package/dist/components/Chat/ChatMessageList/chat-scroll.d.ts +45 -0
  18. package/dist/components/Chat/ChatMessageList/chat-scroll.js +52 -0
  19. package/dist/components/Chat/ChatMessageList/index.d.ts +93 -0
  20. package/dist/components/Chat/ChatMessageList/index.js +3 -0
  21. package/dist/components/Chat/CitationChip/CitationChip.svelte +107 -0
  22. package/dist/components/Chat/CitationChip/CitationChip.svelte.d.ts +4 -0
  23. package/dist/components/Chat/CitationChip/citation-chip.variants.d.ts +40 -0
  24. package/dist/components/Chat/CitationChip/citation-chip.variants.js +42 -0
  25. package/dist/components/Chat/CitationChip/index.d.ts +69 -0
  26. package/dist/components/Chat/CitationChip/index.js +2 -0
  27. package/dist/components/Chat/CodeBlock/CodeBlock.svelte +120 -0
  28. package/dist/components/Chat/CodeBlock/CodeBlock.svelte.d.ts +4 -0
  29. package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +40 -0
  30. package/dist/components/Chat/CodeBlock/code-block.variants.js +51 -0
  31. package/dist/components/Chat/CodeBlock/index.d.ts +55 -0
  32. package/dist/components/Chat/CodeBlock/index.js +2 -0
  33. package/dist/components/Chat/PromptInput/PromptInput.svelte +432 -0
  34. package/dist/components/Chat/PromptInput/PromptInput.svelte.d.ts +4 -0
  35. package/dist/components/Chat/PromptInput/index.d.ts +165 -0
  36. package/dist/components/Chat/PromptInput/index.js +2 -0
  37. package/dist/components/Chat/PromptInput/prompt-input.variants.d.ts +90 -0
  38. package/dist/components/Chat/PromptInput/prompt-input.variants.js +102 -0
  39. package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte +107 -0
  40. package/dist/components/Chat/ReasoningDisclosure/ReasoningDisclosure.svelte.d.ts +4 -0
  41. package/dist/components/Chat/ReasoningDisclosure/index.d.ts +85 -0
  42. package/dist/components/Chat/ReasoningDisclosure/index.js +2 -0
  43. package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.d.ts +20 -0
  44. package/dist/components/Chat/ReasoningDisclosure/reasoning-disclosure.variants.js +31 -0
  45. package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte +99 -0
  46. package/dist/components/Chat/StreamingMarkdown/MdBlock.svelte.d.ts +9 -0
  47. package/dist/components/Chat/StreamingMarkdown/MdInline.svelte +82 -0
  48. package/dist/components/Chat/StreamingMarkdown/MdInline.svelte.d.ts +9 -0
  49. package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte +118 -0
  50. package/dist/components/Chat/StreamingMarkdown/StreamingMarkdown.svelte.d.ts +4 -0
  51. package/dist/components/Chat/StreamingMarkdown/index.d.ts +86 -0
  52. package/dist/components/Chat/StreamingMarkdown/index.js +2 -0
  53. package/dist/components/Chat/StreamingMarkdown/md-context.d.ts +58 -0
  54. package/dist/components/Chat/StreamingMarkdown/md-context.js +1 -0
  55. package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.d.ts +145 -0
  56. package/dist/components/Chat/StreamingMarkdown/streaming-markdown.variants.js +73 -0
  57. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +174 -0
  58. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte.d.ts +4 -0
  59. package/dist/components/Chat/ToolCallCard/index.d.ts +84 -0
  60. package/dist/components/Chat/ToolCallCard/index.js +2 -0
  61. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +38 -0
  62. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +38 -0
  63. package/dist/components/Chat/chat.types.d.ts +82 -0
  64. package/dist/components/Chat/chat.types.js +7 -0
  65. package/dist/components/Chat/index.d.ts +22 -0
  66. package/dist/components/Chat/index.js +14 -0
  67. package/dist/components/Chat/markdown/blocks.d.ts +54 -0
  68. package/dist/components/Chat/markdown/blocks.js +462 -0
  69. package/dist/components/Chat/markdown/inline.d.ts +31 -0
  70. package/dist/components/Chat/markdown/inline.js +446 -0
  71. package/dist/components/Chat/markdown/repair.d.ts +23 -0
  72. package/dist/components/Chat/markdown/repair.js +337 -0
  73. package/dist/components/Chat/markdown/streaming-harness.d.ts +23 -0
  74. package/dist/components/Chat/markdown/streaming-harness.js +62 -0
  75. package/dist/components/Chat/markdown/types.d.ts +139 -0
  76. package/dist/components/Chat/markdown/types.js +13 -0
  77. package/dist/components/Chat/markdown/url-policy.d.ts +33 -0
  78. package/dist/components/Chat/markdown/url-policy.js +109 -0
  79. package/dist/components/FileUpload/FileUpload.svelte +35 -123
  80. package/dist/components/FileUpload/FileUpload.svelte.d.ts +1 -1
  81. package/dist/components/FileUpload/index.d.ts +2 -28
  82. package/dist/components/PinInput/PinInput.svelte +321 -0
  83. package/dist/components/PinInput/PinInput.svelte.d.ts +4 -0
  84. package/dist/components/PinInput/index.d.ts +98 -0
  85. package/dist/components/PinInput/index.js +2 -0
  86. package/dist/components/PinInput/pin-input.variants.d.ts +96 -0
  87. package/dist/components/PinInput/pin-input.variants.js +111 -0
  88. package/dist/components/QRCode/QRCode.svelte +107 -0
  89. package/dist/components/QRCode/QRCode.svelte.d.ts +4 -0
  90. package/dist/components/QRCode/index.d.ts +74 -0
  91. package/dist/components/QRCode/index.js +3 -0
  92. package/dist/components/QRCode/qr-code.variants.d.ts +25 -0
  93. package/dist/components/QRCode/qr-code.variants.js +24 -0
  94. package/dist/components/QRCode/qr-encode.d.ts +22 -0
  95. package/dist/components/QRCode/qr-encode.js +528 -0
  96. package/dist/components/TimeInput/TimeInput.svelte +521 -0
  97. package/dist/components/TimeInput/TimeInput.svelte.d.ts +4 -0
  98. package/dist/components/TimeInput/index.d.ts +93 -0
  99. package/dist/components/TimeInput/index.js +2 -0
  100. package/dist/components/TimeInput/time-input.variants.d.ts +131 -0
  101. package/dist/components/TimeInput/time-input.variants.js +120 -0
  102. package/dist/components/index.d.ts +7 -0
  103. package/dist/components/index.js +4 -0
  104. package/dist/i18n/index.d.ts +18 -0
  105. package/dist/primitives/Collapsible/Collapsible.svelte +5 -0
  106. package/dist/primitives/SplitPane/SplitPane.svelte +293 -0
  107. package/dist/primitives/SplitPane/SplitPane.svelte.d.ts +4 -0
  108. package/dist/primitives/SplitPane/index.d.ts +88 -0
  109. package/dist/primitives/SplitPane/index.js +2 -0
  110. package/dist/primitives/SplitPane/split-pane.utils.d.ts +73 -0
  111. package/dist/primitives/SplitPane/split-pane.utils.js +94 -0
  112. package/dist/primitives/SplitPane/split-pane.variants.d.ts +40 -0
  113. package/dist/primitives/SplitPane/split-pane.variants.js +56 -0
  114. package/dist/primitives/index.d.ts +2 -0
  115. package/dist/primitives/index.js +1 -0
  116. package/dist/translations/de.d.ts +6 -0
  117. package/dist/translations/de.js +6 -0
  118. package/dist/translations/en.d.ts +6 -0
  119. package/dist/translations/en.js +6 -0
  120. package/dist/utils/file-intake.d.ts +78 -0
  121. package/dist/utils/file-intake.js +144 -0
  122. package/dist/utils/index.d.ts +1 -0
  123. package/dist/utils/index.js +1 -0
  124. 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;