@unoverse-platform/studio 0.1.103 → 0.1.104

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 (79) hide show
  1. package/package.json +4 -2
  2. package/screens/AtomsView.tsx +2 -1
  3. package/screens/ComponentsView.tsx +2 -1
  4. package/screens/NativeAppHost.tsx +1 -2
  5. package/screens/TemplatesLibraryView.tsx +14 -1
  6. package/screens/TemplatesView.tsx +2 -1
  7. package/screens/index.ts +4 -0
  8. package/screens/react/unoverse.tsx +281 -0
  9. package/vendor/base/src/agent-mcp/index.ts +1 -1
  10. package/vendor/base/src/agent-mcp/invoke.ts +6 -2
  11. package/vendor/base/src/agent-mcp/tools.ts +14 -5
  12. package/vendor/base/src/agent-mcp/types.ts +6 -0
  13. package/vendor/base/src/auth/auth.ts +90 -23
  14. package/vendor/base/src/definitions/load/compile/sections.ts +79 -0
  15. package/vendor/base/src/definitions/load/compile/template.ts +23 -2
  16. package/vendor/base/src/definitions/model.ts +9 -0
  17. package/vendor/base/src/definitions/registry/catalog.ts +6 -7
  18. package/vendor/base/src/executor.ts +25 -1
  19. package/vendor/base/src/manifests/executor/toolBridge.ts +25 -19
  20. package/vendor/base/src/manifests/runtime/channels/final.ts +4 -1
  21. package/vendor/base/src/manifests/runtime/duplex/session.ts +3 -9
  22. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +28 -25
  23. package/vendor/base/src/platform/serviceCalls.ts +13 -0
  24. package/vendor/sdk/appEngine.ts +363 -0
  25. package/vendor/sdk/lib/actions.ts +36 -0
  26. package/vendor/sdk/lib/citations.ts +48 -0
  27. package/vendor/sdk/lib/core/apps.ts +18 -0
  28. package/vendor/sdk/lib/core/client.ts +3 -3
  29. package/vendor/sdk/lib/core/store.ts +6 -0
  30. package/vendor/sdk/lib/dom/app.ts +546 -0
  31. package/vendor/sdk/lib/dom/chrome.ts +125 -0
  32. package/vendor/sdk/lib/dom/component.ts +333 -0
  33. package/vendor/sdk/lib/dom/connection.ts +309 -0
  34. package/vendor/sdk/lib/dom/element.ts +69 -0
  35. package/vendor/sdk/lib/dom/isolate.ts +37 -0
  36. package/vendor/sdk/lib/dom/markdown.ts +388 -0
  37. package/vendor/sdk/lib/dom/markdownParse.ts +440 -0
  38. package/vendor/sdk/lib/dom/mounted.ts +29 -0
  39. package/vendor/sdk/lib/dom/primitives.ts +689 -0
  40. package/vendor/sdk/lib/dom/region.ts +195 -0
  41. package/vendor/sdk/lib/dom/render.ts +177 -0
  42. package/vendor/sdk/lib/dom/theme.ts +38 -0
  43. package/vendor/sdk/lib/dom/types.ts +36 -0
  44. package/vendor/sdk/lib/index.ts +26 -17
  45. package/vendor/sdk/lib/orb/canvas.ts +139 -0
  46. package/vendor/sdk/lib/orb/index.ts +8 -18
  47. package/vendor/sdk/lib/orb/presets.ts +1 -1
  48. package/vendor/sdk/lib/orb/{types.ts → states.ts} +0 -24
  49. package/vendor/sdk/lib/realtime/capture.ts +152 -0
  50. package/vendor/sdk/lib/realtime/playback.ts +129 -0
  51. package/vendor/sdk/lib/realtime/socket.ts +117 -0
  52. package/vendor/sdk/lib/style.ts +45 -37
  53. package/vendor/sdk/lib/voiceService.ts +344 -0
  54. package/vendor/sdk/main.ts +107 -0
  55. package/vendor/sdk/mcpAppBridge.ts +125 -102
  56. package/vendor/sdk/pageInterfaces.ts +452 -0
  57. package/vendor/sdk/responseView.ts +193 -0
  58. package/vite.config.ts +3 -3
  59. package/vendor/sdk/appEngine.tsx +0 -395
  60. package/vendor/sdk/lib/UnoverseComponent.tsx +0 -325
  61. package/vendor/sdk/lib/app.tsx +0 -730
  62. package/vendor/sdk/lib/connection.tsx +0 -433
  63. package/vendor/sdk/lib/isolate.tsx +0 -70
  64. package/vendor/sdk/lib/orb/ThinkingOrb.tsx +0 -131
  65. package/vendor/sdk/lib/orb/theme.ts +0 -83
  66. package/vendor/sdk/lib/primitives.tsx +0 -943
  67. package/vendor/sdk/lib/realtime/useAudioCapture.ts +0 -237
  68. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +0 -157
  69. package/vendor/sdk/lib/realtime/useRealtimeWebSocket.ts +0 -164
  70. package/vendor/sdk/lib/render.tsx +0 -244
  71. package/vendor/sdk/lib/streamed.tsx +0 -30
  72. package/vendor/sdk/lib/theme.ts +0 -55
  73. package/vendor/sdk/lib/transition/MountArrangement.tsx +0 -71
  74. package/vendor/sdk/lib/transition/index.ts +0 -9
  75. package/vendor/sdk/lib/transition/viewTransition.ts +0 -29
  76. package/vendor/sdk/lib/voice.tsx +0 -422
  77. package/vendor/sdk/main.tsx +0 -125
  78. package/vendor/sdk/pageInterfaces.tsx +0 -388
  79. package/vendor/sdk/responseView.tsx +0 -82
@@ -0,0 +1,440 @@
1
+ /**
2
+ * THE TEXT PARSER: the markdown a component's text may carry, read into a tree of tags
3
+ * (docs/unoverse/design/authoring/UNOVERSE_MARKDOWN.md §11 is the whole list). Our own code,
4
+ * no package. It decides structure only; ./markdown draws each tag with the served recipe.
5
+ *
6
+ * Anything not on the list shows as the words typed, HTML included: a model's answer cannot
7
+ * place elements on the page.
8
+ */
9
+ import type { MarkdownTree } from "./types";
10
+
11
+ type El = Exclude<MarkdownTree, string>;
12
+ const el = (tag: string, props: Record<string, unknown> = {}, children: MarkdownTree[] = []): El => ({ tag, props, children });
13
+
14
+ /** A heading's id: its words, lower case, accents folded, spaces as hyphens. */
15
+ export function slugify(s: string): string {
16
+ return s
17
+ .replace(/[ÀÁÂÃÄÅàáâãä忯]/g, "a")
18
+ .replace(/[çÇ]/g, "c")
19
+ .replace(/[ðÐ]/g, "d")
20
+ .replace(/[ÈÉÊËéèêë]/g, "e")
21
+ .replace(/[ÏïÎîÍíÌì]/g, "i")
22
+ .replace(/[Ññ]/g, "n")
23
+ .replace(/[øØœŒÕõÔôÓóÒò]/g, "o")
24
+ .replace(/[ÜüÛûÚúÙù]/g, "u")
25
+ .replace(/[ŸÿÝý]/g, "y")
26
+ .replace(/[^a-z0-9- ]/gi, "")
27
+ .replace(/ /g, "-")
28
+ .toLowerCase();
29
+ }
30
+
31
+ // ── block patterns ──────────────────────────────────────────────────────────
32
+ const BLANK = /^\s*$/;
33
+ const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([^\s`]*)[^`]*$/;
34
+ const ATX = /^ {0,3}(#{1,6})(?:\s+(.*?))?(?:\s+#+)?\s*$/;
35
+ const HR = /^ {0,3}([-*_])(?:\s*\1){2,}\s*$/;
36
+ const QUOTE = /^ {0,3}> ?/;
37
+ const BULLET = /^( *)([-*+])( +|$)/;
38
+ const ORDERED = /^( *)(\d{1,9})\.( +|$)/;
39
+ const FOOTDEF = /^\[\^([^\]]+)\]:\s?(.*)$/;
40
+ const SETEXT = /^ {0,3}(=+|-+)\s*$/;
41
+ const TABLE_RULE = /^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;
42
+
43
+ const isTableStart = (lines: string[], i: number) => lines[i].includes("|") && i + 1 < lines.length && TABLE_RULE.test(lines[i + 1]) && lines[i + 1].includes("-");
44
+
45
+ /** Whether a line opens a block that ends the paragraph above it. */
46
+ const interrupts = (lines: string[], i: number) => {
47
+ const l = lines[i];
48
+ return FENCE.test(l) || ATX.test(l) || HR.test(l) || QUOTE.test(l) || BULLET.test(l) || ORDERED.test(l) || isTableStart(lines, i);
49
+ };
50
+
51
+ /** The cells of one table row, split on unescaped pipes, the outer pipes dropped. */
52
+ function cells(row: string): string[] {
53
+ let s = row.trim();
54
+ if (s.startsWith("|")) s = s.slice(1);
55
+ if (s.endsWith("|") && !s.endsWith("\\|")) s = s.slice(0, -1);
56
+ const out: string[] = [];
57
+ let cur = "";
58
+ for (let i = 0; i < s.length; i++) {
59
+ if (s[i] === "\\" && s[i + 1] === "|") {
60
+ cur += "|";
61
+ i++;
62
+ } else if (s[i] === "|") {
63
+ out.push(cur);
64
+ cur = "";
65
+ } else cur += s[i];
66
+ }
67
+ out.push(cur);
68
+ // A cell of only spaces is empty.
69
+ return out.map((c) => (c.trim() ? c : ""));
70
+ }
71
+
72
+ const alignOf = (rule: string): string | undefined => {
73
+ const r = rule.trim();
74
+ return r.startsWith(":") && r.endsWith(":") ? "center" : r.startsWith(":") ? "left" : r.endsWith(":") ? "right" : undefined;
75
+ };
76
+
77
+ interface Footnotes {
78
+ defs: [string, string][];
79
+ }
80
+
81
+ /** Read lines into blocks. */
82
+ function blocks(lines: string[], notes: Footnotes): MarkdownTree[] {
83
+ const out: MarkdownTree[] = [];
84
+ let i = 0;
85
+ while (i < lines.length) {
86
+ const line = lines[i];
87
+ if (BLANK.test(line)) {
88
+ i++;
89
+ continue;
90
+ }
91
+ let m: RegExpExecArray | null;
92
+ // Fenced code: kept exactly as written.
93
+ if ((m = FENCE.exec(line))) {
94
+ const fence = m[1];
95
+ const lang = m[2];
96
+ const body: string[] = [];
97
+ i++;
98
+ while (i < lines.length && !new RegExp(`^ {0,3}${fence[0]}{${fence.length},}\\s*$`).test(lines[i])) body.push(lines[i++]);
99
+ i++;
100
+ out.push(el("pre", {}, [el("code", lang ? { className: `lang-${lang}` } : {}, [body.join("\n") + (body.length ? "\n" : "")])]));
101
+ continue;
102
+ }
103
+ // Indented code (four spaces).
104
+ if (/^ {4}\S/.test(line)) {
105
+ const body: string[] = [];
106
+ while (i < lines.length && (/^ {4}/.test(lines[i]) || (BLANK.test(lines[i]) && /^ {4}/.test(lines[i + 1] ?? "")))) body.push(lines[i++].slice(4));
107
+ out.push(el("pre", {}, [el("code", {}, [body.join("\n")])]));
108
+ continue;
109
+ }
110
+ if ((m = ATX.exec(line))) {
111
+ const text = (m[2] ?? "").trim();
112
+ out.push(el(`h${m[1].length}`, { id: slugify(text) }, inline(text, notes)));
113
+ i++;
114
+ continue;
115
+ }
116
+ if (HR.test(line)) {
117
+ out.push(el("hr"));
118
+ i++;
119
+ continue;
120
+ }
121
+ if (QUOTE.test(line)) {
122
+ const body: string[] = [];
123
+ while (i < lines.length && !BLANK.test(lines[i]) && (QUOTE.test(lines[i]) || body.length)) {
124
+ if (!QUOTE.test(lines[i]) && interrupts(lines, i)) break;
125
+ body.push(lines[i++].replace(QUOTE, ""));
126
+ // A `>` line that is empty keeps the quote going across a paragraph break.
127
+ while (i < lines.length && /^ {0,3}>\s*$/.test(lines[i])) body.push(lines[i++].replace(QUOTE, ""));
128
+ }
129
+ out.push(el("blockquote", {}, blocks(body, notes)));
130
+ continue;
131
+ }
132
+ if (isTableStart(lines, i)) {
133
+ const head = cells(lines[i]);
134
+ const aligns = cells(lines[i + 1]).map(alignOf);
135
+ i += 2;
136
+ const rows: string[][] = [];
137
+ while (i < lines.length && !BLANK.test(lines[i]) && lines[i].includes("|")) rows.push(cells(lines[i++]));
138
+ const style = (c: number) => (aligns[c] ? { textAlign: aligns[c] } : {});
139
+ out.push(
140
+ el("table", {}, [
141
+ el("thead", {}, [el("tr", {}, head.map((h, c) => el("th", { style: style(c) }, inline(h, notes))))]),
142
+ el("tbody", {}, rows.map((r) => el("tr", {}, r.map((d, c) => el("td", { style: style(c) }, inline(d, notes)))))),
143
+ ]),
144
+ );
145
+ continue;
146
+ }
147
+ if (BULLET.test(line) || ORDERED.test(line)) {
148
+ const [list, next] = readList(lines, i, notes);
149
+ out.push(list);
150
+ i = next;
151
+ continue;
152
+ }
153
+ if ((m = FOOTDEF.exec(line))) {
154
+ notes.defs.push([m[1], m[2]]);
155
+ i++;
156
+ continue;
157
+ }
158
+ // A paragraph: lines up to a blank or a block that interrupts it.
159
+ const para: string[] = [line];
160
+ i++;
161
+ while (i < lines.length && !BLANK.test(lines[i])) {
162
+ if (SETEXT.test(lines[i])) {
163
+ const level = lines[i].trim()[0] === "=" ? 1 : 2;
164
+ i++;
165
+ out.push(el(`h${level}`, {}, inline(para.join("\n").trim(), notes)));
166
+ para.length = 0;
167
+ break;
168
+ }
169
+ if (interrupts(lines, i)) break;
170
+ para.push(lines[i++]);
171
+ }
172
+ if (para.length) out.push(el("p", {}, inline(para.join("\n").trim(), notes)));
173
+ }
174
+ return out;
175
+ }
176
+
177
+ /** A list from line `i`: its items, each with its own lines; returns the list and the next line. */
178
+ function readList(lines: string[], start: number, notes: Footnotes): [El, number] {
179
+ const first = (BULLET.exec(lines[start]) ?? ORDERED.exec(lines[start]))!;
180
+ const ordered = !BULLET.test(lines[start]);
181
+ const indent = first[1].length;
182
+ const items: string[][] = [];
183
+ let loose = false;
184
+ let i = start;
185
+ const marker = (l: string) => {
186
+ const m = ordered ? ORDERED.exec(l) : BULLET.exec(l);
187
+ return m && m[1].length <= indent + 1 ? m : null;
188
+ };
189
+ while (i < lines.length) {
190
+ const m = marker(lines[i]);
191
+ if (!m) break;
192
+ const content = m[0].length;
193
+ const item = [lines[i].slice(content)];
194
+ i++;
195
+ while (i < lines.length) {
196
+ const l = lines[i];
197
+ if (BLANK.test(l)) {
198
+ // A blank line inside the list: the item goes on if what follows is indented, the
199
+ // list goes on if another item follows, and either way the list is loose.
200
+ let j = i;
201
+ while (j < lines.length && BLANK.test(lines[j])) j++;
202
+ if (j < lines.length && /^\s+/.test(lines[j]) && lines[j].search(/\S/) >= content) {
203
+ loose = true;
204
+ for (; i < j; i++) item.push("");
205
+ continue;
206
+ }
207
+ if (j < lines.length && marker(lines[j])) loose = true;
208
+ break;
209
+ }
210
+ if (marker(l)) break;
211
+ const lead = l.search(/\S/);
212
+ // A line indented to the item's text belongs to it; an unindented one continues its
213
+ // paragraph unless it opens a block.
214
+ if (lead >= content || lead > indent) item.push(l.slice(Math.min(lead, content)));
215
+ else if (!interrupts(lines, i)) item.push(l.trim());
216
+ else break;
217
+ i++;
218
+ }
219
+ items.push(item);
220
+ while (i < lines.length && BLANK.test(lines[i]) && marker(lines[i + 1] ?? "")) i++;
221
+ }
222
+ const lis = items.map((item) => listItem(item, loose, notes));
223
+ return [el(ordered ? "ol" : "ul", ordered ? { start: Number(first[2]) } : {}, lis), i];
224
+ }
225
+
226
+ /** One item: a checkbox if it opens with `[ ]` or `[x]`, then its text and any nested blocks. */
227
+ function listItem(lines: string[], loose: boolean, notes: Footnotes): El {
228
+ const kids: MarkdownTree[] = [];
229
+ const task = /^\[([ xX])\] /.exec(lines[0]);
230
+ if (task) {
231
+ kids.push(el("input", { checked: task[1] !== " ", readOnly: true, type: "checkbox" }));
232
+ lines = [" " + lines[0].slice(4), ...lines.slice(1)];
233
+ }
234
+ if (loose) return el("li", {}, [...kids, ...blocks(lines, notes)]);
235
+ // Tight: the item's own text inline, then whatever block follows it (a nested list).
236
+ let n = 0;
237
+ while (n < lines.length && !(n > 0 && interrupts(lines, n))) n++;
238
+ const text = lines.slice(0, n).join("\n");
239
+ const rest = lines.slice(n);
240
+ const words = inline(task ? text : text.trim(), notes);
241
+ if (rest.length && words.length) {
242
+ const last = words[words.length - 1];
243
+ if (typeof last === "string") words[words.length - 1] = last + "\n";
244
+ else words.push("\n");
245
+ }
246
+ return el("li", {}, [...kids, ...words, ...blocks(rest, notes)]);
247
+ }
248
+
249
+ // ── inline ──────────────────────────────────────────────────────────────────
250
+ const PUNCT = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/;
251
+ const UNSAFE_HREF = /^\s*(javascript|vbscript|data):/i;
252
+ const URL_AT = /^https?:\/\/[^\s<]+[^\s<.,;:!?'")\]]/;
253
+
254
+ /** A link target `(href "title")` from `s[i]` = "("; returns it and the index after ")". */
255
+ function target(s: string, i: number): { href: string; title?: string; end: number } | null {
256
+ let depth = 0;
257
+ let j = i;
258
+ for (; j < s.length; j++) {
259
+ if (s[j] === "\\") {
260
+ j++;
261
+ continue;
262
+ }
263
+ if (s[j] === "(") depth++;
264
+ else if (s[j] === ")" && --depth === 0) break;
265
+ else if (s[j] === "\n" && s[j + 1] === "\n") return null;
266
+ }
267
+ if (j >= s.length) return null;
268
+ const inner = s.slice(i + 1, j).trim();
269
+ const m = /^(\S*?)(?:\s+["'(]([\s\S]*)["')])?$/.exec(inner);
270
+ if (!m || /\s/.test(m[1])) return null;
271
+ return { href: m[1], title: m[2], end: j + 1 };
272
+ }
273
+
274
+ /** The `]` that closes the `[` at `i`, nesting allowed. */
275
+ function closeBracket(s: string, i: number): number {
276
+ let depth = 0;
277
+ for (let j = i; j < s.length; j++) {
278
+ if (s[j] === "\\") j++;
279
+ else if (s[j] === "[") depth++;
280
+ else if (s[j] === "]" && --depth === 0) return j;
281
+ }
282
+ return -1;
283
+ }
284
+
285
+ const word = (c: string | undefined) => c != null && /[\p{L}\p{N}]/u.test(c);
286
+ const space = (c: string | undefined) => c == null || /\s/.test(c);
287
+
288
+ /** The run of `ch` that closes one opened with `len` of them at `from`, or -1. */
289
+ function closer(s: string, from: number, ch: string, len: number): number {
290
+ for (let j = from; j < s.length; j++) {
291
+ if (s[j] === "\\") {
292
+ j++;
293
+ continue;
294
+ }
295
+ if (s[j] === "`") {
296
+ const run = /^`+/.exec(s.slice(j))![0];
297
+ const end = s.indexOf(run, j + run.length);
298
+ if (end > 0) j = end + run.length - 1;
299
+ continue;
300
+ }
301
+ if (s[j] !== ch) continue;
302
+ let k = j;
303
+ while (s[k] === ch) k++;
304
+ const run = k - j;
305
+ // A closing run sits right after text, and `_` never closes inside a word.
306
+ if (run >= len && !space(s[j - 1]) && !(ch === "_" && word(s[k]))) return run === len || len === 3 ? j : j + run - len;
307
+ j = k - 1;
308
+ }
309
+ return -1;
310
+ }
311
+
312
+ const TAGS: Record<string, [string, number]> = { "~": ["del", 2], "=": ["mark", 2] };
313
+
314
+ /** Read a line's marks into a tree; adjacent text is one string. */
315
+ function inline(s: string, notes: Footnotes, inLink = false): MarkdownTree[] {
316
+ const out: MarkdownTree[] = [];
317
+ let text = "";
318
+ const flush = () => {
319
+ if (text) out.push(text);
320
+ text = "";
321
+ };
322
+ const push = (t: MarkdownTree) => {
323
+ flush();
324
+ out.push(t);
325
+ };
326
+ let i = 0;
327
+ while (i < s.length) {
328
+ const c = s[i];
329
+ if (c === "\\" && PUNCT.test(s[i + 1] ?? "")) {
330
+ text += s[i + 1];
331
+ i += 2;
332
+ continue;
333
+ }
334
+ // Two spaces before a line end: a line break.
335
+ if (c === "\n" && / {2,}$/.test(text)) {
336
+ text = text.replace(/ +$/, "");
337
+ push(el("br"));
338
+ i++;
339
+ continue;
340
+ }
341
+ if (c === "`") {
342
+ const run = /^`+/.exec(s.slice(i))![0];
343
+ const end = s.indexOf(run, i + run.length);
344
+ if (end > 0) {
345
+ push(el("code", {}, [s.slice(i + run.length, end)]));
346
+ i = end + run.length;
347
+ continue;
348
+ }
349
+ text += run;
350
+ i += run.length;
351
+ continue;
352
+ }
353
+ if (c === "!" && s[i + 1] === "[") {
354
+ const close = closeBracket(s, i + 1);
355
+ const t = close > 0 && s[close + 1] === "(" ? target(s, close + 1) : null;
356
+ if (t) {
357
+ push(el("img", { alt: s.slice(i + 2, close), ...(t.title != null ? { title: t.title } : {}), src: t.href }));
358
+ i = t.end;
359
+ continue;
360
+ }
361
+ }
362
+ if (c === "[") {
363
+ // A footnote marker `[^id]`.
364
+ const fn = /^\[\^([^\]\s]+)\](?!:)/.exec(s.slice(i));
365
+ if (fn) {
366
+ push(el("a", { href: `#${slugify(fn[1])}` }, [el("sup", {}, [fn[1]])]));
367
+ i += fn[0].length;
368
+ continue;
369
+ }
370
+ const close = closeBracket(s, i);
371
+ const t = close > 0 && s[close + 1] === "(" ? target(s, close + 1) : null;
372
+ if (t) {
373
+ const props: Record<string, unknown> = UNSAFE_HREF.test(t.href) ? {} : { href: t.href };
374
+ if (t.title != null) props.title = t.title;
375
+ push(el("a", props, inline(s.slice(i + 1, close), notes, true)));
376
+ i = t.end;
377
+ continue;
378
+ }
379
+ }
380
+ if (c === "<") {
381
+ const auto = /^<((?:https?:\/\/|mailto:)[^\s>]+)>/.exec(s.slice(i));
382
+ if (auto) {
383
+ push(el("a", { href: auto[1] }, [auto[1]]));
384
+ i += auto[0].length;
385
+ continue;
386
+ }
387
+ }
388
+ if (c === "h" && !inLink && !word(s[i - 1])) {
389
+ const url = URL_AT.exec(s.slice(i));
390
+ if (url) {
391
+ push(el("a", { href: url[0] }, [url[0]]));
392
+ i += url[0].length;
393
+ continue;
394
+ }
395
+ }
396
+ if (c === "*" || c === "_") {
397
+ let k = i;
398
+ while (s[k] === c) k++;
399
+ const len = Math.min(k - i, 3);
400
+ const opens = !space(s[k]) && !(c === "_" && word(s[i - 1]));
401
+ const end = opens ? closer(s, k, c, len) : -1;
402
+ if (end > 0) {
403
+ const inner = inline(s.slice(i + len, end), notes, inLink);
404
+ push(len === 3 ? el("strong", {}, [el("em", {}, inner)]) : el(len === 2 ? "strong" : "em", {}, inner));
405
+ i = end + len;
406
+ continue;
407
+ }
408
+ text += s.slice(i, k);
409
+ i = k;
410
+ continue;
411
+ }
412
+ if (TAGS[c] && s[i + 1] === c && !space(s[i + 2])) {
413
+ const [tag, len] = TAGS[c];
414
+ const end = s.indexOf(c.repeat(len), i + len);
415
+ if (end > 0 && !space(s[end - 1])) {
416
+ push(el(tag, {}, inline(s.slice(i + len, end), notes, inLink)));
417
+ i = end + len;
418
+ continue;
419
+ }
420
+ }
421
+ text += c;
422
+ i++;
423
+ }
424
+ flush();
425
+ return out;
426
+ }
427
+
428
+ /**
429
+ * The text as tags. One block is returned as itself; none or several, and any footnotes, come
430
+ * inside one `div`, the notes last in a `footer`.
431
+ */
432
+ export function parseMarkdown(source: string): MarkdownTree {
433
+ // An HTML comment is a note to whoever reads the source, never shown.
434
+ const lines = source.replace(/<!--[\s\S]*?-->/g, "").replace(/\r\n?/g, "\n").replace(/\t/g, " ").split("\n");
435
+ const notes: Footnotes = { defs: [] };
436
+ const out = blocks(lines, notes);
437
+ if (notes.defs.length)
438
+ out.push(el("footer", {}, notes.defs.map(([id, body]) => el("div", { id: slugify(id) }, [id, ...inline(`: ${body}`, notes)]))));
439
+ return out.length === 1 ? out[0] : el("div", {}, out);
440
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Work that needs the element ON THE PAGE (measuring, observers, the orb's drawing) waits for
3
+ * it: queued while a view updates, run once the update is over and the host is in the
4
+ * document. A host that is not there yet is checked again each frame.
5
+ */
6
+ export function mountQueue(onPage: () => boolean): (fn: () => void) => void {
7
+ let queue: (() => void)[] = [];
8
+ let scheduled = false;
9
+ const run = () => {
10
+ scheduled = false;
11
+ if (!queue.length) return;
12
+ if (!onPage()) {
13
+ if (typeof requestAnimationFrame === "function") {
14
+ scheduled = true;
15
+ requestAnimationFrame(run);
16
+ }
17
+ return;
18
+ }
19
+ const jobs = queue;
20
+ queue = [];
21
+ for (const job of jobs) job();
22
+ };
23
+ return (fn) => {
24
+ queue.push(fn);
25
+ if (scheduled) return;
26
+ scheduled = true;
27
+ queueMicrotask(run);
28
+ };
29
+ }