@jacobbubu/md-to-lark 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +171 -0
  3. package/dist/btt/build-tree.js +79 -0
  4. package/dist/btt/index.js +1 -0
  5. package/dist/btt/types.js +1 -0
  6. package/dist/cli/publish-md-to-lark.js +15 -0
  7. package/dist/commands/publish-md/args.js +224 -0
  8. package/dist/commands/publish-md/command.js +97 -0
  9. package/dist/commands/publish-md/index.js +1 -0
  10. package/dist/commands/publish-md/input-resolver.js +48 -0
  11. package/dist/commands/publish-md/mermaid-render.js +17 -0
  12. package/dist/commands/publish-md/pipeline-transform.js +4 -0
  13. package/dist/commands/publish-md/preset-loader.js +113 -0
  14. package/dist/commands/publish-md/presets/medium.js +7 -0
  15. package/dist/commands/publish-md/presets/zh-format.js +8 -0
  16. package/dist/commands/publish-md/title-policy.js +93 -0
  17. package/dist/index.js +1 -0
  18. package/dist/interop/btt-to-last.js +79 -0
  19. package/dist/interop/codec-btt-to-last.js +435 -0
  20. package/dist/interop/codec-last-to-btt.js +383 -0
  21. package/dist/interop/codec-shared.js +722 -0
  22. package/dist/interop/index.js +2 -0
  23. package/dist/interop/last-to-btt.js +17 -0
  24. package/dist/lark/block-types.js +42 -0
  25. package/dist/lark/client.js +36 -0
  26. package/dist/lark/docx/ops.js +596 -0
  27. package/dist/lark/docx/render-btt.js +156 -0
  28. package/dist/lark/docx/render-models.js +1 -0
  29. package/dist/lark/docx/render-payload.js +338 -0
  30. package/dist/lark/docx/render-post-process.js +98 -0
  31. package/dist/lark/docx/render-table.js +87 -0
  32. package/dist/lark/docx/render-types.js +7 -0
  33. package/dist/lark/index.js +2 -0
  34. package/dist/lark/types.js +1 -0
  35. package/dist/last/api.js +1687 -0
  36. package/dist/last/index.js +3 -0
  37. package/dist/last/preview-terminal.js +296 -0
  38. package/dist/last/textual-block-types.js +19 -0
  39. package/dist/last/to-markdown.js +303 -0
  40. package/dist/last/types.js +11 -0
  41. package/dist/pipeline/hast-to-last.js +946 -0
  42. package/dist/pipeline/index.js +3 -0
  43. package/dist/pipeline/markdown/md-to-hast.js +34 -0
  44. package/dist/pipeline/markdown/prepare-markdown.js +1049 -0
  45. package/dist/preview/index.js +1 -0
  46. package/dist/preview/markdown-terminal.js +350 -0
  47. package/dist/publish/asset-adapter.js +123 -0
  48. package/dist/publish/btt-patch.js +65 -0
  49. package/dist/publish/common.js +139 -0
  50. package/dist/publish/ids.js +9 -0
  51. package/dist/publish/index.js +7 -0
  52. package/dist/publish/last-normalize.js +327 -0
  53. package/dist/publish/process-file.js +228 -0
  54. package/dist/publish/runtime.js +133 -0
  55. package/dist/publish/stage-cache.js +56 -0
  56. package/dist/shared/rate-limiter.js +18 -0
  57. package/dist/shared/retry.js +141 -0
  58. package/package.json +78 -0
@@ -0,0 +1,1687 @@
1
+ import { LAST_TEXTUAL_BLOCK_TYPE_SET } from './textual-block-types.js';
2
+ function deepClone(value) {
3
+ return JSON.parse(JSON.stringify(value));
4
+ }
5
+ function isDocument(model) {
6
+ return !('mode' in model && model.mode === 'fragment');
7
+ }
8
+ function isTextualBlockNode(block) {
9
+ return LAST_TEXTUAL_BLOCK_TYPE_SET.has(block.type);
10
+ }
11
+ function getTopLevelBlockIds(model) {
12
+ if (!isDocument(model)) {
13
+ return [...model.topLevel];
14
+ }
15
+ const root = model.blocks[model.rootId];
16
+ return root ? [...root.children] : [];
17
+ }
18
+ function setTopLevelBlockIds(model, ids) {
19
+ if (!isDocument(model)) {
20
+ model.topLevel = [...ids];
21
+ return;
22
+ }
23
+ const root = model.blocks[model.rootId];
24
+ if (!root)
25
+ return;
26
+ root.children = [...ids];
27
+ }
28
+ function parseNumericSuffix(value, prefix) {
29
+ if (!value.startsWith(prefix))
30
+ return 0;
31
+ const n = Number(value.slice(prefix.length));
32
+ return Number.isFinite(n) && n > 0 ? n : 0;
33
+ }
34
+ function seedNextCounters(model) {
35
+ let maxBlock = 0;
36
+ let maxInline = 0;
37
+ for (const block of Object.values(model.blocks)) {
38
+ maxBlock = Math.max(maxBlock, parseNumericSuffix(block.id, 'b_'));
39
+ if (!isTextualBlockNode(block))
40
+ continue;
41
+ for (const inline of block.payload.inlines) {
42
+ maxInline = Math.max(maxInline, parseNumericSuffix(inline.id, 'i_'));
43
+ }
44
+ }
45
+ return {
46
+ block: maxBlock + 1,
47
+ inline: maxInline + 1,
48
+ };
49
+ }
50
+ function nextBlockId(state) {
51
+ let candidate = `b_${state.nextBlockCounter}`;
52
+ while (state.model.blocks[candidate]) {
53
+ state.nextBlockCounter += 1;
54
+ candidate = `b_${state.nextBlockCounter}`;
55
+ }
56
+ state.nextBlockCounter += 1;
57
+ return candidate;
58
+ }
59
+ function nextInlineId(state) {
60
+ const id = `i_${state.nextInlineCounter}`;
61
+ state.nextInlineCounter += 1;
62
+ return id;
63
+ }
64
+ function toSearchText(inline) {
65
+ switch (inline.kind) {
66
+ case 'text_run':
67
+ return { text: inline.text ?? '', editable: true };
68
+ case 'mention_user':
69
+ return { text: inline.userId ?? '', editable: false };
70
+ case 'equation':
71
+ return { text: inline.latex ?? '', editable: false };
72
+ case 'mention_doc':
73
+ return { text: inline.title ?? '', editable: false };
74
+ case 'link_preview':
75
+ return { text: inline.title ?? inline.url ?? '', editable: false };
76
+ case 'reminder':
77
+ case 'inline_block':
78
+ case 'inline_file':
79
+ return { text: '', editable: false };
80
+ default:
81
+ return { text: '', editable: false };
82
+ }
83
+ }
84
+ function blockText(block) {
85
+ if (!isTextualBlockNode(block))
86
+ return '';
87
+ let out = '';
88
+ for (const inline of block.payload.inlines) {
89
+ out += toSearchText(inline).text;
90
+ }
91
+ return out;
92
+ }
93
+ function regexTest(text, pattern) {
94
+ const probe = new RegExp(pattern.source, pattern.flags);
95
+ return probe.test(text);
96
+ }
97
+ function buildScopeForTopLevelTextBlock(scopeId, block) {
98
+ let normalizedText = '';
99
+ const segments = [];
100
+ for (const inline of block.payload.inlines) {
101
+ const projection = toSearchText(inline);
102
+ if (projection.text.length === 0)
103
+ continue;
104
+ const from = normalizedText.length;
105
+ normalizedText += projection.text;
106
+ const to = normalizedText.length;
107
+ segments.push({
108
+ inlineId: inline.id,
109
+ inlineKind: inline.kind,
110
+ from,
111
+ to,
112
+ editable: projection.editable,
113
+ });
114
+ }
115
+ return {
116
+ id: scopeId,
117
+ blockId: block.id,
118
+ blockType: block.type,
119
+ normalizedText,
120
+ segments,
121
+ };
122
+ }
123
+ export function rebuildLASTIndexes(model) {
124
+ const byType = {};
125
+ for (const block of Object.values(model.blocks)) {
126
+ const ids = byType[block.type] ?? [];
127
+ ids.push(block.id);
128
+ byType[block.type] = ids;
129
+ }
130
+ const textScopes = {};
131
+ const textScopeByBlockId = {};
132
+ let scopeCounter = 1;
133
+ for (const blockId of getTopLevelBlockIds(model)) {
134
+ const block = model.blocks[blockId];
135
+ if (!block || !isTextualBlockNode(block) || block.type === 'page') {
136
+ continue;
137
+ }
138
+ const scopeId = `scope_${scopeCounter}`;
139
+ scopeCounter += 1;
140
+ const scope = buildScopeForTopLevelTextBlock(scopeId, block);
141
+ textScopes[scopeId] = scope;
142
+ textScopeByBlockId[block.id] = scopeId;
143
+ }
144
+ return {
145
+ byType,
146
+ textScopes,
147
+ textScopeByBlockId,
148
+ };
149
+ }
150
+ function ensureIndexes(state) {
151
+ state.model.indexes = rebuildLASTIndexes(state.model);
152
+ }
153
+ function ensureTxn(state) {
154
+ if (state.active)
155
+ return;
156
+ state.checkpoint = deepClone(state.model);
157
+ state.stagedOps = [];
158
+ state.warnings = [];
159
+ state.active = true;
160
+ }
161
+ function uniqueOrdered(ids) {
162
+ const seen = new Set();
163
+ const out = [];
164
+ for (const id of ids) {
165
+ if (seen.has(id))
166
+ continue;
167
+ seen.add(id);
168
+ out.push(id);
169
+ }
170
+ return out;
171
+ }
172
+ function matcherFromSelector(input) {
173
+ if (input === undefined) {
174
+ return () => true;
175
+ }
176
+ if (typeof input === 'function') {
177
+ return (node, idx) => input(idx, node);
178
+ }
179
+ if (typeof input === 'string') {
180
+ const raw = input.trim();
181
+ if (!raw || raw === '*') {
182
+ return () => true;
183
+ }
184
+ const tokens = raw
185
+ .split(',')
186
+ .map((x) => x.trim())
187
+ .filter((x) => x.length > 0);
188
+ const predicates = tokens.map((token) => {
189
+ if (token === '*') {
190
+ return (_node) => true;
191
+ }
192
+ if (token.startsWith('#')) {
193
+ const id = token.slice(1);
194
+ return (node) => node.id === id;
195
+ }
196
+ const attrMatch = token.match(/^\[([^=\]]+)(?:=(.+))?\]$/);
197
+ if (attrMatch) {
198
+ const key = attrMatch[1]?.trim() ?? '';
199
+ const rawValue = attrMatch[2]?.trim();
200
+ const expected = rawValue ? stripQuotes(rawValue) : undefined;
201
+ return (node) => {
202
+ const value = getPathValue(node, key);
203
+ if (expected === undefined)
204
+ return value !== undefined;
205
+ return String(value) === expected;
206
+ };
207
+ }
208
+ return (node) => node.type === token;
209
+ });
210
+ return (node) => predicates.some((p) => p(node));
211
+ }
212
+ if (isSelectionLike(input)) {
213
+ const ids = new Set(input.ids());
214
+ return (node) => ids.has(node.id);
215
+ }
216
+ if (isBlockNode(input)) {
217
+ return (node) => node.id === input.id;
218
+ }
219
+ const descriptor = input;
220
+ return (node) => {
221
+ if (descriptor.ids && !descriptor.ids.includes(node.id))
222
+ return false;
223
+ if (descriptor.types && !descriptor.types.includes(node.type))
224
+ return false;
225
+ if (descriptor.bttIds && !descriptor.bttIds.includes(node.bttId ?? ''))
226
+ return false;
227
+ if (descriptor.attrs) {
228
+ for (const [k, v] of Object.entries(descriptor.attrs)) {
229
+ const current = getPathValue(node, k);
230
+ if (current !== v)
231
+ return false;
232
+ }
233
+ }
234
+ if (descriptor.hasText !== undefined) {
235
+ const text = blockText(node);
236
+ if (typeof descriptor.hasText === 'string') {
237
+ if (!text.includes(descriptor.hasText))
238
+ return false;
239
+ }
240
+ else if (!regexTest(text, descriptor.hasText)) {
241
+ return false;
242
+ }
243
+ }
244
+ return true;
245
+ };
246
+ }
247
+ function stripQuotes(value) {
248
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
249
+ return value.slice(1, -1);
250
+ }
251
+ return value;
252
+ }
253
+ function isSelectionLike(value) {
254
+ return (typeof value === 'object' &&
255
+ value !== null &&
256
+ 'ids' in value &&
257
+ typeof value.ids === 'function');
258
+ }
259
+ function isBlockNode(value) {
260
+ return typeof value === 'object' && value !== null && 'id' in value && 'type' in value && 'children' in value;
261
+ }
262
+ function getPathValue(target, path) {
263
+ if (!path)
264
+ return undefined;
265
+ const parts = path.split('.').filter((x) => x.length > 0);
266
+ let current = target;
267
+ for (const part of parts) {
268
+ if (typeof current !== 'object' || current === null) {
269
+ return undefined;
270
+ }
271
+ current = current[part];
272
+ }
273
+ return current;
274
+ }
275
+ function setPathValue(target, path, value) {
276
+ const parts = path.split('.').filter((x) => x.length > 0);
277
+ if (parts.length === 0 || typeof target !== 'object' || target === null)
278
+ return;
279
+ let cursor = target;
280
+ for (let i = 0; i < parts.length - 1; i += 1) {
281
+ const key = parts[i];
282
+ if (!key)
283
+ continue;
284
+ const current = cursor[key];
285
+ if (typeof current === 'object' && current !== null) {
286
+ cursor = current;
287
+ continue;
288
+ }
289
+ const next = {};
290
+ cursor[key] = next;
291
+ cursor = next;
292
+ }
293
+ const tail = parts[parts.length - 1];
294
+ if (!tail)
295
+ return;
296
+ cursor[tail] = value;
297
+ }
298
+ function deletePathValue(target, path) {
299
+ const parts = path.split('.').filter((x) => x.length > 0);
300
+ if (parts.length === 0 || typeof target !== 'object' || target === null)
301
+ return;
302
+ let cursor = target;
303
+ for (let i = 0; i < parts.length - 1; i += 1) {
304
+ const key = parts[i];
305
+ if (!key)
306
+ return;
307
+ const current = cursor[key];
308
+ if (typeof current !== 'object' || current === null) {
309
+ return;
310
+ }
311
+ cursor = current;
312
+ }
313
+ const tail = parts[parts.length - 1];
314
+ if (!tail)
315
+ return;
316
+ delete cursor[tail];
317
+ }
318
+ function getSiblingContext(model, block) {
319
+ if (block.parentId) {
320
+ const parent = model.blocks[block.parentId];
321
+ if (!parent)
322
+ return null;
323
+ const idx = parent.children.indexOf(block.id);
324
+ if (idx < 0)
325
+ return null;
326
+ return { list: parent.children, index: idx };
327
+ }
328
+ if (isDocument(model)) {
329
+ if (block.id === model.rootId) {
330
+ return null;
331
+ }
332
+ const root = model.blocks[model.rootId];
333
+ if (!root)
334
+ return null;
335
+ const idx = root.children.indexOf(block.id);
336
+ if (idx < 0)
337
+ return null;
338
+ return { list: root.children, index: idx };
339
+ }
340
+ const idx = model.topLevel.indexOf(block.id);
341
+ if (idx < 0)
342
+ return null;
343
+ return { list: model.topLevel, index: idx };
344
+ }
345
+ function gatherDescendantIds(model, id) {
346
+ const out = [];
347
+ const stack = [id];
348
+ while (stack.length > 0) {
349
+ const currentId = stack.pop();
350
+ if (!currentId)
351
+ continue;
352
+ const node = model.blocks[currentId];
353
+ if (!node)
354
+ continue;
355
+ for (let i = node.children.length - 1; i >= 0; i -= 1) {
356
+ const childId = node.children[i];
357
+ if (!childId)
358
+ continue;
359
+ out.push(childId);
360
+ stack.push(childId);
361
+ }
362
+ }
363
+ return out;
364
+ }
365
+ function removeSubtree(model, rootId) {
366
+ const removed = [rootId, ...gatherDescendantIds(model, rootId)];
367
+ const root = model.blocks[rootId];
368
+ if (!root)
369
+ return [];
370
+ const sibling = getSiblingContext(model, root);
371
+ if (sibling) {
372
+ sibling.list.splice(sibling.index, 1);
373
+ }
374
+ for (const id of removed) {
375
+ delete model.blocks[id];
376
+ }
377
+ return removed;
378
+ }
379
+ function asNodeList(input) {
380
+ return Array.isArray(input) ? input : [input];
381
+ }
382
+ function materializeInsertionNodes(state, nodes, parentId) {
383
+ const out = [];
384
+ for (const node of asNodeList(nodes)) {
385
+ const cloned = deepClone(node);
386
+ cloned.id = nextBlockId(state);
387
+ cloned.parentId = parentId;
388
+ cloned.children = [];
389
+ out.push(cloned);
390
+ }
391
+ return out;
392
+ }
393
+ function textualInlineFromText(state, text) {
394
+ return {
395
+ id: nextInlineId(state),
396
+ kind: 'text_run',
397
+ marks: {
398
+ bold: false,
399
+ italic: false,
400
+ strikethrough: false,
401
+ underline: false,
402
+ inlineCode: false,
403
+ },
404
+ text,
405
+ };
406
+ }
407
+ function advanceStringIndex(input, index, unicode) {
408
+ if (!unicode)
409
+ return index + 1;
410
+ if (index < 0 || index >= input.length)
411
+ return index + 1;
412
+ const first = input.charCodeAt(index);
413
+ if (first < 0xd800 || first > 0xdbff || index + 1 >= input.length) {
414
+ return index + 1;
415
+ }
416
+ const second = input.charCodeAt(index + 1);
417
+ if (second < 0xdc00 || second > 0xdfff) {
418
+ return index + 1;
419
+ }
420
+ return index + 2;
421
+ }
422
+ function collectRegexMatches(text, pattern, maxMatches = Number.POSITIVE_INFINITY) {
423
+ if (maxMatches <= 0) {
424
+ return [];
425
+ }
426
+ const matches = [];
427
+ const flags = pattern.global ? pattern.flags : pattern.flags.replace(/g/g, '');
428
+ const probe = new RegExp(pattern.source, flags);
429
+ if (!pattern.global) {
430
+ const found = probe.exec(text);
431
+ if (!found)
432
+ return [];
433
+ return [
434
+ {
435
+ match: found[0],
436
+ captures: found.slice(1).map((item) => (item === undefined ? undefined : String(item))),
437
+ ...(found.groups ? { namedGroups: { ...found.groups } } : {}),
438
+ start: found.index,
439
+ end: found.index + found[0].length,
440
+ },
441
+ ];
442
+ }
443
+ while (matches.length < maxMatches) {
444
+ const found = probe.exec(text);
445
+ if (!found)
446
+ break;
447
+ matches.push({
448
+ match: found[0],
449
+ captures: found.slice(1).map((item) => (item === undefined ? undefined : String(item))),
450
+ ...(found.groups ? { namedGroups: { ...found.groups } } : {}),
451
+ start: found.index,
452
+ end: found.index + found[0].length,
453
+ });
454
+ if (found[0].length === 0) {
455
+ probe.lastIndex = advanceStringIndex(text, probe.lastIndex, probe.unicode);
456
+ }
457
+ }
458
+ return matches;
459
+ }
460
+ function expandReplacementTemplate(template, found, input) {
461
+ let out = '';
462
+ for (let i = 0; i < template.length; i += 1) {
463
+ const ch = template[i];
464
+ if (ch !== '$' || i + 1 >= template.length) {
465
+ out += ch;
466
+ continue;
467
+ }
468
+ const next = template[i + 1] ?? '';
469
+ if (next === '$') {
470
+ out += '$';
471
+ i += 1;
472
+ continue;
473
+ }
474
+ if (next === '&') {
475
+ out += found.match;
476
+ i += 1;
477
+ continue;
478
+ }
479
+ if (next === '`') {
480
+ out += input.slice(0, found.start);
481
+ i += 1;
482
+ continue;
483
+ }
484
+ if (next === "'") {
485
+ out += input.slice(found.end);
486
+ i += 1;
487
+ continue;
488
+ }
489
+ if (next === '<') {
490
+ const close = template.indexOf('>', i + 2);
491
+ if (close < 0) {
492
+ out += '$<';
493
+ i += 1;
494
+ continue;
495
+ }
496
+ const name = template.slice(i + 2, close);
497
+ if (found.namedGroups) {
498
+ out += found.namedGroups[name] ?? '';
499
+ }
500
+ else {
501
+ out += `$<${name}>`;
502
+ }
503
+ i = close;
504
+ continue;
505
+ }
506
+ if (next >= '0' && next <= '9') {
507
+ let consumed = 1;
508
+ let captureText = null;
509
+ if (next !== '0') {
510
+ const one = Number(next);
511
+ const next2 = template[i + 2];
512
+ if (next2 && next2 >= '0' && next2 <= '9') {
513
+ const two = Number(`${next}${next2}`);
514
+ if (two > 0 && two <= found.captures.length) {
515
+ captureText = found.captures[two - 1] ?? '';
516
+ consumed = 2;
517
+ }
518
+ }
519
+ if (captureText === null && one > 0 && one <= found.captures.length) {
520
+ captureText = found.captures[one - 1] ?? '';
521
+ consumed = 1;
522
+ }
523
+ }
524
+ if (captureText !== null) {
525
+ out += captureText;
526
+ }
527
+ else {
528
+ out += `$${next}`;
529
+ }
530
+ i += consumed;
531
+ continue;
532
+ }
533
+ out += `$${next}`;
534
+ i += 1;
535
+ }
536
+ return out;
537
+ }
538
+ function resolveReplacementText(source, found, replacement) {
539
+ if (typeof replacement === 'function') {
540
+ const callback = replacement;
541
+ const args = [found.match, ...found.captures, found.start, source];
542
+ if (found.namedGroups) {
543
+ args.push(found.namedGroups);
544
+ }
545
+ return String(callback(...args));
546
+ }
547
+ return expandReplacementTemplate(replacement, found, source);
548
+ }
549
+ function marksEqual(a, b) {
550
+ return JSON.stringify(a) === JSON.stringify(b);
551
+ }
552
+ function marksAtIndex(slices, index) {
553
+ if (slices.length === 0) {
554
+ return {
555
+ bold: false,
556
+ italic: false,
557
+ strikethrough: false,
558
+ underline: false,
559
+ inlineCode: false,
560
+ };
561
+ }
562
+ for (const slice of slices) {
563
+ if (index >= slice.start && index < slice.end) {
564
+ return deepClone(slice.run.marks);
565
+ }
566
+ }
567
+ const first = slices[0];
568
+ if (!first) {
569
+ return {
570
+ bold: false,
571
+ italic: false,
572
+ strikethrough: false,
573
+ underline: false,
574
+ inlineCode: false,
575
+ };
576
+ }
577
+ if (index <= first.start) {
578
+ return deepClone(first.run.marks);
579
+ }
580
+ const last = slices[slices.length - 1];
581
+ if (!last) {
582
+ return deepClone(first.run.marks);
583
+ }
584
+ return deepClone(last.run.marks);
585
+ }
586
+ function appendSourceSegments(slices, from, to, out) {
587
+ if (to <= from)
588
+ return;
589
+ for (const slice of slices) {
590
+ const left = Math.max(from, slice.start);
591
+ const right = Math.min(to, slice.end);
592
+ if (left >= right)
593
+ continue;
594
+ const part = slice.text.slice(left - slice.start, right - slice.start);
595
+ if (part.length === 0)
596
+ continue;
597
+ out.push({
598
+ text: part,
599
+ marks: deepClone(slice.run.marks),
600
+ });
601
+ }
602
+ }
603
+ function mergeSegments(segments) {
604
+ const out = [];
605
+ for (const segment of segments) {
606
+ if (segment.text.length === 0)
607
+ continue;
608
+ const prev = out[out.length - 1];
609
+ if (prev && marksEqual(prev.marks, segment.marks)) {
610
+ prev.text += segment.text;
611
+ continue;
612
+ }
613
+ out.push({
614
+ text: segment.text,
615
+ marks: deepClone(segment.marks),
616
+ });
617
+ }
618
+ return out;
619
+ }
620
+ function replaceTextRunCluster(state, cluster, pattern, replacement, maxMatches) {
621
+ if (maxMatches <= 0) {
622
+ return {
623
+ changed: false,
624
+ matchCount: 0,
625
+ inlines: cluster.map((inline) => deepClone(inline)),
626
+ };
627
+ }
628
+ const slices = [];
629
+ let cursor = 0;
630
+ for (const run of cluster) {
631
+ const text = run.text ?? '';
632
+ slices.push({
633
+ run,
634
+ text,
635
+ start: cursor,
636
+ end: cursor + text.length,
637
+ });
638
+ cursor += text.length;
639
+ }
640
+ const source = slices.map((slice) => slice.text).join('');
641
+ if (source.length === 0) {
642
+ return {
643
+ changed: false,
644
+ matchCount: 0,
645
+ inlines: cluster.map((inline) => deepClone(inline)),
646
+ };
647
+ }
648
+ const matches = collectRegexMatches(source, pattern, maxMatches);
649
+ if (matches.length === 0) {
650
+ return {
651
+ changed: false,
652
+ matchCount: 0,
653
+ inlines: cluster.map((inline) => deepClone(inline)),
654
+ };
655
+ }
656
+ const segments = [];
657
+ let consumed = 0;
658
+ for (const found of matches) {
659
+ if (found.start < consumed) {
660
+ continue;
661
+ }
662
+ appendSourceSegments(slices, consumed, found.start, segments);
663
+ const nextText = resolveReplacementText(source, found, replacement);
664
+ if (nextText.length > 0) {
665
+ segments.push({
666
+ text: nextText,
667
+ marks: marksAtIndex(slices, found.start),
668
+ });
669
+ }
670
+ consumed = found.end;
671
+ }
672
+ appendSourceSegments(slices, consumed, source.length, segments);
673
+ const merged = mergeSegments(segments);
674
+ const nextText = merged.map((item) => item.text).join('');
675
+ if (nextText === source) {
676
+ return {
677
+ changed: false,
678
+ matchCount: 0,
679
+ inlines: cluster.map((inline) => deepClone(inline)),
680
+ };
681
+ }
682
+ const inlines = merged.map((segment) => ({
683
+ id: nextInlineId(state),
684
+ kind: 'text_run',
685
+ marks: deepClone(segment.marks),
686
+ text: segment.text,
687
+ }));
688
+ return {
689
+ changed: true,
690
+ matchCount: matches.length,
691
+ inlines,
692
+ };
693
+ }
694
+ function applyScopeReplaceToBlock(state, block, pattern, replacement) {
695
+ const sourceInlines = block.payload.inlines;
696
+ const nextInlines = [];
697
+ let changed = false;
698
+ let remaining = pattern.global ? Number.POSITIVE_INFINITY : 1;
699
+ let cluster = [];
700
+ const flushCluster = () => {
701
+ if (cluster.length === 0)
702
+ return;
703
+ if (remaining <= 0) {
704
+ nextInlines.push(...cluster.map((inline) => deepClone(inline)));
705
+ cluster = [];
706
+ return;
707
+ }
708
+ const result = replaceTextRunCluster(state, cluster, pattern, replacement, remaining);
709
+ nextInlines.push(...result.inlines);
710
+ if (result.changed) {
711
+ changed = true;
712
+ }
713
+ if (Number.isFinite(remaining)) {
714
+ remaining -= result.matchCount;
715
+ }
716
+ cluster = [];
717
+ };
718
+ for (const inline of sourceInlines) {
719
+ if (inline.kind === 'text_run') {
720
+ cluster.push(inline);
721
+ continue;
722
+ }
723
+ flushCluster();
724
+ nextInlines.push(deepClone(inline));
725
+ }
726
+ flushCluster();
727
+ if (!changed) {
728
+ return false;
729
+ }
730
+ block.payload.inlines = nextInlines;
731
+ return true;
732
+ }
733
+ class LASTJQSelectionImpl {
734
+ ctx;
735
+ selectedIds;
736
+ constructor(ctx, ids) {
737
+ this.ctx = ctx;
738
+ this.selectedIds = uniqueOrdered(ids);
739
+ }
740
+ create(ids) {
741
+ return new LASTJQSelectionImpl(this.ctx, ids);
742
+ }
743
+ nodesTyped() {
744
+ const out = [];
745
+ for (const id of this.selectedIds) {
746
+ const node = this.ctx.state.model.blocks[id];
747
+ if (!node)
748
+ continue;
749
+ out.push(node);
750
+ }
751
+ return out;
752
+ }
753
+ filteredIds(selector) {
754
+ const matcher = matcherFromSelector(selector);
755
+ const out = [];
756
+ this.nodesTyped().forEach((node, idx) => {
757
+ if (matcher(node, idx)) {
758
+ out.push(node.id);
759
+ }
760
+ });
761
+ return out;
762
+ }
763
+ get() {
764
+ return this.nodesTyped();
765
+ }
766
+ toArray() {
767
+ return this.get();
768
+ }
769
+ ids() {
770
+ return [...this.selectedIds];
771
+ }
772
+ length() {
773
+ return this.get().length;
774
+ }
775
+ isEmpty() {
776
+ return this.length() === 0;
777
+ }
778
+ each(fn) {
779
+ this.get().forEach((node, idx) => fn(idx, node));
780
+ return this;
781
+ }
782
+ map(fn) {
783
+ return this.get().map((node, idx) => fn(idx, node));
784
+ }
785
+ find(selector) {
786
+ const matcher = matcherFromSelector(selector);
787
+ const model = this.ctx.state.model;
788
+ const out = [];
789
+ for (const id of this.selectedIds) {
790
+ const descendants = gatherDescendantIds(model, id);
791
+ for (const descendantId of descendants) {
792
+ const node = model.blocks[descendantId];
793
+ if (!node)
794
+ continue;
795
+ if (matcher(node, out.length)) {
796
+ out.push(descendantId);
797
+ }
798
+ }
799
+ }
800
+ return this.create(out);
801
+ }
802
+ filter(selector) {
803
+ if (typeof selector === 'function') {
804
+ const ids = this.get()
805
+ .filter((node, idx) => selector(idx, node))
806
+ .map((node) => node.id);
807
+ return this.create(ids);
808
+ }
809
+ return this.create(this.filteredIds(selector));
810
+ }
811
+ not(selector) {
812
+ const matcher = matcherFromSelector(selector);
813
+ const ids = this.get()
814
+ .filter((node, idx) => !matcher(node, idx))
815
+ .map((node) => node.id);
816
+ return this.create(ids);
817
+ }
818
+ is(selector) {
819
+ const matcher = matcherFromSelector(selector);
820
+ return this.get().some((node, idx) => matcher(node, idx));
821
+ }
822
+ has(selector) {
823
+ const matcher = matcherFromSelector(selector);
824
+ const model = this.ctx.state.model;
825
+ const ids = [];
826
+ for (const id of this.selectedIds) {
827
+ const descendantIds = gatherDescendantIds(model, id);
828
+ let matched = false;
829
+ for (const descendantId of descendantIds) {
830
+ const node = model.blocks[descendantId];
831
+ if (!node)
832
+ continue;
833
+ if (matcher(node, 0)) {
834
+ matched = true;
835
+ break;
836
+ }
837
+ }
838
+ if (matched) {
839
+ ids.push(id);
840
+ }
841
+ }
842
+ return this.create(ids);
843
+ }
844
+ byType(...types) {
845
+ const set = new Set(types);
846
+ const ids = this.get()
847
+ .filter((node) => set.has(node.type))
848
+ .map((node) => node.id);
849
+ return this.create(ids);
850
+ }
851
+ byId(...ids) {
852
+ const set = new Set(ids);
853
+ const out = this.selectedIds.filter((id) => set.has(id));
854
+ return this.create(out);
855
+ }
856
+ byBttId(...bttIds) {
857
+ const set = new Set(bttIds);
858
+ const out = this.get()
859
+ .filter((node) => set.has(node.bttId ?? ''))
860
+ .map((node) => node.id);
861
+ return this.create(out);
862
+ }
863
+ parent(selector) {
864
+ const parents = [];
865
+ for (const node of this.get()) {
866
+ if (node.parentId) {
867
+ parents.push(node.parentId);
868
+ }
869
+ else if (isDocument(this.ctx.state.model)) {
870
+ const doc = this.ctx.state.model;
871
+ if (node.id !== doc.rootId) {
872
+ parents.push(doc.rootId);
873
+ }
874
+ }
875
+ }
876
+ return this.create(parents).filter(selector);
877
+ }
878
+ parents(selector) {
879
+ const model = this.ctx.state.model;
880
+ const out = [];
881
+ for (const node of this.get()) {
882
+ let cursor = node.parentId;
883
+ while (cursor) {
884
+ out.push(cursor);
885
+ const parent = model.blocks[cursor];
886
+ cursor = parent?.parentId ?? null;
887
+ }
888
+ if (!node.parentId && isDocument(model) && node.id !== model.rootId) {
889
+ out.push(model.rootId);
890
+ }
891
+ }
892
+ return this.create(out).filter(selector);
893
+ }
894
+ children(selector) {
895
+ const out = [];
896
+ for (const node of this.get()) {
897
+ out.push(...node.children);
898
+ }
899
+ return this.create(out).filter(selector);
900
+ }
901
+ descendants(selector) {
902
+ const model = this.ctx.state.model;
903
+ const out = [];
904
+ for (const id of this.selectedIds) {
905
+ out.push(...gatherDescendantIds(model, id));
906
+ }
907
+ return this.create(out).filter(selector);
908
+ }
909
+ siblings(selector) {
910
+ const model = this.ctx.state.model;
911
+ const out = [];
912
+ for (const node of this.get()) {
913
+ const ctx = getSiblingContext(model, node);
914
+ if (!ctx)
915
+ continue;
916
+ for (const siblingId of ctx.list) {
917
+ if (siblingId === node.id)
918
+ continue;
919
+ out.push(siblingId);
920
+ }
921
+ }
922
+ return this.create(out).filter(selector);
923
+ }
924
+ next(selector) {
925
+ const model = this.ctx.state.model;
926
+ const out = [];
927
+ for (const node of this.get()) {
928
+ const sibling = getSiblingContext(model, node);
929
+ if (!sibling)
930
+ continue;
931
+ const candidate = sibling.list[sibling.index + 1];
932
+ if (candidate)
933
+ out.push(candidate);
934
+ }
935
+ return this.create(out).filter(selector);
936
+ }
937
+ prev(selector) {
938
+ const model = this.ctx.state.model;
939
+ const out = [];
940
+ for (const node of this.get()) {
941
+ const sibling = getSiblingContext(model, node);
942
+ if (!sibling)
943
+ continue;
944
+ const candidate = sibling.list[sibling.index - 1];
945
+ if (candidate)
946
+ out.push(candidate);
947
+ }
948
+ return this.create(out).filter(selector);
949
+ }
950
+ closest(selector) {
951
+ const matcher = matcherFromSelector(selector);
952
+ const model = this.ctx.state.model;
953
+ const out = [];
954
+ for (const node of this.get()) {
955
+ let cursor = node;
956
+ while (cursor) {
957
+ if (matcher(cursor, 0)) {
958
+ out.push(cursor.id);
959
+ break;
960
+ }
961
+ if (!cursor.parentId) {
962
+ if (isDocument(model) && cursor.id !== model.rootId) {
963
+ cursor = model.blocks[model.rootId];
964
+ continue;
965
+ }
966
+ break;
967
+ }
968
+ cursor = model.blocks[cursor.parentId];
969
+ }
970
+ }
971
+ return this.create(out);
972
+ }
973
+ eq(index) {
974
+ const arr = this.get();
975
+ const normalized = index >= 0 ? index : arr.length + index;
976
+ if (normalized < 0 || normalized >= arr.length) {
977
+ return this.create([]);
978
+ }
979
+ const node = arr[normalized];
980
+ if (!node) {
981
+ return this.create([]);
982
+ }
983
+ return this.create([node.id]);
984
+ }
985
+ first() {
986
+ return this.eq(0);
987
+ }
988
+ last() {
989
+ return this.eq(-1);
990
+ }
991
+ slice(start, end) {
992
+ const ids = this.get()
993
+ .slice(start, end)
994
+ .map((node) => node.id);
995
+ return this.create(ids);
996
+ }
997
+ contains(text) {
998
+ const ids = this.get()
999
+ .filter((node) => blockText(node).includes(text))
1000
+ .map((node) => node.id);
1001
+ return this.create(ids);
1002
+ }
1003
+ matches(pattern) {
1004
+ const ids = this.get()
1005
+ .filter((node) => regexTest(blockText(node), pattern))
1006
+ .map((node) => node.id);
1007
+ return this.create(ids);
1008
+ }
1009
+ text(value) {
1010
+ const nodes = this.get();
1011
+ if (value === undefined) {
1012
+ return nodes.map((node) => blockText(node)).join('');
1013
+ }
1014
+ ensureTxn(this.ctx.state);
1015
+ nodes.forEach((node, idx) => {
1016
+ if (!isTextualBlockNode(node))
1017
+ return;
1018
+ const old = blockText(node);
1019
+ const next = typeof value === 'function' ? value(idx, old) : value;
1020
+ node.payload.inlines = next.length === 0 ? [] : [textualInlineFromText(this.ctx.state, next)];
1021
+ });
1022
+ this.ctx.state.stagedOps.push({
1023
+ kind: 'text_set',
1024
+ targets: nodes.map((n) => n.id),
1025
+ });
1026
+ ensureIndexes(this.ctx.state);
1027
+ return this;
1028
+ }
1029
+ replaceText(pattern, replacement) {
1030
+ const nodes = this.get();
1031
+ ensureTxn(this.ctx.state);
1032
+ for (const node of nodes) {
1033
+ if (!isTextualBlockNode(node))
1034
+ continue;
1035
+ applyScopeReplaceToBlock(this.ctx.state, node, pattern, replacement);
1036
+ }
1037
+ this.ctx.state.stagedOps.push({
1038
+ kind: 'text_replace',
1039
+ targets: nodes.map((n) => n.id),
1040
+ pattern: pattern.source,
1041
+ flags: pattern.flags,
1042
+ });
1043
+ ensureIndexes(this.ctx.state);
1044
+ return this;
1045
+ }
1046
+ inlines(value) {
1047
+ const nodes = this.get();
1048
+ if (value === undefined) {
1049
+ const out = [];
1050
+ for (const node of nodes) {
1051
+ if (!isTextualBlockNode(node))
1052
+ continue;
1053
+ out.push(...node.payload.inlines);
1054
+ }
1055
+ return out;
1056
+ }
1057
+ ensureTxn(this.ctx.state);
1058
+ nodes.forEach((node, idx) => {
1059
+ if (!isTextualBlockNode(node))
1060
+ return;
1061
+ const old = node.payload.inlines;
1062
+ const nextRaw = typeof value === 'function' ? value(idx, old) : value;
1063
+ node.payload.inlines = nextRaw.map((inline) => {
1064
+ const cloned = deepClone(inline);
1065
+ if (!cloned.id) {
1066
+ cloned.id = nextInlineId(this.ctx.state);
1067
+ }
1068
+ return cloned;
1069
+ });
1070
+ });
1071
+ this.ctx.state.stagedOps.push({
1072
+ kind: 'inlines_set',
1073
+ targets: nodes.map((n) => n.id),
1074
+ });
1075
+ ensureIndexes(this.ctx.state);
1076
+ return this;
1077
+ }
1078
+ attr(name, value) {
1079
+ const nodes = this.get();
1080
+ const first = nodes[0];
1081
+ if (value === undefined) {
1082
+ return first ? getPathValue(first, `selector.attrs.${name}`) : undefined;
1083
+ }
1084
+ ensureTxn(this.ctx.state);
1085
+ nodes.forEach((node, idx) => {
1086
+ const oldValue = getPathValue(node, `selector.attrs.${name}`);
1087
+ const nextValue = typeof value === 'function' ? value(idx, oldValue) : value;
1088
+ if (!node.selector) {
1089
+ node.selector = {};
1090
+ }
1091
+ if (!node.selector.attrs) {
1092
+ node.selector.attrs = {};
1093
+ }
1094
+ node.selector.attrs[name] = nextValue;
1095
+ });
1096
+ this.ctx.state.stagedOps.push({
1097
+ kind: 'attr_set',
1098
+ targets: nodes.map((n) => n.id),
1099
+ name,
1100
+ });
1101
+ return this;
1102
+ }
1103
+ removeAttr(name) {
1104
+ const nodes = this.get();
1105
+ ensureTxn(this.ctx.state);
1106
+ nodes.forEach((node) => {
1107
+ if (!node.selector?.attrs)
1108
+ return;
1109
+ delete node.selector.attrs[name];
1110
+ if (Object.keys(node.selector.attrs).length === 0) {
1111
+ delete node.selector.attrs;
1112
+ }
1113
+ if (Object.keys(node.selector).length === 0) {
1114
+ delete node.selector;
1115
+ }
1116
+ });
1117
+ this.ctx.state.stagedOps.push({
1118
+ kind: 'attr_remove',
1119
+ targets: nodes.map((n) => n.id),
1120
+ name,
1121
+ });
1122
+ return this;
1123
+ }
1124
+ prop(name, value) {
1125
+ const nodes = this.get();
1126
+ const first = nodes[0];
1127
+ if (value === undefined) {
1128
+ return first ? getPathValue(first, name) : undefined;
1129
+ }
1130
+ ensureTxn(this.ctx.state);
1131
+ nodes.forEach((node, idx) => {
1132
+ const oldValue = getPathValue(node, name);
1133
+ const nextValue = typeof value === 'function' ? value(idx, oldValue) : value;
1134
+ setPathValue(node, name, nextValue);
1135
+ });
1136
+ this.ctx.state.stagedOps.push({
1137
+ kind: 'prop_set',
1138
+ targets: nodes.map((n) => n.id),
1139
+ name,
1140
+ });
1141
+ return this;
1142
+ }
1143
+ css(nameOrPatch, value) {
1144
+ const nodes = this.get();
1145
+ const first = nodes[0];
1146
+ if (typeof nameOrPatch === 'string' && value === undefined) {
1147
+ if (!first || !isTextualBlockNode(first))
1148
+ return undefined;
1149
+ return getPathValue(first.payload.style, nameOrPatch);
1150
+ }
1151
+ ensureTxn(this.ctx.state);
1152
+ if (typeof nameOrPatch === 'string') {
1153
+ nodes.forEach((node, idx) => {
1154
+ if (!isTextualBlockNode(node))
1155
+ return;
1156
+ const oldValue = getPathValue(node.payload.style, nameOrPatch);
1157
+ const nextValue = typeof value === 'function' ? value(idx, oldValue) : value;
1158
+ setPathValue(node.payload.style, nameOrPatch, nextValue);
1159
+ });
1160
+ this.ctx.state.stagedOps.push({
1161
+ kind: 'style_set',
1162
+ targets: nodes.map((n) => n.id),
1163
+ name: nameOrPatch,
1164
+ });
1165
+ ensureIndexes(this.ctx.state);
1166
+ return this;
1167
+ }
1168
+ const patch = nameOrPatch;
1169
+ nodes.forEach((node) => {
1170
+ if (!isTextualBlockNode(node))
1171
+ return;
1172
+ for (const [k, v] of Object.entries(patch)) {
1173
+ setPathValue(node.payload.style, k, v);
1174
+ }
1175
+ });
1176
+ this.ctx.state.stagedOps.push({
1177
+ kind: 'style_set',
1178
+ targets: nodes.map((n) => n.id),
1179
+ name: '[patch]',
1180
+ });
1181
+ ensureIndexes(this.ctx.state);
1182
+ return this;
1183
+ }
1184
+ append(node) {
1185
+ const targets = this.get();
1186
+ ensureTxn(this.ctx.state);
1187
+ for (const target of targets) {
1188
+ const inserted = materializeInsertionNodes(this.ctx.state, node, target.id);
1189
+ for (const child of inserted) {
1190
+ this.ctx.state.model.blocks[child.id] = child;
1191
+ target.children.push(child.id);
1192
+ }
1193
+ }
1194
+ this.ctx.state.stagedOps.push({
1195
+ kind: 'insert',
1196
+ mode: 'append',
1197
+ targets: targets.map((x) => x.id),
1198
+ count: asNodeList(node).length,
1199
+ });
1200
+ ensureIndexes(this.ctx.state);
1201
+ return this;
1202
+ }
1203
+ prepend(node) {
1204
+ const targets = this.get();
1205
+ ensureTxn(this.ctx.state);
1206
+ for (const target of targets) {
1207
+ const inserted = materializeInsertionNodes(this.ctx.state, node, target.id);
1208
+ for (const child of inserted) {
1209
+ this.ctx.state.model.blocks[child.id] = child;
1210
+ }
1211
+ target.children = [...inserted.map((x) => x.id), ...target.children];
1212
+ }
1213
+ this.ctx.state.stagedOps.push({
1214
+ kind: 'insert',
1215
+ mode: 'prepend',
1216
+ targets: targets.map((x) => x.id),
1217
+ count: asNodeList(node).length,
1218
+ });
1219
+ ensureIndexes(this.ctx.state);
1220
+ return this;
1221
+ }
1222
+ before(node) {
1223
+ const targets = this.get();
1224
+ ensureTxn(this.ctx.state);
1225
+ for (const target of targets) {
1226
+ const sibling = getSiblingContext(this.ctx.state.model, target);
1227
+ if (!sibling)
1228
+ continue;
1229
+ const parentId = target.parentId;
1230
+ const inserted = materializeInsertionNodes(this.ctx.state, node, parentId);
1231
+ for (const n of inserted) {
1232
+ this.ctx.state.model.blocks[n.id] = n;
1233
+ }
1234
+ sibling.list.splice(sibling.index, 0, ...inserted.map((x) => x.id));
1235
+ }
1236
+ this.ctx.state.stagedOps.push({
1237
+ kind: 'insert',
1238
+ mode: 'before',
1239
+ targets: targets.map((x) => x.id),
1240
+ count: asNodeList(node).length,
1241
+ });
1242
+ ensureIndexes(this.ctx.state);
1243
+ return this;
1244
+ }
1245
+ after(node) {
1246
+ const targets = this.get();
1247
+ ensureTxn(this.ctx.state);
1248
+ for (const target of targets) {
1249
+ const sibling = getSiblingContext(this.ctx.state.model, target);
1250
+ if (!sibling)
1251
+ continue;
1252
+ const parentId = target.parentId;
1253
+ const inserted = materializeInsertionNodes(this.ctx.state, node, parentId);
1254
+ for (const n of inserted) {
1255
+ this.ctx.state.model.blocks[n.id] = n;
1256
+ }
1257
+ sibling.list.splice(sibling.index + 1, 0, ...inserted.map((x) => x.id));
1258
+ }
1259
+ this.ctx.state.stagedOps.push({
1260
+ kind: 'insert',
1261
+ mode: 'after',
1262
+ targets: targets.map((x) => x.id),
1263
+ count: asNodeList(node).length,
1264
+ });
1265
+ ensureIndexes(this.ctx.state);
1266
+ return this;
1267
+ }
1268
+ replaceWith(node) {
1269
+ const targets = this.get();
1270
+ ensureTxn(this.ctx.state);
1271
+ for (const target of targets) {
1272
+ const sibling = getSiblingContext(this.ctx.state.model, target);
1273
+ if (!sibling)
1274
+ continue;
1275
+ const parentId = target.parentId;
1276
+ const inserted = materializeInsertionNodes(this.ctx.state, node, parentId);
1277
+ for (const n of inserted) {
1278
+ this.ctx.state.model.blocks[n.id] = n;
1279
+ }
1280
+ sibling.list.splice(sibling.index, 1, ...inserted.map((x) => x.id));
1281
+ const removedIds = gatherDescendantIds(this.ctx.state.model, target.id);
1282
+ for (const removedId of [target.id, ...removedIds]) {
1283
+ delete this.ctx.state.model.blocks[removedId];
1284
+ }
1285
+ }
1286
+ this.ctx.state.stagedOps.push({
1287
+ kind: 'insert',
1288
+ mode: 'replace',
1289
+ targets: targets.map((x) => x.id),
1290
+ count: asNodeList(node).length,
1291
+ });
1292
+ ensureIndexes(this.ctx.state);
1293
+ return this;
1294
+ }
1295
+ remove() {
1296
+ const ids = this.ids();
1297
+ ensureTxn(this.ctx.state);
1298
+ for (const id of ids) {
1299
+ if (!this.ctx.state.model.blocks[id])
1300
+ continue;
1301
+ if (isDocument(this.ctx.state.model) && id === this.ctx.state.model.rootId) {
1302
+ this.ctx.state.warnings.push('skip removing document root page');
1303
+ continue;
1304
+ }
1305
+ removeSubtree(this.ctx.state.model, id);
1306
+ }
1307
+ this.ctx.state.stagedOps.push({
1308
+ kind: 'remove',
1309
+ targets: ids,
1310
+ });
1311
+ ensureIndexes(this.ctx.state);
1312
+ return this;
1313
+ }
1314
+ empty() {
1315
+ const nodes = this.get();
1316
+ ensureTxn(this.ctx.state);
1317
+ for (const node of nodes) {
1318
+ if (isTextualBlockNode(node)) {
1319
+ node.payload.inlines = [];
1320
+ }
1321
+ for (const childId of [...node.children]) {
1322
+ removeSubtree(this.ctx.state.model, childId);
1323
+ }
1324
+ node.children = [];
1325
+ }
1326
+ this.ctx.state.stagedOps.push({
1327
+ kind: 'empty',
1328
+ targets: nodes.map((n) => n.id),
1329
+ });
1330
+ ensureIndexes(this.ctx.state);
1331
+ return this;
1332
+ }
1333
+ clone(deep = true) {
1334
+ const model = this.ctx.state.model;
1335
+ const targets = this.get();
1336
+ ensureTxn(this.ctx.state);
1337
+ const clonedIds = [];
1338
+ const cloneOne = (sourceId, parentId) => {
1339
+ const source = model.blocks[sourceId];
1340
+ if (!source)
1341
+ return null;
1342
+ const cloned = deepClone(source);
1343
+ cloned.id = nextBlockId(this.ctx.state);
1344
+ cloned.parentId = parentId;
1345
+ cloned.children = [];
1346
+ model.blocks[cloned.id] = cloned;
1347
+ if (deep) {
1348
+ for (const childId of source.children) {
1349
+ const childCloneId = cloneOne(childId, cloned.id);
1350
+ if (childCloneId)
1351
+ cloned.children.push(childCloneId);
1352
+ }
1353
+ }
1354
+ return cloned.id;
1355
+ };
1356
+ for (const target of targets) {
1357
+ const sibling = getSiblingContext(model, target);
1358
+ if (!sibling)
1359
+ continue;
1360
+ const rootCloneId = cloneOne(target.id, target.parentId);
1361
+ if (!rootCloneId)
1362
+ continue;
1363
+ sibling.list.splice(sibling.index + 1, 0, rootCloneId);
1364
+ clonedIds.push(rootCloneId);
1365
+ }
1366
+ this.ctx.state.stagedOps.push({
1367
+ kind: 'clone',
1368
+ targets: targets.map((x) => x.id),
1369
+ deep,
1370
+ });
1371
+ ensureIndexes(this.ctx.state);
1372
+ return this.create(clonedIds);
1373
+ }
1374
+ detach() {
1375
+ const ids = this.ids();
1376
+ ensureTxn(this.ctx.state);
1377
+ for (const id of ids) {
1378
+ const block = this.ctx.state.model.blocks[id];
1379
+ if (!block)
1380
+ continue;
1381
+ if (isDocument(this.ctx.state.model) && id === this.ctx.state.model.rootId) {
1382
+ this.ctx.state.warnings.push('skip detaching document root page');
1383
+ continue;
1384
+ }
1385
+ removeSubtree(this.ctx.state.model, id);
1386
+ }
1387
+ this.ctx.state.stagedOps.push({
1388
+ kind: 'detach',
1389
+ targets: ids,
1390
+ });
1391
+ ensureIndexes(this.ctx.state);
1392
+ return this;
1393
+ }
1394
+ }
1395
+ class LASTJQScopeSelectionImpl {
1396
+ ctx;
1397
+ scopeIds;
1398
+ constructor(ctx, ids) {
1399
+ this.ctx = ctx;
1400
+ this.scopeIds = uniqueOrdered(ids);
1401
+ }
1402
+ create(ids) {
1403
+ return new LASTJQScopeSelectionImpl(this.ctx, ids);
1404
+ }
1405
+ ids() {
1406
+ return [...this.scopeIds];
1407
+ }
1408
+ byBlockId(blockId) {
1409
+ const next = this.scopeIds.filter((scopeId) => {
1410
+ const scope = this.ctx.state.model.indexes.textScopes[scopeId];
1411
+ return scope?.blockId === blockId;
1412
+ });
1413
+ return this.create(next);
1414
+ }
1415
+ matches(pattern) {
1416
+ const next = this.scopeIds.filter((scopeId) => {
1417
+ const scope = this.ctx.state.model.indexes.textScopes[scopeId];
1418
+ if (!scope)
1419
+ return false;
1420
+ return regexTest(scope.normalizedText, pattern);
1421
+ });
1422
+ return this.create(next);
1423
+ }
1424
+ replace(find, replacement) {
1425
+ ensureTxn(this.ctx.state);
1426
+ for (const scopeId of this.scopeIds) {
1427
+ const scope = this.ctx.state.model.indexes.textScopes[scopeId];
1428
+ if (!scope)
1429
+ continue;
1430
+ const block = this.ctx.state.model.blocks[scope.blockId];
1431
+ if (!block || !isTextualBlockNode(block))
1432
+ continue;
1433
+ applyScopeReplaceToBlock(this.ctx.state, block, find, replacement);
1434
+ }
1435
+ this.ctx.state.stagedOps.push({
1436
+ kind: 'scope_replace',
1437
+ scopes: [...this.scopeIds],
1438
+ pattern: find.source,
1439
+ flags: find.flags,
1440
+ });
1441
+ ensureIndexes(this.ctx.state);
1442
+ return createDollarProxy(this.ctx);
1443
+ }
1444
+ }
1445
+ function buildAllIds(model) {
1446
+ return Object.keys(model.blocks);
1447
+ }
1448
+ function selectIds(model, selector) {
1449
+ const matcher = matcherFromSelector(selector);
1450
+ const out = [];
1451
+ const all = buildAllIds(model);
1452
+ all.forEach((id, idx) => {
1453
+ const node = model.blocks[id];
1454
+ if (!node)
1455
+ return;
1456
+ if (matcher(node, idx)) {
1457
+ out.push(id);
1458
+ }
1459
+ });
1460
+ return out;
1461
+ }
1462
+ function operationToChange(op) {
1463
+ switch (op.kind) {
1464
+ case 'scope_replace':
1465
+ return {
1466
+ op: op.kind,
1467
+ targets: [],
1468
+ detail: {
1469
+ scopes: op.scopes,
1470
+ pattern: op.pattern,
1471
+ flags: op.flags,
1472
+ },
1473
+ };
1474
+ case 'insert':
1475
+ return {
1476
+ op: op.kind,
1477
+ targets: op.targets,
1478
+ detail: {
1479
+ mode: op.mode,
1480
+ count: op.count,
1481
+ },
1482
+ };
1483
+ case 'clone':
1484
+ return {
1485
+ op: op.kind,
1486
+ targets: op.targets,
1487
+ detail: {
1488
+ deep: op.deep,
1489
+ },
1490
+ };
1491
+ case 'attr_set':
1492
+ case 'attr_remove':
1493
+ case 'prop_set':
1494
+ case 'style_set':
1495
+ return {
1496
+ op: op.kind,
1497
+ targets: op.targets,
1498
+ detail: {
1499
+ name: op.name,
1500
+ },
1501
+ };
1502
+ case 'text_replace':
1503
+ return {
1504
+ op: op.kind,
1505
+ targets: op.targets,
1506
+ detail: {
1507
+ pattern: op.pattern,
1508
+ flags: op.flags,
1509
+ },
1510
+ };
1511
+ case 'plugin':
1512
+ return {
1513
+ op: op.kind,
1514
+ targets: op.targets,
1515
+ detail: {
1516
+ name: op.name,
1517
+ ...op.detail,
1518
+ },
1519
+ };
1520
+ default:
1521
+ return {
1522
+ op: op.kind,
1523
+ targets: 'targets' in op ? op.targets : [],
1524
+ };
1525
+ }
1526
+ }
1527
+ function makePlan(state) {
1528
+ return {
1529
+ schema: 'LASTMutationPlan',
1530
+ version: '1.0.0',
1531
+ docId: state.model.id,
1532
+ createdAt: new Date().toISOString(),
1533
+ ops: deepClone(state.stagedOps),
1534
+ };
1535
+ }
1536
+ function createDollarProxy(ctx) {
1537
+ const callable = ((selector) => {
1538
+ const ids = selectIds(ctx.state.model, selector);
1539
+ return new LASTJQSelectionImpl(ctx, ids);
1540
+ });
1541
+ Object.defineProperty(callable, 'model', {
1542
+ enumerable: true,
1543
+ configurable: false,
1544
+ get: () => ctx.state.model,
1545
+ });
1546
+ const pluginRegistry = {
1547
+ extend(methods) {
1548
+ for (const [name, fn] of Object.entries(methods)) {
1549
+ if (typeof fn !== 'function')
1550
+ continue;
1551
+ Object.defineProperty(LASTJQSelectionImpl.prototype, name, {
1552
+ configurable: true,
1553
+ enumerable: false,
1554
+ writable: true,
1555
+ value: function pluginInvoker(...args) {
1556
+ const result = fn.apply(this, args);
1557
+ const selection = this;
1558
+ const selectionIds = selection.ids();
1559
+ const op = {
1560
+ kind: 'plugin',
1561
+ name,
1562
+ targets: selectionIds,
1563
+ };
1564
+ ensureTxn(ctx.state);
1565
+ ctx.state.stagedOps.push(op);
1566
+ return result;
1567
+ },
1568
+ });
1569
+ }
1570
+ },
1571
+ };
1572
+ Object.defineProperty(callable, 'fn', {
1573
+ enumerable: true,
1574
+ configurable: false,
1575
+ writable: false,
1576
+ value: pluginRegistry,
1577
+ });
1578
+ callable.begin = () => {
1579
+ ctx.state.checkpoint = deepClone(ctx.state.model);
1580
+ ctx.state.stagedOps = [];
1581
+ ctx.state.warnings = [];
1582
+ ctx.state.active = true;
1583
+ return callable;
1584
+ };
1585
+ callable.plan = () => makePlan(ctx.state);
1586
+ callable.commit = (options) => {
1587
+ const shouldRebuild = options?.rebuildIndexes ?? ctx.defaultRebuildIndexesOnCommit;
1588
+ try {
1589
+ const plan = makePlan(ctx.state);
1590
+ ctx.state.hooks?.beforeCommit?.(plan);
1591
+ if (shouldRebuild) {
1592
+ ensureIndexes(ctx.state);
1593
+ }
1594
+ const result = {
1595
+ ok: true,
1596
+ next: deepClone(ctx.state.model),
1597
+ indexes: deepClone(ctx.state.model.indexes),
1598
+ changes: ctx.state.stagedOps.map(operationToChange),
1599
+ warnings: [...ctx.state.warnings],
1600
+ };
1601
+ ctx.state.active = false;
1602
+ ctx.state.checkpoint = null;
1603
+ ctx.state.stagedOps = [];
1604
+ ctx.state.warnings = [];
1605
+ ctx.state.hooks?.afterCommit?.(result);
1606
+ return result;
1607
+ }
1608
+ catch (error) {
1609
+ const normalized = error instanceof Error ? error : new Error(String(error));
1610
+ ctx.state.hooks?.onError?.(normalized);
1611
+ if (ctx.state.checkpoint) {
1612
+ ctx.state.model = deepClone(ctx.state.checkpoint);
1613
+ }
1614
+ ctx.state.active = false;
1615
+ ctx.state.checkpoint = null;
1616
+ ctx.state.stagedOps = [];
1617
+ ctx.state.warnings = [];
1618
+ throw normalized;
1619
+ }
1620
+ };
1621
+ callable.rollback = () => {
1622
+ if (ctx.state.checkpoint) {
1623
+ ctx.state.model = deepClone(ctx.state.checkpoint);
1624
+ ensureIndexes(ctx.state);
1625
+ }
1626
+ ctx.state.active = false;
1627
+ ctx.state.checkpoint = null;
1628
+ ctx.state.stagedOps = [];
1629
+ ctx.state.warnings = [];
1630
+ return callable;
1631
+ };
1632
+ callable.byScope = (selector) => {
1633
+ const allIds = Object.keys(ctx.state.model.indexes.textScopes);
1634
+ let filtered = allIds;
1635
+ if (selector?.blockId) {
1636
+ filtered = filtered.filter((scopeId) => {
1637
+ const scope = ctx.state.model.indexes.textScopes[scopeId];
1638
+ return scope?.blockId === selector.blockId;
1639
+ });
1640
+ }
1641
+ if (selector?.pattern) {
1642
+ filtered = filtered.filter((scopeId) => {
1643
+ const scope = ctx.state.model.indexes.textScopes[scopeId];
1644
+ if (!scope)
1645
+ return false;
1646
+ return selector.pattern?.test(scope.normalizedText) ?? false;
1647
+ });
1648
+ }
1649
+ return new LASTJQScopeSelectionImpl(ctx, filtered);
1650
+ };
1651
+ return callable;
1652
+ }
1653
+ export function createLASTDollar(model, options) {
1654
+ const base = deepClone(model);
1655
+ base.indexes = rebuildLASTIndexes(base);
1656
+ const seeds = seedNextCounters(base);
1657
+ const state = {
1658
+ original: deepClone(base),
1659
+ model: base,
1660
+ checkpoint: null,
1661
+ stagedOps: [],
1662
+ warnings: [],
1663
+ active: false,
1664
+ nextBlockCounter: seeds.block,
1665
+ nextInlineCounter: seeds.inline,
1666
+ };
1667
+ const ctx = {
1668
+ state,
1669
+ defaultRebuildIndexesOnCommit: options?.rebuildIndexesOnCommit ?? true,
1670
+ };
1671
+ return createDollarProxy(ctx);
1672
+ }
1673
+ export function createLASTApi(model, options) {
1674
+ const $ = createLASTDollar(model, options);
1675
+ return {
1676
+ get model() {
1677
+ return $.model;
1678
+ },
1679
+ $,
1680
+ compile() {
1681
+ return $.plan();
1682
+ },
1683
+ commit(commitOptions) {
1684
+ return $.commit(commitOptions);
1685
+ },
1686
+ };
1687
+ }