@x12i/editor 0.0.0-stage → 1.1.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/index.js ADDED
@@ -0,0 +1,1326 @@
1
+ import {
2
+ BLOCK_ID_NODES,
3
+ BLOCK_TYPES,
4
+ CALLOUT_VARIANTS,
5
+ DOCUMENT_VERSION,
6
+ EMOJI_SHORTCODES,
7
+ HIGHLIGHT_TOKENS,
8
+ MARK_TYPES,
9
+ TEXT_COLOR_TOKENS,
10
+ TIPTAP_MARKS,
11
+ TIPTAP_NODES,
12
+ buildOutlineTree,
13
+ collectIds,
14
+ countCharacters,
15
+ countWords,
16
+ createEmptyDocument,
17
+ createParagraph,
18
+ createSeededRandom,
19
+ createSequentialIdGenerator,
20
+ defaultExportLabels,
21
+ documentToHtml,
22
+ documentToMarkdown,
23
+ documentToPlainText,
24
+ documentToTiptap,
25
+ emojiForShortcode,
26
+ emojiToText,
27
+ escapeHtml,
28
+ escapeMarkdown,
29
+ findDuplicateIds,
30
+ generateBlockId,
31
+ getDocumentStats,
32
+ getOutline,
33
+ highlightToCss,
34
+ inlineToPlainText,
35
+ isHighlightToken,
36
+ isKnownBlockType,
37
+ isSafeUrl,
38
+ isTextColorToken,
39
+ normalizeDocument,
40
+ normalizeInline,
41
+ normalizeUrlInput,
42
+ sanitizeMediaUrl,
43
+ sanitizeUrl,
44
+ slugifyHeading,
45
+ textColorToCss,
46
+ tiptapToDocument,
47
+ tokenToCssVarName,
48
+ walkBlocks
49
+ } from "./chunk-FYXMOAJT.js";
50
+
51
+ // src/markdown-import.ts
52
+ var ENTITIES = {
53
+ "&": "&",
54
+ "&lt;": "<",
55
+ "&gt;": ">",
56
+ "&quot;": '"',
57
+ "&#39;": "'",
58
+ "&apos;": "'",
59
+ "&nbsp;": "\xA0"
60
+ };
61
+ function decodeEntities(text) {
62
+ return text.replace(/&(?:amp|lt|gt|quot|apos|nbsp|#39);/g, (entity) => ENTITIES[entity] ?? entity);
63
+ }
64
+ var PUNCTUATION = /[!-/:-@[-`{-~\p{P}\p{S}]/u;
65
+ var WHITESPACE = /\s/;
66
+ function findClosingBracket(text, start) {
67
+ let depth = 0;
68
+ for (let index = start; index < text.length; index += 1) {
69
+ const char = text[index];
70
+ if (char === "\\") {
71
+ index += 1;
72
+ } else if (char === "`") {
73
+ const run = /^`+/.exec(text.slice(index))?.[0] ?? "`";
74
+ const close = text.indexOf(run, index + run.length);
75
+ if (close !== -1) index = close + run.length - 1;
76
+ else index += run.length - 1;
77
+ } else if (char === "[") {
78
+ depth += 1;
79
+ } else if (char === "]") {
80
+ depth -= 1;
81
+ if (depth === 0) return index;
82
+ }
83
+ }
84
+ return -1;
85
+ }
86
+ function parseLinkTarget(text, start) {
87
+ if (text[start] !== "(") return null;
88
+ let index = start + 1;
89
+ while (index < text.length && /\s/.test(text[index] ?? "")) index += 1;
90
+ let href;
91
+ if (text[index] === "<") {
92
+ const close = text.indexOf(">", index);
93
+ if (close === -1) return null;
94
+ href = text.slice(index + 1, close);
95
+ index = close + 1;
96
+ } else {
97
+ let depth = 0;
98
+ const begin = index;
99
+ while (index < text.length) {
100
+ const char = text[index];
101
+ if (char === "\\") {
102
+ index += 2;
103
+ continue;
104
+ }
105
+ if (char === "(") depth += 1;
106
+ else if (char === ")") {
107
+ if (depth === 0) break;
108
+ depth -= 1;
109
+ } else if (/\s/.test(char)) break;
110
+ index += 1;
111
+ }
112
+ href = text.slice(begin, index).replace(/\\([!-/:-@[-`{-~])/g, "$1");
113
+ }
114
+ while (index < text.length && /\s/.test(text[index] ?? "")) index += 1;
115
+ let title;
116
+ const quote = text[index];
117
+ if (quote === '"' || quote === "'") {
118
+ let close = index + 1;
119
+ while (close < text.length && text[close] !== quote) close += text[close] === "\\" ? 2 : 1;
120
+ if (close >= text.length) return null;
121
+ title = text.slice(index + 1, close).replace(/\\(["'\\])/g, "$1");
122
+ index = close + 1;
123
+ while (index < text.length && /\s/.test(text[index] ?? "")) index += 1;
124
+ }
125
+ if (text[index] !== ")") return null;
126
+ return { href: decodeEntities(href), title, end: index + 1 };
127
+ }
128
+ function withMark(nodes, mark) {
129
+ return nodes.map((node) => node.type === "text" ? { ...node, marks: [...node.marks ?? [], mark] } : node);
130
+ }
131
+ var TAG = /^<(\/?)([a-zA-Z][a-zA-Z0-9]*)((?:\s+[a-zA-Z-]+(?:="[^"]*")?)*)\s*(\/?)>/;
132
+ var AUTOLINK = /^<((?:https?:\/\/|mailto:)[^<>\s]+)>/i;
133
+ var BARE_URL = /^https?:\/\/[^\s<]+/i;
134
+ function attributeOf(source, name) {
135
+ const match = new RegExp(`\\s${name}="([^"]*)"`).exec(source);
136
+ return match ? decodeEntities(match[1] ?? "") : void 0;
137
+ }
138
+ function htmlMark(tag, attributes, closing) {
139
+ if (closing && tag === "mark") return { type: "highlight", token: "" };
140
+ if (closing && tag === "span") return { type: "color", token: "" };
141
+ switch (tag) {
142
+ case "u":
143
+ return { type: "underline" };
144
+ case "strong":
145
+ case "b":
146
+ return { type: "bold" };
147
+ case "em":
148
+ case "i":
149
+ return { type: "italic" };
150
+ case "s":
151
+ case "del":
152
+ case "strike":
153
+ return { type: "strike" };
154
+ case "mark": {
155
+ const token = attributeOf(attributes, "data-token");
156
+ return token !== void 0 && isHighlightToken(token) ? { type: "highlight", token } : null;
157
+ }
158
+ case "span": {
159
+ const token = attributeOf(attributes, "data-color-token");
160
+ return token !== void 0 && isTextColorToken(token) ? { type: "color", token } : null;
161
+ }
162
+ default:
163
+ return null;
164
+ }
165
+ }
166
+ function tokenize(text) {
167
+ const tokens = [];
168
+ let buffer = "";
169
+ const flush = () => {
170
+ if (buffer !== "") {
171
+ tokens.push({ k: "text", text: buffer });
172
+ buffer = "";
173
+ }
174
+ };
175
+ let index = 0;
176
+ while (index < text.length) {
177
+ const char = text[index];
178
+ const rest = text.slice(index);
179
+ if (char === "\\") {
180
+ const next = text[index + 1];
181
+ if (next === "\n") {
182
+ buffer += "\n";
183
+ index += 2;
184
+ continue;
185
+ }
186
+ if (next !== void 0 && /[!-/:-@[-`{-~]/.test(next)) {
187
+ buffer += next;
188
+ index += 2;
189
+ continue;
190
+ }
191
+ buffer += char;
192
+ index += 1;
193
+ continue;
194
+ }
195
+ if (char === "\n") {
196
+ if (/ {2,}$/.test(buffer)) buffer = `${buffer.replace(/ +$/, "")}
197
+ `;
198
+ else buffer = `${buffer.replace(/ +$/, "")} `;
199
+ index += 1;
200
+ while (text[index] === " ") index += 1;
201
+ continue;
202
+ }
203
+ if (char === "`") {
204
+ const run = /^`+/.exec(rest)?.[0] ?? "`";
205
+ let close = -1;
206
+ let search = index + run.length;
207
+ while (search < text.length) {
208
+ const found = text.indexOf(run, search);
209
+ if (found === -1) break;
210
+ const after = text[found + run.length];
211
+ if (after !== "`" && text[found - 1] !== "`") {
212
+ close = found;
213
+ break;
214
+ }
215
+ search = found + 1;
216
+ }
217
+ if (close === -1) {
218
+ buffer += run;
219
+ index += run.length;
220
+ continue;
221
+ }
222
+ let code = text.slice(index + run.length, close).replace(/\n/g, " ");
223
+ if (code.length > 1 && code.startsWith(" ") && code.endsWith(" ") && code.trim() !== "") code = code.slice(1, -1);
224
+ flush();
225
+ tokens.push({ k: "nodes", nodes: [{ type: "text", text: code, marks: [{ type: "code" }] }] });
226
+ index = close + run.length;
227
+ continue;
228
+ }
229
+ if (char === "[" || char === "!" && text[index + 1] === "[") {
230
+ const isImage = char === "!";
231
+ const bracket = isImage ? index + 1 : index;
232
+ const close = findClosingBracket(text, bracket);
233
+ if (close !== -1) {
234
+ const target = parseLinkTarget(text, close + 1);
235
+ if (target) {
236
+ const label = text.slice(bracket + 1, close);
237
+ flush();
238
+ const mention = /^kit:mention\/(person|page|date)\/(.+)$/.exec(target.href);
239
+ if (!isImage && mention) {
240
+ tokens.push({
241
+ k: "nodes",
242
+ nodes: [
243
+ {
244
+ type: "mention",
245
+ kind: mention[1],
246
+ id: decodeURIComponent(mention[2] ?? ""),
247
+ label: label.replace(/^@/, "").replace(/\\([!-/:-@[-`{-~])/g, "$1")
248
+ }
249
+ ]
250
+ });
251
+ } else {
252
+ const inner = parseInlineNodes(isImage && label === "" ? target.href : label);
253
+ tokens.push({ k: "nodes", nodes: withMark(inner, { type: "link", href: target.href }) });
254
+ }
255
+ index = target.end;
256
+ continue;
257
+ }
258
+ }
259
+ }
260
+ if (char === "<") {
261
+ const auto = AUTOLINK.exec(rest);
262
+ if (auto) {
263
+ flush();
264
+ const href = auto[1];
265
+ tokens.push({
266
+ k: "nodes",
267
+ nodes: [{ type: "text", text: href.replace(/^mailto:/i, ""), marks: [{ type: "link", href }] }]
268
+ });
269
+ index += auto[0].length;
270
+ continue;
271
+ }
272
+ const tag = TAG.exec(rest);
273
+ if (tag) {
274
+ const name = tag[2].toLowerCase();
275
+ if (name === "br") {
276
+ buffer += "\n";
277
+ index += tag[0].length;
278
+ continue;
279
+ }
280
+ const mark = htmlMark(name, tag[3], tag[1] === "/");
281
+ if (mark) {
282
+ flush();
283
+ tokens.push({ k: "mark", mark, open: tag[1] !== "/" });
284
+ index += tag[0].length;
285
+ continue;
286
+ }
287
+ }
288
+ }
289
+ if (char === "*" || char === "_" || char === "~") {
290
+ const run = new RegExp(`^\\${char}+`).exec(rest)?.[0] ?? char;
291
+ if (char === "~" && run.length !== 2) {
292
+ buffer += run;
293
+ index += run.length;
294
+ continue;
295
+ }
296
+ const before = index === 0 ? " " : text[index - 1];
297
+ const after = index + run.length >= text.length ? " " : text[index + run.length];
298
+ const beforeSpace = WHITESPACE.test(before);
299
+ const afterSpace = WHITESPACE.test(after);
300
+ const beforePunct = PUNCTUATION.test(before);
301
+ const afterPunct = PUNCTUATION.test(after);
302
+ const leftFlanking = !afterSpace && (!afterPunct || beforeSpace || beforePunct);
303
+ const rightFlanking = !beforeSpace && (!beforePunct || afterSpace || afterPunct);
304
+ const open = char === "_" ? leftFlanking && (!rightFlanking || beforePunct) : leftFlanking;
305
+ const close = char === "_" ? rightFlanking && (!leftFlanking || afterPunct) : rightFlanking;
306
+ flush();
307
+ tokens.push({ k: "delim", char, count: run.length, open, close });
308
+ index += run.length;
309
+ continue;
310
+ }
311
+ if (char === ":") {
312
+ const shortcode = /^:([a-z0-9_+-]+):/i.exec(rest);
313
+ if (shortcode && emojiForShortcode(shortcode[1]) !== null) {
314
+ flush();
315
+ tokens.push({ k: "nodes", nodes: [{ type: "emoji", shortcode: shortcode[1] }] });
316
+ index += shortcode[0].length;
317
+ continue;
318
+ }
319
+ }
320
+ if ((char === "h" || char === "H") && (index === 0 || !/[\p{L}\p{N}]/u.test(text[index - 1]))) {
321
+ const url = BARE_URL.exec(rest);
322
+ if (url) {
323
+ let value = url[0];
324
+ while (/[.,;:!?'"*_~]$/.test(value) || value.endsWith(")") && !value.includes("(")) value = value.slice(0, -1);
325
+ if (value.length > "https://".length) {
326
+ flush();
327
+ tokens.push({ k: "nodes", nodes: [{ type: "text", text: value, marks: [{ type: "link", href: value }] }] });
328
+ index += value.length;
329
+ continue;
330
+ }
331
+ }
332
+ }
333
+ buffer += char;
334
+ index += 1;
335
+ }
336
+ flush();
337
+ return tokens;
338
+ }
339
+ function resolveEmphasis(tokens) {
340
+ const list = [...tokens];
341
+ for (let index = 0; index < list.length; index += 1) {
342
+ const token = list[index];
343
+ if (token.k !== "mark" || !token.open) continue;
344
+ let closeAt = -1;
345
+ for (let search = index + 1; search < list.length; search += 1) {
346
+ const candidate = list[search];
347
+ if (candidate.k === "mark" && !candidate.open && candidate.mark.type === token.mark.type) {
348
+ closeAt = search;
349
+ break;
350
+ }
351
+ }
352
+ if (closeAt === -1) {
353
+ list[index] = { k: "text", text: "" };
354
+ continue;
355
+ }
356
+ const children = list.slice(index + 1, closeAt);
357
+ list.splice(index, closeAt - index + 1, { k: "span", mark: token.mark, children: resolveEmphasis(children) });
358
+ }
359
+ for (let index = 0; index < list.length; index += 1) {
360
+ const token = list[index];
361
+ if (token.k === "mark") list[index] = { k: "text", text: "" };
362
+ }
363
+ for (let closerIndex = 0; closerIndex < list.length; closerIndex += 1) {
364
+ let closer = list[closerIndex];
365
+ if (closer.k !== "delim" || !closer.close) continue;
366
+ while (closer.k === "delim" && closer.count > 0 && closer.close) {
367
+ let openerIndex = -1;
368
+ for (let search = closerIndex - 1; search >= 0; search -= 1) {
369
+ const candidate = list[search];
370
+ if (candidate.k === "delim" && candidate.char === closer.char && candidate.open && candidate.count > 0) {
371
+ if (closer.char === "~" && (candidate.count < 2 || closer.count < 2)) continue;
372
+ openerIndex = search;
373
+ break;
374
+ }
375
+ }
376
+ if (openerIndex === -1) break;
377
+ const opener = list[openerIndex];
378
+ const use = closer.char === "~" ? 2 : opener.count >= 2 && closer.count >= 2 ? 2 : 1;
379
+ const mark = closer.char === "~" ? { type: "strike" } : use === 2 ? { type: "bold" } : { type: "italic" };
380
+ const children = list.slice(openerIndex + 1, closerIndex);
381
+ opener.count -= use;
382
+ closer.count -= use;
383
+ list.splice(openerIndex + 1, closerIndex - openerIndex - 1, { k: "span", mark, children });
384
+ closerIndex = openerIndex + 2;
385
+ closer = list[closerIndex];
386
+ if (closer.k !== "delim") break;
387
+ }
388
+ }
389
+ return list;
390
+ }
391
+ function tokensToNodes(tokens, inherited) {
392
+ const out = [];
393
+ const push = (text, marks) => {
394
+ if (text !== "") out.push(marks.length > 0 ? { type: "text", text, marks } : { type: "text", text });
395
+ };
396
+ for (const token of tokens) {
397
+ switch (token.k) {
398
+ case "text":
399
+ push(decodeEntities(token.text), inherited);
400
+ break;
401
+ case "delim":
402
+ push(token.char.repeat(token.count), inherited);
403
+ break;
404
+ case "nodes":
405
+ for (const node of token.nodes) {
406
+ out.push(node.type === "text" ? { ...node, marks: [...inherited, ...node.marks ?? []] } : node);
407
+ }
408
+ break;
409
+ case "span":
410
+ out.push(...tokensToNodes(token.children, [...inherited, token.mark]));
411
+ break;
412
+ default:
413
+ break;
414
+ }
415
+ }
416
+ return out;
417
+ }
418
+ function parseInlineNodes(text) {
419
+ return tokensToNodes(resolveEmphasis(tokenize(text)), []);
420
+ }
421
+ function markdownToInline(markdown) {
422
+ return normalizeInline(parseInlineNodes(markdown.trim()));
423
+ }
424
+ var HEADING = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?(?:[ \t]+#+)?[ \t]*$/;
425
+ var FENCE = /^ {0,3}(`{3,}|~{3,})[ \t]*([^`]*?)[ \t]*$/;
426
+ var THEMATIC = /^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$/;
427
+ var BULLET = /^( {0,3})([-+*])(?:[ \t]+(.*))?$/;
428
+ var ORDERED = /^( {0,3})(\d{1,9})([.)])(?:[ \t]+(.*))?$/;
429
+ var QUOTE = /^ {0,3}>[ ]?(.*)$/;
430
+ var TABLE_DELIMITER = /^\s*\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)*\|?\s*$/;
431
+ var SETEXT = /^ {0,3}(=+|-+)[ \t]*$/;
432
+ var EQUATION_FENCE = /^ {0,3}\$\$[ \t]*$/;
433
+ var EQUATION_ONE_LINE = /^ {0,3}\$\$(.+)\$\$[ \t]*$/;
434
+ function expandTabs(line) {
435
+ return line.replace(/^[ \t]+/, (indent) => indent.replace(/\t/g, " "));
436
+ }
437
+ function indentOf(line) {
438
+ return /^ */.exec(line)?.[0].length ?? 0;
439
+ }
440
+ function isBlank(line) {
441
+ return line === void 0 || line.trim() === "";
442
+ }
443
+ function matchListMarker(line) {
444
+ if (THEMATIC.test(line)) return null;
445
+ const bullet = BULLET.exec(line);
446
+ if (bullet) {
447
+ const indent = bullet[1].length;
448
+ const text = bullet[3] ?? "";
449
+ const gap = text === "" ? 1 : /^( {0,3}[-+*])([ \t]+)/.exec(line)?.[2]?.replace(/\t/g, " ").length ?? 1;
450
+ return { ordered: false, delimiter: bullet[2], indent, contentOffset: indent + 1 + Math.min(gap, 4), text };
451
+ }
452
+ const ordered = ORDERED.exec(line);
453
+ if (ordered) {
454
+ const indent = ordered[1].length;
455
+ const text = ordered[4] ?? "";
456
+ const marker = ordered[2].length + 1;
457
+ const gap = text === "" ? 1 : /^( {0,3}\d{1,9}[.)])([ \t]+)/.exec(line)?.[2]?.replace(/\t/g, " ").length ?? 1;
458
+ return { ordered: true, delimiter: ordered[3], indent, contentOffset: indent + marker + Math.min(gap, 4), text };
459
+ }
460
+ return null;
461
+ }
462
+ function startsBlock(line, previousIsParagraph) {
463
+ if (HEADING.test(line) || FENCE.test(line) || THEMATIC.test(line) || QUOTE.test(line)) return true;
464
+ if (EQUATION_FENCE.test(line) || EQUATION_ONE_LINE.test(line)) return true;
465
+ if (/^ {0,3}<(details|!--|video|audio)/i.test(line) || /^ {0,3}\[TOC\]\s*$/i.test(line)) return true;
466
+ const marker = matchListMarker(line);
467
+ if (marker) {
468
+ if (previousIsParagraph && (marker.text === "" || marker.ordered && !/^ {0,3}1[.)]/.test(line))) return false;
469
+ return true;
470
+ }
471
+ return false;
472
+ }
473
+ var CALLOUT_ALIASES = {
474
+ info: "info",
475
+ note: "info",
476
+ tip: "success",
477
+ success: "success",
478
+ warning: "warning",
479
+ caution: "danger",
480
+ danger: "danger",
481
+ important: "neutral",
482
+ neutral: "neutral"
483
+ };
484
+ function splitTableRow(line) {
485
+ let trimmed = line.trim();
486
+ if (trimmed.startsWith("|")) trimmed = trimmed.slice(1);
487
+ if (trimmed.endsWith("|") && !trimmed.endsWith("\\|")) trimmed = trimmed.slice(0, -1);
488
+ const cells = [];
489
+ let current = "";
490
+ let inCode = false;
491
+ for (let index = 0; index < trimmed.length; index += 1) {
492
+ const char = trimmed[index];
493
+ if (char === "\\" && trimmed[index + 1] === "|") {
494
+ current += "|";
495
+ index += 1;
496
+ } else if (char === "`") {
497
+ inCode = !inCode;
498
+ current += char;
499
+ } else if (char === "|" && !inCode) {
500
+ cells.push(current.trim());
501
+ current = "";
502
+ } else {
503
+ current += char;
504
+ }
505
+ }
506
+ cells.push(current.trim());
507
+ return cells;
508
+ }
509
+ function joinParagraphInline(parts) {
510
+ const joined = [];
511
+ parts.forEach((part, index) => {
512
+ if (index > 0) joined.push({ type: "text", text: "\n" });
513
+ joined.push(...part);
514
+ });
515
+ return normalizeInline(joined);
516
+ }
517
+ function plainOf(block) {
518
+ if ("content" in block && Array.isArray(block.content)) {
519
+ return block.content.map((node) => node.type === "text" ? node.text : node.type === "mention" ? `@${node.label}` : `:${node.shortcode}:`).join("");
520
+ }
521
+ if (block.type === "code") return block.code;
522
+ return "";
523
+ }
524
+ function itemFromLines(lines, task, ctx) {
525
+ const blocks = parseBlocks(lines, ctx);
526
+ const children = [];
527
+ const runs = [];
528
+ for (const block of blocks) {
529
+ if (block.type === "paragraph") runs.push(block.content);
530
+ else if (block.type === "bulleted-list" || block.type === "numbered-list" || block.type === "task-list") {
531
+ for (const item2 of block.items) children.push(item2);
532
+ } else {
533
+ const text = plainOf(block);
534
+ if (text !== "") runs.push([{ type: "text", text }]);
535
+ }
536
+ }
537
+ const base = { id: ctx.generateId(), content: joinParagraphInline(runs) };
538
+ const item = task ? { ...base, checked: false } : base;
539
+ if (children.length > 0) item.children = children;
540
+ return item;
541
+ }
542
+ function parseList(lines, start, ctx) {
543
+ const first = matchListMarker(lines[start]);
544
+ const items = [];
545
+ let index = start;
546
+ let isTask;
547
+ while (index < lines.length) {
548
+ const marker = matchListMarker(lines[index]);
549
+ if (!marker || marker.ordered !== first.ordered || marker.delimiter !== first.delimiter) break;
550
+ if (index !== start && marker.indent >= first.contentOffset) break;
551
+ const itemLines = [marker.text];
552
+ let cursor = index + 1;
553
+ let previousBlank = false;
554
+ while (cursor < lines.length) {
555
+ const line = lines[cursor];
556
+ if (isBlank(line)) {
557
+ itemLines.push("");
558
+ previousBlank = true;
559
+ cursor += 1;
560
+ continue;
561
+ }
562
+ const indent = indentOf(line);
563
+ if (indent >= marker.contentOffset) {
564
+ itemLines.push(line.slice(marker.contentOffset));
565
+ } else if (!previousBlank && !startsBlock(line, !isBlank(itemLines[itemLines.length - 1])) && !matchListMarker(line)) {
566
+ itemLines.push(line.trimStart());
567
+ } else {
568
+ break;
569
+ }
570
+ previousBlank = false;
571
+ cursor += 1;
572
+ }
573
+ let trailing = 0;
574
+ while (itemLines.length > 1 && isBlank(itemLines[itemLines.length - 1])) {
575
+ itemLines.pop();
576
+ trailing += 1;
577
+ }
578
+ cursor -= trailing;
579
+ let checked;
580
+ const taskMatch = /^\[([ xX])\](?:[ \t]+(.*))?$/.exec(itemLines[0]);
581
+ if (isTask === void 0) isTask = taskMatch !== null;
582
+ else if (taskMatch !== null !== isTask) break;
583
+ if (isTask && taskMatch) {
584
+ checked = taskMatch[1] !== " ";
585
+ itemLines[0] = taskMatch[2] ?? "";
586
+ }
587
+ const item = itemFromLines(itemLines, isTask === true, ctx);
588
+ if (isTask === true) item.checked = checked === true;
589
+ items.push(item);
590
+ index = cursor;
591
+ let lookahead = index;
592
+ while (lookahead < lines.length && isBlank(lines[lookahead])) lookahead += 1;
593
+ const sibling = lookahead < lines.length ? matchListMarker(lines[lookahead]) : null;
594
+ if (sibling && sibling.ordered === first.ordered && sibling.delimiter === first.delimiter && sibling.indent < first.contentOffset) {
595
+ index = lookahead;
596
+ } else {
597
+ break;
598
+ }
599
+ }
600
+ const type = isTask ? "task-list" : first.ordered ? "numbered-list" : "bulleted-list";
601
+ return { block: { id: ctx.generateId(), type, items }, next: index };
602
+ }
603
+ function parseCalloutFromQuote(inner, ctx) {
604
+ const head = inner[0];
605
+ if (!head || head.type !== "paragraph") return null;
606
+ const firstText = head.content[0];
607
+ if (!firstText || firstText.type !== "text") return null;
608
+ const match = /^\[!([A-Za-z]+)\][ \t]*\n?/.exec(firstText.text);
609
+ if (!match) return null;
610
+ const variant = CALLOUT_ALIASES[match[1].toLowerCase()];
611
+ if (!variant) return null;
612
+ const content = [{ ...firstText, text: firstText.text.slice(match[0].length) }, ...head.content.slice(1)];
613
+ const runs = [normalizeInline(content)];
614
+ const trailing = [];
615
+ for (const block of inner.slice(1)) {
616
+ if (block.type === "paragraph") runs.push(block.content);
617
+ else trailing.push(block);
618
+ }
619
+ return [{ id: ctx.generateId(), type: "callout", variant, content: joinParagraphInline(runs) }, ...trailing];
620
+ }
621
+ function parseKitFence(info, code, ctx) {
622
+ const kind = info.toLowerCase();
623
+ if (kind === "mermaid") {
624
+ return { id: ctx.generateId(), type: "diagram", engine: "mermaid", source: code };
625
+ }
626
+ if (!kind.startsWith("kit-")) return null;
627
+ let data;
628
+ try {
629
+ data = JSON.parse(code);
630
+ } catch {
631
+ return null;
632
+ }
633
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return null;
634
+ const record = data;
635
+ switch (kind) {
636
+ case "kit-chart":
637
+ return { id: ctx.generateId(), type: "chart", spec: record };
638
+ case "kit-diagram": {
639
+ const engine = record.engine === "canvas" || record.engine === "excalidraw" ? record.engine : "mermaid";
640
+ const block = { id: ctx.generateId(), type: "diagram", engine };
641
+ if (typeof record.diagram === "object" && record.diagram !== null) block.diagram = record.diagram;
642
+ if (typeof record.source === "string") block.source = record.source;
643
+ return block;
644
+ }
645
+ case "kit-data-view":
646
+ return {
647
+ id: ctx.generateId(),
648
+ type: "data-view",
649
+ dataset: typeof record.dataset === "string" ? record.dataset : "",
650
+ view: record.view
651
+ };
652
+ case "kit-ai": {
653
+ const block = {
654
+ id: ctx.generateId(),
655
+ type: "ai",
656
+ prompt: typeof record.prompt === "string" ? record.prompt : ""
657
+ };
658
+ if (Array.isArray(record.result)) block.result = record.result;
659
+ return block;
660
+ }
661
+ case "kit-block":
662
+ return typeof record.type === "string" ? { ...record, id: typeof record.id === "string" ? record.id : ctx.generateId() } : null;
663
+ default:
664
+ return null;
665
+ }
666
+ }
667
+ function parseMediaTag(line, ctx) {
668
+ const match = /^ {0,3}<(video|audio)\b([^>]*)>\s*<\/\1>\s*$|^ {0,3}<(video|audio)\b([^>]*)\/>\s*$/i.exec(line);
669
+ if (!match) return null;
670
+ const kind = (match[1] ?? match[3] ?? "video").toLowerCase();
671
+ const attributes = ` ${match[2] ?? match[4] ?? ""}`;
672
+ const src = attributeOf(attributes, "src");
673
+ if (src === void 0) return null;
674
+ const block = { id: ctx.generateId(), type: kind, src };
675
+ const title = attributeOf(attributes, "title");
676
+ if (title !== void 0 && title !== "") block.caption = [{ type: "text", text: title }];
677
+ return block;
678
+ }
679
+ var IMAGE_ONLY = /^\s*!\[((?:[^\]\\]|\\.)*)\]\(\s*(<[^>]*>|[^\s)]*)(?:\s+"((?:[^"\\]|\\.)*)")?\s*\)\s*$/;
680
+ function parseImageOnly(text, ctx) {
681
+ const match = IMAGE_ONLY.exec(text);
682
+ if (!match) return null;
683
+ const src = decodeEntities(match[2].replace(/^<|>$/g, ""));
684
+ const block = {
685
+ id: ctx.generateId(),
686
+ type: "image",
687
+ src,
688
+ alt: match[1].replace(/\\([!-/:-@[-`{-~])/g, "$1")
689
+ };
690
+ if (match[3] !== void 0 && match[3] !== "") {
691
+ block.caption = [{ type: "text", text: match[3].replace(/\\(["\\])/g, "$1") }];
692
+ }
693
+ return block;
694
+ }
695
+ function parseDetails(lines, start, ctx) {
696
+ if (!/^ {0,3}<details(\s[^>]*)?>\s*$/i.test(lines[start])) return null;
697
+ let depth = 0;
698
+ let end = -1;
699
+ for (let index = start; index < lines.length; index += 1) {
700
+ const line = lines[index];
701
+ if (/^ {0,3}<details(\s[^>]*)?>\s*$/i.test(line)) depth += 1;
702
+ if (/^ {0,3}<\/details>\s*$/i.test(line)) {
703
+ depth -= 1;
704
+ if (depth === 0) {
705
+ end = index;
706
+ break;
707
+ }
708
+ }
709
+ }
710
+ if (end === -1) return null;
711
+ const body = lines.slice(start + 1, end);
712
+ let summary = [];
713
+ let rest = body;
714
+ const summaryIndex = body.findIndex((line) => /<summary[^>]*>/i.test(line));
715
+ if (summaryIndex !== -1) {
716
+ const joined = body.slice(summaryIndex).join("\n");
717
+ const match = /<summary[^>]*>([\s\S]*?)<\/summary>/i.exec(joined);
718
+ if (match) {
719
+ const summaryText = decodeEntities(match[1].replace(/<[^>]+>/g, "").trim());
720
+ summary = summaryText === "" ? [] : [{ type: "text", text: summaryText }];
721
+ const consumed = joined.slice(0, (match.index ?? 0) + match[0].length).split("\n").length;
722
+ rest = [...body.slice(0, summaryIndex), ...body.slice(summaryIndex + consumed)];
723
+ }
724
+ }
725
+ return {
726
+ block: { id: ctx.generateId(), type: "toggle", summary, children: parseBlocks(rest, ctx) },
727
+ next: end + 1
728
+ };
729
+ }
730
+ function parseBlocks(rawLines, ctx) {
731
+ const lines = rawLines.map(expandTabs);
732
+ const blocks = [];
733
+ let index = 0;
734
+ while (index < lines.length) {
735
+ const line = lines[index];
736
+ if (isBlank(line)) {
737
+ index += 1;
738
+ continue;
739
+ }
740
+ const fenceMatch = FENCE.exec(line);
741
+ if (fenceMatch) {
742
+ const marker = fenceMatch[1];
743
+ const info = fenceMatch[2].split(/\s+/)[0] ?? "";
744
+ const fenceIndent = indentOf(line);
745
+ const body = [];
746
+ index += 1;
747
+ while (index < lines.length) {
748
+ const current = lines[index];
749
+ const closing = new RegExp(`^ {0,3}\\${marker[0]}{${marker.length},}[ \\t]*$`).test(current);
750
+ if (closing) {
751
+ index += 1;
752
+ break;
753
+ }
754
+ body.push(current.replace(new RegExp(`^ {0,${fenceIndent}}`), ""));
755
+ index += 1;
756
+ }
757
+ const code = body.join("\n");
758
+ const kit = parseKitFence(info, code, ctx);
759
+ if (kit) blocks.push(kit);
760
+ else {
761
+ const block = { id: ctx.generateId(), type: "code", code };
762
+ if (info !== "") block.language = info;
763
+ blocks.push(block);
764
+ }
765
+ continue;
766
+ }
767
+ if (EQUATION_FENCE.test(line)) {
768
+ const body = [];
769
+ index += 1;
770
+ while (index < lines.length && !EQUATION_FENCE.test(lines[index])) {
771
+ body.push(lines[index].trimEnd());
772
+ index += 1;
773
+ }
774
+ index += 1;
775
+ blocks.push({ id: ctx.generateId(), type: "equation", latex: body.join("\n").trim() });
776
+ continue;
777
+ }
778
+ const oneLine = EQUATION_ONE_LINE.exec(line);
779
+ if (oneLine) {
780
+ blocks.push({ id: ctx.generateId(), type: "equation", latex: oneLine[1].trim() });
781
+ index += 1;
782
+ continue;
783
+ }
784
+ const heading = HEADING.exec(line);
785
+ if (heading) {
786
+ const level = Math.min(4, heading[1].length);
787
+ blocks.push({ id: ctx.generateId(), type: "heading", level, content: markdownToInline(heading[2] ?? "") });
788
+ index += 1;
789
+ continue;
790
+ }
791
+ if (THEMATIC.test(line)) {
792
+ blocks.push({ id: ctx.generateId(), type: "divider" });
793
+ index += 1;
794
+ continue;
795
+ }
796
+ if (/^ {0,3}<!--\s*-->\s*$/.test(line)) {
797
+ index += 1;
798
+ continue;
799
+ }
800
+ if (/^ {0,3}<!--\s*page-break\s*-->\s*$/i.test(line)) {
801
+ blocks.push({ id: ctx.generateId(), type: "page-break" });
802
+ index += 1;
803
+ continue;
804
+ }
805
+ if (/^ {0,3}(\[TOC\]|<!--\s*toc\s*-->)\s*$/i.test(line)) {
806
+ blocks.push({ id: ctx.generateId(), type: "table-of-contents" });
807
+ index += 1;
808
+ continue;
809
+ }
810
+ const media = parseMediaTag(line, ctx);
811
+ if (media) {
812
+ blocks.push(media);
813
+ index += 1;
814
+ continue;
815
+ }
816
+ const details = parseDetails(lines, index, ctx);
817
+ if (details) {
818
+ blocks.push(details.block);
819
+ index = details.next;
820
+ continue;
821
+ }
822
+ if (QUOTE.test(line)) {
823
+ const inner = [];
824
+ while (index < lines.length) {
825
+ const current = lines[index];
826
+ const quote = QUOTE.exec(current);
827
+ if (quote) {
828
+ inner.push(quote[1]);
829
+ } else if (!isBlank(current) && !startsBlock(current, true) && inner.length > 0 && !isBlank(inner[inner.length - 1])) {
830
+ inner.push(current.trim());
831
+ } else break;
832
+ index += 1;
833
+ }
834
+ const parsed = parseBlocks(inner, ctx);
835
+ const callout = parseCalloutFromQuote(parsed, ctx);
836
+ if (callout) {
837
+ blocks.push(...callout);
838
+ } else {
839
+ const runs = [];
840
+ const trailing = [];
841
+ for (const block of parsed) {
842
+ if (block.type === "paragraph" || block.type === "heading") runs.push(block.content);
843
+ else trailing.push(block);
844
+ }
845
+ if (runs.length > 0 || trailing.length === 0) {
846
+ blocks.push({ id: ctx.generateId(), type: "quote", content: joinParagraphInline(runs) });
847
+ }
848
+ blocks.push(...trailing);
849
+ }
850
+ continue;
851
+ }
852
+ if (matchListMarker(line)) {
853
+ const result = parseList(lines, index, ctx);
854
+ blocks.push(result.block);
855
+ index = result.next;
856
+ continue;
857
+ }
858
+ if (line.includes("|") && index + 1 < lines.length && TABLE_DELIMITER.test(lines[index + 1]) && lines[index + 1].includes("-")) {
859
+ const header = splitTableRow(line);
860
+ const delimiter = splitTableRow(lines[index + 1]);
861
+ if (header.length === delimiter.length) {
862
+ const rows = [header.map((text2) => ({ content: markdownToInline(text2) }))];
863
+ index += 2;
864
+ while (index < lines.length && !isBlank(lines[index]) && lines[index].includes("|")) {
865
+ const cells = splitTableRow(lines[index]);
866
+ const row = [];
867
+ for (let column = 0; column < header.length; column += 1) {
868
+ row.push({ content: markdownToInline(cells[column] ?? "") });
869
+ }
870
+ rows.push(row);
871
+ index += 1;
872
+ }
873
+ blocks.push({ id: ctx.generateId(), type: "table", headerRow: true, rows });
874
+ continue;
875
+ }
876
+ }
877
+ const paragraph = [line];
878
+ index += 1;
879
+ let setext = null;
880
+ while (index < lines.length) {
881
+ const current = lines[index];
882
+ if (isBlank(current)) break;
883
+ const underline = SETEXT.exec(current);
884
+ if (underline) {
885
+ setext = underline[1].startsWith("=") ? 1 : 2;
886
+ index += 1;
887
+ break;
888
+ }
889
+ if (startsBlock(current, true)) break;
890
+ if (current.includes("|") && TABLE_DELIMITER.test(lines[index + 1] ?? "") && (lines[index + 1] ?? "").includes("-")) break;
891
+ paragraph.push(current);
892
+ index += 1;
893
+ }
894
+ const text = paragraph.map((part, position) => position === paragraph.length - 1 ? part.trimEnd() : part).join("\n").trim();
895
+ if (setext !== null) {
896
+ blocks.push({ id: ctx.generateId(), type: "heading", level: setext, content: markdownToInline(text.replace(/\n/g, " ")) });
897
+ continue;
898
+ }
899
+ const image = parseImageOnly(text, ctx);
900
+ if (image) {
901
+ blocks.push(image);
902
+ continue;
903
+ }
904
+ blocks.push({ id: ctx.generateId(), type: "paragraph", content: markdownToInline(text) });
905
+ }
906
+ return blocks;
907
+ }
908
+ function markdownToDocument(markdown, options = {}) {
909
+ const ctx = { generateId: options.generateId ?? generateBlockId };
910
+ const lines = markdown.replace(/\r\n?/g, "\n").split("\n");
911
+ const doc = { version: DOCUMENT_VERSION, blocks: parseBlocks(lines, ctx) };
912
+ if (options.title !== void 0) doc.title = options.title;
913
+ return doc;
914
+ }
915
+
916
+ // src/diff.ts
917
+ function signatureOf(block) {
918
+ const copy = { ...block };
919
+ if (block.type === "toggle") copy.children = (block.children ?? []).map((child) => child.id);
920
+ if (block.type === "columns") copy.columns = (block.columns ?? []).map((column) => column.map((child) => child.id));
921
+ if (block.type === "ai" && block.result) copy.result = block.result.map((child) => child.id);
922
+ return JSON.stringify(copy);
923
+ }
924
+ function indexBlocks(doc) {
925
+ const order = [];
926
+ const entries = /* @__PURE__ */ new Map();
927
+ walkBlocks(doc.blocks ?? [], (block, parent) => {
928
+ order.push(block.id);
929
+ entries.set(block.id, { signature: signatureOf(block), parentId: parent?.id ?? null });
930
+ });
931
+ return { order, entries };
932
+ }
933
+ function longestCommonSubsequence(a, b) {
934
+ const table = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
935
+ for (let i2 = a.length - 1; i2 >= 0; i2 -= 1) {
936
+ for (let j2 = b.length - 1; j2 >= 0; j2 -= 1) {
937
+ const row = table[i2];
938
+ row[j2] = a[i2] === b[j2] ? table[i2 + 1][j2 + 1] + 1 : Math.max(table[i2 + 1][j2], row[j2 + 1]);
939
+ }
940
+ }
941
+ const common = /* @__PURE__ */ new Set();
942
+ let i = 0;
943
+ let j = 0;
944
+ while (i < a.length && j < b.length) {
945
+ if (a[i] === b[j]) {
946
+ common.add(a[i]);
947
+ i += 1;
948
+ j += 1;
949
+ } else if (table[i + 1][j] >= table[i][j + 1]) {
950
+ i += 1;
951
+ } else {
952
+ j += 1;
953
+ }
954
+ }
955
+ return common;
956
+ }
957
+ function diffDocuments(previous, next) {
958
+ const before = indexBlocks(previous);
959
+ const after = indexBlocks(next);
960
+ const added = after.order.filter((id) => !before.entries.has(id));
961
+ const removed = before.order.filter((id) => !after.entries.has(id));
962
+ const shared = after.order.filter((id) => before.entries.has(id));
963
+ const changed = [];
964
+ const candidates = [];
965
+ for (const id of shared) {
966
+ if (before.entries.get(id)?.signature !== after.entries.get(id)?.signature) changed.push(id);
967
+ else candidates.push(id);
968
+ }
969
+ const beforeShared = before.order.filter((id) => after.entries.has(id));
970
+ const stable = longestCommonSubsequence(beforeShared, shared);
971
+ const moved = [];
972
+ const unchanged = [];
973
+ for (const id of candidates) {
974
+ const sameParent = before.entries.get(id)?.parentId === after.entries.get(id)?.parentId;
975
+ if (stable.has(id) && sameParent) unchanged.push(id);
976
+ else moved.push(id);
977
+ }
978
+ return { added, removed, changed, moved, unchanged };
979
+ }
980
+
981
+ // src/validate.ts
982
+ function isRecord(value) {
983
+ return typeof value === "object" && value !== null && !Array.isArray(value);
984
+ }
985
+ function createCollector() {
986
+ const issues = [];
987
+ return {
988
+ issues,
989
+ add(code, severity, path, message) {
990
+ issues.push({ code, severity, path, message });
991
+ }
992
+ };
993
+ }
994
+ function checkInline(inline, path, c) {
995
+ if (!Array.isArray(inline)) {
996
+ c.add("invalid-inline", "error", path, "Inline content must be an array.");
997
+ return;
998
+ }
999
+ inline.forEach((node, index) => {
1000
+ const here = `${path}[${index}]`;
1001
+ if (!isRecord(node)) {
1002
+ c.add("invalid-inline", "error", here, "An inline node must be an object.");
1003
+ return;
1004
+ }
1005
+ if (node.type === "text") {
1006
+ if (typeof node.text !== "string") c.add("invalid-inline", "error", here, "A text node needs a string `text`.");
1007
+ if (node.marks === void 0) return;
1008
+ if (!Array.isArray(node.marks)) {
1009
+ c.add("invalid-inline", "error", `${here}.marks`, "Marks must be an array.");
1010
+ return;
1011
+ }
1012
+ node.marks.forEach((mark, markIndex) => {
1013
+ const markPath = `${here}.marks[${markIndex}]`;
1014
+ if (!isRecord(mark)) {
1015
+ c.add("invalid-inline", "error", markPath, "A mark must be an object.");
1016
+ return;
1017
+ }
1018
+ switch (mark.type) {
1019
+ case "bold":
1020
+ case "italic":
1021
+ case "underline":
1022
+ case "strike":
1023
+ case "code":
1024
+ break;
1025
+ case "link":
1026
+ if (typeof mark.href !== "string") c.add("invalid-inline", "error", markPath, "A link mark needs a string `href`.");
1027
+ else if (sanitizeUrl(mark.href) === null) c.add("unsafe-url", "warning", markPath, `The link "${mark.href}" is not a safe URL and will not render.`);
1028
+ break;
1029
+ case "color":
1030
+ if (!isTextColorToken(mark.token)) c.add("unknown-token", "warning", markPath, `"${String(mark.token)}" is not an allowed text color token.`);
1031
+ break;
1032
+ case "highlight":
1033
+ if (!isHighlightToken(mark.token)) c.add("unknown-token", "warning", markPath, `"${String(mark.token)}" is not an allowed highlight token.`);
1034
+ break;
1035
+ default:
1036
+ c.add("invalid-inline", "warning", markPath, `The mark "${String(mark.type)}" is not supported.`);
1037
+ }
1038
+ });
1039
+ } else if (node.type === "mention") {
1040
+ if (typeof node.id !== "string" || typeof node.label !== "string") c.add("invalid-inline", "error", here, "A mention needs string `id` and `label`.");
1041
+ } else if (node.type === "emoji") {
1042
+ if (typeof node.shortcode !== "string") c.add("invalid-inline", "error", here, "An emoji needs a string `shortcode`.");
1043
+ } else {
1044
+ c.add("invalid-inline", "warning", here, `The inline node "${String(node.type)}" is not supported.`);
1045
+ }
1046
+ });
1047
+ }
1048
+ function checkItems(items, path, task, c) {
1049
+ if (!Array.isArray(items)) {
1050
+ c.add("invalid-field", "error", path, "`items` must be an array.");
1051
+ return;
1052
+ }
1053
+ items.forEach((item, index) => {
1054
+ const here = `${path}[${index}]`;
1055
+ if (!isRecord(item)) {
1056
+ c.add("invalid-field", "error", here, "A list item must be an object.");
1057
+ return;
1058
+ }
1059
+ if (typeof item.id !== "string" || item.id === "") c.add("missing-id", "error", here, "A list item needs a string `id`.");
1060
+ checkInline(item.content, `${here}.content`, c);
1061
+ if (task && typeof item.checked !== "boolean") c.add("invalid-field", "error", `${here}.checked`, "A task item needs a boolean `checked`.");
1062
+ if (item.children !== void 0) checkItems(item.children, `${here}.children`, task, c);
1063
+ });
1064
+ }
1065
+ function checkUrl(value, path, c, media) {
1066
+ if (typeof value !== "string") {
1067
+ c.add("invalid-field", "error", path, "Expected a string URL.");
1068
+ return;
1069
+ }
1070
+ const safe = media ? sanitizeMediaUrl(value) : sanitizeUrl(value);
1071
+ if (safe === null && value !== "") c.add("unsafe-url", "warning", path, `"${value.slice(0, 40)}" is not a safe URL and will not render.`);
1072
+ }
1073
+ function checkBlocks(blocks, path, c) {
1074
+ if (!Array.isArray(blocks)) {
1075
+ c.add("invalid-field", "error", path, "Expected an array of blocks.");
1076
+ return;
1077
+ }
1078
+ blocks.forEach((block, index) => checkBlock(block, `${path}[${index}]`, c));
1079
+ }
1080
+ function checkBlock(block, path, c) {
1081
+ if (!isRecord(block)) {
1082
+ c.add("invalid-block", "error", path, "A block must be an object.");
1083
+ return;
1084
+ }
1085
+ if (typeof block.id !== "string" || block.id === "") c.add("missing-id", "error", path, "A block needs a string `id`.");
1086
+ if (!isKnownBlockType(block.type)) {
1087
+ c.add("unknown-block-type", "warning", path, `The block type "${String(block.type)}" is not known; it is kept as an opaque block.`);
1088
+ return;
1089
+ }
1090
+ switch (block.type) {
1091
+ case "paragraph":
1092
+ case "quote":
1093
+ checkInline(block.content, `${path}.content`, c);
1094
+ break;
1095
+ case "heading":
1096
+ if (![1, 2, 3, 4].includes(block.level)) c.add("invalid-field", "error", `${path}.level`, "A heading level must be 1, 2, 3 or 4.");
1097
+ checkInline(block.content, `${path}.content`, c);
1098
+ break;
1099
+ case "callout":
1100
+ if (!CALLOUT_VARIANTS.includes(block.variant)) c.add("invalid-field", "error", `${path}.variant`, "Unknown callout variant.");
1101
+ checkInline(block.content, `${path}.content`, c);
1102
+ break;
1103
+ case "toggle":
1104
+ checkInline(block.summary, `${path}.summary`, c);
1105
+ checkBlocks(block.children, `${path}.children`, c);
1106
+ break;
1107
+ case "bulleted-list":
1108
+ case "numbered-list":
1109
+ checkItems(block.items, `${path}.items`, false, c);
1110
+ break;
1111
+ case "task-list":
1112
+ checkItems(block.items, `${path}.items`, true, c);
1113
+ break;
1114
+ case "code":
1115
+ if (typeof block.code !== "string") c.add("invalid-field", "error", `${path}.code`, "A code block needs a string `code`.");
1116
+ break;
1117
+ case "equation":
1118
+ if (typeof block.latex !== "string") c.add("invalid-field", "error", `${path}.latex`, "An equation needs a string `latex`.");
1119
+ break;
1120
+ case "table":
1121
+ if (!Array.isArray(block.rows) || block.rows.some((row) => !Array.isArray(row))) {
1122
+ c.add("invalid-field", "error", `${path}.rows`, "`rows` must be an array of arrays of cells.");
1123
+ } else {
1124
+ block.rows.forEach(
1125
+ (row, rowIndex) => row.forEach((cell, columnIndex) => {
1126
+ const here = `${path}.rows[${rowIndex}][${columnIndex}]`;
1127
+ if (!isRecord(cell)) c.add("invalid-field", "error", here, "A table cell must be an object.");
1128
+ else checkInline(cell.content, `${here}.content`, c);
1129
+ })
1130
+ );
1131
+ }
1132
+ break;
1133
+ case "image":
1134
+ checkUrl(block.src, `${path}.src`, c, true);
1135
+ if (typeof block.alt !== "string") c.add("invalid-field", "error", `${path}.alt`, "An image needs a string `alt` (an empty string marks it decorative).");
1136
+ if (block.caption !== void 0) checkInline(block.caption, `${path}.caption`, c);
1137
+ break;
1138
+ case "video":
1139
+ case "audio":
1140
+ checkUrl(block.src, `${path}.src`, c, true);
1141
+ if (block.caption !== void 0) checkInline(block.caption, `${path}.caption`, c);
1142
+ break;
1143
+ case "file":
1144
+ checkUrl(block.src, `${path}.src`, c, false);
1145
+ if (typeof block.name !== "string") c.add("invalid-field", "error", `${path}.name`, "A file needs a string `name`.");
1146
+ break;
1147
+ case "bookmark":
1148
+ checkUrl(block.url, `${path}.url`, c, false);
1149
+ break;
1150
+ case "columns":
1151
+ if (!Array.isArray(block.columns)) c.add("invalid-field", "error", `${path}.columns`, "`columns` must be an array of block arrays.");
1152
+ else block.columns.forEach((column, index) => checkBlocks(column, `${path}.columns[${index}]`, c));
1153
+ break;
1154
+ case "chart":
1155
+ if (!isRecord(block.spec)) c.add("invalid-field", "error", `${path}.spec`, "A chart needs a `spec` object.");
1156
+ break;
1157
+ case "diagram":
1158
+ if (!["mermaid", "canvas", "excalidraw"].includes(block.engine)) c.add("invalid-field", "error", `${path}.engine`, "Unknown diagram engine.");
1159
+ break;
1160
+ case "data-view":
1161
+ if (typeof block.dataset !== "string") c.add("invalid-field", "error", `${path}.dataset`, "A data view needs a string `dataset`.");
1162
+ if (!isRecord(block.view)) c.add("invalid-field", "error", `${path}.view`, "A data view needs a `view` object.");
1163
+ break;
1164
+ case "ai":
1165
+ if (typeof block.prompt !== "string") c.add("invalid-field", "error", `${path}.prompt`, "An AI block needs a string `prompt`.");
1166
+ if (block.result !== void 0) checkBlocks(block.result, `${path}.result`, c);
1167
+ break;
1168
+ default:
1169
+ break;
1170
+ }
1171
+ }
1172
+ function validateDocument(value) {
1173
+ const c = createCollector();
1174
+ if (!isRecord(value)) {
1175
+ c.add("invalid-document", "error", "", "A document must be an object.");
1176
+ return { valid: false, issues: c.issues };
1177
+ }
1178
+ if (value.version !== DOCUMENT_VERSION) {
1179
+ c.add("unsupported-version", "error", "version", `Expected document version ${DOCUMENT_VERSION}, got ${String(value.version)}.`);
1180
+ }
1181
+ if (value.title !== void 0 && typeof value.title !== "string") c.add("invalid-field", "error", "title", "`title` must be a string.");
1182
+ checkBlocks(value.blocks, "blocks", c);
1183
+ if (c.issues.every((issue) => issue.code !== "invalid-field" || issue.path !== "blocks")) {
1184
+ const seen = /* @__PURE__ */ new Set();
1185
+ const reported = /* @__PURE__ */ new Set();
1186
+ try {
1187
+ for (const id of collectIds(value)) {
1188
+ if (seen.has(id) && !reported.has(id)) {
1189
+ reported.add(id);
1190
+ c.add("duplicate-id", "error", "", `The id "${id}" is used more than once.`);
1191
+ }
1192
+ seen.add(id);
1193
+ }
1194
+ } catch {
1195
+ }
1196
+ }
1197
+ return { valid: c.issues.every((issue) => issue.severity !== "error"), issues: c.issues };
1198
+ }
1199
+ function isEditorDocument(value) {
1200
+ return validateDocument(value).valid;
1201
+ }
1202
+ function sanitizeInline(inline) {
1203
+ return (inline ?? []).map((node) => {
1204
+ if (node.type !== "text" || !node.marks) return node;
1205
+ const marks = node.marks.filter((mark) => {
1206
+ if (mark.type === "link") return sanitizeUrl(mark.href) !== null;
1207
+ if (mark.type === "color") return isTextColorToken(mark.token);
1208
+ if (mark.type === "highlight") return isHighlightToken(mark.token);
1209
+ return true;
1210
+ });
1211
+ const { marks: _marks, ...rest } = node;
1212
+ void _marks;
1213
+ return marks.length > 0 ? { ...rest, marks } : rest;
1214
+ });
1215
+ }
1216
+ function sanitizeItems(items) {
1217
+ return items.map((item) => {
1218
+ const copy = { ...item, content: sanitizeInline(item.content) };
1219
+ if (item.children) copy.children = sanitizeItems(item.children);
1220
+ return copy;
1221
+ });
1222
+ }
1223
+ function sanitizeBlock(block) {
1224
+ switch (block.type) {
1225
+ case "paragraph":
1226
+ case "heading":
1227
+ case "quote":
1228
+ case "callout":
1229
+ return { ...block, content: sanitizeInline(block.content) };
1230
+ case "toggle":
1231
+ return { ...block, summary: sanitizeInline(block.summary), children: sanitizeBlocks(block.children ?? []) };
1232
+ case "bulleted-list":
1233
+ case "numbered-list":
1234
+ return { ...block, items: sanitizeItems(block.items ?? []) };
1235
+ case "task-list":
1236
+ return { ...block, items: sanitizeItems(block.items ?? []) };
1237
+ case "table":
1238
+ return {
1239
+ ...block,
1240
+ rows: (block.rows ?? []).map((row) => row.map((cell) => ({ ...cell, content: sanitizeInline(cell.content) })))
1241
+ };
1242
+ case "image": {
1243
+ const copy = { ...block, src: sanitizeMediaUrl(block.src) ?? "" };
1244
+ if (block.caption) copy.caption = sanitizeInline(block.caption);
1245
+ return copy;
1246
+ }
1247
+ case "video":
1248
+ case "audio": {
1249
+ const copy = { ...block, src: sanitizeMediaUrl(block.src) ?? "" };
1250
+ if (block.caption) copy.caption = sanitizeInline(block.caption);
1251
+ return copy;
1252
+ }
1253
+ case "file":
1254
+ return { ...block, src: sanitizeUrl(block.src) ?? "" };
1255
+ case "bookmark":
1256
+ return { ...block, url: sanitizeUrl(block.url) ?? "" };
1257
+ case "columns":
1258
+ return { ...block, columns: (block.columns ?? []).map(sanitizeBlocks) };
1259
+ case "ai":
1260
+ return block.result ? { ...block, result: sanitizeBlocks(block.result) } : block;
1261
+ default:
1262
+ return block;
1263
+ }
1264
+ }
1265
+ function sanitizeBlocks(blocks) {
1266
+ return blocks.map(sanitizeBlock);
1267
+ }
1268
+ function sanitizeDocument(doc) {
1269
+ return { ...doc, blocks: sanitizeBlocks(doc.blocks ?? []) };
1270
+ }
1271
+ export {
1272
+ BLOCK_ID_NODES,
1273
+ BLOCK_TYPES,
1274
+ CALLOUT_VARIANTS,
1275
+ DOCUMENT_VERSION,
1276
+ EMOJI_SHORTCODES,
1277
+ HIGHLIGHT_TOKENS,
1278
+ MARK_TYPES,
1279
+ TEXT_COLOR_TOKENS,
1280
+ TIPTAP_MARKS,
1281
+ TIPTAP_NODES,
1282
+ buildOutlineTree,
1283
+ collectIds,
1284
+ countCharacters,
1285
+ countWords,
1286
+ createEmptyDocument,
1287
+ createParagraph,
1288
+ createSeededRandom,
1289
+ createSequentialIdGenerator,
1290
+ defaultExportLabels,
1291
+ diffDocuments,
1292
+ documentToHtml,
1293
+ documentToMarkdown,
1294
+ documentToPlainText,
1295
+ documentToTiptap,
1296
+ emojiForShortcode,
1297
+ emojiToText,
1298
+ escapeHtml,
1299
+ escapeMarkdown,
1300
+ findDuplicateIds,
1301
+ generateBlockId,
1302
+ getDocumentStats,
1303
+ getOutline,
1304
+ highlightToCss,
1305
+ inlineToPlainText,
1306
+ isEditorDocument,
1307
+ isHighlightToken,
1308
+ isKnownBlockType,
1309
+ isSafeUrl,
1310
+ isTextColorToken,
1311
+ markdownToDocument,
1312
+ markdownToInline,
1313
+ normalizeDocument,
1314
+ normalizeInline,
1315
+ normalizeUrlInput,
1316
+ sanitizeDocument,
1317
+ sanitizeMediaUrl,
1318
+ sanitizeUrl,
1319
+ slugifyHeading,
1320
+ textColorToCss,
1321
+ tiptapToDocument,
1322
+ tokenToCssVarName,
1323
+ validateDocument,
1324
+ walkBlocks
1325
+ };
1326
+ //# sourceMappingURL=index.js.map