web-doc 0.7.0 → 0.9.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 (65) hide show
  1. package/THIRD_PARTY_NOTICES.md +10 -3
  2. package/dist/contracts.d.ts +6 -0
  3. package/dist/edit/ai/outline.d.ts +47 -0
  4. package/dist/edit/ai/outline.js +338 -0
  5. package/dist/edit/ai/targets.d.ts +16 -0
  6. package/dist/edit/ai/targets.js +309 -0
  7. package/dist/edit/ai/tools.d.ts +28 -0
  8. package/dist/edit/ai/tools.js +607 -0
  9. package/dist/edit/ai/types.d.ts +175 -0
  10. package/dist/edit/ai/types.js +1 -0
  11. package/dist/edit/docx/elements.js +19 -1
  12. package/dist/edit/docx/engine.d.ts +10 -4
  13. package/dist/edit/docx/engine.js +80 -6
  14. package/dist/edit/docx/model.d.ts +2 -0
  15. package/dist/edit/docx/model.js +11 -0
  16. package/dist/edit/docx/operations.d.ts +10 -0
  17. package/dist/edit/docx/provider.d.ts +4 -1
  18. package/dist/edit/docx/provider.js +3 -0
  19. package/dist/edit/docx/session.d.ts +12 -1
  20. package/dist/edit/docx/session.js +41 -0
  21. package/dist/edit/docx/structure-ops.js +39 -4
  22. package/dist/edit/docx/table-ops.js +26 -4
  23. package/dist/edit/docx/text-ops.d.ts +22 -0
  24. package/dist/edit/docx/text-ops.js +49 -16
  25. package/dist/edit/docx/text.js +21 -0
  26. package/dist/edit/docx/tracked.d.ts +52 -0
  27. package/dist/edit/docx/tracked.js +347 -0
  28. package/dist/edit/docx/types.d.ts +24 -1
  29. package/dist/edit/docx/write.d.ts +19 -5
  30. package/dist/edit/docx/write.js +31 -8
  31. package/dist/edit/engine.d.ts +14 -3
  32. package/dist/edit/history.d.ts +27 -3
  33. package/dist/edit/history.js +29 -7
  34. package/dist/edit/pdf/engine/document.js +48 -3
  35. package/dist/edit/pdf/engine/elements.d.ts +6 -0
  36. package/dist/edit/pdf/engine/fonts.js +13 -10
  37. package/dist/edit/pdf/engine/pages.js +9 -1
  38. package/dist/edit/pdf/range-map.d.ts +4 -2
  39. package/dist/edit/pdf/schemas.js +4 -1
  40. package/dist/edit/pdf/session.d.ts +10 -0
  41. package/dist/edit/pdf/session.js +39 -0
  42. package/dist/edit/pdf/types.d.ts +15 -3
  43. package/dist/edit/pptx/handler.js +10 -2
  44. package/dist/edit/pptx/session.d.ts +10 -0
  45. package/dist/edit/pptx/session.js +31 -0
  46. package/dist/edit/session.d.ts +11 -0
  47. package/dist/edit/session.js +269 -27
  48. package/dist/edit/types.d.ts +17 -3
  49. package/dist/edit/worker-engine.d.ts +2 -2
  50. package/dist/edit/worker-engine.js +2 -2
  51. package/dist/fonts/THIRD_PARTY_NOTICES.md +6 -3
  52. package/dist/fonts/manifest.json +4 -4
  53. package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
  54. package/dist/fuzzy-alignment.d.ts +11 -4
  55. package/dist/fuzzy-alignment.js +3 -9
  56. package/dist/headless.d.ts +1 -1
  57. package/dist/headless.js +4 -1
  58. package/dist/index.d.ts +5 -2
  59. package/dist/index.js +11 -2
  60. package/dist/limits.js +3 -0
  61. package/dist/worker-protocol.d.ts +1 -1
  62. package/dist/workers/fuzzy-search-worker.js +1 -1
  63. package/dist/workers/ooxml-edit-worker.js +1415 -860
  64. package/dist/workers/pdf-edit-worker.js +64 -16
  65. package/package.json +1 -1
@@ -2452,6 +2452,10 @@ var Reader = class _Reader {
2452
2452
  paragraph(paragraph) {
2453
2453
  for (const child of paragraph.children) this.#child(child, void 0);
2454
2454
  if (this.#field) this.#closeField();
2455
+ const pPr = paragraph.children.find(
2456
+ (child) => child.local === "pPr" && child.namespace === W_NS
2457
+ );
2458
+ if (pPr && paragraphMarkTracked(pPr)) this.#tracked();
2455
2459
  }
2456
2460
  #child(node, wrapper) {
2457
2461
  if (node.namespace !== W_NS) {
@@ -2653,6 +2657,17 @@ var Reader = class _Reader {
2653
2657
  this.text += item.text;
2654
2658
  }
2655
2659
  };
2660
+ var MARK_REVISIONS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo"]);
2661
+ function paragraphMarkTracked(pPr) {
2662
+ for (const child of pPr.children) {
2663
+ if (child.namespace !== W_NS) continue;
2664
+ if (child.local === "rPr" && child.children.some(
2665
+ (mark) => mark.namespace === W_NS && MARK_REVISIONS.has(mark.local)
2666
+ ))
2667
+ return true;
2668
+ }
2669
+ return false;
2670
+ }
2656
2671
  function readParagraphText(part, paragraph) {
2657
2672
  const reader = new Reader(part);
2658
2673
  reader.paragraph(paragraph);
@@ -2692,6 +2707,17 @@ var DocxModel = class _DocxModel {
2692
2707
  unauthoredIds;
2693
2708
  takenIds;
2694
2709
  styles;
2710
+ #maxRevisionId;
2711
+ /** The largest `w:id` of the main part, over revisions and range markup alike; 0 without any. */
2712
+ get maxRevisionId() {
2713
+ if (this.#maxRevisionId === void 0) {
2714
+ let max = 0;
2715
+ for (const match of this.document.text.matchAll(/ w:id="(\d{1,9})"/g))
2716
+ max = Math.max(max, Number(match[1]));
2717
+ this.#maxRevisionId = max;
2718
+ }
2719
+ return this.#maxRevisionId;
2720
+ }
2695
2721
  /**
2696
2722
  * Builds the index. `unauthored` carries the ids of the paragraphs that
2697
2723
  * have no `w14:paraId`, in document order, as the engine tracks them
@@ -3274,13 +3300,27 @@ function paragraphElement(model, record, base) {
3274
3300
  id: record.elementId,
3275
3301
  kind: "paragraph",
3276
3302
  text: record.text.text,
3277
- ...record.table ? { parentId: record.table.elementId } : {},
3303
+ ...record.table ? { parentId: record.table.elementId, ...cellOf(record.table, record) } : {},
3278
3304
  textStyle: resolveTextStyle(model.styles, record.pPr, first?.rPr),
3279
3305
  paragraphStyle: resolveParagraphStyle(model.styles, record.pPr),
3280
3306
  ...record.readOnlyReason ? { readOnlyReason: record.readOnlyReason } : {},
3281
3307
  operations: operationsOf("paragraph", record.readOnlyReason !== void 0)
3282
3308
  };
3283
3309
  }
3310
+ var CELLS = /* @__PURE__ */ new WeakMap();
3311
+ function cellOf(table, record) {
3312
+ let cells = CELLS.get(table);
3313
+ if (!cells) {
3314
+ cells = /* @__PURE__ */ new Map();
3315
+ for (const [row, cellsOfRow] of table.rows.entries())
3316
+ for (const [column, cell2] of cellsOfRow.entries())
3317
+ for (const paragraph of cell2.paragraphs)
3318
+ cells.set(paragraph, { row, column });
3319
+ CELLS.set(table, cells);
3320
+ }
3321
+ const cell = cells.get(record);
3322
+ return cell ? { cell } : {};
3323
+ }
3284
3324
 
3285
3325
  // src/edit/docx/write.ts
3286
3326
  var MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006";
@@ -3448,8 +3488,13 @@ function mergedProperties(part, source, tag, order, set) {
3448
3488
  function toggleXml(local, on) {
3449
3489
  return on ? `<w:${local}/>` : `<w:${local} w:val="0"/>`;
3450
3490
  }
3451
- function changedRunProperties(part, rPr, change, styles) {
3491
+ function changedRunProperties(part, rPr, change, styles, revision) {
3452
3492
  const set = /* @__PURE__ */ new Map();
3493
+ if (revision !== void 0)
3494
+ set.set(
3495
+ "rPrChange",
3496
+ `<w:rPrChange${revision}>${wrapped("w:rPr", innerPropertiesXml(part, rPr, ["rPrChange"]))}</w:rPrChange>`
3497
+ );
3453
3498
  if (change.bold !== void 0) {
3454
3499
  set.set("b", toggleXml("b", change.bold));
3455
3500
  set.set("bCs", toggleXml("bCs", change.bold));
@@ -3497,8 +3542,19 @@ function fontsXml(part, rPr, family) {
3497
3542
  const rest = kept.map((a) => ` ${a.name}="${a.rawValue}"`).join("");
3498
3543
  return `<w:rFonts w:ascii="${face}" w:hAnsi="${face}"${rest}/>`;
3499
3544
  }
3500
- function changedParagraphProperties(part, pPr, change) {
3545
+ function wrapped(tag, inner) {
3546
+ return inner.length === 0 ? `<${tag}/>` : `<${tag}>${inner}</${tag}>`;
3547
+ }
3548
+ function innerPropertiesXml(part, properties, without) {
3549
+ return (properties?.children ?? []).filter((child) => !without.includes(child.local)).map((child) => sliceOf(part, child)).join("");
3550
+ }
3551
+ function changedParagraphProperties(part, pPr, change, revision) {
3501
3552
  const set = /* @__PURE__ */ new Map();
3553
+ if (revision !== void 0)
3554
+ set.set(
3555
+ "pPrChange",
3556
+ `<w:pPrChange${revision}>${wrapped("w:pPr", innerPropertiesXml(part, pPr, ["rPr", "sectPr", "pPrChange"]))}</w:pPrChange>`
3557
+ );
3502
3558
  if (change.align !== void 0)
3503
3559
  set.set("jc", `<w:jc w:val="${alignValue(change.align)}"/>`);
3504
3560
  if (change.spacing !== void 0) {
@@ -3544,11 +3600,11 @@ function alignValue(align) {
3544
3600
  return "left";
3545
3601
  }
3546
3602
  }
3547
- function paragraphMarkProperties(part, pPr, change, styles) {
3603
+ function paragraphMarkProperties(part, pPr, change, styles, revision) {
3548
3604
  const rPr = pPr?.children.find(
3549
3605
  (child) => child.local === "rPr" && child.namespace === W_NS
3550
3606
  );
3551
- const changed = changedRunProperties(part, rPr, change, styles);
3607
+ const changed = changedRunProperties(part, rPr, change, styles, revision);
3552
3608
  return mergedProperties(
3553
3609
  part,
3554
3610
  pPr,
@@ -3599,538 +3655,653 @@ function namespacePatches(part) {
3599
3655
  return items;
3600
3656
  }
3601
3657
 
3602
- // src/edit/docx/structure-ops.ts
3603
- var MEDIA_FOLDER = "/word/media";
3604
- var WP_NS = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
3605
- var A_NS = "http://schemas.openxmlformats.org/drawingml/2006/main";
3606
- var PIC_NS = "http://schemas.openxmlformats.org/drawingml/2006/picture";
3607
- var R_NS = OFFICE_RELATIONSHIPS.slice(0, -1);
3608
- var EMU_PER_PT = 12700;
3609
- function blockNode(record) {
3610
- return record.node;
3658
+ // src/edit/docx/text-ops.ts
3659
+ function topOf(node, container) {
3660
+ let current = node;
3661
+ while (current.parent && current.parent !== container)
3662
+ current = current.parent;
3663
+ return current;
3611
3664
  }
3612
- function placement2(operation3, context, issue) {
3613
- const named = [operation3.before, operation3.after].filter(
3614
- (value) => value !== void 0
3615
- );
3616
- if (named.length !== 1) {
3617
- issue(
3618
- "/before",
3619
- "invalid-value",
3620
- "Exactly one of before and after names the reference element"
3621
- );
3622
- return void 0;
3623
- }
3624
- const side = operation3.before !== void 0 ? "before" : "after";
3625
- const id = operation3[side];
3626
- const record = context.model.byId.get(id);
3627
- if (!record || record.kind !== "paragraph" && record.kind !== "table") {
3628
- issue(
3629
- `/${side}`,
3630
- record ? "invalid-target" : "unknown-target",
3631
- record ? `Element ${id} is not a paragraph or a table` : `No element ${id}`
3632
- );
3633
- return void 0;
3665
+ function unitsOf(container, items) {
3666
+ const byTop = /* @__PURE__ */ new Map();
3667
+ for (const item of items) {
3668
+ const top = topOf(item.run, container);
3669
+ const list = byTop.get(top) ?? [];
3670
+ list.push(item);
3671
+ byTop.set(top, list);
3634
3672
  }
3635
- return { record, side };
3636
- }
3637
- function referenceRunProperties(part, record) {
3638
- const paragraph = record.kind === "paragraph" ? record : record.rows[0]?.[0]?.paragraphs[0];
3639
- if (!paragraph) return void 0;
3640
- const first = firstTextItem(paragraph.text);
3641
- if (first?.rPr) return first.rPr;
3642
- return paragraph.pPr?.children.find(
3643
- (child) => child.local === "rPr" && child.namespace === W_NS
3673
+ const groupEnd = /* @__PURE__ */ new Map();
3674
+ for (const item of items)
3675
+ if (item.fieldRuns && item.fieldRuns.length > 1) {
3676
+ const first = topOf(item.fieldRuns[0], container);
3677
+ const last = topOf(item.fieldRuns.at(-1), container);
3678
+ if (first !== last) groupEnd.set(first, last);
3679
+ }
3680
+ const units = [];
3681
+ let position = 0;
3682
+ const children = container.children.filter(
3683
+ (child) => !(child.local === "pPr" && child.namespace === W_NS)
3644
3684
  );
3645
- }
3646
- function paragraphIdsUnder(model, node) {
3647
- const out = [];
3648
- if (node.local === "p" && node.namespace === W_NS) {
3649
- const own = model.paragraphIds.get(node);
3650
- if (own) out.push(own);
3651
- }
3652
- for (const paragraph of model.document.findAll("p", node)) {
3653
- if (paragraph === node || paragraph.namespace !== W_NS) continue;
3654
- const id = model.paragraphIds.get(paragraph);
3655
- if (id) out.push(id);
3656
- }
3657
- return out;
3658
- }
3659
- function elementIdsUnder(model, node) {
3660
- const out = [];
3661
- for (const record of model.records) {
3662
- let current = record.node;
3663
- while (current && current !== node) current = current.parent;
3664
- if (current === node) out.push(record.elementId);
3665
- }
3666
- return out;
3667
- }
3668
- function stampedSlice(model, node) {
3669
- const part = model.document;
3670
- const targets = [
3671
- ...node.local === "p" && node.namespace === W_NS ? [node] : [],
3672
- ...part.findAll("p", node).filter((p) => p !== node && p.namespace === W_NS)
3673
- ].filter((paragraph) => {
3674
- const id = model.paragraphIds.get(paragraph);
3675
- return id !== void 0 && model.unauthoredSet.has(id);
3676
- }).sort((a, b) => b.start - a.start);
3677
- let xml = sliceOf(part, node);
3678
- const stamped = [];
3679
- for (const paragraph of targets) {
3680
- const id = model.paragraphIds.get(paragraph);
3681
- if (part.attribute(paragraph, "w14:paraId")) {
3682
- stamped.push(id);
3683
- continue;
3685
+ for (let index = 0; index < children.length; index += 1) {
3686
+ const child = children[index];
3687
+ const end = groupEnd.get(child);
3688
+ const nodes = [child];
3689
+ if (end) {
3690
+ let cursor = index + 1;
3691
+ while (cursor < children.length && children[cursor - 1] !== end)
3692
+ nodes.push(children[cursor++]);
3693
+ index = cursor - 1;
3684
3694
  }
3685
- const last = paragraph.attributes.at(-1);
3686
- const from = last ? last.end : paragraph.start + 1 + paragraph.name.length;
3687
- const close = part.text.indexOf(">", from);
3688
- let at = close;
3689
- if (part.text[close - 1] === "/") at = close - 1;
3690
- const offset = at - node.start;
3691
- xml = `${xml.slice(0, offset)} w14:paraId="${id}"${xml.slice(offset)}`;
3692
- stamped.push(id);
3695
+ const own = nodes.flatMap((node) => byTop.get(node) ?? []);
3696
+ const kind = own.length === 0 ? "zero" : end || child.local === "fldSimple" || own.some((item) => item.fieldRuns) ? "field" : child.local === "r" ? "run" : "wrapper";
3697
+ const start = own.length > 0 ? own[0].start : position;
3698
+ const stop = own.length > 0 ? own.at(-1).end : position;
3699
+ units.push({ nodes, kind, start, end: stop, items: own });
3700
+ position = stop;
3693
3701
  }
3694
- return { xml, stamped: stamped.reverse() };
3702
+ return units;
3695
3703
  }
3696
- function firstCellParagraph(record) {
3697
- const table = record.table;
3698
- return table && table.rows[0]?.[0]?.paragraphs[0] === record ? table : void 0;
3704
+ function rPrOf(part, item) {
3705
+ return item ? sliceOf(part, item.rPr) : "";
3699
3706
  }
3700
- function tableRemap(table, newFirstParagraphId) {
3701
- return table.id === newFirstParagraphId ? void 0 : { [table.elementId]: `tbl:${newFirstParagraphId}` };
3707
+ function startTagOf(part, node) {
3708
+ return part.text.slice(node.start, node.contentStart);
3702
3709
  }
3703
- function commit(context, items, result, transaction = context.pkg.transaction()) {
3704
- const part = context.model.document;
3705
- transaction.patch(part, [...namespacePatches(part), ...items]);
3706
- return transaction.commit().then((change) => ({ ...result, warnings: change.warnings }));
3710
+ function endTagOf(part, node) {
3711
+ return part.text.slice(node.contentEnd, node.end);
3707
3712
  }
3708
- var insertParagraphHandler = {
3709
- async validate(operation3, context, issue) {
3710
- const problem = textProblem(normalizeText(operation3.text));
3711
- if (problem) issue("/text", "invalid-text", `The text holds ${problem}`);
3712
- if (operation3.style?.color !== void 0) {
3713
- const colour = colorProblem(operation3.style.color);
3714
- if (colour) issue("/style/color", "invalid-value", `Colour: ${colour}`);
3715
- }
3716
- if (operation3.style?.fontFamily !== void 0) {
3717
- const font = attributeProblem(operation3.style.fontFamily);
3718
- if (font) issue("/style/fontFamily", "invalid-value", `Font: ${font}`);
3713
+ function contentOf(part, items, from, to, includeStart) {
3714
+ let out = "";
3715
+ for (const item of items) {
3716
+ if (item.start === item.end) {
3717
+ const after = includeStart ? item.start >= from : item.start > from;
3718
+ if (after && item.start <= to) out += sliceOf(part, item.child);
3719
+ continue;
3719
3720
  }
3720
- placement2(operation3, context, issue);
3721
- },
3722
- async apply(operation3, context) {
3723
- const { record, side } = placement2(operation3, context, () => {
3724
- });
3725
- const part = context.model.document;
3726
- const node = blockNode(record);
3727
- const sourceRPr = referenceRunProperties(part, record);
3728
- const rPr = operation3.style ? changedRunProperties(
3729
- part,
3730
- sourceRPr,
3731
- operation3.style,
3732
- context.model.styles
3733
- ) : sliceOf(part, sourceRPr);
3734
- const pPr = record.kind === "paragraph" ? paragraphPropertiesWithoutSection(part, record.pPr) : "";
3735
- const segments = normalizeText(operation3.text).split("\n");
3736
- const createdIds = [];
3737
- const xml = segments.map((segment) => {
3738
- const id = context.freshParagraphId();
3739
- createdIds.push(`p:${id}`);
3740
- return paragraphXml(
3741
- void 0,
3742
- id,
3743
- pPr,
3744
- runXml(rPr, runContentXml(segment))
3721
+ const begin = Math.max(item.start, from);
3722
+ const stop = Math.min(item.end, to);
3723
+ if (stop <= begin) continue;
3724
+ if (item.kind === "text" && item.child.local === "t")
3725
+ out += runContentXml(
3726
+ item.text.slice(begin - item.start, stop - item.start)
3745
3727
  );
3746
- });
3747
- const items = side === "before" ? xml.map((paragraph) => patches.insertBefore(part, node, paragraph)) : xml.map((paragraph) => patches.insertAfter(part, node, paragraph));
3748
- const table = side === "before" && record.kind === "paragraph" ? firstCellParagraph(record) : void 0;
3749
- const remappedIds = table ? tableRemap(table, createdIds[0].slice(2)) : void 0;
3750
- return commit(context, items, {
3751
- createdIds,
3752
- reflowFrom: record.id,
3753
- ...remappedIds ? { remappedIds } : {}
3754
- });
3728
+ else if (begin === item.start && stop === item.end)
3729
+ out += sliceOf(part, item.child);
3755
3730
  }
3756
- };
3757
- function bodyTailPatch(context, remaining) {
3758
- const { model } = context;
3759
- const last = remaining.at(-1);
3760
- if (last && last.kind === "paragraph") return void 0;
3761
- const id = context.freshParagraphId();
3762
- const xml = paragraphXml(void 0, id, "", "");
3763
- const part = model.document;
3764
- const patch = last ? patches.insertAfter(part, last.node, xml) : model.bodySectPr ? patches.insertBefore(part, model.bodySectPr, xml) : patches.appendChild(part, model.body, xml);
3765
- return { patch, id };
3731
+ return out;
3766
3732
  }
3767
- function otherParagraphsIn(model, record) {
3768
- if (record.table) {
3769
- for (const row of record.table.rows)
3770
- for (const cell of row)
3771
- if (cell.paragraphs.includes(record))
3772
- return cell.paragraphs.some((candidate) => candidate !== record);
3773
- return false;
3733
+ function droppedInlines(model, record, items, from, to) {
3734
+ const removedIds = [];
3735
+ const removedParagraphIds = [];
3736
+ for (const item of items) {
3737
+ if (item.kind !== "picture" && item.kind !== "object" && item.kind !== "anchor")
3738
+ continue;
3739
+ const inside = item.start === item.end ? from < item.start && item.start < to : item.start >= from && item.end <= to;
3740
+ if (!inside) continue;
3741
+ const inline = record.inlines.find(
3742
+ (candidate) => candidate.node === item.child
3743
+ );
3744
+ if (inline) removedIds.push(inline.elementId);
3745
+ for (const paragraph of model.document.findAll("p", item.child)) {
3746
+ const id = model.paragraphIds.get(paragraph);
3747
+ if (id && paragraph.namespace === W_NS) removedParagraphIds.push(id);
3748
+ }
3774
3749
  }
3775
- return model.blocks.some(
3776
- (block) => block.kind === "paragraph" && block !== record
3777
- );
3750
+ return { removedIds, removedParagraphIds };
3778
3751
  }
3779
- var deleteElementHandler = {
3780
- async validate(operation3, context, issue) {
3781
- const record = context.model.byId.get(operation3.target);
3782
- if (!record || record.kind === "other") {
3783
- issue(
3784
- "/target",
3785
- record ? "invalid-target" : "unknown-target",
3786
- record ? `Element ${operation3.target} cannot be deleted` : `No element ${operation3.target}`
3752
+ function splitAt(part, container, items, start, end, insert, fallbackRPr) {
3753
+ let before = "";
3754
+ let after = "";
3755
+ let inserted = false;
3756
+ let nested = false;
3757
+ let rPr = fallbackRPr;
3758
+ let lastRPr;
3759
+ const raw = (unit) => unit.nodes.map((node) => sliceOf(part, node)).join("");
3760
+ const place = (style) => {
3761
+ rPr = style ?? lastRPr ?? fallbackRPr;
3762
+ before += insert(rPr);
3763
+ inserted = true;
3764
+ };
3765
+ for (const unit of unitsOf(container, items)) {
3766
+ if (unit.kind === "zero") {
3767
+ if (inserted) after += raw(unit);
3768
+ else before += raw(unit);
3769
+ continue;
3770
+ }
3771
+ if (unit.end <= start) {
3772
+ before += raw(unit);
3773
+ lastRPr = rPrOf(part, unit.items.at(-1));
3774
+ if (!inserted && start === end && unit.end === start) place(lastRPr);
3775
+ continue;
3776
+ }
3777
+ if (unit.start >= end) {
3778
+ if (!inserted) place(lastRPr ?? rPrOf(part, unit.items[0]));
3779
+ after += raw(unit);
3780
+ continue;
3781
+ }
3782
+ if (unit.start >= start && unit.end <= end) {
3783
+ if (!inserted) place(rPrOf(part, unit.items[0]));
3784
+ continue;
3785
+ }
3786
+ if (unit.kind === "run") {
3787
+ const style = rPrOf(part, unit.items[0]);
3788
+ const head = runXml(
3789
+ style,
3790
+ contentOf(part, unit.items, unit.start, start, true)
3787
3791
  );
3788
- return;
3792
+ const tail = runXml(
3793
+ style,
3794
+ contentOf(part, unit.items, end, unit.end, start < end)
3795
+ );
3796
+ if (!inserted) {
3797
+ before += head;
3798
+ place(style);
3799
+ after += tail;
3800
+ } else after += tail;
3801
+ continue;
3789
3802
  }
3790
- if (record.kind === "paragraph") {
3791
- if (record.sectPr)
3792
- issue(
3793
- "/target",
3794
- "section-break",
3795
- "A paragraph that ends a section cannot be deleted"
3796
- );
3797
- else if (record.readOnlyReason)
3803
+ if (unit.kind === "wrapper" && unit.start < start && end < unit.end) {
3804
+ const wrapper = unit.nodes[0];
3805
+ const inner = splitAt(
3806
+ part,
3807
+ wrapper,
3808
+ unit.items,
3809
+ start,
3810
+ end,
3811
+ insert,
3812
+ fallbackRPr
3813
+ );
3814
+ before += startTagOf(part, wrapper) + inner.before + inner.after + endTagOf(part, wrapper);
3815
+ rPr = inner.rPr;
3816
+ inserted = true;
3817
+ nested = true;
3818
+ continue;
3819
+ }
3820
+ throw new Error("A range cannot cut a field or a wrapper");
3821
+ }
3822
+ if (!inserted) place(lastRPr);
3823
+ return { before, after, rPr, nested };
3824
+ }
3825
+ async function textTarget(id, context, issue) {
3826
+ const record = context.model.byId.get(id);
3827
+ if (!record || record.kind !== "paragraph") {
3828
+ issue(
3829
+ "/target",
3830
+ record ? "invalid-target" : "unknown-target",
3831
+ record ? `Element ${id} is not a paragraph` : `No element ${id}`
3832
+ );
3833
+ return void 0;
3834
+ }
3835
+ if (record.readOnlyReason) {
3836
+ issue(
3837
+ "/target",
3838
+ "invalid-target",
3839
+ `Paragraph ${id} is read-only (${record.readOnlyReason})`
3840
+ );
3841
+ return void 0;
3842
+ }
3843
+ return { record, part: context.model.document };
3844
+ }
3845
+ function checkRange(target, id, range, issue, structural) {
3846
+ const text = target.record.text.text;
3847
+ const length = text.length;
3848
+ if (!range) return { start: 0, end: length };
3849
+ if (range.start.elementId !== id || range.end.elementId !== id) {
3850
+ issue("/range", "invalid-range", "Both ends must lie on the target");
3851
+ return void 0;
3852
+ }
3853
+ const { start, end } = { start: range.start.offset, end: range.end.offset };
3854
+ if (start > length || end > length || start > end) {
3855
+ issue(
3856
+ "/range",
3857
+ "invalid-range",
3858
+ `Offsets must satisfy 0 \u2264 start \u2264 end \u2264 ${length}`
3859
+ );
3860
+ return void 0;
3861
+ }
3862
+ for (const offset of [start, end])
3863
+ if (offset > 0 && offset < length && (text.charCodeAt(offset) & 64512) === 56320 && (text.charCodeAt(offset - 1) & 64512) === 55296) {
3864
+ issue("/range", "invalid-range", "A range cannot split a surrogate pair");
3865
+ return void 0;
3866
+ }
3867
+ if (!structural) return { start, end };
3868
+ const inside = (unit, offset) => unit.start < offset && offset < unit.end;
3869
+ const check = (container, items) => {
3870
+ for (const unit of unitsOf(container, items)) {
3871
+ if (unit.kind === "field" && (inside(unit, start) || inside(unit, end))) {
3798
3872
  issue(
3799
- "/target",
3800
- "invalid-target",
3801
- `Paragraph ${operation3.target} is read-only (${record.readOnlyReason})`
3873
+ "/range",
3874
+ "invalid-range",
3875
+ "A range cannot start or end inside a field"
3802
3876
  );
3803
- else if (!otherParagraphsIn(context.model, record))
3877
+ return false;
3878
+ }
3879
+ if (unit.kind !== "wrapper") continue;
3880
+ const startIn = inside(unit, start);
3881
+ const endIn = inside(unit, end);
3882
+ if (startIn !== endIn) {
3804
3883
  issue(
3805
- "/target",
3806
- "last-paragraph",
3807
- "The last paragraph of a body or a cell cannot be deleted"
3884
+ "/range",
3885
+ "invalid-range",
3886
+ "A range cannot cut the edge of a hyperlink or content control"
3808
3887
  );
3888
+ return false;
3889
+ }
3890
+ if (startIn && !check(unit.nodes[0], unit.items)) return false;
3809
3891
  }
3892
+ return true;
3893
+ };
3894
+ return check(target.record.node, target.record.text.items) ? { start, end } : void 0;
3895
+ }
3896
+ function commitParagraph(context, target, items, extra = {}) {
3897
+ const { part, record } = target;
3898
+ const transaction = context.pkg.transaction();
3899
+ transaction.patch(part, [...namespacePatches(part), ...items]);
3900
+ return transaction.commit().then((change) => ({
3901
+ createdIds: [],
3902
+ warnings: change.warnings,
3903
+ ...context.model.unauthoredSet.has(record.id) ? { stamped: [record.id] } : {},
3904
+ reflowFrom: record.id,
3905
+ ...extra
3906
+ }));
3907
+ }
3908
+ var replaceTextHandler = {
3909
+ async validate(operation3, context, issue) {
3910
+ const text = normalizeText(operation3.text);
3911
+ const problem = textProblem(text);
3912
+ if (problem) issue("/text", "invalid-text", `The text holds ${problem}`);
3913
+ const target = await textTarget(operation3.target, context, issue);
3914
+ if (!target) return;
3915
+ const range = checkRange(
3916
+ target,
3917
+ operation3.target,
3918
+ operation3.range,
3919
+ issue,
3920
+ true
3921
+ );
3922
+ if (range && context.tracked && trackedRangeProblem(
3923
+ target.record,
3924
+ range.start,
3925
+ range.end,
3926
+ issue,
3927
+ "/range"
3928
+ ))
3929
+ return;
3930
+ if (!range || !text.includes("\n")) return;
3931
+ const probe = splitAt(
3932
+ target.part,
3933
+ target.record.node,
3934
+ target.record.text.items,
3935
+ range.start,
3936
+ range.end,
3937
+ () => "",
3938
+ ""
3939
+ );
3940
+ if (probe.nested)
3941
+ issue(
3942
+ "/text",
3943
+ "invalid-range",
3944
+ "A paragraph break cannot be inserted inside a hyperlink or content control"
3945
+ );
3810
3946
  },
3811
3947
  async apply(operation3, context) {
3812
- const { model } = context;
3813
- const found = model.byId.get(operation3.target);
3814
- const part = model.document;
3815
- if (found.kind !== "paragraph" && found.kind !== "table")
3816
- return deleteInline(context, found);
3817
- const record = found;
3818
- const node = blockNode(record);
3819
- const items = [patches.removeElement(part, node)];
3820
- const createdIds = [];
3821
- if (model.blocks.includes(record)) {
3822
- const tail = bodyTailPatch(
3823
- context,
3824
- model.blocks.filter((block) => block !== record)
3825
- );
3826
- if (tail) {
3827
- createdIds.push(`p:${tail.id}`);
3828
- items.push(tail.patch);
3829
- }
3830
- }
3831
- const removedParagraphIds = paragraphIdsUnder(model, node);
3832
- const table = record.kind === "paragraph" ? firstCellParagraph(record) : void 0;
3833
- const successor = table?.rows[0]?.[0]?.paragraphs[1];
3834
- const remappedIds = table && successor ? tableRemap(table, successor.id) : void 0;
3835
- return commit(context, items, {
3948
+ const target = await textTarget(operation3.target, context, () => {
3949
+ });
3950
+ const { start, end } = checkRange(
3951
+ target,
3952
+ operation3.target,
3953
+ operation3.range,
3954
+ () => {
3955
+ },
3956
+ true
3957
+ );
3958
+ const { items, createdIds, removedIds, removedParagraphIds } = replacedParagraph(
3959
+ context,
3960
+ target.record,
3961
+ start,
3962
+ end,
3963
+ normalizeText(operation3.text)
3964
+ );
3965
+ return commitParagraph(context, target, items, {
3836
3966
  createdIds,
3837
- removedIds: elementIdsUnder(model, node),
3838
- removedParagraphIds,
3839
- reflowFrom: record.id,
3840
- ...remappedIds ? { remappedIds } : {}
3967
+ ...removedIds.length > 0 ? { removedIds } : {},
3968
+ ...removedParagraphIds.length > 0 ? { removedParagraphIds } : {}
3841
3969
  });
3842
3970
  }
3843
3971
  };
3844
- async function deleteInline(context, record) {
3845
- const { model } = context;
3846
- const part = model.document;
3847
- const transaction = context.pkg.transaction();
3848
- const blip = part.find("blip", record.node);
3849
- const rId = blip ? part.attribute(blip, "r:embed") ?? part.attribute(blip, "r:link") : void 0;
3850
- if (rId) {
3851
- const uses = part.findAll("blip").filter(
3852
- (candidate) => candidate !== blip && (part.attribute(candidate, "r:embed") === rId || part.attribute(candidate, "r:link") === rId)
3972
+ function replacedParagraph(context, record, start, end, text) {
3973
+ const part = context.model.document;
3974
+ const segments = text.split("\n");
3975
+ const single = segments.length === 1;
3976
+ const items = [];
3977
+ const createdIds = [];
3978
+ const copiedPPr = paragraphPropertiesWithoutSection(part, record.pPr);
3979
+ if (context.tracked) {
3980
+ const split = trackedSplit(
3981
+ context,
3982
+ record,
3983
+ start,
3984
+ end,
3985
+ paragraphMarkRPr(part, record)
3853
3986
  );
3854
- const elsewhere = new RegExp(
3855
- `r:(?:id|embed|link|pict)="${rId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`
3987
+ const inserted = (segment) => insertedRunXml(context, split.rPr, segment);
3988
+ const originalPPr = sliceOf(part, record.pPr);
3989
+ const firstPPr = single ? originalPPr : markedParagraphProperties(context, record.pPr, "ins");
3990
+ items.push(
3991
+ patches.replaceElement(
3992
+ part,
3993
+ record.node,
3994
+ paragraphXml(
3995
+ record.node,
3996
+ record.id,
3997
+ firstPPr,
3998
+ split.before + split.deleted + inserted(segments[0]) + (single ? split.after : "")
3999
+ )
4000
+ )
3856
4001
  );
3857
- const others = uses.length > 0 || elsewhere.test(stripNode(part, record.node));
3858
- if (!others) transaction.removeRelationship(model.mainPart, rId);
4002
+ segments.slice(1).forEach((segment, index) => {
4003
+ const id = context.freshParagraphId();
4004
+ createdIds.push(`p:${id}`);
4005
+ const last = index === segments.length - 2;
4006
+ items.push(
4007
+ patches.insertAfter(
4008
+ part,
4009
+ record.node,
4010
+ paragraphXml(
4011
+ record.node,
4012
+ id,
4013
+ last ? originalPPr : markedParagraphProperties(context, record.pPr, "ins"),
4014
+ inserted(segment) + (last ? split.after : "")
4015
+ )
4016
+ )
4017
+ );
4018
+ });
4019
+ } else {
4020
+ const split = splitAt(
4021
+ part,
4022
+ record.node,
4023
+ record.text.items,
4024
+ start,
4025
+ end,
4026
+ (rPr) => runXml(rPr, runContentXml(segments[0])),
4027
+ paragraphMarkRPr(part, record)
4028
+ );
4029
+ const pPr = sliceOf(part, record.pPr);
4030
+ items.push(
4031
+ patches.replaceElement(
4032
+ part,
4033
+ record.node,
4034
+ paragraphXml(
4035
+ record.node,
4036
+ record.id,
4037
+ pPr,
4038
+ split.before + (single ? split.after : "")
4039
+ )
4040
+ )
4041
+ );
4042
+ segments.slice(1).forEach((segment, index) => {
4043
+ const id = context.freshParagraphId();
4044
+ createdIds.push(`p:${id}`);
4045
+ const last = index === segments.length - 2;
4046
+ items.push(
4047
+ patches.insertAfter(
4048
+ part,
4049
+ record.node,
4050
+ paragraphXml(
4051
+ record.node,
4052
+ id,
4053
+ copiedPPr,
4054
+ runXml(split.rPr, runContentXml(segment)) + (last ? split.after : "")
4055
+ )
4056
+ )
4057
+ );
4058
+ });
3859
4059
  }
3860
- const removedParagraphIds = paragraphIdsUnder(model, record.node);
3861
- return commit(
3862
- context,
3863
- [patches.removeElement(part, record.node)],
3864
- {
3865
- createdIds: [],
3866
- removedIds: [record.elementId],
3867
- ...removedParagraphIds.length > 0 ? { removedParagraphIds } : {},
3868
- reflowFrom: record.paragraph.id
3869
- },
3870
- transaction
4060
+ const dropped = droppedInlines(
4061
+ context.model,
4062
+ record,
4063
+ record.text.items,
4064
+ start,
4065
+ end
4066
+ );
4067
+ const removedIds = dropped.removedIds;
4068
+ if (!single)
4069
+ for (const inline of record.inlines) {
4070
+ const item = record.text.items.find(
4071
+ (candidate) => candidate.child === inline.node
4072
+ );
4073
+ if (item && item.start >= end && !removedIds.includes(inline.elementId))
4074
+ removedIds.push(inline.elementId);
4075
+ }
4076
+ return {
4077
+ items,
4078
+ createdIds,
4079
+ removedIds,
4080
+ removedParagraphIds: dropped.removedParagraphIds
4081
+ };
4082
+ }
4083
+ function paragraphMarkRPr(part, record) {
4084
+ const rPr = record.pPr?.children.find(
4085
+ (child) => child.local === "rPr" && child.namespace === W_NS
3871
4086
  );
4087
+ return sliceOf(part, rPr);
3872
4088
  }
3873
- function stripNode(part, node) {
3874
- return part.text.slice(0, node.start) + part.text.slice(node.end);
4089
+ function restyledRun(part, run2, rPr) {
4090
+ const content = run2.children.filter((child) => !(child.local === "rPr" && child.namespace === W_NS)).map((child) => sliceOf(part, child)).join("");
4091
+ return `<w:r>${rPr}${content}</w:r>`;
3875
4092
  }
3876
- var moveElementHandler = {
3877
- async validate(operation3, context, issue) {
3878
- const record = context.model.byId.get(operation3.target);
3879
- if (!record || record.kind !== "paragraph" && record.kind !== "table") {
3880
- issue(
3881
- "/target",
3882
- record ? "invalid-target" : "unknown-target",
3883
- record ? `Element ${operation3.target} is not a paragraph or a table` : `No element ${operation3.target}`
3884
- );
3885
- return;
4093
+ function restyled(part, container, items, start, end, change, context) {
4094
+ let out = "";
4095
+ const changed = (rPr) => changedRunProperties(
4096
+ part,
4097
+ rPr,
4098
+ change,
4099
+ context.model.styles,
4100
+ context.tracked ? revisionAttributes(context) : void 0
4101
+ );
4102
+ for (const unit of unitsOf(container, items)) {
4103
+ const raw = unit.nodes.map((node) => sliceOf(part, node)).join("");
4104
+ const overlaps = unit.start < end && start < unit.end ? true : unit.start === unit.end && start < unit.start && unit.start < end;
4105
+ if (unit.kind === "zero" || !overlaps) {
4106
+ out += raw;
4107
+ continue;
3886
4108
  }
3887
- if (record.kind === "paragraph" && record.sectPr) {
3888
- issue(
3889
- "/target",
3890
- "section-break",
3891
- "A paragraph that ends a section cannot move"
4109
+ if (unit.kind === "run") {
4110
+ const run2 = unit.nodes[0];
4111
+ const own = rPrOf(part, unit.items[0]);
4112
+ const rPr = unit.items[0]?.rPr;
4113
+ out += runXml(
4114
+ own,
4115
+ contentOf(
4116
+ part,
4117
+ unit.items,
4118
+ unit.start,
4119
+ Math.min(start, unit.end),
4120
+ true
4121
+ )
3892
4122
  );
3893
- return;
3894
- }
3895
- if (record.kind === "paragraph" && record.readOnlyReason) {
3896
- issue(
3897
- "/target",
3898
- "invalid-target",
3899
- `Paragraph ${operation3.target} is read-only (${record.readOnlyReason})`
4123
+ out += runXml(
4124
+ changed(rPr),
4125
+ contentOf(
4126
+ part,
4127
+ unit.items,
4128
+ Math.max(unit.start, start),
4129
+ Math.min(unit.end, end),
4130
+ false
4131
+ )
3900
4132
  );
3901
- return;
3902
- }
3903
- const reference = placement2(operation3, context, issue);
3904
- if (!reference) return;
3905
- if (reference.record === record) {
3906
- issue(
3907
- `/${reference.side}`,
3908
- "invalid-target",
3909
- "An element cannot move next to itself"
4133
+ out += runXml(
4134
+ own,
4135
+ contentOf(part, unit.items, Math.max(end, unit.start), unit.end, false)
3910
4136
  );
3911
- return;
4137
+ continue;
3912
4138
  }
3913
- if (blockNode(reference.record).parent !== blockNode(record).parent)
3914
- issue(
3915
- `/${reference.side}`,
3916
- "invalid-target",
3917
- "The reference must be in the same body or cell as the element"
3918
- );
4139
+ if (unit.kind === "field") {
4140
+ for (const node of unit.nodes)
4141
+ if (node.local === "r" && node.namespace === W_NS)
4142
+ out += restyledRun(
4143
+ part,
4144
+ node,
4145
+ changed(
4146
+ node.children.find(
4147
+ (child) => child.local === "rPr" && child.namespace === W_NS
4148
+ )
4149
+ )
4150
+ );
4151
+ else if (node.local === "fldSimple")
4152
+ out += startTagOf(part, node) + restyled(part, node, unit.items, start, end, change, context) + endTagOf(part, node);
4153
+ else out += sliceOf(part, node);
4154
+ continue;
4155
+ }
4156
+ const wrapper = unit.nodes[0];
4157
+ out += startTagOf(part, wrapper) + restyled(part, wrapper, unit.items, start, end, change, context) + endTagOf(part, wrapper);
4158
+ }
4159
+ return out;
4160
+ }
4161
+ var setTextStyleHandler = {
4162
+ async validate(operation3, context, issue) {
4163
+ const { style } = operation3;
4164
+ if (style.color !== void 0) {
4165
+ const problem = colorProblem(style.color);
4166
+ if (problem)
4167
+ issue("/style/color", "invalid-value", `Colour: ${problem}`);
4168
+ }
4169
+ if (style.fontFamily !== void 0) {
4170
+ const problem = attributeProblem(style.fontFamily);
4171
+ if (problem)
4172
+ issue("/style/fontFamily", "invalid-value", `Font: ${problem}`);
4173
+ }
4174
+ const target = await textTarget(operation3.target, context, issue);
4175
+ if (target)
4176
+ checkRange(target, operation3.target, operation3.range, issue, false);
3919
4177
  },
3920
4178
  async apply(operation3, context) {
3921
- const { model } = context;
3922
- const record = model.byId.get(operation3.target);
3923
- const { record: reference, side } = placement2(
3924
- operation3,
3925
- context,
4179
+ const target = await textTarget(operation3.target, context, () => {
4180
+ });
4181
+ const { part, record } = target;
4182
+ const { start, end } = checkRange(
4183
+ target,
4184
+ operation3.target,
4185
+ operation3.range,
3926
4186
  () => {
3927
- }
4187
+ },
4188
+ false
3928
4189
  );
3929
- const part = model.document;
3930
- const node = blockNode(record);
3931
- const target = blockNode(reference);
3932
- const { xml, stamped } = stampedSlice(model, node);
3933
- const items = [
3934
- patches.removeElement(part, node),
3935
- side === "before" ? patches.insertBefore(part, target, xml) : patches.insertAfter(part, target, xml)
3936
- ];
3937
- const createdIds = [];
3938
- if (model.blocks.includes(record) && model.blocks.includes(reference)) {
3939
- const without = model.blocks.filter((block) => block !== record);
3940
- const at = without.indexOf(reference);
3941
- without.splice(side === "before" ? at : at + 1, 0, record);
3942
- const tail = bodyTailPatch(context, without);
3943
- if (tail) {
3944
- createdIds.push(`p:${tail.id}`);
3945
- items.push(tail.patch);
3946
- }
3947
- }
3948
- const order = (candidate) => model.records.indexOf(candidate);
3949
- const first = order(record) < order(reference) ? record : reference;
3950
- let remappedIds;
3951
- if (record.kind === "paragraph" && record.table) {
3952
- const cell = record.table.rows[0]?.[0];
3953
- if (cell && cell.paragraphs.includes(record)) {
3954
- const without = cell.paragraphs.filter((p) => p !== record);
3955
- const at = without.indexOf(reference);
3956
- const reordered = [...without];
3957
- if (at >= 0)
3958
- reordered.splice(side === "before" ? at : at + 1, 0, record);
3959
- const head = reordered[0];
3960
- if (head) remappedIds = tableRemap(record.table, head.id);
3961
- }
3962
- }
3963
- return commit(context, items, {
3964
- createdIds,
3965
- stamped,
3966
- reflowFrom: first.id,
3967
- ...remappedIds ? { remappedIds } : {}
3968
- });
3969
- }
3970
- };
3971
- async function existingMedia(context, bytes, mimeType) {
3972
- const extension = `.${extensionForMime(mimeType)}`;
3973
- for (const name of context.pkg.currentPartNames) {
3974
- if (!name.startsWith(`${MEDIA_FOLDER}/`) || !name.endsWith(extension))
3975
- continue;
3976
- const entry = context.pkg.entry(name);
3977
- if (entry && entry.uncompressedSize !== bytes.length) continue;
3978
- const candidate = await context.pkg.part(name);
3979
- if (candidate.length !== bytes.length) continue;
3980
- let same = true;
3981
- for (let index = 0; index < bytes.length; index += 1)
3982
- if (candidate[index] !== bytes[index]) {
3983
- same = false;
3984
- break;
3985
- }
3986
- if (same) return name;
3987
- }
3988
- return void 0;
3989
- }
3990
- function matchesMime(bytes, mimeType) {
3991
- if (mimeType === "image/png")
3992
- return bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
3993
- return bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255;
3994
- }
3995
- function nextDrawingId(part) {
3996
- let max = 0;
3997
- for (const node of part.findAll("docPr")) {
3998
- if (node.namespace !== WP_NS) continue;
3999
- const id = Number(part.attribute(node, "id") ?? NaN);
4000
- if (Number.isInteger(id)) max = Math.max(max, id);
4190
+ if (Object.keys(operation3.style).length === 0)
4191
+ return { createdIds: [], warnings: [] };
4192
+ const content = restyled(
4193
+ part,
4194
+ record.node,
4195
+ record.text.items,
4196
+ start,
4197
+ end,
4198
+ operation3.style,
4199
+ context
4200
+ );
4201
+ const pPr = end >= record.text.text.length ? paragraphMarkProperties(
4202
+ part,
4203
+ record.pPr,
4204
+ operation3.style,
4205
+ context.model.styles,
4206
+ context.tracked ? revisionAttributes(context) : void 0
4207
+ ) : sliceOf(part, record.pPr);
4208
+ return commitParagraph(context, target, [
4209
+ patches.replaceElement(
4210
+ part,
4211
+ record.node,
4212
+ paragraphXml(record.node, record.id, pPr, content)
4213
+ )
4214
+ ]);
4001
4215
  }
4002
- return max + 1;
4003
- }
4004
- function drawingNamespacePatches(part) {
4005
- const root = part.root;
4006
- const has = (name) => root.attributes.some((attribute) => attribute.name === name);
4007
- const items = [];
4008
- if (!has("xmlns:wp"))
4009
- items.push(patches.setAttribute(part, root, "xmlns:wp", WP_NS));
4010
- if (!has("xmlns:r"))
4011
- items.push(patches.setAttribute(part, root, "xmlns:r", R_NS));
4012
- return items;
4013
- }
4014
- var insertImageHandler = {
4216
+ };
4217
+ var setParagraphStyleHandler = {
4015
4218
  async validate(operation3, context, issue) {
4016
- placement2(operation3, context, issue);
4017
- try {
4018
- const bytes = resolveBinary(operation3.data, context.assets);
4019
- if (bytes.length === 0)
4020
- issue("/data", "invalid-value", "The image has no bytes");
4021
- else if (!matchesMime(bytes, operation3.mimeType))
4022
- issue(
4023
- "/data",
4024
- "invalid-value",
4025
- `The bytes are not a ${operation3.mimeType} image`
4026
- );
4027
- } catch {
4028
- issue(
4029
- "/data",
4030
- "unknown-asset",
4031
- "The asset is not known to the session"
4032
- );
4033
- }
4219
+ await textTarget(operation3.target, context, issue);
4034
4220
  },
4035
4221
  async apply(operation3, context) {
4036
- const { model } = context;
4037
- const { record, side } = placement2(operation3, context, () => {
4222
+ const target = await textTarget(operation3.target, context, () => {
4038
4223
  });
4039
- const part = model.document;
4040
- const bytes = resolveBinary(operation3.data, context.assets);
4041
- const transaction = context.pkg.transaction();
4042
- const existing = await existingMedia(context, bytes, operation3.mimeType);
4043
- const rId = existing ? await transaction.addRelationship(
4044
- model.mainPart,
4045
- IMAGE_RELATIONSHIP_TYPE,
4046
- relativeTarget(model.mainPart, existing)
4047
- ) : (await transaction.addMedia(
4048
- model.mainPart,
4049
- MEDIA_FOLDER,
4050
- bytes,
4051
- operation3.mimeType
4052
- )).rId;
4053
- const cx = Math.round(operation3.size.width * EMU_PER_PT);
4054
- const cy = Math.round(operation3.size.height * EMU_PER_PT);
4055
- const drawingId = nextDrawingId(part);
4056
- const id = context.freshParagraphId();
4057
- const drawing = `<w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${cx}" cy="${cy}"/><wp:effectExtent l="0" t="0" r="0" b="0"/><wp:docPr id="${drawingId}" name="Picture ${drawingId}"/><wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="${A_NS}" noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic xmlns:a="${A_NS}"><a:graphicData uri="${PIC_NS}"><pic:pic xmlns:pic="${PIC_NS}"><pic:nvPicPr><pic:cNvPr id="0" name="Picture ${drawingId}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${rId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
4058
- const xml = paragraphXml(void 0, id, "", drawing);
4059
- const node = blockNode(record);
4060
- const items = [
4061
- ...drawingNamespacePatches(part),
4062
- side === "before" ? patches.insertBefore(part, node, xml) : patches.insertAfter(part, node, xml)
4063
- ];
4064
- return commit(
4065
- context,
4066
- items,
4067
- { createdIds: [`p:${id}`, `img:${id}.0`], reflowFrom: record.id },
4068
- transaction
4224
+ const { part, record } = target;
4225
+ const { style } = operation3;
4226
+ if (style.align === void 0 && (style.spacing === void 0 || Object.keys(style.spacing).length === 0))
4227
+ return { createdIds: [], warnings: [] };
4228
+ const pPr = changedParagraphProperties(
4229
+ part,
4230
+ record.pPr,
4231
+ style,
4232
+ context.tracked ? revisionAttributes(context) : void 0
4233
+ );
4234
+ const node = record.node;
4235
+ const content = node.selfClosing ? "" : part.text.slice(
4236
+ record.pPr ? record.pPr.end : node.contentStart,
4237
+ node.contentEnd
4069
4238
  );
4239
+ return commitParagraph(context, target, [
4240
+ patches.replaceElement(
4241
+ part,
4242
+ node,
4243
+ paragraphXml(node, record.id, pPr, content)
4244
+ )
4245
+ ]);
4070
4246
  }
4071
4247
  };
4072
4248
 
4073
- // src/edit/docx/text-ops.ts
4074
- function topOf(node, container) {
4249
+ // src/edit/docx/tracked.ts
4250
+ function revisionAttributes(context) {
4251
+ const tracked = context.tracked;
4252
+ return ` w:id="${context.nextRevisionId()}" w:author="${escapeAttributeValue(tracked.author)}"${tracked.date === void 0 ? "" : ` w:date="${escapeAttributeValue(tracked.date)}"`}`;
4253
+ }
4254
+ function unsupportedTracked(issue, path, what) {
4255
+ issue(
4256
+ path,
4257
+ "unsupported-change-mode",
4258
+ `${what} has no tracked form; apply it directly and review with checkpoints`
4259
+ );
4260
+ }
4261
+ function topOf2(node, container) {
4075
4262
  let current = node;
4076
4263
  while (current.parent && current.parent !== container)
4077
4264
  current = current.parent;
4078
4265
  return current;
4079
4266
  }
4080
- function unitsOf(container, items) {
4081
- const byTop = /* @__PURE__ */ new Map();
4082
- for (const item of items) {
4083
- const top = topOf(item.run, container);
4084
- const list = byTop.get(top) ?? [];
4085
- list.push(item);
4086
- byTop.set(top, list);
4087
- }
4088
- const groupEnd = /* @__PURE__ */ new Map();
4089
- for (const item of items)
4090
- if (item.fieldRuns && item.fieldRuns.length > 1) {
4091
- const first = topOf(item.fieldRuns[0], container);
4092
- const last = topOf(item.fieldRuns.at(-1), container);
4093
- if (first !== last) groupEnd.set(first, last);
4094
- }
4095
- const units = [];
4096
- let position = 0;
4097
- const children = container.children.filter(
4098
- (child) => !(child.local === "pPr" && child.namespace === W_NS)
4099
- );
4100
- for (let index = 0; index < children.length; index += 1) {
4101
- const child = children[index];
4102
- const end = groupEnd.get(child);
4103
- const nodes = [child];
4104
- if (end) {
4105
- let cursor = index + 1;
4106
- while (cursor < children.length && children[cursor - 1] !== end)
4107
- nodes.push(children[cursor++]);
4108
- index = cursor - 1;
4267
+ function trackedRangeProblem(record, start, end, issue, path) {
4268
+ for (const item of record.text.items) {
4269
+ const overlaps = item.start === item.end ? start < item.start && item.start < end : item.start < end && start < item.end;
4270
+ if (!overlaps) continue;
4271
+ const top = topOf2(item.run, record.node);
4272
+ const plainRun = item.run.local === "r" && item.run.namespace === W_NS;
4273
+ if (!plainRun || item.fieldRuns || item.kind === "field" || top !== item.run || item.wrapper !== void 0) {
4274
+ unsupportedTracked(
4275
+ issue,
4276
+ path,
4277
+ "A change touching a hyperlink, content control, field or equation"
4278
+ );
4279
+ return true;
4109
4280
  }
4110
- const own = nodes.flatMap((node) => byTop.get(node) ?? []);
4111
- const kind = own.length === 0 ? "zero" : end || child.local === "fldSimple" || own.some((item) => item.fieldRuns) ? "field" : child.local === "r" ? "run" : "wrapper";
4112
- const start = own.length > 0 ? own[0].start : position;
4113
- const stop = own.length > 0 ? own.at(-1).end : position;
4114
- units.push({ nodes, kind, start, end: stop, items: own });
4115
- position = stop;
4116
4281
  }
4117
- return units;
4282
+ return false;
4118
4283
  }
4119
- function rPrOf(part, item) {
4120
- return item ? sliceOf(part, item.rPr) : "";
4284
+ function deletedContent(content) {
4285
+ return content.replaceAll("<w:t>", "<w:delText>").replaceAll("<w:t ", "<w:delText ").replaceAll("</w:t>", "</w:delText>").replaceAll("<w:instrText", "<w:delInstrText").replaceAll("</w:instrText>", "</w:delInstrText>");
4121
4286
  }
4122
- function startTagOf(part, node) {
4123
- return part.text.slice(node.start, node.contentStart);
4287
+ function deletedRunXml(rPr, content) {
4288
+ return content.length === 0 ? "" : `<w:r>${rPr}${deletedContent(content)}</w:r>`;
4124
4289
  }
4125
- function endTagOf(part, node) {
4126
- return part.text.slice(node.contentEnd, node.end);
4290
+ function runContentOf(part, run2) {
4291
+ return run2.children.filter((child) => !(child.local === "rPr" && child.namespace === W_NS)).map((child) => sliceOf(part, child)).join("");
4127
4292
  }
4128
- function contentOf(part, items, from, to, includeStart) {
4293
+ function runPropertiesOf(part, run2) {
4294
+ const rPr = run2.children.find(
4295
+ (child) => child.local === "rPr" && child.namespace === W_NS
4296
+ );
4297
+ return sliceOf(part, rPr);
4298
+ }
4299
+ function deletedContentOf(part, items, from, to) {
4129
4300
  let out = "";
4130
4301
  for (const item of items) {
4131
4302
  if (item.start === item.end) {
4132
- const after = includeStart ? item.start >= from : item.start > from;
4133
- if (after && item.start <= to) out += sliceOf(part, item.child);
4303
+ if (from < item.start && item.start < to)
4304
+ out += sliceOf(part, item.child);
4134
4305
  continue;
4135
4306
  }
4136
4307
  const begin = Math.max(item.start, from);
@@ -4145,451 +4316,732 @@ function contentOf(part, items, from, to, includeStart) {
4145
4316
  }
4146
4317
  return out;
4147
4318
  }
4148
- function droppedInlines(model, record, items, from, to) {
4149
- const removedIds = [];
4150
- const removedParagraphIds = [];
4151
- for (const item of items) {
4152
- if (item.kind !== "picture" && item.kind !== "object" && item.kind !== "anchor")
4153
- continue;
4154
- const inside = item.start === item.end ? from < item.start && item.start < to : item.start >= from && item.end <= to;
4155
- if (!inside) continue;
4156
- const inline = record.inlines.find(
4157
- (candidate) => candidate.node === item.child
4158
- );
4159
- if (inline) removedIds.push(inline.elementId);
4160
- for (const paragraph of model.document.findAll("p", item.child)) {
4161
- const id = model.paragraphIds.get(paragraph);
4162
- if (id && paragraph.namespace === W_NS) removedParagraphIds.push(id);
4163
- }
4164
- }
4165
- return { removedIds, removedParagraphIds };
4166
- }
4167
- function splitAt(part, container, items, start, end, insert, fallbackRPr) {
4319
+ function trackedSplit(context, record, start, end, fallbackRPr) {
4320
+ const part = context.model.document;
4168
4321
  let before = "";
4169
4322
  let after = "";
4170
- let inserted = false;
4171
- let nested = false;
4172
- let rPr = fallbackRPr;
4323
+ const deletedRuns = [];
4324
+ let rPr;
4173
4325
  let lastRPr;
4326
+ let passed = false;
4174
4327
  const raw = (unit) => unit.nodes.map((node) => sliceOf(part, node)).join("");
4175
- const place = (style) => {
4176
- rPr = style ?? lastRPr ?? fallbackRPr;
4177
- before += insert(rPr);
4178
- inserted = true;
4179
- };
4180
- for (const unit of unitsOf(container, items)) {
4328
+ for (const unit of unitsOf(record.node, record.text.items)) {
4181
4329
  if (unit.kind === "zero") {
4182
- if (inserted) after += raw(unit);
4330
+ if (passed) after += raw(unit);
4183
4331
  else before += raw(unit);
4184
4332
  continue;
4185
4333
  }
4186
4334
  if (unit.end <= start) {
4187
4335
  before += raw(unit);
4188
4336
  lastRPr = rPrOf(part, unit.items.at(-1));
4189
- if (!inserted && start === end && unit.end === start) place(lastRPr);
4337
+ if (start === end && unit.end === start) {
4338
+ rPr ??= lastRPr;
4339
+ passed = true;
4340
+ }
4190
4341
  continue;
4191
4342
  }
4192
4343
  if (unit.start >= end) {
4193
- if (!inserted) place(lastRPr ?? rPrOf(part, unit.items[0]));
4344
+ rPr ??= lastRPr ?? rPrOf(part, unit.items[0]);
4345
+ passed = true;
4194
4346
  after += raw(unit);
4195
4347
  continue;
4196
4348
  }
4349
+ if (unit.kind !== "run")
4350
+ throw new Error("A tracked change cannot cut a field or a wrapper");
4351
+ const run2 = unit.nodes[0];
4352
+ const style = runPropertiesOf(part, run2);
4197
4353
  if (unit.start >= start && unit.end <= end) {
4198
- if (!inserted) place(rPrOf(part, unit.items[0]));
4199
- continue;
4200
- }
4201
- if (unit.kind === "run") {
4202
- const style = rPrOf(part, unit.items[0]);
4203
- const head = runXml(
4204
- style,
4205
- contentOf(part, unit.items, unit.start, start, true)
4206
- );
4207
- const tail = runXml(
4208
- style,
4209
- contentOf(part, unit.items, end, unit.end, start < end)
4210
- );
4211
- if (!inserted) {
4212
- before += head;
4213
- place(style);
4214
- after += tail;
4215
- } else after += tail;
4216
- continue;
4217
- }
4218
- if (unit.kind === "wrapper" && unit.start < start && end < unit.end) {
4219
- const wrapper = unit.nodes[0];
4220
- const inner = splitAt(
4354
+ deletedRuns.push(deletedRunXml(style, runContentOf(part, run2)));
4355
+ } else {
4356
+ const head = contentOf(part, unit.items, unit.start, start, true);
4357
+ const middle = deletedContentOf(
4221
4358
  part,
4222
- wrapper,
4223
4359
  unit.items,
4224
- start,
4225
- end,
4226
- insert,
4227
- fallbackRPr
4360
+ Math.max(unit.start, start),
4361
+ Math.min(unit.end, end)
4228
4362
  );
4229
- before += startTagOf(part, wrapper) + inner.before + inner.after + endTagOf(part, wrapper);
4230
- rPr = inner.rPr;
4231
- inserted = true;
4232
- nested = true;
4363
+ const tail = contentOf(part, unit.items, end, unit.end, start < end);
4364
+ if (head) before += runXml(style, head);
4365
+ if (middle) deletedRuns.push(deletedRunXml(style, middle));
4366
+ if (tail) after += runXml(style, tail);
4367
+ }
4368
+ rPr ??= style;
4369
+ if (unit.end >= end) passed = true;
4370
+ }
4371
+ const deleted = deletedRuns.length > 0 ? `<w:del${revisionAttributes(context)}>${deletedRuns.join("")}</w:del>` : "";
4372
+ return { before, deleted, rPr: rPr ?? lastRPr ?? fallbackRPr, after };
4373
+ }
4374
+ function insertedRunXml(context, rPr, text) {
4375
+ const content = runContentXml(text);
4376
+ return content.length === 0 ? "" : `<w:ins${revisionAttributes(context)}>${runXml(rPr, content)}</w:ins>`;
4377
+ }
4378
+ function markedParagraphProperties(context, pPr, mark) {
4379
+ const part = context.model.document;
4380
+ const rPr = pPr?.children.find(
4381
+ (child) => child.local === "rPr" && child.namespace === W_NS
4382
+ );
4383
+ const inner = innerPropertiesXml(part, rPr, ["ins", "del"]);
4384
+ const marked = `<w:rPr><w:${mark}${revisionAttributes(context)}/>${inner}</w:rPr>`;
4385
+ return mergedProperties(
4386
+ part,
4387
+ pPr,
4388
+ "w:pPr",
4389
+ PPR_ORDER,
4390
+ /* @__PURE__ */ new Map([
4391
+ ["rPr", marked],
4392
+ ["sectPr", null]
4393
+ ])
4394
+ );
4395
+ }
4396
+ function deletedParagraphXml(context, record) {
4397
+ const part = context.model.document;
4398
+ const pPr = markedParagraphProperties(context, record.pPr, "del");
4399
+ let content = "";
4400
+ let open = [];
4401
+ const flush = () => {
4402
+ if (open.length > 0)
4403
+ content += `<w:del${revisionAttributes(context)}>${open.join("")}</w:del>`;
4404
+ open = [];
4405
+ };
4406
+ for (const child of record.node.children) {
4407
+ if (child.local === "pPr" && child.namespace === W_NS) continue;
4408
+ if (child.local === "r" && child.namespace === W_NS) {
4409
+ const run2 = deletedRunXml(
4410
+ runPropertiesOf(part, child),
4411
+ runContentOf(part, child)
4412
+ );
4413
+ if (run2) open.push(run2);
4233
4414
  continue;
4234
4415
  }
4235
- throw new Error("A range cannot cut a field or a wrapper");
4236
- }
4237
- if (!inserted) place(lastRPr);
4238
- return { before, after, rPr, nested };
4416
+ flush();
4417
+ content += sliceOf(part, child);
4418
+ }
4419
+ flush();
4420
+ return paragraphXml(record.node, record.id, pPr, content);
4421
+ }
4422
+ var RUN_REVISIONS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo"]);
4423
+ function revisionsOf(part, paragraph) {
4424
+ const out = [];
4425
+ const attributes = (node) => {
4426
+ const author = part.attribute(node, "w:author");
4427
+ const date = part.attribute(node, "w:date");
4428
+ const raw = part.attribute(node, "w:id");
4429
+ return {
4430
+ id: raw !== void 0 && /^\d+$/.test(raw) ? Number(raw) : -1,
4431
+ ...author === void 0 ? {} : { author },
4432
+ ...date === void 0 ? {} : { date }
4433
+ };
4434
+ };
4435
+ const visit = (node, inRPr, inPPr) => {
4436
+ for (const child of node.children) {
4437
+ if (child.namespace !== W_NS) {
4438
+ visit(child, inRPr, inPPr);
4439
+ continue;
4440
+ }
4441
+ if (child.local === "pPr") {
4442
+ visit(child, false, true);
4443
+ continue;
4444
+ }
4445
+ if (child.local === "rPr") {
4446
+ visit(child, true, inPPr);
4447
+ continue;
4448
+ }
4449
+ if (inRPr && RUN_REVISIONS.has(child.local)) {
4450
+ out.push({
4451
+ start: child.start,
4452
+ revision: {
4453
+ kind: child.local,
4454
+ scope: inPPr ? "mark" : "runs",
4455
+ ...attributes(child),
4456
+ ...inPPr ? { text: "\n" } : {}
4457
+ }
4458
+ });
4459
+ continue;
4460
+ }
4461
+ if (inRPr && child.local === "rPrChange") {
4462
+ out.push({
4463
+ start: child.start,
4464
+ revision: {
4465
+ kind: "rPrChange",
4466
+ scope: inPPr ? "mark" : "runs",
4467
+ ...attributes(child),
4468
+ ...inPPr || !node.parent ? {} : { text: textOf(part, node.parent) }
4469
+ }
4470
+ });
4471
+ continue;
4472
+ }
4473
+ if (inPPr && child.local === "pPrChange") {
4474
+ out.push({
4475
+ start: child.start,
4476
+ revision: {
4477
+ kind: "pPrChange",
4478
+ scope: "paragraph",
4479
+ ...attributes(child)
4480
+ }
4481
+ });
4482
+ continue;
4483
+ }
4484
+ if (!inRPr && !inPPr && RUN_REVISIONS.has(child.local)) {
4485
+ out.push({
4486
+ start: child.start,
4487
+ revision: {
4488
+ kind: child.local,
4489
+ scope: "runs",
4490
+ ...attributes(child),
4491
+ text: textOf(part, child)
4492
+ }
4493
+ });
4494
+ visit(child, false, false);
4495
+ continue;
4496
+ }
4497
+ visit(child, inRPr, inPPr);
4498
+ }
4499
+ };
4500
+ visit(paragraph, false, false);
4501
+ return out.sort((a, b) => a.start - b.start).map((entry) => Object.freeze(entry.revision));
4502
+ }
4503
+ function textOf(part, node) {
4504
+ let text = "";
4505
+ const visit = (current) => {
4506
+ for (const child of current.children) {
4507
+ if (child.namespace === W_NS) {
4508
+ if (child.local === "t" || child.local === "delText") {
4509
+ text += part.textOf(child);
4510
+ continue;
4511
+ }
4512
+ if (child.local === "tab") {
4513
+ text += TAB;
4514
+ continue;
4515
+ }
4516
+ if (child.local === "br" || child.local === "cr") {
4517
+ text += LINE_BREAK;
4518
+ continue;
4519
+ }
4520
+ if (child.local === "rPr" || child.local === "pPr") continue;
4521
+ }
4522
+ visit(child);
4523
+ }
4524
+ };
4525
+ visit(node);
4526
+ return text;
4239
4527
  }
4240
- async function textTarget(id, context, issue) {
4241
- const record = context.model.byId.get(id);
4242
- if (!record || record.kind !== "paragraph") {
4528
+
4529
+ // src/edit/docx/structure-ops.ts
4530
+ var MEDIA_FOLDER = "/word/media";
4531
+ var WP_NS = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
4532
+ var A_NS = "http://schemas.openxmlformats.org/drawingml/2006/main";
4533
+ var PIC_NS = "http://schemas.openxmlformats.org/drawingml/2006/picture";
4534
+ var R_NS = OFFICE_RELATIONSHIPS.slice(0, -1);
4535
+ var EMU_PER_PT = 12700;
4536
+ function blockNode(record) {
4537
+ return record.node;
4538
+ }
4539
+ function placement2(operation3, context, issue) {
4540
+ const named = [operation3.before, operation3.after].filter(
4541
+ (value) => value !== void 0
4542
+ );
4543
+ if (named.length !== 1) {
4243
4544
  issue(
4244
- "/target",
4245
- record ? "invalid-target" : "unknown-target",
4246
- record ? `Element ${id} is not a paragraph` : `No element ${id}`
4545
+ "/before",
4546
+ "invalid-value",
4547
+ "Exactly one of before and after names the reference element"
4247
4548
  );
4248
4549
  return void 0;
4249
4550
  }
4250
- if (record.readOnlyReason) {
4551
+ const side = operation3.before !== void 0 ? "before" : "after";
4552
+ const id = operation3[side];
4553
+ const record = context.model.byId.get(id);
4554
+ if (!record || record.kind !== "paragraph" && record.kind !== "table") {
4251
4555
  issue(
4252
- "/target",
4253
- "invalid-target",
4254
- `Paragraph ${id} is read-only (${record.readOnlyReason})`
4556
+ `/${side}`,
4557
+ record ? "invalid-target" : "unknown-target",
4558
+ record ? `Element ${id} is not a paragraph or a table` : `No element ${id}`
4255
4559
  );
4256
4560
  return void 0;
4257
4561
  }
4258
- return { record, part: context.model.document };
4562
+ return { record, side };
4259
4563
  }
4260
- function checkRange(target, id, range, issue, structural) {
4261
- const text = target.record.text.text;
4262
- const length = text.length;
4263
- if (!range) return { start: 0, end: length };
4264
- if (range.start.elementId !== id || range.end.elementId !== id) {
4265
- issue("/range", "invalid-range", "Both ends must lie on the target");
4266
- return void 0;
4564
+ function referenceRunProperties(part, record) {
4565
+ const paragraph = record.kind === "paragraph" ? record : record.rows[0]?.[0]?.paragraphs[0];
4566
+ if (!paragraph) return void 0;
4567
+ const first = firstTextItem(paragraph.text);
4568
+ if (first?.rPr) return first.rPr;
4569
+ return paragraph.pPr?.children.find(
4570
+ (child) => child.local === "rPr" && child.namespace === W_NS
4571
+ );
4572
+ }
4573
+ function paragraphIdsUnder(model, node) {
4574
+ const out = [];
4575
+ if (node.local === "p" && node.namespace === W_NS) {
4576
+ const own = model.paragraphIds.get(node);
4577
+ if (own) out.push(own);
4267
4578
  }
4268
- const { start, end } = { start: range.start.offset, end: range.end.offset };
4269
- if (start > length || end > length || start > end) {
4270
- issue(
4271
- "/range",
4272
- "invalid-range",
4273
- `Offsets must satisfy 0 \u2264 start \u2264 end \u2264 ${length}`
4274
- );
4275
- return void 0;
4579
+ for (const paragraph of model.document.findAll("p", node)) {
4580
+ if (paragraph === node || paragraph.namespace !== W_NS) continue;
4581
+ const id = model.paragraphIds.get(paragraph);
4582
+ if (id) out.push(id);
4276
4583
  }
4277
- for (const offset of [start, end])
4278
- if (offset > 0 && offset < length && (text.charCodeAt(offset) & 64512) === 56320 && (text.charCodeAt(offset - 1) & 64512) === 55296) {
4279
- issue("/range", "invalid-range", "A range cannot split a surrogate pair");
4280
- return void 0;
4281
- }
4282
- if (!structural) return { start, end };
4283
- const inside = (unit, offset) => unit.start < offset && offset < unit.end;
4284
- const check = (container, items) => {
4285
- for (const unit of unitsOf(container, items)) {
4286
- if (unit.kind === "field" && (inside(unit, start) || inside(unit, end))) {
4287
- issue(
4288
- "/range",
4289
- "invalid-range",
4290
- "A range cannot start or end inside a field"
4291
- );
4292
- return false;
4293
- }
4294
- if (unit.kind !== "wrapper") continue;
4295
- const startIn = inside(unit, start);
4296
- const endIn = inside(unit, end);
4297
- if (startIn !== endIn) {
4298
- issue(
4299
- "/range",
4300
- "invalid-range",
4301
- "A range cannot cut the edge of a hyperlink or content control"
4302
- );
4303
- return false;
4304
- }
4305
- if (startIn && !check(unit.nodes[0], unit.items)) return false;
4584
+ return out;
4585
+ }
4586
+ function elementIdsUnder(model, node) {
4587
+ const out = [];
4588
+ for (const record of model.records) {
4589
+ let current = record.node;
4590
+ while (current && current !== node) current = current.parent;
4591
+ if (current === node) out.push(record.elementId);
4592
+ }
4593
+ return out;
4594
+ }
4595
+ function stampedSlice(model, node) {
4596
+ const part = model.document;
4597
+ const targets = [
4598
+ ...node.local === "p" && node.namespace === W_NS ? [node] : [],
4599
+ ...part.findAll("p", node).filter((p) => p !== node && p.namespace === W_NS)
4600
+ ].filter((paragraph) => {
4601
+ const id = model.paragraphIds.get(paragraph);
4602
+ return id !== void 0 && model.unauthoredSet.has(id);
4603
+ }).sort((a, b) => b.start - a.start);
4604
+ let xml = sliceOf(part, node);
4605
+ const stamped = [];
4606
+ for (const paragraph of targets) {
4607
+ const id = model.paragraphIds.get(paragraph);
4608
+ if (part.attribute(paragraph, "w14:paraId")) {
4609
+ stamped.push(id);
4610
+ continue;
4306
4611
  }
4307
- return true;
4308
- };
4309
- return check(target.record.node, target.record.text.items) ? { start, end } : void 0;
4612
+ const last = paragraph.attributes.at(-1);
4613
+ const from = last ? last.end : paragraph.start + 1 + paragraph.name.length;
4614
+ const close = part.text.indexOf(">", from);
4615
+ let at = close;
4616
+ if (part.text[close - 1] === "/") at = close - 1;
4617
+ const offset = at - node.start;
4618
+ xml = `${xml.slice(0, offset)} w14:paraId="${id}"${xml.slice(offset)}`;
4619
+ stamped.push(id);
4620
+ }
4621
+ return { xml, stamped: stamped.reverse() };
4310
4622
  }
4311
- function commitParagraph(context, target, items, extra = {}) {
4312
- const { part, record } = target;
4313
- const transaction = context.pkg.transaction();
4623
+ function firstCellParagraph(record) {
4624
+ const table = record.table;
4625
+ return table && table.rows[0]?.[0]?.paragraphs[0] === record ? table : void 0;
4626
+ }
4627
+ function tableRemap(table, newFirstParagraphId) {
4628
+ return table.id === newFirstParagraphId ? void 0 : { [table.elementId]: `tbl:${newFirstParagraphId}` };
4629
+ }
4630
+ function commit(context, items, result, transaction = context.pkg.transaction()) {
4631
+ const part = context.model.document;
4314
4632
  transaction.patch(part, [...namespacePatches(part), ...items]);
4315
- return transaction.commit().then((change) => ({
4316
- createdIds: [],
4317
- warnings: change.warnings,
4318
- ...context.model.unauthoredSet.has(record.id) ? { stamped: [record.id] } : {},
4319
- reflowFrom: record.id,
4320
- ...extra
4321
- }));
4633
+ return transaction.commit().then((change) => ({ ...result, warnings: change.warnings }));
4322
4634
  }
4323
- var replaceTextHandler = {
4635
+ var insertParagraphHandler = {
4324
4636
  async validate(operation3, context, issue) {
4325
- const text = normalizeText(operation3.text);
4326
- const problem = textProblem(text);
4637
+ const problem = textProblem(normalizeText(operation3.text));
4327
4638
  if (problem) issue("/text", "invalid-text", `The text holds ${problem}`);
4328
- const target = await textTarget(operation3.target, context, issue);
4329
- if (!target) return;
4330
- const range = checkRange(
4331
- target,
4332
- operation3.target,
4333
- operation3.range,
4334
- issue,
4335
- true
4336
- );
4337
- if (!range || !text.includes("\n")) return;
4338
- const probe = splitAt(
4339
- target.part,
4340
- target.record.node,
4341
- target.record.text.items,
4342
- range.start,
4343
- range.end,
4344
- () => "",
4345
- ""
4346
- );
4347
- if (probe.nested)
4639
+ if (operation3.style?.color !== void 0) {
4640
+ const colour = colorProblem(operation3.style.color);
4641
+ if (colour) issue("/style/color", "invalid-value", `Colour: ${colour}`);
4642
+ }
4643
+ if (operation3.style?.fontFamily !== void 0) {
4644
+ const font = attributeProblem(operation3.style.fontFamily);
4645
+ if (font) issue("/style/fontFamily", "invalid-value", `Font: ${font}`);
4646
+ }
4647
+ placement2(operation3, context, issue);
4648
+ },
4649
+ async apply(operation3, context) {
4650
+ const { record, side } = placement2(operation3, context, () => {
4651
+ });
4652
+ const part = context.model.document;
4653
+ const node = blockNode(record);
4654
+ const sourceRPr = referenceRunProperties(part, record);
4655
+ const rPr = operation3.style ? changedRunProperties(
4656
+ part,
4657
+ sourceRPr,
4658
+ operation3.style,
4659
+ context.model.styles
4660
+ ) : sliceOf(part, sourceRPr);
4661
+ const sourcePPr = record.kind === "paragraph" ? record.pPr : void 0;
4662
+ const pPr = paragraphPropertiesWithoutSection(part, sourcePPr);
4663
+ const segments = normalizeText(operation3.text).split("\n");
4664
+ const createdIds = [];
4665
+ const xml = segments.map((segment) => {
4666
+ const id = context.freshParagraphId();
4667
+ createdIds.push(`p:${id}`);
4668
+ return context.tracked ? paragraphXml(
4669
+ void 0,
4670
+ id,
4671
+ markedParagraphProperties(context, sourcePPr, "ins"),
4672
+ insertedRunXml(context, rPr, segment)
4673
+ ) : paragraphXml(
4674
+ void 0,
4675
+ id,
4676
+ pPr,
4677
+ runXml(rPr, runContentXml(segment))
4678
+ );
4679
+ });
4680
+ const items = side === "before" ? xml.map((paragraph) => patches.insertBefore(part, node, paragraph)) : xml.map((paragraph) => patches.insertAfter(part, node, paragraph));
4681
+ const table = side === "before" && record.kind === "paragraph" ? firstCellParagraph(record) : void 0;
4682
+ const remappedIds = table ? tableRemap(table, createdIds[0].slice(2)) : void 0;
4683
+ return commit(context, items, {
4684
+ createdIds,
4685
+ reflowFrom: record.id,
4686
+ ...remappedIds ? { remappedIds } : {}
4687
+ });
4688
+ }
4689
+ };
4690
+ function bodyTailPatch(context, remaining) {
4691
+ const { model } = context;
4692
+ const last = remaining.at(-1);
4693
+ if (last && last.kind === "paragraph") return void 0;
4694
+ const id = context.freshParagraphId();
4695
+ const xml = paragraphXml(void 0, id, "", "");
4696
+ const part = model.document;
4697
+ const patch = last ? patches.insertAfter(part, last.node, xml) : model.bodySectPr ? patches.insertBefore(part, model.bodySectPr, xml) : patches.appendChild(part, model.body, xml);
4698
+ return { patch, id };
4699
+ }
4700
+ function otherParagraphsIn(model, record) {
4701
+ if (record.table) {
4702
+ for (const row of record.table.rows)
4703
+ for (const cell of row)
4704
+ if (cell.paragraphs.includes(record))
4705
+ return cell.paragraphs.some((candidate) => candidate !== record);
4706
+ return false;
4707
+ }
4708
+ return model.blocks.some(
4709
+ (block) => block.kind === "paragraph" && block !== record
4710
+ );
4711
+ }
4712
+ var deleteElementHandler = {
4713
+ async validate(operation3, context, issue) {
4714
+ const record = context.model.byId.get(operation3.target);
4715
+ if (!record || record.kind === "other") {
4348
4716
  issue(
4349
- "/text",
4350
- "invalid-range",
4351
- "A paragraph break cannot be inserted inside a hyperlink or content control"
4717
+ "/target",
4718
+ record ? "invalid-target" : "unknown-target",
4719
+ record ? `Element ${operation3.target} cannot be deleted` : `No element ${operation3.target}`
4352
4720
  );
4721
+ return;
4722
+ }
4723
+ if (context.tracked && record.kind !== "paragraph") {
4724
+ unsupportedTracked(issue, "/target", "Deleting a table or a picture");
4725
+ return;
4726
+ }
4727
+ if (record.kind === "paragraph") {
4728
+ if (context.tracked && !record.readOnlyReason && trackedRangeProblem(
4729
+ record,
4730
+ 0,
4731
+ record.text.text.length,
4732
+ issue,
4733
+ "/target"
4734
+ ))
4735
+ return;
4736
+ if (record.sectPr)
4737
+ issue(
4738
+ "/target",
4739
+ "section-break",
4740
+ "A paragraph that ends a section cannot be deleted"
4741
+ );
4742
+ else if (record.readOnlyReason)
4743
+ issue(
4744
+ "/target",
4745
+ "invalid-target",
4746
+ `Paragraph ${operation3.target} is read-only (${record.readOnlyReason})`
4747
+ );
4748
+ else if (!otherParagraphsIn(context.model, record))
4749
+ issue(
4750
+ "/target",
4751
+ "last-paragraph",
4752
+ "The last paragraph of a body or a cell cannot be deleted"
4753
+ );
4754
+ }
4353
4755
  },
4354
4756
  async apply(operation3, context) {
4355
- const target = await textTarget(operation3.target, context, () => {
4356
- });
4357
- const { start, end } = checkRange(
4358
- target,
4359
- operation3.target,
4360
- operation3.range,
4361
- () => {
4362
- },
4363
- true
4364
- );
4365
- const { items, createdIds, removedIds, removedParagraphIds } = replacedParagraph(
4366
- context,
4367
- target.record,
4368
- start,
4369
- end,
4370
- normalizeText(operation3.text)
4371
- );
4372
- return commitParagraph(context, target, items, {
4757
+ const { model } = context;
4758
+ const found = model.byId.get(operation3.target);
4759
+ const part = model.document;
4760
+ if (found.kind !== "paragraph" && found.kind !== "table")
4761
+ return deleteInline(context, found);
4762
+ const record = found;
4763
+ const node = blockNode(record);
4764
+ if (context.tracked && record.kind === "paragraph") {
4765
+ const nested = paragraphIdsUnder(model, node).filter(
4766
+ (id) => id !== record.id
4767
+ );
4768
+ return commit(
4769
+ context,
4770
+ [
4771
+ patches.replaceElement(
4772
+ part,
4773
+ node,
4774
+ deletedParagraphXml(context, record)
4775
+ )
4776
+ ],
4777
+ {
4778
+ createdIds: [],
4779
+ removedIds: record.inlines.map((inline) => inline.elementId),
4780
+ ...nested.length > 0 ? { removedParagraphIds: nested } : {},
4781
+ ...model.unauthoredSet.has(record.id) ? { stamped: [record.id] } : {},
4782
+ reflowFrom: record.id
4783
+ }
4784
+ );
4785
+ }
4786
+ const items = [patches.removeElement(part, node)];
4787
+ const createdIds = [];
4788
+ if (model.blocks.includes(record)) {
4789
+ const tail = bodyTailPatch(
4790
+ context,
4791
+ model.blocks.filter((block) => block !== record)
4792
+ );
4793
+ if (tail) {
4794
+ createdIds.push(`p:${tail.id}`);
4795
+ items.push(tail.patch);
4796
+ }
4797
+ }
4798
+ const removedParagraphIds = paragraphIdsUnder(model, node);
4799
+ const table = record.kind === "paragraph" ? firstCellParagraph(record) : void 0;
4800
+ const successor = table?.rows[0]?.[0]?.paragraphs[1];
4801
+ const remappedIds = table && successor ? tableRemap(table, successor.id) : void 0;
4802
+ return commit(context, items, {
4373
4803
  createdIds,
4374
- ...removedIds.length > 0 ? { removedIds } : {},
4375
- ...removedParagraphIds.length > 0 ? { removedParagraphIds } : {}
4804
+ removedIds: elementIdsUnder(model, node),
4805
+ removedParagraphIds,
4806
+ reflowFrom: record.id,
4807
+ ...remappedIds ? { remappedIds } : {}
4376
4808
  });
4377
4809
  }
4378
4810
  };
4379
- function replacedParagraph(context, record, start, end, text) {
4380
- const part = context.model.document;
4381
- const segments = text.split("\n");
4382
- const split = splitAt(
4383
- part,
4384
- record.node,
4385
- record.text.items,
4386
- start,
4387
- end,
4388
- (rPr) => runXml(rPr, runContentXml(segments[0])),
4389
- paragraphMarkRPr(part, record)
4390
- );
4391
- const pPr = sliceOf(part, record.pPr);
4392
- const single = segments.length === 1;
4393
- const first = paragraphXml(
4394
- record.node,
4395
- record.id,
4396
- pPr,
4397
- split.before + (single ? split.after : "")
4398
- );
4399
- const items = [patches.replaceElement(part, record.node, first)];
4400
- const createdIds = [];
4401
- const copiedPPr = paragraphPropertiesWithoutSection(part, record.pPr);
4402
- segments.slice(1).forEach((segment, index) => {
4403
- const id = context.freshParagraphId();
4404
- createdIds.push(`p:${id}`);
4405
- const last = index === segments.length - 2;
4406
- items.push(
4407
- patches.insertAfter(
4408
- part,
4409
- record.node,
4410
- paragraphXml(
4411
- record.node,
4412
- id,
4413
- copiedPPr,
4414
- runXml(split.rPr, runContentXml(segment)) + (last ? split.after : "")
4415
- )
4416
- )
4811
+ async function deleteInline(context, record) {
4812
+ const { model } = context;
4813
+ const part = model.document;
4814
+ const transaction = context.pkg.transaction();
4815
+ const blip = part.find("blip", record.node);
4816
+ const rId = blip ? part.attribute(blip, "r:embed") ?? part.attribute(blip, "r:link") : void 0;
4817
+ if (rId) {
4818
+ const uses = part.findAll("blip").filter(
4819
+ (candidate) => candidate !== blip && (part.attribute(candidate, "r:embed") === rId || part.attribute(candidate, "r:link") === rId)
4417
4820
  );
4418
- });
4419
- const dropped = droppedInlines(
4420
- context.model,
4421
- record,
4422
- record.text.items,
4423
- start,
4424
- end
4425
- );
4426
- const removedIds = dropped.removedIds;
4427
- if (!single)
4428
- for (const inline of record.inlines) {
4429
- const item = record.text.items.find(
4430
- (candidate) => candidate.child === inline.node
4431
- );
4432
- if (item && item.start >= end && !removedIds.includes(inline.elementId))
4433
- removedIds.push(inline.elementId);
4434
- }
4435
- return {
4436
- items,
4437
- createdIds,
4438
- removedIds,
4439
- removedParagraphIds: dropped.removedParagraphIds
4440
- };
4441
- }
4442
- function paragraphMarkRPr(part, record) {
4443
- const rPr = record.pPr?.children.find(
4444
- (child) => child.local === "rPr" && child.namespace === W_NS
4821
+ const elsewhere = new RegExp(
4822
+ `r:(?:id|embed|link|pict)="${rId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`
4823
+ );
4824
+ const others = uses.length > 0 || elsewhere.test(stripNode(part, record.node));
4825
+ if (!others) transaction.removeRelationship(model.mainPart, rId);
4826
+ }
4827
+ const removedParagraphIds = paragraphIdsUnder(model, record.node);
4828
+ return commit(
4829
+ context,
4830
+ [patches.removeElement(part, record.node)],
4831
+ {
4832
+ createdIds: [],
4833
+ removedIds: [record.elementId],
4834
+ ...removedParagraphIds.length > 0 ? { removedParagraphIds } : {},
4835
+ reflowFrom: record.paragraph.id
4836
+ },
4837
+ transaction
4445
4838
  );
4446
- return sliceOf(part, rPr);
4447
4839
  }
4448
- function restyledRun(part, run2, rPr) {
4449
- const content = run2.children.filter((child) => !(child.local === "rPr" && child.namespace === W_NS)).map((child) => sliceOf(part, child)).join("");
4450
- return `<w:r>${rPr}${content}</w:r>`;
4840
+ function stripNode(part, node) {
4841
+ return part.text.slice(0, node.start) + part.text.slice(node.end);
4451
4842
  }
4452
- function restyled(part, container, items, start, end, change, context) {
4453
- let out = "";
4454
- const changed = (rPr) => changedRunProperties(part, rPr, change, context.model.styles);
4455
- for (const unit of unitsOf(container, items)) {
4456
- const raw = unit.nodes.map((node) => sliceOf(part, node)).join("");
4457
- const overlaps = unit.start < end && start < unit.end ? true : unit.start === unit.end && start < unit.start && unit.start < end;
4458
- if (unit.kind === "zero" || !overlaps) {
4459
- out += raw;
4460
- continue;
4843
+ var moveElementHandler = {
4844
+ async validate(operation3, context, issue) {
4845
+ if (context.tracked) {
4846
+ unsupportedTracked(issue, "", "Moving an element");
4847
+ return;
4461
4848
  }
4462
- if (unit.kind === "run") {
4463
- const run2 = unit.nodes[0];
4464
- const own = rPrOf(part, unit.items[0]);
4465
- const rPr = unit.items[0]?.rPr;
4466
- out += runXml(
4467
- own,
4468
- contentOf(
4469
- part,
4470
- unit.items,
4471
- unit.start,
4472
- Math.min(start, unit.end),
4473
- true
4474
- )
4475
- );
4476
- out += runXml(
4477
- changed(rPr),
4478
- contentOf(
4479
- part,
4480
- unit.items,
4481
- Math.max(unit.start, start),
4482
- Math.min(unit.end, end),
4483
- false
4484
- )
4485
- );
4486
- out += runXml(
4487
- own,
4488
- contentOf(part, unit.items, Math.max(end, unit.start), unit.end, false)
4849
+ const record = context.model.byId.get(operation3.target);
4850
+ if (!record || record.kind !== "paragraph" && record.kind !== "table") {
4851
+ issue(
4852
+ "/target",
4853
+ record ? "invalid-target" : "unknown-target",
4854
+ record ? `Element ${operation3.target} is not a paragraph or a table` : `No element ${operation3.target}`
4489
4855
  );
4490
- continue;
4491
- }
4492
- if (unit.kind === "field") {
4493
- for (const node of unit.nodes)
4494
- if (node.local === "r" && node.namespace === W_NS)
4495
- out += restyledRun(
4496
- part,
4497
- node,
4498
- changed(
4499
- node.children.find(
4500
- (child) => child.local === "rPr" && child.namespace === W_NS
4501
- )
4502
- )
4503
- );
4504
- else if (node.local === "fldSimple")
4505
- out += startTagOf(part, node) + restyled(part, node, unit.items, start, end, change, context) + endTagOf(part, node);
4506
- else out += sliceOf(part, node);
4507
- continue;
4856
+ return;
4508
4857
  }
4509
- const wrapper = unit.nodes[0];
4510
- out += startTagOf(part, wrapper) + restyled(part, wrapper, unit.items, start, end, change, context) + endTagOf(part, wrapper);
4511
- }
4512
- return out;
4513
- }
4514
- var setTextStyleHandler = {
4515
- async validate(operation3, context, issue) {
4516
- const { style } = operation3;
4517
- if (style.color !== void 0) {
4518
- const problem = colorProblem(style.color);
4519
- if (problem)
4520
- issue("/style/color", "invalid-value", `Colour: ${problem}`);
4858
+ if (record.kind === "paragraph" && record.sectPr) {
4859
+ issue(
4860
+ "/target",
4861
+ "section-break",
4862
+ "A paragraph that ends a section cannot move"
4863
+ );
4864
+ return;
4521
4865
  }
4522
- if (style.fontFamily !== void 0) {
4523
- const problem = attributeProblem(style.fontFamily);
4524
- if (problem)
4525
- issue("/style/fontFamily", "invalid-value", `Font: ${problem}`);
4866
+ if (record.kind === "paragraph" && record.readOnlyReason) {
4867
+ issue(
4868
+ "/target",
4869
+ "invalid-target",
4870
+ `Paragraph ${operation3.target} is read-only (${record.readOnlyReason})`
4871
+ );
4872
+ return;
4526
4873
  }
4527
- const target = await textTarget(operation3.target, context, issue);
4528
- if (target)
4529
- checkRange(target, operation3.target, operation3.range, issue, false);
4874
+ const reference = placement2(operation3, context, issue);
4875
+ if (!reference) return;
4876
+ if (reference.record === record) {
4877
+ issue(
4878
+ `/${reference.side}`,
4879
+ "invalid-target",
4880
+ "An element cannot move next to itself"
4881
+ );
4882
+ return;
4883
+ }
4884
+ if (blockNode(reference.record).parent !== blockNode(record).parent)
4885
+ issue(
4886
+ `/${reference.side}`,
4887
+ "invalid-target",
4888
+ "The reference must be in the same body or cell as the element"
4889
+ );
4530
4890
  },
4531
4891
  async apply(operation3, context) {
4532
- const target = await textTarget(operation3.target, context, () => {
4533
- });
4534
- const { part, record } = target;
4535
- const { start, end } = checkRange(
4536
- target,
4537
- operation3.target,
4538
- operation3.range,
4892
+ const { model } = context;
4893
+ const record = model.byId.get(operation3.target);
4894
+ const { record: reference, side } = placement2(
4895
+ operation3,
4896
+ context,
4539
4897
  () => {
4540
- },
4541
- false
4542
- );
4543
- if (Object.keys(operation3.style).length === 0)
4544
- return { createdIds: [], warnings: [] };
4545
- const content = restyled(
4546
- part,
4547
- record.node,
4548
- record.text.items,
4549
- start,
4550
- end,
4551
- operation3.style,
4552
- context
4898
+ }
4553
4899
  );
4554
- const pPr = end >= record.text.text.length ? paragraphMarkProperties(
4555
- part,
4556
- record.pPr,
4557
- operation3.style,
4558
- context.model.styles
4559
- ) : sliceOf(part, record.pPr);
4560
- return commitParagraph(context, target, [
4561
- patches.replaceElement(
4562
- part,
4563
- record.node,
4564
- paragraphXml(record.node, record.id, pPr, content)
4565
- )
4566
- ]);
4900
+ const part = model.document;
4901
+ const node = blockNode(record);
4902
+ const target = blockNode(reference);
4903
+ const { xml, stamped } = stampedSlice(model, node);
4904
+ const items = [
4905
+ patches.removeElement(part, node),
4906
+ side === "before" ? patches.insertBefore(part, target, xml) : patches.insertAfter(part, target, xml)
4907
+ ];
4908
+ const createdIds = [];
4909
+ if (model.blocks.includes(record) && model.blocks.includes(reference)) {
4910
+ const without = model.blocks.filter((block) => block !== record);
4911
+ const at = without.indexOf(reference);
4912
+ without.splice(side === "before" ? at : at + 1, 0, record);
4913
+ const tail = bodyTailPatch(context, without);
4914
+ if (tail) {
4915
+ createdIds.push(`p:${tail.id}`);
4916
+ items.push(tail.patch);
4917
+ }
4918
+ }
4919
+ const order = (candidate) => model.records.indexOf(candidate);
4920
+ const first = order(record) < order(reference) ? record : reference;
4921
+ let remappedIds;
4922
+ if (record.kind === "paragraph" && record.table) {
4923
+ const cell = record.table.rows[0]?.[0];
4924
+ if (cell && cell.paragraphs.includes(record)) {
4925
+ const without = cell.paragraphs.filter((p) => p !== record);
4926
+ const at = without.indexOf(reference);
4927
+ const reordered = [...without];
4928
+ if (at >= 0)
4929
+ reordered.splice(side === "before" ? at : at + 1, 0, record);
4930
+ const head = reordered[0];
4931
+ if (head) remappedIds = tableRemap(record.table, head.id);
4932
+ }
4933
+ }
4934
+ return commit(context, items, {
4935
+ createdIds,
4936
+ stamped,
4937
+ reflowFrom: first.id,
4938
+ ...remappedIds ? { remappedIds } : {}
4939
+ });
4567
4940
  }
4568
4941
  };
4569
- var setParagraphStyleHandler = {
4942
+ async function existingMedia(context, bytes, mimeType) {
4943
+ const extension = `.${extensionForMime(mimeType)}`;
4944
+ for (const name of context.pkg.currentPartNames) {
4945
+ if (!name.startsWith(`${MEDIA_FOLDER}/`) || !name.endsWith(extension))
4946
+ continue;
4947
+ const entry = context.pkg.entry(name);
4948
+ if (entry && entry.uncompressedSize !== bytes.length) continue;
4949
+ const candidate = await context.pkg.part(name);
4950
+ if (candidate.length !== bytes.length) continue;
4951
+ let same = true;
4952
+ for (let index = 0; index < bytes.length; index += 1)
4953
+ if (candidate[index] !== bytes[index]) {
4954
+ same = false;
4955
+ break;
4956
+ }
4957
+ if (same) return name;
4958
+ }
4959
+ return void 0;
4960
+ }
4961
+ function matchesMime(bytes, mimeType) {
4962
+ if (mimeType === "image/png")
4963
+ return bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
4964
+ return bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255;
4965
+ }
4966
+ function nextDrawingId(part) {
4967
+ let max = 0;
4968
+ for (const node of part.findAll("docPr")) {
4969
+ if (node.namespace !== WP_NS) continue;
4970
+ const id = Number(part.attribute(node, "id") ?? NaN);
4971
+ if (Number.isInteger(id)) max = Math.max(max, id);
4972
+ }
4973
+ return max + 1;
4974
+ }
4975
+ function drawingNamespacePatches(part) {
4976
+ const root = part.root;
4977
+ const has = (name) => root.attributes.some((attribute) => attribute.name === name);
4978
+ const items = [];
4979
+ if (!has("xmlns:wp"))
4980
+ items.push(patches.setAttribute(part, root, "xmlns:wp", WP_NS));
4981
+ if (!has("xmlns:r"))
4982
+ items.push(patches.setAttribute(part, root, "xmlns:r", R_NS));
4983
+ return items;
4984
+ }
4985
+ var insertImageHandler = {
4570
4986
  async validate(operation3, context, issue) {
4571
- await textTarget(operation3.target, context, issue);
4987
+ if (context.tracked) {
4988
+ unsupportedTracked(issue, "", "Inserting a picture");
4989
+ return;
4990
+ }
4991
+ placement2(operation3, context, issue);
4992
+ try {
4993
+ const bytes = resolveBinary(operation3.data, context.assets);
4994
+ if (bytes.length === 0)
4995
+ issue("/data", "invalid-value", "The image has no bytes");
4996
+ else if (!matchesMime(bytes, operation3.mimeType))
4997
+ issue(
4998
+ "/data",
4999
+ "invalid-value",
5000
+ `The bytes are not a ${operation3.mimeType} image`
5001
+ );
5002
+ } catch {
5003
+ issue(
5004
+ "/data",
5005
+ "unknown-asset",
5006
+ "The asset is not known to the session"
5007
+ );
5008
+ }
4572
5009
  },
4573
5010
  async apply(operation3, context) {
4574
- const target = await textTarget(operation3.target, context, () => {
5011
+ const { model } = context;
5012
+ const { record, side } = placement2(operation3, context, () => {
4575
5013
  });
4576
- const { part, record } = target;
4577
- const { style } = operation3;
4578
- if (style.align === void 0 && (style.spacing === void 0 || Object.keys(style.spacing).length === 0))
4579
- return { createdIds: [], warnings: [] };
4580
- const pPr = changedParagraphProperties(part, record.pPr, style);
4581
- const node = record.node;
4582
- const content = node.selfClosing ? "" : part.text.slice(
4583
- record.pPr ? record.pPr.end : node.contentStart,
4584
- node.contentEnd
5014
+ const part = model.document;
5015
+ const bytes = resolveBinary(operation3.data, context.assets);
5016
+ const transaction = context.pkg.transaction();
5017
+ const existing = await existingMedia(context, bytes, operation3.mimeType);
5018
+ const rId = existing ? await transaction.addRelationship(
5019
+ model.mainPart,
5020
+ IMAGE_RELATIONSHIP_TYPE,
5021
+ relativeTarget(model.mainPart, existing)
5022
+ ) : (await transaction.addMedia(
5023
+ model.mainPart,
5024
+ MEDIA_FOLDER,
5025
+ bytes,
5026
+ operation3.mimeType
5027
+ )).rId;
5028
+ const cx = Math.round(operation3.size.width * EMU_PER_PT);
5029
+ const cy = Math.round(operation3.size.height * EMU_PER_PT);
5030
+ const drawingId = nextDrawingId(part);
5031
+ const id = context.freshParagraphId();
5032
+ const drawing = `<w:r><w:drawing><wp:inline distT="0" distB="0" distL="0" distR="0"><wp:extent cx="${cx}" cy="${cy}"/><wp:effectExtent l="0" t="0" r="0" b="0"/><wp:docPr id="${drawingId}" name="Picture ${drawingId}"/><wp:cNvGraphicFramePr><a:graphicFrameLocks xmlns:a="${A_NS}" noChangeAspect="1"/></wp:cNvGraphicFramePr><a:graphic xmlns:a="${A_NS}"><a:graphicData uri="${PIC_NS}"><pic:pic xmlns:pic="${PIC_NS}"><pic:nvPicPr><pic:cNvPr id="0" name="Picture ${drawingId}"/><pic:cNvPicPr/></pic:nvPicPr><pic:blipFill><a:blip r:embed="${rId}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill><pic:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${cx}" cy="${cy}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr></pic:pic></a:graphicData></a:graphic></wp:inline></w:drawing></w:r>`;
5033
+ const xml = paragraphXml(void 0, id, "", drawing);
5034
+ const node = blockNode(record);
5035
+ const items = [
5036
+ ...drawingNamespacePatches(part),
5037
+ side === "before" ? patches.insertBefore(part, node, xml) : patches.insertAfter(part, node, xml)
5038
+ ];
5039
+ return commit(
5040
+ context,
5041
+ items,
5042
+ { createdIds: [`p:${id}`, `img:${id}.0`], reflowFrom: record.id },
5043
+ transaction
4585
5044
  );
4586
- return commitParagraph(context, target, [
4587
- patches.replaceElement(
4588
- part,
4589
- node,
4590
- paragraphXml(node, record.id, pPr, content)
4591
- )
4592
- ]);
4593
5045
  }
4594
5046
  };
4595
5047
 
@@ -4685,6 +5137,10 @@ function commit2(context, items, result) {
4685
5137
  }
4686
5138
  var insertTableHandler = {
4687
5139
  async validate(operation3, context, issue) {
5140
+ if (context.tracked) {
5141
+ unsupportedTracked(issue, "", "Inserting a table");
5142
+ return;
5143
+ }
4688
5144
  placement3(operation3, context, issue);
4689
5145
  const columns = operation3.rows[0]?.length ?? 0;
4690
5146
  if (operation3.rows.some((row) => row.length !== columns))
@@ -4789,6 +5245,17 @@ var setTableCellHandler = {
4789
5245
  "invalid-target",
4790
5246
  `The cell's paragraph is read-only (${first.readOnlyReason})`
4791
5247
  );
5248
+ else if (context.tracked) {
5249
+ for (const paragraph of cell.paragraphs)
5250
+ if (paragraph.readOnlyReason || trackedRangeProblem(
5251
+ paragraph,
5252
+ 0,
5253
+ paragraph.text.text.length,
5254
+ issue,
5255
+ "/target"
5256
+ ))
5257
+ return;
5258
+ }
4792
5259
  },
4793
5260
  async apply(operation3, context) {
4794
5261
  const { model } = context;
@@ -4807,13 +5274,28 @@ var setTableCellHandler = {
4807
5274
  const items = [...replaced.items];
4808
5275
  const removedIds = [...replaced.removedIds];
4809
5276
  const removedParagraphIds = [...replaced.removedParagraphIds];
5277
+ const stamped = model.unauthoredSet.has(first.id) ? [first.id] : [];
4810
5278
  for (const paragraph of rest) {
4811
- items.push(patches.removeElement(part, paragraph.node));
4812
- removedIds.push(
4813
- paragraph.elementId,
4814
- ...paragraph.inlines.map((inline) => inline.elementId)
4815
- );
4816
- removedParagraphIds.push(paragraph.id);
5279
+ if (context.tracked) {
5280
+ if (model.unauthoredSet.has(paragraph.id)) stamped.push(paragraph.id);
5281
+ items.push(
5282
+ patches.replaceElement(
5283
+ part,
5284
+ paragraph.node,
5285
+ deletedParagraphXml(context, paragraph)
5286
+ )
5287
+ );
5288
+ removedIds.push(
5289
+ ...paragraph.inlines.map((inline) => inline.elementId)
5290
+ );
5291
+ } else {
5292
+ items.push(patches.removeElement(part, paragraph.node));
5293
+ removedIds.push(
5294
+ paragraph.elementId,
5295
+ ...paragraph.inlines.map((inline) => inline.elementId)
5296
+ );
5297
+ removedParagraphIds.push(paragraph.id);
5298
+ }
4817
5299
  for (const nested of part.findAll("p", paragraph.node)) {
4818
5300
  const id = model.paragraphIds.get(nested);
4819
5301
  if (id && nested !== paragraph.node) removedParagraphIds.push(id);
@@ -4823,7 +5305,7 @@ var setTableCellHandler = {
4823
5305
  createdIds: replaced.createdIds,
4824
5306
  ...removedIds.length > 0 ? { removedIds } : {},
4825
5307
  ...removedParagraphIds.length > 0 ? { removedParagraphIds } : {},
4826
- ...model.unauthoredSet.has(first.id) ? { stamped: [first.id] } : {},
5308
+ ...stamped.length > 0 ? { stamped } : {},
4827
5309
  reflowFrom: record.id
4828
5310
  });
4829
5311
  }
@@ -4860,6 +5342,44 @@ function unauthoredIdsOf(bytes) {
4860
5342
  const text = new TextDecoder().decode(bytes);
4861
5343
  return [...text.matchAll(/<p id="([0-9A-F]{8})"\/>/g)].map((m) => m[1]);
4862
5344
  }
5345
+ function trackedOf(mode) {
5346
+ if (mode.changeMode !== "tracked") return void 0;
5347
+ return {
5348
+ author: mode.author ?? "",
5349
+ ...mode.timestamp === void 0 ? {} : { date: mode.timestamp }
5350
+ };
5351
+ }
5352
+ var DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
5353
+ function trackedModeIssues(tracked) {
5354
+ if (!tracked) return [];
5355
+ const issues = [];
5356
+ const author = attributeProblem(tracked.author);
5357
+ if (tracked.author.trim().length === 0)
5358
+ issues.push({
5359
+ operationIndex: -1,
5360
+ path: "/author",
5361
+ code: "required",
5362
+ message: "Tracked changes name their author; pass ApplyOptions.author"
5363
+ });
5364
+ else if (author)
5365
+ issues.push({
5366
+ operationIndex: -1,
5367
+ path: "/author",
5368
+ code: "invalid-value",
5369
+ message: `The author holds ${author}`
5370
+ });
5371
+ if (tracked.date !== void 0) {
5372
+ const date = attributeProblem(tracked.date);
5373
+ if (date || !DATE_TIME.test(tracked.date) || Number.isNaN(Date.parse(tracked.date)))
5374
+ issues.push({
5375
+ operationIndex: -1,
5376
+ path: "/timestamp",
5377
+ code: "invalid-value",
5378
+ message: date ? `The timestamp holds ${date}` : "The timestamp must be an ISO 8601 date-time"
5379
+ });
5380
+ }
5381
+ return issues;
5382
+ }
4863
5383
  var DocxEditEngine = class _DocxEditEngine {
4864
5384
  schemas = docxOperationSchemas;
4865
5385
  #original;
@@ -4922,9 +5442,12 @@ var DocxEditEngine = class _DocxEditEngine {
4922
5442
  });
4923
5443
  return pending;
4924
5444
  }
4925
- async validate(operations, signal) {
5445
+ async validate(operations, signal, mode = {}) {
4926
5446
  const issues = [];
4927
- const base = await this.#context(0, signal);
5447
+ const tracked = trackedOf(mode);
5448
+ const modeIssues = trackedModeIssues(tracked);
5449
+ if (modeIssues.length > 0) return modeIssues;
5450
+ const base = await this.#context(0, signal, 0, /* @__PURE__ */ new Set(), tracked);
4928
5451
  for (const [index, operation3] of operations.entries()) {
4929
5452
  const context = { ...base, operationIndex: index };
4930
5453
  const handler = docxHandlers.get(operation3.op);
@@ -4967,6 +5490,9 @@ var DocxEditEngine = class _DocxEditEngine {
4967
5490
  async apply(input, signal) {
4968
5491
  const batch = Array.isArray(input) ? { stateId: this.#nextStateId(), operations: input } : input;
4969
5492
  this.#stateId = Math.max(this.#stateId, batch.stateId);
5493
+ const tracked = trackedOf(batch);
5494
+ const modeIssues = trackedModeIssues(tracked);
5495
+ if (modeIssues.length > 0) throw invalidOperationError(modeIssues);
4970
5496
  const snapshot = this.#pkg.snapshot();
4971
5497
  const unauthored = this.#unauthored ? [...this.#unauthored] : void 0;
4972
5498
  const createdIds = [];
@@ -4990,7 +5516,8 @@ var DocxEditEngine = class _DocxEditEngine {
4990
5516
  batch.stateId,
4991
5517
  signal,
4992
5518
  index,
4993
- issued
5519
+ issued,
5520
+ tracked
4994
5521
  );
4995
5522
  const issues = [];
4996
5523
  await handler.validate(
@@ -5042,10 +5569,11 @@ var DocxEditEngine = class _DocxEditEngine {
5042
5569
  this.#stateId += 1;
5043
5570
  return this.#stateId;
5044
5571
  }
5045
- async #context(stateId, signal, operationIndex = 0, issued = /* @__PURE__ */ new Set()) {
5572
+ async #context(stateId, signal, operationIndex = 0, issued = /* @__PURE__ */ new Set(), tracked) {
5046
5573
  const model = await this.model(signal);
5047
5574
  const taken = /* @__PURE__ */ new Set([...model.takenIds, ...issued]);
5048
5575
  let count = 0;
5576
+ let revisions = 0;
5049
5577
  return {
5050
5578
  pkg: this.#pkg,
5051
5579
  model,
@@ -5059,9 +5587,21 @@ var DocxEditEngine = class _DocxEditEngine {
5059
5587
  issued.add(id);
5060
5588
  taken.add(id);
5061
5589
  return id;
5590
+ },
5591
+ ...tracked ? { tracked } : {},
5592
+ nextRevisionId: () => {
5593
+ revisions += 1;
5594
+ return model.maxRevisionId + revisions;
5062
5595
  }
5063
5596
  };
5064
5597
  }
5598
+ /** The revisions of a paragraph; none for another element or an unknown id. */
5599
+ async revisions(id, signal) {
5600
+ const model = await this.model(signal);
5601
+ const record = model.byId.get(id);
5602
+ if (!record || record.kind !== "paragraph") return [];
5603
+ return revisionsOf(model.document, record.node);
5604
+ }
5065
5605
  async materialize(purposeOrSignal = "show", options = {}, signal = new AbortController().signal) {
5066
5606
  return (await this.materializeDocument(purposeOrSignal, options, signal)).bytes;
5067
5607
  }
@@ -8497,6 +9037,16 @@ function createOoxmlEditHandler() {
8497
9037
  );
8498
9038
  return state;
8499
9039
  };
9040
+ const docx = () => {
9041
+ const current = engine();
9042
+ if (!(current instanceof DocxEditEngine))
9043
+ throw new ViewerError(
9044
+ "edit-unsupported",
9045
+ "The open document is not a Word document",
9046
+ { details: { format: "pptx", reason: "no-reads" } }
9047
+ );
9048
+ return current;
9049
+ };
8500
9050
  const pptx = () => {
8501
9051
  const current = engine();
8502
9052
  if (!(current instanceof PptxEditEngine))
@@ -8529,8 +9079,8 @@ function createOoxmlEditHandler() {
8529
9079
  return result;
8530
9080
  }
8531
9081
  case "edit-validate": {
8532
- const { operations } = payload;
8533
- return engine().validate(operations, signal);
9082
+ const { operations, mode } = payload;
9083
+ return engine().validate(operations, signal, mode);
8534
9084
  }
8535
9085
  case "edit-apply": {
8536
9086
  const { batch } = payload;
@@ -8581,6 +9131,11 @@ function createOoxmlEditHandler() {
8581
9131
  return pptx().slides(signal);
8582
9132
  case "edit-pptx-layouts":
8583
9133
  return pptx().layouts(signal);
9134
+ case "edit-docx-revisions":
9135
+ return docx().revisions(
9136
+ payload.id,
9137
+ signal
9138
+ );
8584
9139
  case "edit-dispose":
8585
9140
  await state?.dispose();
8586
9141
  state = void 0;