@ai-matrx/rich-editor 0.0.0 → 0.1.1

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 (58) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +1 -1
  3. package/README.md +13 -3
  4. package/dist/core/block-insert.d.ts +27 -0
  5. package/dist/core/block-insert.js +24 -0
  6. package/dist/core/caret-context.d.ts +24 -0
  7. package/dist/core/caret-context.js +73 -0
  8. package/dist/core/clipboard-text.d.ts +5 -0
  9. package/dist/core/clipboard-text.js +47 -0
  10. package/dist/core/commands.d.ts +65 -0
  11. package/dist/core/commands.js +299 -0
  12. package/dist/core/extensions.d.ts +29 -0
  13. package/dist/core/extensions.js +326 -0
  14. package/dist/core/find-replace.d.ts +34 -0
  15. package/dist/core/find-replace.js +67 -0
  16. package/dist/core/history-approval.d.ts +11 -0
  17. package/dist/core/history-approval.js +43 -0
  18. package/dist/core/host-value.d.ts +29 -0
  19. package/dist/core/host-value.js +10 -0
  20. package/dist/core/html-to-markdown.d.ts +3 -0
  21. package/dist/core/html-to-markdown.js +12 -0
  22. package/dist/core/markdown-format.d.ts +18 -0
  23. package/dist/core/markdown-format.js +354 -0
  24. package/dist/core/markdown-parse.d.ts +35 -0
  25. package/dist/core/markdown-parse.js +533 -0
  26. package/dist/core/markdown-serialize.d.ts +27 -0
  27. package/dist/core/markdown-serialize.js +394 -0
  28. package/dist/core/outline.d.ts +17 -0
  29. package/dist/core/outline.js +41 -0
  30. package/dist/core/paste-html.d.ts +10 -0
  31. package/dist/core/paste-html.js +198 -0
  32. package/dist/core/paste-markdown.d.ts +18 -0
  33. package/dist/core/paste-markdown.js +70 -0
  34. package/dist/core/placeholders.d.ts +22 -0
  35. package/dist/core/placeholders.js +60 -0
  36. package/dist/core/save-plan.d.ts +47 -0
  37. package/dist/core/save-plan.js +287 -0
  38. package/dist/core/shortcuts.d.ts +13 -0
  39. package/dist/core/shortcuts.js +50 -0
  40. package/dist/core/source-format.d.ts +28 -0
  41. package/dist/core/source-format.js +104 -0
  42. package/dist/core/text-metrics.d.ts +11 -0
  43. package/dist/core/text-metrics.js +35 -0
  44. package/dist/core/variable-name.d.ts +13 -0
  45. package/dist/core/variable-name.js +13 -0
  46. package/dist/core/variables.d.ts +37 -0
  47. package/dist/core/variables.js +77 -0
  48. package/dist/core/visual-document.d.ts +52 -0
  49. package/dist/core/visual-document.js +142 -0
  50. package/dist/core/visual-find.d.ts +27 -0
  51. package/dist/core/visual-find.js +93 -0
  52. package/dist/format/format-target.d.ts +39 -0
  53. package/dist/format/format-target.js +79 -0
  54. package/dist/in-place/caret-handoff.d.ts +3 -0
  55. package/dist/in-place/caret-handoff.js +14 -0
  56. package/dist/in-place/in-place-session.d.ts +24 -0
  57. package/dist/in-place/in-place-session.js +32 -0
  58. package/package.json +95 -5
@@ -0,0 +1,394 @@
1
+ import { assertTableReadsBack, freshRow, respliceRow, splitRowSegments } from "@ai-matrx/rich-content/utils/table-source";
2
+ function createSerializeContext(adjacency = /* @__PURE__ */ new Map(), reuse) {
3
+ return { adjacency, claimed: /* @__PURE__ */ new Set(), reuse };
4
+ }
5
+ const DEFAULT_BULLET = "-";
6
+ const DEFAULT_BOLD = "**";
7
+ const DEFAULT_ITALIC = "*";
8
+ const DEFAULT_STRIKE = "~~";
9
+ function attr(node, key) {
10
+ const value = node.attrs[key];
11
+ return value === void 0 || value === null ? null : value;
12
+ }
13
+ function claim(node, ctx) {
14
+ const id = attr(node, "mdId");
15
+ if (!id || ctx.claimed.has(id)) return null;
16
+ ctx.claimed.add(id);
17
+ return id;
18
+ }
19
+ function separator(prevId, id, fallback, ctx) {
20
+ if (prevId && id) {
21
+ const adjacency = ctx.adjacency.get(prevId);
22
+ if (adjacency && adjacency.next === id) return adjacency.trail;
23
+ }
24
+ return fallback;
25
+ }
26
+ function isDroppableEmpty(node) {
27
+ return node.isTextblock && node.content.size === 0;
28
+ }
29
+ function serializeChildren(parent, kind, ctx) {
30
+ let out = "";
31
+ let prevId = null;
32
+ let first = true;
33
+ parent.forEach((child) => {
34
+ const id = claim(child, ctx);
35
+ const reused = id && ctx.reuse ? ctx.reuse(id, child) : null;
36
+ const body = reused ?? serializeBlock(child, ctx);
37
+ if (body === "" && isDroppableEmpty(child)) return;
38
+ if (!first) {
39
+ const fallback = kind === "listItem" && (child.type.name === "bulletList" || child.type.name === "orderedList") ? "\n" : "\n\n";
40
+ out += separator(prevId, id, fallback, ctx);
41
+ }
42
+ out += body;
43
+ first = false;
44
+ prevId = id;
45
+ });
46
+ if (kind === "wrapper" && prevId) {
47
+ const adjacency = ctx.adjacency.get(prevId);
48
+ if (adjacency && adjacency.next === null) out += adjacency.trail;
49
+ }
50
+ return out;
51
+ }
52
+ function serializeBlock(node, ctx) {
53
+ switch (node.type.name) {
54
+ case "paragraph":
55
+ return serializeInline(node);
56
+ case "heading":
57
+ return serializeHeading(node);
58
+ case "bulletList":
59
+ case "orderedList":
60
+ return serializeList(node, ctx);
61
+ case "listItem":
62
+ return serializeListItem(node, DEFAULT_BULLET, ctx);
63
+ case "blockquote":
64
+ return serializeBlockquote(node, ctx);
65
+ case "horizontalRule":
66
+ return attr(node, "mdRaw") ?? "---";
67
+ case "table":
68
+ return serializeTable(node);
69
+ case "sourceLocked":
70
+ case "islandBlock":
71
+ return String(node.attrs.raw ?? "");
72
+ case "sourceBlock":
73
+ return serializeChildren(node, "wrapper", ctx);
74
+ case "doc":
75
+ return serializeChildren(node, "doc", ctx);
76
+ default:
77
+ return node.textContent;
78
+ }
79
+ }
80
+ function serializeHeading(node) {
81
+ const level = Number(node.attrs.level) || 1;
82
+ const text = serializeInline(node);
83
+ let open = attr(node, "mdOpen");
84
+ let close = attr(node, "mdClose") ?? "";
85
+ if (open !== null) {
86
+ const setext = close.startsWith("\n");
87
+ if (setext) {
88
+ if (level > 2) open = null;
89
+ else {
90
+ const underline = level === 1 ? "=" : "-";
91
+ const lineLength = Math.max(3, close.length - 1);
92
+ const expected = close.slice(1).trimEnd();
93
+ if (!expected.split("").every((char) => char === underline)) {
94
+ close = "\n" + underline.repeat(lineLength);
95
+ }
96
+ return open + text + close;
97
+ }
98
+ } else {
99
+ const atx = /^ {0,3}(#{1,6})(?=[ \t]|$)/.exec(open);
100
+ if (!atx || (atx[1] ?? "").length !== level) open = null;
101
+ }
102
+ }
103
+ if (open === null) {
104
+ open = `${"#".repeat(level)} `;
105
+ close = "";
106
+ }
107
+ return open + text.replace(/\n/g, " ") + close;
108
+ }
109
+ function nextMarker(ordered, prev, start, index) {
110
+ if (!ordered) return prev && /^[-*+]$/.test(prev) ? prev : DEFAULT_BULLET;
111
+ if (prev) {
112
+ const match = /^(\d+)([.)])$/.exec(prev);
113
+ if (match) return `${Number(match[1]) + 1}${match[2]}`;
114
+ }
115
+ return `${start + index}.`;
116
+ }
117
+ function serializeList(list, ctx) {
118
+ const ordered = list.type.name === "orderedList";
119
+ const start = Number(list.attrs.start ?? 1) || 1;
120
+ let out = "";
121
+ let prevId = null;
122
+ let prevMarker = null;
123
+ let index = 0;
124
+ list.forEach((item) => {
125
+ const id = claim(item, ctx);
126
+ const stored = attr(item, "mdMarker");
127
+ const marker = stored && (ordered ? /^\d+[.)]$/.test(stored) : /^[-*+]$/.test(stored)) ? stored : nextMarker(ordered, prevMarker, start, index);
128
+ const body = serializeListItem(item, marker, ctx);
129
+ if (index > 0) out += separator(prevId, id, "\n", ctx);
130
+ out += body;
131
+ prevId = id;
132
+ prevMarker = marker;
133
+ index += 1;
134
+ });
135
+ return out;
136
+ }
137
+ function serializeListItem(item, marker, ctx) {
138
+ const lead = attr(item, "mdLead") ?? "";
139
+ const after = attr(item, "mdAfter") ?? " ";
140
+ const task = attr(item, "mdTask") ?? "";
141
+ const body = serializeChildren(item, "listItem", ctx);
142
+ const head = lead + marker;
143
+ if (body === "") return task ? `${head}${after || " "}${task}`.trimEnd() : head;
144
+ const indent = attr(item, "mdIndent") ?? (lead + marker + (after || " ")).length;
145
+ const pad = " ".repeat(indent);
146
+ const lines = body.split("\n");
147
+ let out = head + (after || " ") + task + (lines[0] ?? "");
148
+ for (const line of lines.slice(1)) out += `
149
+ ${line === "" ? "" : pad + line}`;
150
+ return out;
151
+ }
152
+ function serializeBlockquote(node, ctx) {
153
+ const prefix = attr(node, "mdPrefix") ?? "> ";
154
+ const alert = attr(node, "mdAlert");
155
+ const children = serializeChildren(node, "blockquote", ctx);
156
+ const body = alert ? children ? `${alert}
157
+ ${children}` : alert : children;
158
+ return body.split("\n").map((line) => line === "" ? prefix.trimEnd() : prefix + line).join("\n");
159
+ }
160
+ function cellText(cell) {
161
+ const paragraph = cell.firstChild;
162
+ return paragraph ? serializeInline(paragraph) : "";
163
+ }
164
+ function delimiterFor(align, width) {
165
+ if (width === void 0) {
166
+ if (align === "left") return ":---";
167
+ if (align === "right") return "---:";
168
+ if (align === "center") return ":---:";
169
+ return "---";
170
+ }
171
+ const n = Math.max(width, 3);
172
+ if (align === "left") return `:${"-".repeat(n - 1)}`;
173
+ if (align === "right") return `${"-".repeat(n - 1)}:`;
174
+ if (align === "center") return `:${"-".repeat(n - 2)}:`;
175
+ return "-".repeat(n);
176
+ }
177
+ function alignOfDelimiter(cell) {
178
+ const core = cell.trim();
179
+ const left = core.startsWith(":");
180
+ const right = core.endsWith(":");
181
+ if (left && right) return "center";
182
+ if (left) return "left";
183
+ if (right) return "right";
184
+ return null;
185
+ }
186
+ function respliceDelimiter(stored, aligns, lead, trail) {
187
+ const segments = splitRowSegments(stored);
188
+ const first = lead ? 1 : 0;
189
+ const last = trail ? segments.length - 1 : segments.length;
190
+ const cells = segments.slice(first, last);
191
+ if (cells.length !== aligns.length) return null;
192
+ const next = cells.map((cell, index) => {
193
+ const align = aligns[index] ?? null;
194
+ if (alignOfDelimiter(cell) === align) return cell;
195
+ const core = cell.trim();
196
+ const start = cell.indexOf(core);
197
+ return `${cell.slice(0, start)}${delimiterFor(align, core.length)}${cell.slice(start + core.length)}`;
198
+ });
199
+ return [...segments.slice(0, first), ...next, ...segments.slice(last)].join("|");
200
+ }
201
+ function parseJsonArray(value) {
202
+ if (!value) return null;
203
+ try {
204
+ const parsed = JSON.parse(value);
205
+ return Array.isArray(parsed) ? parsed : null;
206
+ } catch {
207
+ return null;
208
+ }
209
+ }
210
+ function serializeTable(table) {
211
+ const pipes = attr(table, "mdPipes") ?? "both";
212
+ const lead = pipes === "both" || pipes === "lead";
213
+ const trail = pipes === "both" || pipes === "trail";
214
+ const lines = [];
215
+ let aligns = [];
216
+ table.forEach((row, _offset, index) => {
217
+ const cells = [];
218
+ row.forEach((cell) => cells.push(cell));
219
+ if (index === 0) {
220
+ aligns = cells.map((cell) => cell.attrs.align ?? null);
221
+ }
222
+ const texts = cells.map(cellText);
223
+ const stored = parseJsonArray(attr(row, "mdCells"));
224
+ const raw = attr(row, "mdRaw");
225
+ let line;
226
+ if (raw !== null && stored && JSON.stringify(stored) === JSON.stringify(texts)) {
227
+ line = raw;
228
+ } else {
229
+ line = respliceRow(parseJsonArray(attr(row, "mdSegs")), stored, texts) ?? freshRow(texts, lead, trail);
230
+ }
231
+ lines.push(line);
232
+ if (index === 0) {
233
+ const storedAligns = attr(table, "mdAligns");
234
+ const delim = attr(table, "mdDelim");
235
+ const fresh = () => freshRow(aligns.map((align) => delimiterFor(align)), lead, trail);
236
+ lines.push(
237
+ delim === null ? fresh() : storedAligns === JSON.stringify(aligns) ? delim : respliceDelimiter(delim, aligns, lead, trail) ?? fresh()
238
+ );
239
+ }
240
+ });
241
+ const written = lines.join("\n");
242
+ assertTableReadsBack(written);
243
+ return written;
244
+ }
245
+ const MARK_RANK = {
246
+ link: 0,
247
+ bold: 1,
248
+ italic: 2,
249
+ strike: 3,
250
+ code: 4
251
+ };
252
+ const ESCAPABLE = /[!-/:-@[-`{-~]/g;
253
+ function leafText(node) {
254
+ if (node.isText) {
255
+ const text = node.text ?? "";
256
+ return node.marks.some((mark) => mark.type.name === "mdEscape") ? text.replace(ESCAPABLE, "\\$&") : text;
257
+ }
258
+ if (node.type.name === "inlineIsland") return String(node.attrs.raw ?? "");
259
+ if (node.type.name === "hardBreak") return attr(node, "mdRaw") ?? "\n";
260
+ return node.textContent;
261
+ }
262
+ function literalText(node, followed) {
263
+ const text = leafText(node);
264
+ if (!node.isText || node.marks.some((mark) => mark.type.name === "mdEscape" || mark.type.name === "code")) return text;
265
+ let out = text.replace(/\\(?=[!-/:-@[-`{-~])/g, "\\\\");
266
+ if (followed && /(^|[^\\])(\\\\)*\\$/.test(out)) out += "\\";
267
+ return out;
268
+ }
269
+ function longestBacktickRun(text) {
270
+ let longest = 0;
271
+ for (const match of text.matchAll(/`+/g)) {
272
+ longest = Math.max(longest, match[0].length);
273
+ }
274
+ return longest;
275
+ }
276
+ function markDelimiters(mark, runText) {
277
+ switch (mark.type.name) {
278
+ case "bold": {
279
+ const marker = attr(mark, "mdMarker") ?? DEFAULT_BOLD;
280
+ return [marker, marker];
281
+ }
282
+ case "italic": {
283
+ const marker = attr(mark, "mdMarker") ?? DEFAULT_ITALIC;
284
+ return [marker, marker];
285
+ }
286
+ case "strike": {
287
+ const marker = attr(mark, "mdMarker") ?? DEFAULT_STRIKE;
288
+ return [marker, marker];
289
+ }
290
+ case "code": {
291
+ const open = attr(mark, "mdOpen");
292
+ const close = attr(mark, "mdClose");
293
+ if (open !== null && close !== null) return [open, close];
294
+ const ticks = "`".repeat(longestBacktickRun(runText) + 1);
295
+ const pad = runText.startsWith("`") || runText.endsWith("`") ? " " : "";
296
+ return [ticks + pad, pad + ticks];
297
+ }
298
+ case "link": {
299
+ const href = String(mark.attrs.href ?? "");
300
+ const form = attr(mark, "mdForm");
301
+ const hrefUnchanged = href === attr(mark, "mdTailHref");
302
+ if ((form === "angle" || form === "bare") && hrefUnchanged && runText === attr(mark, "mdText")) {
303
+ return form === "angle" ? ["<", ">"] : ["", ""];
304
+ }
305
+ const storedTail = attr(mark, "mdTail");
306
+ const title = attr(mark, "title");
307
+ const tail = form === "inline" && storedTail !== null && hrefUnchanged ? storedTail : `(${href}${title ? ` "${title}"` : ""})`;
308
+ return ["[", `]${tail}`];
309
+ }
310
+ default:
311
+ return ["", ""];
312
+ }
313
+ }
314
+ const EDGE_SENSITIVE_MARKS = /* @__PURE__ */ new Set(["bold", "italic", "strike"]);
315
+ function hoistEdgeWhitespace(items) {
316
+ const out = [...items];
317
+ let changed = true;
318
+ while (changed) {
319
+ changed = false;
320
+ for (let i = 0; i < out.length && !changed; i += 1) {
321
+ const node = out[i];
322
+ if (!node?.isText || !node.text) continue;
323
+ const text = node.text;
324
+ for (const mark of node.marks) {
325
+ if (!EDGE_SENSITIVE_MARKS.has(mark.type.name)) continue;
326
+ const startsRun = !mark.isInSet(out[i - 1]?.marks ?? []);
327
+ const endsRun = !mark.isInSet(out[i + 1]?.marks ?? []);
328
+ const lead = startsRun ? /^\s+/.exec(text)?.[0] ?? "" : "";
329
+ const trail = endsRun && lead.length < text.length ? /\s+$/.exec(text)?.[0] ?? "" : "";
330
+ if (!lead && !trail) continue;
331
+ const bare = mark.removeFromSet(node.marks);
332
+ const body = text.slice(lead.length, text.length - trail.length);
333
+ const pieces = [];
334
+ if (lead) pieces.push(node.type.schema.text(lead, bare));
335
+ if (body) pieces.push(node.type.schema.text(body, node.marks));
336
+ if (trail) pieces.push(node.type.schema.text(trail, bare));
337
+ out.splice(i, 1, ...pieces);
338
+ changed = true;
339
+ break;
340
+ }
341
+ }
342
+ }
343
+ return out;
344
+ }
345
+ function serializeInline(parent) {
346
+ const children = [];
347
+ parent.forEach((child) => children.push(child));
348
+ const items = hoistEdgeWhitespace(children);
349
+ const others = (node) => (node?.marks ?? []).filter((m) => m.type.name !== "code");
350
+ const sameOthers = (a, b) => {
351
+ const x = others(a);
352
+ const y = others(b);
353
+ return x.length === y.length && x.every((mark) => mark.isInSet(y));
354
+ };
355
+ const runEnd = (from, mark) => {
356
+ let end = from;
357
+ while (end < items.length && mark.isInSet(items[end]?.marks ?? []) && (mark.type.name !== "code" || sameOthers(items[from], items[end]))) {
358
+ end += 1;
359
+ }
360
+ return end;
361
+ };
362
+ const runText = (from, to) => items.slice(from, to).map((node) => leafText(node)).join("");
363
+ let out = "";
364
+ const stack = [];
365
+ items.forEach((node, index) => {
366
+ const marks = node.marks;
367
+ let keep = 0;
368
+ while (keep < stack.length && stack[keep]?.mark.isInSet(marks) && (stack[keep]?.end ?? 0) > index) keep += 1;
369
+ for (let k = stack.length - 1; k >= keep; k -= 1) out += stack[k]?.close ?? "";
370
+ stack.length = keep;
371
+ const toOpen = marks.filter((mark) => !stack.some((open) => open.mark.eq(mark))).map((mark) => ({ mark, end: runEnd(index, mark) }));
372
+ toOpen.sort((a, b) => {
373
+ if (a.end !== b.end) return b.end - a.end;
374
+ const da = attr(a.mark, "mdDepth") ?? Number.MAX_SAFE_INTEGER;
375
+ const db = attr(b.mark, "mdDepth") ?? Number.MAX_SAFE_INTEGER;
376
+ if (da !== db) return da - db;
377
+ return (MARK_RANK[a.mark.type.name] ?? 9) - (MARK_RANK[b.mark.type.name] ?? 9);
378
+ });
379
+ for (const { mark, end } of toOpen) {
380
+ const [open, close] = markDelimiters(mark, runText(index, end));
381
+ out += open;
382
+ stack.push({ mark, close, end });
383
+ }
384
+ out += literalText(node, index < items.length - 1 || node.marks.some((mark) => mark.type.name !== "mdEscape"));
385
+ });
386
+ for (let k = stack.length - 1; k >= 0; k -= 1) out += stack[k]?.close ?? "";
387
+ return out;
388
+ }
389
+ export {
390
+ createSerializeContext,
391
+ serializeBlock,
392
+ serializeChildren,
393
+ serializeInline
394
+ };
@@ -0,0 +1,17 @@
1
+ export interface OutlineEntry {
2
+ level: number;
3
+ text: string;
4
+ slug: string;
5
+ /** UTF-16 offset of the heading line in the source. */
6
+ offset: number;
7
+ }
8
+ /**
9
+ * Plain heading text: inline markdown markers and link targets dropped.
10
+ * `{{variables}}` stay exactly as written, and an underscore INSIDE a word
11
+ * (`site_name`, `file_name_here`) is text, not emphasis.
12
+ */
13
+ export declare function headingPlainText(raw: string): string;
14
+ /** Slugs for a list of heading texts, de-duplicated the way the renderer does (`-1`, `-2`). */
15
+ export declare function slugsFor(texts: readonly string[]): string[];
16
+ /** Outline of the source text: ATX and setext headings outside every island. */
17
+ export declare function outlineOf(text: string): OutlineEntry[];
@@ -0,0 +1,41 @@
1
+ import GithubSlugger from "github-slugger";
2
+ import { tokenizeSource } from "@ai-matrx/content-ir/source";
3
+ function headingPlainText(raw) {
4
+ const kept = [];
5
+ const hold = (text) => `\0${kept.push(text) - 1}\0`;
6
+ return raw.replace(/\{\{[^{}\n]*\}\}/g, hold).replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*~`]+/g, "").replace(/(?<![\p{L}\p{N}])_+|_+(?![\p{L}\p{N}])/gu, "").replace(/\s+#+\s*$/, "").replace(/\u0000(\d+)\u0000/g, (_, index) => kept[Number(index)] ?? "").trim();
7
+ }
8
+ function slugsFor(texts) {
9
+ const slugger = new GithubSlugger();
10
+ return texts.map((text) => slugger.slug(text));
11
+ }
12
+ function outlineOf(text) {
13
+ const found = [];
14
+ for (const block of tokenizeSource(text)) {
15
+ if (block.kind !== "prose") continue;
16
+ const lines = block.raw.split("\n");
17
+ let offset = block.start;
18
+ lines.forEach((line, index) => {
19
+ const atx = /^ {0,3}(#{1,6})[ \t]+(.+?)\s*$/.exec(line) ?? /^ {0,3}(#{1,6})\s*$/.exec(line);
20
+ if (atx) {
21
+ found.push({ level: atx[1]?.length ?? 1, text: headingPlainText(atx[2] ?? ""), offset });
22
+ } else {
23
+ const next = lines[index + 1];
24
+ if (line.trim() && next !== void 0 && /^ {0,3}(=+|-+)[ \t]*$/.test(next) && !/^ {0,3}[-*+] /.test(line)) {
25
+ const level = next.trim().startsWith("=") ? 1 : 2;
26
+ if (level === 1 || next.trim().length >= 2) {
27
+ found.push({ level, text: headingPlainText(line), offset });
28
+ }
29
+ }
30
+ }
31
+ offset += line.length + 1;
32
+ });
33
+ }
34
+ const slugs = slugsFor(found.map((entry) => entry.text));
35
+ return found.map((entry, index) => ({ ...entry, slug: slugs[index] ?? "" }));
36
+ }
37
+ export {
38
+ headingPlainText,
39
+ outlineOf,
40
+ slugsFor
41
+ };
@@ -0,0 +1,10 @@
1
+ export interface PastedHtmlResult {
2
+ html: string;
3
+ /** Merged cells split so every column lines up (colspan + rowspan extras). */
4
+ mergedCellsSplit: number;
5
+ tables: number;
6
+ }
7
+ /** Normalize pasted HTML (see header): literal markdown characters escaped, tables squared. */
8
+ export declare function normalizePastedHtml(html: string): PastedHtmlResult;
9
+ /** The one sentence a person sees when merged cells were split. */
10
+ export declare function mergedCellsNotice(count: number): string | null;
@@ -0,0 +1,198 @@
1
+ const BLOCK_TAGS = /* @__PURE__ */ new Set([
2
+ "P",
3
+ "DIV",
4
+ "SECTION",
5
+ "ARTICLE",
6
+ "HEADER",
7
+ "FOOTER",
8
+ "ASIDE",
9
+ "NAV",
10
+ "H1",
11
+ "H2",
12
+ "H3",
13
+ "H4",
14
+ "H5",
15
+ "H6",
16
+ "BLOCKQUOTE",
17
+ "PRE",
18
+ "FIGURE",
19
+ "FIGCAPTION",
20
+ "DL",
21
+ "DT",
22
+ "DD",
23
+ "HR",
24
+ "ADDRESS",
25
+ "DETAILS",
26
+ "SUMMARY"
27
+ ]);
28
+ const LIST_TAGS = /* @__PURE__ */ new Set(["UL", "OL"]);
29
+ function text(doc, value) {
30
+ return doc.createTextNode(value);
31
+ }
32
+ function flattenCell(cell) {
33
+ const doc = cell.ownerDocument;
34
+ for (const nested of Array.from(cell.querySelectorAll("table"))) {
35
+ nested.replaceWith(text(doc, (nested.textContent ?? "").replace(/\s+/g, " ").trim()));
36
+ }
37
+ for (const br of Array.from(cell.querySelectorAll("br"))) br.replaceWith(text(doc, " "));
38
+ for (const list of Array.from(cell.querySelectorAll("ul, ol")).reverse()) {
39
+ const fragment = doc.createDocumentFragment();
40
+ const items = Array.from(list.children).filter((child) => child.tagName === "LI");
41
+ items.forEach((item, index) => {
42
+ if (index > 0) fragment.append(text(doc, "; "));
43
+ fragment.append(...Array.from(item.childNodes));
44
+ });
45
+ fragment.append(text(doc, " "));
46
+ list.replaceWith(fragment);
47
+ }
48
+ for (const block of Array.from(cell.querySelectorAll("*")).reverse()) {
49
+ if (!BLOCK_TAGS.has(block.tagName) && !LIST_TAGS.has(block.tagName) && block.tagName !== "LI") continue;
50
+ if (block.tagName === "HR") {
51
+ block.replaceWith(text(doc, " "));
52
+ continue;
53
+ }
54
+ const fragment = doc.createDocumentFragment();
55
+ fragment.append(text(doc, " "), ...Array.from(block.childNodes), text(doc, " "));
56
+ block.replaceWith(fragment);
57
+ }
58
+ const walker = doc.createTreeWalker(cell, NodeFilter.SHOW_TEXT);
59
+ const nodes = [];
60
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) nodes.push(node);
61
+ for (const node of nodes) node.data = node.data.replace(/\s+/g, " ");
62
+ const first = nodes[0];
63
+ const last = nodes[nodes.length - 1];
64
+ if (first) first.data = first.data.replace(/^ /, "");
65
+ if (last) last.data = last.data.replace(/ $/, "");
66
+ }
67
+ function rowsOf(table) {
68
+ return Array.from(table.rows);
69
+ }
70
+ function emptyCell(like) {
71
+ return like.ownerDocument.createElement(like.tagName === "TH" ? "th" : "td");
72
+ }
73
+ function span(cell, name) {
74
+ return Math.max(1, Math.min(Number(cell.getAttribute(name)) || 1, 1e3));
75
+ }
76
+ function squareTable(table) {
77
+ const rows = rowsOf(table);
78
+ let added = 0;
79
+ const pending = rows.map(() => []);
80
+ rows.forEach((row, r) => {
81
+ for (const cell of Array.from(row.cells)) {
82
+ const colspan = span(cell, "colspan");
83
+ const rowspan = span(cell, "rowspan");
84
+ cell.removeAttribute("colspan");
85
+ let anchor = cell;
86
+ for (let extra = 1; extra < colspan; extra += 1) {
87
+ const blank = emptyCell(cell);
88
+ if (rowspan > 1) blank.setAttribute("rowspan", String(rowspan));
89
+ anchor.after(blank);
90
+ anchor = blank;
91
+ added += 1;
92
+ }
93
+ }
94
+ for (const [column, like] of (pending[r] ?? []).sort((a, b) => a[0] - b[0])) {
95
+ const blank = emptyCell(like);
96
+ const before = row.cells[column];
97
+ if (before) row.insertBefore(blank, before);
98
+ else row.append(blank);
99
+ added += 1;
100
+ }
101
+ Array.from(row.cells).forEach((cell, column) => {
102
+ const rowspan = span(cell, "rowspan");
103
+ cell.removeAttribute("rowspan");
104
+ for (let down = 1; down < rowspan && r + down < rows.length; down += 1) pending[r + down]?.push([column, cell]);
105
+ });
106
+ });
107
+ const width = Math.max(0, ...rows.map((row) => row.cells.length));
108
+ for (const row of rows) {
109
+ while (row.cells.length < width) {
110
+ row.append(emptyCell(row.cells[0] ?? row.ownerDocument.createElement("td")));
111
+ }
112
+ }
113
+ return added;
114
+ }
115
+ const LITERAL_SKIP = /* @__PURE__ */ new Set(["CODE", "PRE", "KBD", "SAMP", "SCRIPT", "STYLE", "TEXTAREA"]);
116
+ function literalAt(text2, index) {
117
+ const ch = text2[index];
118
+ const prev = text2[index - 1] ?? "";
119
+ const next = text2[index + 1] ?? "";
120
+ const loose = (/\s/.test(prev) || !prev) && (/\s/.test(next) || !next);
121
+ if (ch === "`" || ch === "\\") return true;
122
+ if (ch === "*") return !loose;
123
+ if (ch === "_") return !loose && !(/[\p{L}\p{N}]/u.test(prev) && /[\p{L}\p{N}]/u.test(next));
124
+ if (ch === "~") return prev === "~" || next === "~";
125
+ if (ch === "<") return /[A-Za-z/!?]/.test(next);
126
+ return false;
127
+ }
128
+ function escapeLiteralMarkdown(root) {
129
+ const doc = root.ownerDocument ?? document;
130
+ const walker = doc.createTreeWalker(root, NodeFilter.SHOW_TEXT);
131
+ const texts = [];
132
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
133
+ let parent = node.parentElement;
134
+ let skip = false;
135
+ while (parent) {
136
+ if (LITERAL_SKIP.has(parent.tagName) || parent.hasAttribute("data-md-escape")) {
137
+ skip = true;
138
+ break;
139
+ }
140
+ parent = parent.parentElement;
141
+ }
142
+ const textNode = node;
143
+ if (!skip && /[*`\\_~<]/.test(textNode.data)) texts.push(textNode);
144
+ }
145
+ for (const node of texts) {
146
+ const data = node.data;
147
+ const fragment = doc.createDocumentFragment();
148
+ let run = "";
149
+ for (let index = 0; index < data.length; index += 1) {
150
+ if (!literalAt(data, index)) {
151
+ run += data[index];
152
+ continue;
153
+ }
154
+ if (run) fragment.append(doc.createTextNode(run));
155
+ run = "";
156
+ const span2 = doc.createElement("span");
157
+ span2.setAttribute("data-md-escape", "");
158
+ span2.textContent = data[index] ?? "";
159
+ fragment.append(span2);
160
+ }
161
+ if (run) fragment.append(doc.createTextNode(run));
162
+ node.replaceWith(fragment);
163
+ }
164
+ }
165
+ function normalizePastedHtml(html) {
166
+ const hasTable = /<table[\s>]/i.test(html);
167
+ if (!hasTable && !/[*`\\_~<]/.test(html.replace(/<[^>]*>/g, ""))) return { html, mergedCellsSplit: 0, tables: 0 };
168
+ const template = document.createElement("template");
169
+ template.innerHTML = html;
170
+ escapeLiteralMarkdown(template.content);
171
+ if (!hasTable) return { html: template.innerHTML, mergedCellsSplit: 0, tables: 0 };
172
+ const tables = Array.from(template.content.querySelectorAll("table")).filter(
173
+ // Outermost tables only: a nested one is flattened into its parent's cell.
174
+ (table) => !table.parentElement?.closest("table")
175
+ );
176
+ let mergedCellsSplit = 0;
177
+ for (const table of tables) {
178
+ for (const caption of Array.from(table.querySelectorAll(":scope > caption"))) {
179
+ const line = template.ownerDocument.createElement("p");
180
+ line.append(...Array.from(caption.childNodes));
181
+ table.before(line);
182
+ caption.remove();
183
+ }
184
+ mergedCellsSplit += squareTable(table);
185
+ for (const row of rowsOf(table)) {
186
+ for (const cell of Array.from(row.cells)) flattenCell(cell);
187
+ }
188
+ }
189
+ return { html: template.innerHTML, mergedCellsSplit, tables: tables.length };
190
+ }
191
+ function mergedCellsNotice(count) {
192
+ if (count <= 0) return null;
193
+ return `${count} merged table ${count === 1 ? "cell was" : "cells were"} split into separate cells so every column lines up \u2014 check the pasted table.`;
194
+ }
195
+ export {
196
+ mergedCellsNotice,
197
+ normalizePastedHtml
198
+ };
@@ -0,0 +1,18 @@
1
+ import { Extension } from "@tiptap/core";
2
+ import { Slice, type Schema } from "@tiptap/pm/model";
3
+ import { type Transaction } from "@tiptap/pm/state";
4
+ /** Markdown text → the slice the visual editor inserts for it. */
5
+ export declare function markdownToSlice(text: string, schema: Schema): Slice;
6
+ /**
7
+ * Text a host inserts at the caret (the right-click menu's Insert reference…,
8
+ * a content block, an AI result's Replace, dictation) lands exactly as a paste
9
+ * of that text would: through the ONE parse path. Inserting it as literal
10
+ * characters turned a ```matrx fence into three paragraphs of raw JSON in the
11
+ * visual view (2026-10-02) — a fence must arrive as its island.
12
+ */
13
+ export declare function replaceSelectionWithMarkdown(tr: Transaction, schema: Schema, text: string): void;
14
+ export interface MarkdownTextPasteOptions {
15
+ /** Told when a paste changed shape on the way in (merged table cells split). */
16
+ onNotice: ((message: string) => void) | null;
17
+ }
18
+ export declare const MarkdownTextPaste: Extension<MarkdownTextPasteOptions, any>;