@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,533 @@
1
+ import { splitRowSegments } from "@ai-matrx/rich-content/utils/table-source";
2
+ import { Lexer, Tokenizer } from "@ai-matrx/rich-content/utils/gfm-lexer";
3
+ import { findTableEnd, tableStartsAt } from "@ai-matrx/rich-content/display/markdown-classification/processors/utils/gfm-table-lines";
4
+ import { isPageBreakLine } from "@ai-matrx/print/directives";
5
+ import {
6
+ hasPrivateUseCharacter,
7
+ restorePlaceholders,
8
+ splitPlaceholders,
9
+ withPlaceholders
10
+ } from "./placeholders.js";
11
+ import {
12
+ createSerializeContext,
13
+ serializeBlock
14
+ } from "./markdown-serialize.js";
15
+ const LEXER_OPTIONS = { gfm: true, breaks: false, pedantic: false };
16
+ class RuleTableTokenizer extends Tokenizer {
17
+ // The rule judges the stored bytes: an inline island (`</artifact>`, a variable)
18
+ // stands in the lexed text as a placeholder, so each line is restored first.
19
+ constructor(islands, segmentText) {
20
+ super();
21
+ this.islands = islands;
22
+ const stored = segmentText.split("\n").map((line) => restorePlaceholders(line, islands));
23
+ const lazy = /* @__PURE__ */ new Set();
24
+ const real = /* @__PURE__ */ new Set();
25
+ for (let i = 0; i + 1 < stored.length; i += 1) {
26
+ const key = headerKey(stored, i);
27
+ if (tableStartsAt(stored, i)) real.add(key);
28
+ else if (tableStartsAt(stored.slice(i).map((line) => line.trimStart()), 0)) lazy.add(key);
29
+ }
30
+ for (const key of real) lazy.delete(key);
31
+ this.lazyHeaders = lazy;
32
+ }
33
+ islands;
34
+ /**
35
+ * Headers (with their delimiter row) the rule refuses IN CONTEXT but would open
36
+ * on their own: a table written right under a list item's or quote's text is a
37
+ * lazy continuation of that text in GFM. marked lexes an item's lines already
38
+ * dedented, where that context is gone — so the segment's own lines decide,
39
+ * keyed by the header and delimiter bytes. A key that also opens a real table
40
+ * in the segment is left to marked (ambiguous, never guessed).
41
+ */
42
+ lazyHeaders;
43
+ table(src) {
44
+ const lines = src.split("\n");
45
+ const stored = lines.map((line) => restorePlaceholders(line, this.islands));
46
+ if (this.lazyHeaders.has(headerKey(stored, 0))) return void 0;
47
+ if (!tableStartsAt(stored, 0)) return void 0;
48
+ const end = findTableEnd(stored, 0);
49
+ return super.table(lines.slice(0, end).join("\n") + (end < lines.length ? "\n" : ""));
50
+ }
51
+ }
52
+ const headerKey = (lines, i) => `${(lines[i] ?? "").trim()}
53
+ ${(lines[i + 1] ?? "").trim()}`;
54
+ const PAGE_BREAK_REASON = "page break";
55
+ const LOCK_REASONS = {
56
+ html: "raw HTML",
57
+ code: "an indented code block",
58
+ def: "a link reference definition"
59
+ };
60
+ function splitTrail(raw) {
61
+ const match = /\n*$/.exec(raw);
62
+ const trail = match ? match[0] : "";
63
+ return { body: raw.slice(0, raw.length - trail.length), trail };
64
+ }
65
+ function pushInline(target, node) {
66
+ const last = target[target.length - 1];
67
+ if (node.type === "text" && last?.type === "text" && JSON.stringify(last.marks ?? []) === JSON.stringify(node.marks ?? [])) {
68
+ last.text = (last.text ?? "") + (node.text ?? "");
69
+ return;
70
+ }
71
+ target.push(node);
72
+ }
73
+ const BR_TAG = /^<br\s*\/?>$/i;
74
+ function withMarks(node, marks) {
75
+ return marks.length ? { ...node, marks: marks.map((m) => ({ ...m })) } : node;
76
+ }
77
+ function pushText(out, text, marks, state) {
78
+ for (const piece of splitPlaceholders(text, state.islands)) {
79
+ if (piece.kind === "text") {
80
+ if (piece.text) pushInline(out, withMarks({ type: "text", text: piece.text }, marks));
81
+ } else if (BR_TAG.test(piece.island.raw)) {
82
+ out.push(withMarks({ type: "hardBreak", attrs: { mdRaw: piece.island.raw } }, marks));
83
+ } else {
84
+ out.push(
85
+ withMarks(
86
+ {
87
+ type: "inlineIsland",
88
+ attrs: { raw: piece.island.raw, islandType: piece.island.islandType }
89
+ },
90
+ marks
91
+ )
92
+ );
93
+ }
94
+ }
95
+ }
96
+ function pushRaw(out, raw, islandType, marks, state) {
97
+ out.push(
98
+ withMarks(
99
+ {
100
+ type: "inlineIsland",
101
+ attrs: { raw: restorePlaceholders(raw, state.islands), islandType }
102
+ },
103
+ marks
104
+ )
105
+ );
106
+ }
107
+ function codeSpanParts(raw) {
108
+ const ticks = /^`+/.exec(raw)?.[0] ?? "";
109
+ if (!ticks || !raw.endsWith(ticks) || raw.length <= ticks.length * 2) return null;
110
+ const inner = raw.slice(ticks.length, raw.length - ticks.length);
111
+ if (inner.length >= 2 && inner.startsWith(" ") && inner.endsWith(" ") && inner.trim() !== "") {
112
+ return { open: `${ticks} `, close: ` ${ticks}`, text: inner.slice(1, -1) };
113
+ }
114
+ return { open: ticks, close: ticks, text: inner };
115
+ }
116
+ function inlineJSON(tokens, marks, depth, state, out = []) {
117
+ for (const token of tokens) {
118
+ switch (token.type) {
119
+ case "text": {
120
+ const nested = token.tokens;
121
+ if (nested && nested.length) inlineJSON(nested, marks, depth, state, out);
122
+ else pushText(out, token.raw, marks, state);
123
+ break;
124
+ }
125
+ case "escape":
126
+ pushText(out, token.raw.slice(1), [...marks, { type: "mdEscape" }], state);
127
+ break;
128
+ case "strong":
129
+ inlineJSON(token.tokens, [
130
+ ...marks,
131
+ { type: "bold", attrs: { mdMarker: token.raw.slice(0, 2), mdDepth: depth } }
132
+ ], depth + 1, state, out);
133
+ break;
134
+ case "em":
135
+ inlineJSON(token.tokens, [
136
+ ...marks,
137
+ { type: "italic", attrs: { mdMarker: token.raw.slice(0, 1), mdDepth: depth } }
138
+ ], depth + 1, state, out);
139
+ break;
140
+ case "del":
141
+ inlineJSON(token.tokens, [
142
+ ...marks,
143
+ {
144
+ type: "strike",
145
+ attrs: { mdMarker: token.raw.startsWith("~~") ? "~~" : "~", mdDepth: depth }
146
+ }
147
+ ], depth + 1, state, out);
148
+ break;
149
+ case "codespan": {
150
+ const parts = codeSpanParts(token.raw);
151
+ if (!parts || !parts.text) {
152
+ pushRaw(out, token.raw, "md_raw", marks, state);
153
+ break;
154
+ }
155
+ pushText(out, parts.text, [
156
+ ...marks,
157
+ { type: "code", attrs: { mdOpen: parts.open, mdClose: parts.close, mdDepth: depth } }
158
+ ], state);
159
+ break;
160
+ }
161
+ case "link": {
162
+ const link = token;
163
+ let form = null;
164
+ let tail = "";
165
+ if (link.raw.startsWith(`[${link.text}]`)) {
166
+ form = "inline";
167
+ tail = link.raw.slice(link.text.length + 2);
168
+ } else if (link.raw === `<${link.text}>`) {
169
+ form = "angle";
170
+ } else if (link.raw === link.text) {
171
+ form = "bare";
172
+ }
173
+ if (!form) {
174
+ pushRaw(out, link.raw, "md_raw", marks, state);
175
+ break;
176
+ }
177
+ const href = restorePlaceholders(link.href ?? "", state.islands);
178
+ inlineJSON(link.tokens ?? [], [
179
+ ...marks,
180
+ {
181
+ type: "link",
182
+ attrs: {
183
+ href,
184
+ title: link.title ?? null,
185
+ mdForm: form,
186
+ mdTail: restorePlaceholders(tail, state.islands),
187
+ mdTailHref: href,
188
+ mdText: restorePlaceholders(link.text, state.islands),
189
+ mdDepth: depth
190
+ }
191
+ }
192
+ ], depth + 1, state, out);
193
+ break;
194
+ }
195
+ case "br":
196
+ out.push(withMarks({ type: "hardBreak", attrs: { mdRaw: token.raw } }, marks));
197
+ break;
198
+ case "image":
199
+ pushRaw(out, token.raw, "md_image", marks, state);
200
+ break;
201
+ case "html":
202
+ if (BR_TAG.test(token.raw)) out.push(withMarks({ type: "hardBreak", attrs: { mdRaw: token.raw } }, marks));
203
+ else pushRaw(out, token.raw, "md_html", marks, state);
204
+ break;
205
+ default:
206
+ pushRaw(out, token.raw, "md_raw", marks, state);
207
+ }
208
+ }
209
+ return out;
210
+ }
211
+ function inlineRaw(tokens) {
212
+ return (tokens ?? []).map((token) => token.raw).join("");
213
+ }
214
+ function blockJSON(token, state) {
215
+ const { body } = splitTrail(token.raw);
216
+ switch (token.type) {
217
+ case "paragraph":
218
+ case "text": {
219
+ const tokens = token.tokens;
220
+ const inline = tokens && tokens.length ? tokens : null;
221
+ if (inline && inlineRaw(inline) !== body) return null;
222
+ const content = inline ? inlineJSON(inline, [], 0, state) : (() => {
223
+ const out = [];
224
+ pushText(out, body, [], state);
225
+ return out;
226
+ })();
227
+ return {
228
+ type: "paragraph",
229
+ attrs: { mdId: state.nextId() },
230
+ ...content.length ? { content } : {}
231
+ };
232
+ }
233
+ case "heading": {
234
+ const heading = token;
235
+ const raw = inlineRaw(heading.tokens);
236
+ let open;
237
+ let close;
238
+ const atx = /^ {0,3}#{1,6}(?:[ \t]+|$)/.exec(body);
239
+ if (atx) {
240
+ open = atx[0];
241
+ const rest = body.slice(open.length);
242
+ if (!rest.startsWith(raw)) return null;
243
+ close = rest.slice(raw.length);
244
+ } else {
245
+ if (!body.startsWith(raw)) return null;
246
+ open = "";
247
+ close = body.slice(raw.length);
248
+ if (!close.startsWith("\n")) return null;
249
+ }
250
+ const content = inlineJSON(heading.tokens ?? [], [], 0, state);
251
+ return {
252
+ type: "heading",
253
+ attrs: {
254
+ level: heading.depth,
255
+ mdId: state.nextId(),
256
+ mdOpen: restorePlaceholders(open, state.islands),
257
+ mdClose: restorePlaceholders(close, state.islands)
258
+ },
259
+ ...content.length ? { content } : {}
260
+ };
261
+ }
262
+ case "list": {
263
+ const list = token;
264
+ const items = [];
265
+ const ids = [];
266
+ const trails = [];
267
+ for (const item of list.items) {
268
+ const json = listItemJSON(item, state);
269
+ if (!json) return null;
270
+ items.push(json);
271
+ ids.push(String(json.attrs?.mdId));
272
+ trails.push(splitTrail(item.raw).trail);
273
+ }
274
+ ids.forEach(
275
+ (id, index) => state.adjacency.set(id, { trail: trails[index] ?? "", next: ids[index + 1] ?? null })
276
+ );
277
+ const start = list.ordered ? Number(list.start === "" ? 1 : list.start) || 1 : null;
278
+ return {
279
+ type: list.ordered ? "orderedList" : "bulletList",
280
+ attrs: { mdId: state.nextId(), ...start !== null ? { start } : {} },
281
+ content: items
282
+ };
283
+ }
284
+ case "blockquote": {
285
+ const quote = token;
286
+ const prefix = /^ {0,3}> ?/.exec(quote.raw)?.[0] ?? "> ";
287
+ const children = childrenJSON(quote.tokens, state);
288
+ if (!children || children.length === 0) return null;
289
+ const alert = takeAlertMarker(children);
290
+ if (children.length === 0) return null;
291
+ return {
292
+ type: "blockquote",
293
+ attrs: { mdId: state.nextId(), mdPrefix: prefix, mdAlert: alert },
294
+ content: children
295
+ };
296
+ }
297
+ case "hr":
298
+ return { type: "horizontalRule", attrs: { mdId: state.nextId(), mdRaw: body } };
299
+ case "table": {
300
+ const table = token;
301
+ const lines = body.split("\n");
302
+ if (lines.length !== table.rows.length + 2) return null;
303
+ const header = lines[0] ?? "";
304
+ const headerSegs = splitRowSegments(header);
305
+ const leadPipe = headerSegs.length > 1 && (headerSegs[0] ?? "").trim() === "";
306
+ const trailPipe = headerSegs.length > 1 && (headerSegs[headerSegs.length - 1] ?? "").trim() === "";
307
+ const pipes = leadPipe && trailPipe ? "both" : leadPipe ? "lead" : trailPipe ? "trail" : "none";
308
+ const aligns = table.align.map((align) => align ?? null);
309
+ const cellJSON = (cell, isHeader, index) => {
310
+ const content = inlineJSON(cell.tokens, [], 0, state);
311
+ return {
312
+ type: isHeader ? "tableHeader" : "tableCell",
313
+ attrs: { align: aligns[index] ?? null },
314
+ content: [{ type: "paragraph", ...content.length ? { content } : {} }]
315
+ };
316
+ };
317
+ const rowJSON = (cells, isHeader, line) => ({
318
+ type: "tableRow",
319
+ attrs: {
320
+ mdRaw: restorePlaceholders(line, state.islands),
321
+ mdCells: JSON.stringify(
322
+ cells.map((cell) => restorePlaceholders(inlineRaw(cell.tokens), state.islands))
323
+ ),
324
+ // The row's exact bytes split on its unescaped pipes: an untouched cell
325
+ // is written back as ITS segment, never re-serialized (markdown-serialize.ts).
326
+ mdSegs: JSON.stringify(splitRowSegments(restorePlaceholders(line, state.islands)))
327
+ },
328
+ content: cells.map((cell, index) => cellJSON(cell, isHeader, index))
329
+ });
330
+ return {
331
+ type: "table",
332
+ attrs: {
333
+ mdId: state.nextId(),
334
+ mdDelim: restorePlaceholders(lines[1] ?? "", state.islands),
335
+ mdAligns: JSON.stringify(aligns),
336
+ mdPipes: pipes
337
+ },
338
+ content: [
339
+ rowJSON(table.header, true, header),
340
+ ...table.rows.map((row, index) => rowJSON(row, false, lines[index + 2] ?? ""))
341
+ ]
342
+ };
343
+ }
344
+ default:
345
+ return null;
346
+ }
347
+ }
348
+ const ALERT_MARKER = /^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?=\n|$)/i;
349
+ function takeAlertMarker(children) {
350
+ const first = children[0];
351
+ const text = first?.type === "paragraph" ? first.content?.[0] : void 0;
352
+ if (!first || !text || text.type !== "text" || text.marks?.length) return null;
353
+ const match = ALERT_MARKER.exec(text.text ?? "");
354
+ if (!match) return null;
355
+ const rest = (text.text ?? "").slice(match[0].length);
356
+ if (rest === "") {
357
+ first.content = first.content?.slice(1);
358
+ if (!first.content?.length) children.shift();
359
+ } else if (rest.startsWith("\n")) {
360
+ text.text = rest.slice(1);
361
+ if (!text.text) first.content = first.content?.slice(1);
362
+ } else {
363
+ return null;
364
+ }
365
+ return match[0];
366
+ }
367
+ function listItemJSON(item, state) {
368
+ const head = /^( {0,3})([-*+]|\d{1,9}[.)])([ \t]*)/.exec(item.raw);
369
+ if (!head) return null;
370
+ const tokens = item.tokens.filter((token) => token.type !== "checkbox");
371
+ const checkbox = item.tokens.find((token) => token.type === "checkbox");
372
+ const children = childrenJSON(tokens, state);
373
+ if (!children) return null;
374
+ if (children.length === 0) children.push({ type: "paragraph", attrs: { mdId: state.nextId() } });
375
+ if (children[0]?.type !== "paragraph") return null;
376
+ const lines = splitTrail(item.raw).body.split("\n");
377
+ const continuation = lines.slice(1).find((line) => line.trim() !== "");
378
+ return {
379
+ type: "listItem",
380
+ attrs: {
381
+ mdId: state.nextId(),
382
+ mdLead: head[1] ?? "",
383
+ mdMarker: head[2] ?? "-",
384
+ mdAfter: head[3] ?? "",
385
+ mdTask: checkbox ? checkbox.raw : null,
386
+ mdIndent: continuation ? continuation.length - continuation.trimStart().length : null
387
+ },
388
+ content: children
389
+ };
390
+ }
391
+ function childrenJSON(tokens, state) {
392
+ const out = [];
393
+ const ids = [];
394
+ const trails = [];
395
+ for (const token of tokens) {
396
+ if (token.type === "space") {
397
+ if (trails.length === 0) return null;
398
+ trails[trails.length - 1] += token.raw;
399
+ continue;
400
+ }
401
+ const json = blockJSON(token, state);
402
+ if (!json) return null;
403
+ out.push(json);
404
+ ids.push(String(json.attrs?.mdId));
405
+ trails.push(splitTrail(token.raw).trail);
406
+ }
407
+ ids.forEach(
408
+ (id, index) => state.adjacency.set(id, { trail: trails[index] ?? "", next: ids[index + 1] ?? null })
409
+ );
410
+ return out;
411
+ }
412
+ function lockedJSON(raw, reason, id) {
413
+ return { type: "sourceLocked", attrs: { raw, reason, mdId: id } };
414
+ }
415
+ function parseProseBlock(block, schema, adjacency, nextId, linkDefinitions) {
416
+ if (block.raw.includes("\r")) return { children: [], lockedReason: "Windows line endings" };
417
+ if (hasPrivateUseCharacter(block.raw)) {
418
+ return { children: [], lockedReason: "private-use characters" };
419
+ }
420
+ const { text, islands } = withPlaceholders(block);
421
+ const { segments, lead } = splitAtBlankLines(text);
422
+ if (lead) return { children: [], lockedReason: "leading blank lines" };
423
+ if (segments.some((segment) => segment.text === "")) {
424
+ return { children: [], lockedReason: "markdown the parser could not map" };
425
+ }
426
+ const state = { schema, islands, adjacency, nextId };
427
+ const children = [];
428
+ for (const segment of segments) {
429
+ let tokens;
430
+ try {
431
+ const lexer = new Lexer({ ...LEXER_OPTIONS, tokenizer: new RuleTableTokenizer(islands, segment.text) });
432
+ for (const [label, def] of linkDefinitions ?? []) lexer.tokens.links[label] = { href: def.url, title: def.title ?? void 0 };
433
+ tokens = lexer.lex(segment.text);
434
+ } catch {
435
+ return { children: [], lockedReason: "markdown the parser could not read" };
436
+ }
437
+ if (tokens.map((token) => token.raw).join("") !== segment.text) {
438
+ return { children: [], lockedReason: "markdown the parser could not map" };
439
+ }
440
+ const segmentStart = children.length;
441
+ for (const token of tokens) {
442
+ if (token.type === "space") {
443
+ const last2 = children[children.length - 1];
444
+ if (!last2 || children.length === segmentStart) {
445
+ return { children: [], lockedReason: "leading blank lines" };
446
+ }
447
+ last2.trail += token.raw;
448
+ continue;
449
+ }
450
+ children.push(parseTopToken(token, state));
451
+ }
452
+ const last = children[children.length - 1];
453
+ if (!last || children.length === segmentStart) {
454
+ return { children: [], lockedReason: "markdown the parser could not map" };
455
+ }
456
+ last.trail += segment.after;
457
+ }
458
+ children.forEach(
459
+ (child, index) => adjacency.set(child.id, { trail: child.trail, next: children[index + 1]?.id ?? null })
460
+ );
461
+ if (children.length === 0) return { children, lockedReason: "empty block" };
462
+ if (children.every((child) => child.lockedReason !== null)) {
463
+ return { children, lockedReason: children[0]?.lockedReason ?? "source" };
464
+ }
465
+ return { children, lockedReason: null };
466
+ }
467
+ function splitAtBlankLines(text) {
468
+ if (!/^[ \t]+$/m.test(text)) return { segments: [trimSegmentEnd(text, "")], lead: "" };
469
+ const ranges = [];
470
+ let current = null;
471
+ let position = 0;
472
+ for (const line of text.split("\n")) {
473
+ const lineEnd = position + line.length;
474
+ if (/^[ \t]*$/.test(line)) {
475
+ if (current) ranges.push(current);
476
+ current = null;
477
+ } else if (current) {
478
+ current.end = lineEnd;
479
+ } else {
480
+ current = { start: position, end: lineEnd };
481
+ }
482
+ position = lineEnd + 1;
483
+ }
484
+ if (current) ranges.push(current);
485
+ const segments = ranges.map(
486
+ (range, index) => trimSegmentEnd(
487
+ text.slice(range.start, range.end),
488
+ text.slice(range.end, ranges[index + 1]?.start ?? text.length)
489
+ )
490
+ );
491
+ return { segments, lead: text.slice(0, ranges[0]?.start ?? text.length) };
492
+ }
493
+ function trimSegmentEnd(text, after) {
494
+ const tail = /[ \t]+$/.exec(text)?.[0] ?? "";
495
+ return tail ? { text: text.slice(0, text.length - tail.length), after: tail + after } : { text, after };
496
+ }
497
+ function parseTopToken(token, state) {
498
+ const { schema, islands, adjacency, nextId } = state;
499
+ const { body, trail } = splitTrail(token.raw);
500
+ const raw = restorePlaceholders(body, islands);
501
+ const scratch = new Map(adjacency);
502
+ const scratchState = { ...state, adjacency: scratch };
503
+ const pageBreak = (token.type === "paragraph" || token.type === "html") && isPageBreakLine(raw);
504
+ let json = pageBreak ? null : blockJSON(token, scratchState);
505
+ let lockedReason = pageBreak ? PAGE_BREAK_REASON : null;
506
+ if (pageBreak) {
507
+ } else if (json) {
508
+ try {
509
+ const node = schema.nodeFromJSON(json);
510
+ node.check();
511
+ const written = serializeBlock(node, createSerializeContext(scratch));
512
+ if (written !== raw) {
513
+ json = null;
514
+ lockedReason = "formatting the visual editor cannot keep byte-for-byte";
515
+ }
516
+ } catch {
517
+ json = null;
518
+ lockedReason = "structure the visual editor cannot hold";
519
+ }
520
+ } else {
521
+ lockedReason = LOCK_REASONS[token.type] ?? "formatting the visual editor cannot keep byte-for-byte";
522
+ }
523
+ if (json) {
524
+ for (const [key, value] of scratch) adjacency.set(key, value);
525
+ } else {
526
+ json = lockedJSON(raw, lockedReason ?? "source", nextId());
527
+ }
528
+ return { json, id: String(json.attrs?.mdId), raw, trail, lockedReason };
529
+ }
530
+ export {
531
+ PAGE_BREAK_REASON,
532
+ parseProseBlock
533
+ };
@@ -0,0 +1,27 @@
1
+ import type { Node as PMNode } from "@tiptap/pm/model";
2
+ export interface Adjacency {
3
+ /** The exact bytes that followed this node in the source (its line breaks). */
4
+ readonly trail: string;
5
+ /** The id of the sibling that followed it, if any. */
6
+ readonly next: string | null;
7
+ }
8
+ export interface SerializeContext {
9
+ readonly adjacency: ReadonlyMap<string, Adjacency>;
10
+ /** Ids already written in this pass — a duplicate (copy/paste) is new text. */
11
+ readonly claimed: Set<string>;
12
+ /**
13
+ * Exact stored bytes for an unchanged node, when the caller holds them.
14
+ * Returning null means "serialize it".
15
+ */
16
+ readonly reuse?: (id: string, node: PMNode) => string | null;
17
+ }
18
+ export declare function createSerializeContext(adjacency?: ReadonlyMap<string, Adjacency>, reuse?: SerializeContext["reuse"]): SerializeContext;
19
+ type ContainerKind = "wrapper" | "blockquote" | "listItem" | "doc";
20
+ /** Serialize a container's block children with original spacing. */
21
+ export declare function serializeChildren(parent: PMNode, kind: ContainerKind, ctx: SerializeContext): string;
22
+ /** Serialize one block node (fresh — the caller decides about reuse). */
23
+ export declare function serializeBlock(node: PMNode, ctx: SerializeContext): string;
24
+ export type ColumnAlign = "left" | "center" | "right" | null;
25
+ /** Serialize a textblock's inline content — marks nested as the source nested them. */
26
+ export declare function serializeInline(parent: PMNode): string;
27
+ export {};