web-doc 0.7.0 → 0.8.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 (55) 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 +605 -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/engine.d.ts +10 -4
  12. package/dist/edit/docx/engine.js +80 -6
  13. package/dist/edit/docx/model.d.ts +2 -0
  14. package/dist/edit/docx/model.js +11 -0
  15. package/dist/edit/docx/operations.d.ts +10 -0
  16. package/dist/edit/docx/provider.d.ts +4 -1
  17. package/dist/edit/docx/provider.js +3 -0
  18. package/dist/edit/docx/session.d.ts +12 -1
  19. package/dist/edit/docx/session.js +41 -0
  20. package/dist/edit/docx/structure-ops.js +39 -4
  21. package/dist/edit/docx/table-ops.js +26 -4
  22. package/dist/edit/docx/text-ops.d.ts +22 -0
  23. package/dist/edit/docx/text-ops.js +49 -16
  24. package/dist/edit/docx/text.js +21 -0
  25. package/dist/edit/docx/tracked.d.ts +52 -0
  26. package/dist/edit/docx/tracked.js +347 -0
  27. package/dist/edit/docx/types.d.ts +16 -1
  28. package/dist/edit/docx/write.d.ts +19 -5
  29. package/dist/edit/docx/write.js +31 -8
  30. package/dist/edit/engine.d.ts +14 -3
  31. package/dist/edit/history.d.ts +27 -3
  32. package/dist/edit/history.js +29 -7
  33. package/dist/edit/pdf/range-map.d.ts +4 -2
  34. package/dist/edit/pdf/session.d.ts +10 -0
  35. package/dist/edit/pdf/session.js +39 -0
  36. package/dist/edit/pptx/handler.js +10 -2
  37. package/dist/edit/pptx/session.d.ts +10 -0
  38. package/dist/edit/pptx/session.js +31 -0
  39. package/dist/edit/session.d.ts +11 -0
  40. package/dist/edit/session.js +269 -27
  41. package/dist/edit/types.d.ts +17 -3
  42. package/dist/edit/worker-engine.d.ts +2 -2
  43. package/dist/edit/worker-engine.js +2 -2
  44. package/dist/fuzzy-alignment.d.ts +11 -4
  45. package/dist/fuzzy-alignment.js +3 -9
  46. package/dist/headless.d.ts +1 -1
  47. package/dist/headless.js +4 -1
  48. package/dist/index.d.ts +5 -2
  49. package/dist/index.js +11 -2
  50. package/dist/limits.js +3 -0
  51. package/dist/worker-protocol.d.ts +1 -1
  52. package/dist/workers/fuzzy-search-worker.js +1 -1
  53. package/dist/workers/ooxml-edit-worker.js +1400 -859
  54. package/dist/workers/pdf-edit-worker.js +4 -1
  55. 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
@@ -3448,8 +3474,13 @@ function mergedProperties(part, source, tag, order, set) {
3448
3474
  function toggleXml(local, on) {
3449
3475
  return on ? `<w:${local}/>` : `<w:${local} w:val="0"/>`;
3450
3476
  }
3451
- function changedRunProperties(part, rPr, change, styles) {
3477
+ function changedRunProperties(part, rPr, change, styles, revision) {
3452
3478
  const set = /* @__PURE__ */ new Map();
3479
+ if (revision !== void 0)
3480
+ set.set(
3481
+ "rPrChange",
3482
+ `<w:rPrChange${revision}>${wrapped("w:rPr", innerPropertiesXml(part, rPr, ["rPrChange"]))}</w:rPrChange>`
3483
+ );
3453
3484
  if (change.bold !== void 0) {
3454
3485
  set.set("b", toggleXml("b", change.bold));
3455
3486
  set.set("bCs", toggleXml("bCs", change.bold));
@@ -3497,8 +3528,19 @@ function fontsXml(part, rPr, family) {
3497
3528
  const rest = kept.map((a) => ` ${a.name}="${a.rawValue}"`).join("");
3498
3529
  return `<w:rFonts w:ascii="${face}" w:hAnsi="${face}"${rest}/>`;
3499
3530
  }
3500
- function changedParagraphProperties(part, pPr, change) {
3531
+ function wrapped(tag, inner) {
3532
+ return inner.length === 0 ? `<${tag}/>` : `<${tag}>${inner}</${tag}>`;
3533
+ }
3534
+ function innerPropertiesXml(part, properties, without) {
3535
+ return (properties?.children ?? []).filter((child) => !without.includes(child.local)).map((child) => sliceOf(part, child)).join("");
3536
+ }
3537
+ function changedParagraphProperties(part, pPr, change, revision) {
3501
3538
  const set = /* @__PURE__ */ new Map();
3539
+ if (revision !== void 0)
3540
+ set.set(
3541
+ "pPrChange",
3542
+ `<w:pPrChange${revision}>${wrapped("w:pPr", innerPropertiesXml(part, pPr, ["rPr", "sectPr", "pPrChange"]))}</w:pPrChange>`
3543
+ );
3502
3544
  if (change.align !== void 0)
3503
3545
  set.set("jc", `<w:jc w:val="${alignValue(change.align)}"/>`);
3504
3546
  if (change.spacing !== void 0) {
@@ -3544,11 +3586,11 @@ function alignValue(align) {
3544
3586
  return "left";
3545
3587
  }
3546
3588
  }
3547
- function paragraphMarkProperties(part, pPr, change, styles) {
3589
+ function paragraphMarkProperties(part, pPr, change, styles, revision) {
3548
3590
  const rPr = pPr?.children.find(
3549
3591
  (child) => child.local === "rPr" && child.namespace === W_NS
3550
3592
  );
3551
- const changed = changedRunProperties(part, rPr, change, styles);
3593
+ const changed = changedRunProperties(part, rPr, change, styles, revision);
3552
3594
  return mergedProperties(
3553
3595
  part,
3554
3596
  pPr,
@@ -3599,538 +3641,653 @@ function namespacePatches(part) {
3599
3641
  return items;
3600
3642
  }
3601
3643
 
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;
3644
+ // src/edit/docx/text-ops.ts
3645
+ function topOf(node, container) {
3646
+ let current = node;
3647
+ while (current.parent && current.parent !== container)
3648
+ current = current.parent;
3649
+ return current;
3611
3650
  }
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;
3651
+ function unitsOf(container, items) {
3652
+ const byTop = /* @__PURE__ */ new Map();
3653
+ for (const item of items) {
3654
+ const top = topOf(item.run, container);
3655
+ const list = byTop.get(top) ?? [];
3656
+ list.push(item);
3657
+ byTop.set(top, list);
3634
3658
  }
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
3659
+ const groupEnd = /* @__PURE__ */ new Map();
3660
+ for (const item of items)
3661
+ if (item.fieldRuns && item.fieldRuns.length > 1) {
3662
+ const first = topOf(item.fieldRuns[0], container);
3663
+ const last = topOf(item.fieldRuns.at(-1), container);
3664
+ if (first !== last) groupEnd.set(first, last);
3665
+ }
3666
+ const units = [];
3667
+ let position = 0;
3668
+ const children = container.children.filter(
3669
+ (child) => !(child.local === "pPr" && child.namespace === W_NS)
3644
3670
  );
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;
3671
+ for (let index = 0; index < children.length; index += 1) {
3672
+ const child = children[index];
3673
+ const end = groupEnd.get(child);
3674
+ const nodes = [child];
3675
+ if (end) {
3676
+ let cursor = index + 1;
3677
+ while (cursor < children.length && children[cursor - 1] !== end)
3678
+ nodes.push(children[cursor++]);
3679
+ index = cursor - 1;
3684
3680
  }
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);
3681
+ const own = nodes.flatMap((node) => byTop.get(node) ?? []);
3682
+ const kind = own.length === 0 ? "zero" : end || child.local === "fldSimple" || own.some((item) => item.fieldRuns) ? "field" : child.local === "r" ? "run" : "wrapper";
3683
+ const start = own.length > 0 ? own[0].start : position;
3684
+ const stop = own.length > 0 ? own.at(-1).end : position;
3685
+ units.push({ nodes, kind, start, end: stop, items: own });
3686
+ position = stop;
3693
3687
  }
3694
- return { xml, stamped: stamped.reverse() };
3688
+ return units;
3695
3689
  }
3696
- function firstCellParagraph(record) {
3697
- const table = record.table;
3698
- return table && table.rows[0]?.[0]?.paragraphs[0] === record ? table : void 0;
3690
+ function rPrOf(part, item) {
3691
+ return item ? sliceOf(part, item.rPr) : "";
3699
3692
  }
3700
- function tableRemap(table, newFirstParagraphId) {
3701
- return table.id === newFirstParagraphId ? void 0 : { [table.elementId]: `tbl:${newFirstParagraphId}` };
3693
+ function startTagOf(part, node) {
3694
+ return part.text.slice(node.start, node.contentStart);
3702
3695
  }
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 }));
3696
+ function endTagOf(part, node) {
3697
+ return part.text.slice(node.contentEnd, node.end);
3707
3698
  }
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}`);
3699
+ function contentOf(part, items, from, to, includeStart) {
3700
+ let out = "";
3701
+ for (const item of items) {
3702
+ if (item.start === item.end) {
3703
+ const after = includeStart ? item.start >= from : item.start > from;
3704
+ if (after && item.start <= to) out += sliceOf(part, item.child);
3705
+ continue;
3719
3706
  }
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))
3707
+ const begin = Math.max(item.start, from);
3708
+ const stop = Math.min(item.end, to);
3709
+ if (stop <= begin) continue;
3710
+ if (item.kind === "text" && item.child.local === "t")
3711
+ out += runContentXml(
3712
+ item.text.slice(begin - item.start, stop - item.start)
3745
3713
  );
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
- });
3714
+ else if (begin === item.start && stop === item.end)
3715
+ out += sliceOf(part, item.child);
3755
3716
  }
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 };
3717
+ return out;
3766
3718
  }
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;
3719
+ function droppedInlines(model, record, items, from, to) {
3720
+ const removedIds = [];
3721
+ const removedParagraphIds = [];
3722
+ for (const item of items) {
3723
+ if (item.kind !== "picture" && item.kind !== "object" && item.kind !== "anchor")
3724
+ continue;
3725
+ const inside = item.start === item.end ? from < item.start && item.start < to : item.start >= from && item.end <= to;
3726
+ if (!inside) continue;
3727
+ const inline = record.inlines.find(
3728
+ (candidate) => candidate.node === item.child
3729
+ );
3730
+ if (inline) removedIds.push(inline.elementId);
3731
+ for (const paragraph of model.document.findAll("p", item.child)) {
3732
+ const id = model.paragraphIds.get(paragraph);
3733
+ if (id && paragraph.namespace === W_NS) removedParagraphIds.push(id);
3734
+ }
3774
3735
  }
3775
- return model.blocks.some(
3776
- (block) => block.kind === "paragraph" && block !== record
3777
- );
3736
+ return { removedIds, removedParagraphIds };
3778
3737
  }
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}`
3738
+ function splitAt(part, container, items, start, end, insert, fallbackRPr) {
3739
+ let before = "";
3740
+ let after = "";
3741
+ let inserted = false;
3742
+ let nested = false;
3743
+ let rPr = fallbackRPr;
3744
+ let lastRPr;
3745
+ const raw = (unit) => unit.nodes.map((node) => sliceOf(part, node)).join("");
3746
+ const place = (style) => {
3747
+ rPr = style ?? lastRPr ?? fallbackRPr;
3748
+ before += insert(rPr);
3749
+ inserted = true;
3750
+ };
3751
+ for (const unit of unitsOf(container, items)) {
3752
+ if (unit.kind === "zero") {
3753
+ if (inserted) after += raw(unit);
3754
+ else before += raw(unit);
3755
+ continue;
3756
+ }
3757
+ if (unit.end <= start) {
3758
+ before += raw(unit);
3759
+ lastRPr = rPrOf(part, unit.items.at(-1));
3760
+ if (!inserted && start === end && unit.end === start) place(lastRPr);
3761
+ continue;
3762
+ }
3763
+ if (unit.start >= end) {
3764
+ if (!inserted) place(lastRPr ?? rPrOf(part, unit.items[0]));
3765
+ after += raw(unit);
3766
+ continue;
3767
+ }
3768
+ if (unit.start >= start && unit.end <= end) {
3769
+ if (!inserted) place(rPrOf(part, unit.items[0]));
3770
+ continue;
3771
+ }
3772
+ if (unit.kind === "run") {
3773
+ const style = rPrOf(part, unit.items[0]);
3774
+ const head = runXml(
3775
+ style,
3776
+ contentOf(part, unit.items, unit.start, start, true)
3787
3777
  );
3788
- return;
3778
+ const tail = runXml(
3779
+ style,
3780
+ contentOf(part, unit.items, end, unit.end, start < end)
3781
+ );
3782
+ if (!inserted) {
3783
+ before += head;
3784
+ place(style);
3785
+ after += tail;
3786
+ } else after += tail;
3787
+ continue;
3789
3788
  }
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)
3789
+ if (unit.kind === "wrapper" && unit.start < start && end < unit.end) {
3790
+ const wrapper = unit.nodes[0];
3791
+ const inner = splitAt(
3792
+ part,
3793
+ wrapper,
3794
+ unit.items,
3795
+ start,
3796
+ end,
3797
+ insert,
3798
+ fallbackRPr
3799
+ );
3800
+ before += startTagOf(part, wrapper) + inner.before + inner.after + endTagOf(part, wrapper);
3801
+ rPr = inner.rPr;
3802
+ inserted = true;
3803
+ nested = true;
3804
+ continue;
3805
+ }
3806
+ throw new Error("A range cannot cut a field or a wrapper");
3807
+ }
3808
+ if (!inserted) place(lastRPr);
3809
+ return { before, after, rPr, nested };
3810
+ }
3811
+ async function textTarget(id, context, issue) {
3812
+ const record = context.model.byId.get(id);
3813
+ if (!record || record.kind !== "paragraph") {
3814
+ issue(
3815
+ "/target",
3816
+ record ? "invalid-target" : "unknown-target",
3817
+ record ? `Element ${id} is not a paragraph` : `No element ${id}`
3818
+ );
3819
+ return void 0;
3820
+ }
3821
+ if (record.readOnlyReason) {
3822
+ issue(
3823
+ "/target",
3824
+ "invalid-target",
3825
+ `Paragraph ${id} is read-only (${record.readOnlyReason})`
3826
+ );
3827
+ return void 0;
3828
+ }
3829
+ return { record, part: context.model.document };
3830
+ }
3831
+ function checkRange(target, id, range, issue, structural) {
3832
+ const text = target.record.text.text;
3833
+ const length = text.length;
3834
+ if (!range) return { start: 0, end: length };
3835
+ if (range.start.elementId !== id || range.end.elementId !== id) {
3836
+ issue("/range", "invalid-range", "Both ends must lie on the target");
3837
+ return void 0;
3838
+ }
3839
+ const { start, end } = { start: range.start.offset, end: range.end.offset };
3840
+ if (start > length || end > length || start > end) {
3841
+ issue(
3842
+ "/range",
3843
+ "invalid-range",
3844
+ `Offsets must satisfy 0 \u2264 start \u2264 end \u2264 ${length}`
3845
+ );
3846
+ return void 0;
3847
+ }
3848
+ for (const offset of [start, end])
3849
+ if (offset > 0 && offset < length && (text.charCodeAt(offset) & 64512) === 56320 && (text.charCodeAt(offset - 1) & 64512) === 55296) {
3850
+ issue("/range", "invalid-range", "A range cannot split a surrogate pair");
3851
+ return void 0;
3852
+ }
3853
+ if (!structural) return { start, end };
3854
+ const inside = (unit, offset) => unit.start < offset && offset < unit.end;
3855
+ const check = (container, items) => {
3856
+ for (const unit of unitsOf(container, items)) {
3857
+ if (unit.kind === "field" && (inside(unit, start) || inside(unit, end))) {
3798
3858
  issue(
3799
- "/target",
3800
- "invalid-target",
3801
- `Paragraph ${operation3.target} is read-only (${record.readOnlyReason})`
3859
+ "/range",
3860
+ "invalid-range",
3861
+ "A range cannot start or end inside a field"
3802
3862
  );
3803
- else if (!otherParagraphsIn(context.model, record))
3863
+ return false;
3864
+ }
3865
+ if (unit.kind !== "wrapper") continue;
3866
+ const startIn = inside(unit, start);
3867
+ const endIn = inside(unit, end);
3868
+ if (startIn !== endIn) {
3804
3869
  issue(
3805
- "/target",
3806
- "last-paragraph",
3807
- "The last paragraph of a body or a cell cannot be deleted"
3870
+ "/range",
3871
+ "invalid-range",
3872
+ "A range cannot cut the edge of a hyperlink or content control"
3808
3873
  );
3874
+ return false;
3875
+ }
3876
+ if (startIn && !check(unit.nodes[0], unit.items)) return false;
3809
3877
  }
3878
+ return true;
3879
+ };
3880
+ return check(target.record.node, target.record.text.items) ? { start, end } : void 0;
3881
+ }
3882
+ function commitParagraph(context, target, items, extra = {}) {
3883
+ const { part, record } = target;
3884
+ const transaction = context.pkg.transaction();
3885
+ transaction.patch(part, [...namespacePatches(part), ...items]);
3886
+ return transaction.commit().then((change) => ({
3887
+ createdIds: [],
3888
+ warnings: change.warnings,
3889
+ ...context.model.unauthoredSet.has(record.id) ? { stamped: [record.id] } : {},
3890
+ reflowFrom: record.id,
3891
+ ...extra
3892
+ }));
3893
+ }
3894
+ var replaceTextHandler = {
3895
+ async validate(operation3, context, issue) {
3896
+ const text = normalizeText(operation3.text);
3897
+ const problem = textProblem(text);
3898
+ if (problem) issue("/text", "invalid-text", `The text holds ${problem}`);
3899
+ const target = await textTarget(operation3.target, context, issue);
3900
+ if (!target) return;
3901
+ const range = checkRange(
3902
+ target,
3903
+ operation3.target,
3904
+ operation3.range,
3905
+ issue,
3906
+ true
3907
+ );
3908
+ if (range && context.tracked && trackedRangeProblem(
3909
+ target.record,
3910
+ range.start,
3911
+ range.end,
3912
+ issue,
3913
+ "/range"
3914
+ ))
3915
+ return;
3916
+ if (!range || !text.includes("\n")) return;
3917
+ const probe = splitAt(
3918
+ target.part,
3919
+ target.record.node,
3920
+ target.record.text.items,
3921
+ range.start,
3922
+ range.end,
3923
+ () => "",
3924
+ ""
3925
+ );
3926
+ if (probe.nested)
3927
+ issue(
3928
+ "/text",
3929
+ "invalid-range",
3930
+ "A paragraph break cannot be inserted inside a hyperlink or content control"
3931
+ );
3810
3932
  },
3811
3933
  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, {
3934
+ const target = await textTarget(operation3.target, context, () => {
3935
+ });
3936
+ const { start, end } = checkRange(
3937
+ target,
3938
+ operation3.target,
3939
+ operation3.range,
3940
+ () => {
3941
+ },
3942
+ true
3943
+ );
3944
+ const { items, createdIds, removedIds, removedParagraphIds } = replacedParagraph(
3945
+ context,
3946
+ target.record,
3947
+ start,
3948
+ end,
3949
+ normalizeText(operation3.text)
3950
+ );
3951
+ return commitParagraph(context, target, items, {
3836
3952
  createdIds,
3837
- removedIds: elementIdsUnder(model, node),
3838
- removedParagraphIds,
3839
- reflowFrom: record.id,
3840
- ...remappedIds ? { remappedIds } : {}
3953
+ ...removedIds.length > 0 ? { removedIds } : {},
3954
+ ...removedParagraphIds.length > 0 ? { removedParagraphIds } : {}
3841
3955
  });
3842
3956
  }
3843
3957
  };
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)
3958
+ function replacedParagraph(context, record, start, end, text) {
3959
+ const part = context.model.document;
3960
+ const segments = text.split("\n");
3961
+ const single = segments.length === 1;
3962
+ const items = [];
3963
+ const createdIds = [];
3964
+ const copiedPPr = paragraphPropertiesWithoutSection(part, record.pPr);
3965
+ if (context.tracked) {
3966
+ const split = trackedSplit(
3967
+ context,
3968
+ record,
3969
+ start,
3970
+ end,
3971
+ paragraphMarkRPr(part, record)
3853
3972
  );
3854
- const elsewhere = new RegExp(
3855
- `r:(?:id|embed|link|pict)="${rId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`
3973
+ const inserted = (segment) => insertedRunXml(context, split.rPr, segment);
3974
+ const originalPPr = sliceOf(part, record.pPr);
3975
+ const firstPPr = single ? originalPPr : markedParagraphProperties(context, record.pPr, "ins");
3976
+ items.push(
3977
+ patches.replaceElement(
3978
+ part,
3979
+ record.node,
3980
+ paragraphXml(
3981
+ record.node,
3982
+ record.id,
3983
+ firstPPr,
3984
+ split.before + split.deleted + inserted(segments[0]) + (single ? split.after : "")
3985
+ )
3986
+ )
3856
3987
  );
3857
- const others = uses.length > 0 || elsewhere.test(stripNode(part, record.node));
3858
- if (!others) transaction.removeRelationship(model.mainPart, rId);
3988
+ segments.slice(1).forEach((segment, index) => {
3989
+ const id = context.freshParagraphId();
3990
+ createdIds.push(`p:${id}`);
3991
+ const last = index === segments.length - 2;
3992
+ items.push(
3993
+ patches.insertAfter(
3994
+ part,
3995
+ record.node,
3996
+ paragraphXml(
3997
+ record.node,
3998
+ id,
3999
+ last ? originalPPr : markedParagraphProperties(context, record.pPr, "ins"),
4000
+ inserted(segment) + (last ? split.after : "")
4001
+ )
4002
+ )
4003
+ );
4004
+ });
4005
+ } else {
4006
+ const split = splitAt(
4007
+ part,
4008
+ record.node,
4009
+ record.text.items,
4010
+ start,
4011
+ end,
4012
+ (rPr) => runXml(rPr, runContentXml(segments[0])),
4013
+ paragraphMarkRPr(part, record)
4014
+ );
4015
+ const pPr = sliceOf(part, record.pPr);
4016
+ items.push(
4017
+ patches.replaceElement(
4018
+ part,
4019
+ record.node,
4020
+ paragraphXml(
4021
+ record.node,
4022
+ record.id,
4023
+ pPr,
4024
+ split.before + (single ? split.after : "")
4025
+ )
4026
+ )
4027
+ );
4028
+ segments.slice(1).forEach((segment, index) => {
4029
+ const id = context.freshParagraphId();
4030
+ createdIds.push(`p:${id}`);
4031
+ const last = index === segments.length - 2;
4032
+ items.push(
4033
+ patches.insertAfter(
4034
+ part,
4035
+ record.node,
4036
+ paragraphXml(
4037
+ record.node,
4038
+ id,
4039
+ copiedPPr,
4040
+ runXml(split.rPr, runContentXml(segment)) + (last ? split.after : "")
4041
+ )
4042
+ )
4043
+ );
4044
+ });
3859
4045
  }
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
4046
+ const dropped = droppedInlines(
4047
+ context.model,
4048
+ record,
4049
+ record.text.items,
4050
+ start,
4051
+ end
4052
+ );
4053
+ const removedIds = dropped.removedIds;
4054
+ if (!single)
4055
+ for (const inline of record.inlines) {
4056
+ const item = record.text.items.find(
4057
+ (candidate) => candidate.child === inline.node
4058
+ );
4059
+ if (item && item.start >= end && !removedIds.includes(inline.elementId))
4060
+ removedIds.push(inline.elementId);
4061
+ }
4062
+ return {
4063
+ items,
4064
+ createdIds,
4065
+ removedIds,
4066
+ removedParagraphIds: dropped.removedParagraphIds
4067
+ };
4068
+ }
4069
+ function paragraphMarkRPr(part, record) {
4070
+ const rPr = record.pPr?.children.find(
4071
+ (child) => child.local === "rPr" && child.namespace === W_NS
3871
4072
  );
4073
+ return sliceOf(part, rPr);
3872
4074
  }
3873
- function stripNode(part, node) {
3874
- return part.text.slice(0, node.start) + part.text.slice(node.end);
4075
+ function restyledRun(part, run2, rPr) {
4076
+ const content = run2.children.filter((child) => !(child.local === "rPr" && child.namespace === W_NS)).map((child) => sliceOf(part, child)).join("");
4077
+ return `<w:r>${rPr}${content}</w:r>`;
3875
4078
  }
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;
4079
+ function restyled(part, container, items, start, end, change, context) {
4080
+ let out = "";
4081
+ const changed = (rPr) => changedRunProperties(
4082
+ part,
4083
+ rPr,
4084
+ change,
4085
+ context.model.styles,
4086
+ context.tracked ? revisionAttributes(context) : void 0
4087
+ );
4088
+ for (const unit of unitsOf(container, items)) {
4089
+ const raw = unit.nodes.map((node) => sliceOf(part, node)).join("");
4090
+ const overlaps = unit.start < end && start < unit.end ? true : unit.start === unit.end && start < unit.start && unit.start < end;
4091
+ if (unit.kind === "zero" || !overlaps) {
4092
+ out += raw;
4093
+ continue;
3886
4094
  }
3887
- if (record.kind === "paragraph" && record.sectPr) {
3888
- issue(
3889
- "/target",
3890
- "section-break",
3891
- "A paragraph that ends a section cannot move"
4095
+ if (unit.kind === "run") {
4096
+ const run2 = unit.nodes[0];
4097
+ const own = rPrOf(part, unit.items[0]);
4098
+ const rPr = unit.items[0]?.rPr;
4099
+ out += runXml(
4100
+ own,
4101
+ contentOf(
4102
+ part,
4103
+ unit.items,
4104
+ unit.start,
4105
+ Math.min(start, unit.end),
4106
+ true
4107
+ )
3892
4108
  );
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})`
4109
+ out += runXml(
4110
+ changed(rPr),
4111
+ contentOf(
4112
+ part,
4113
+ unit.items,
4114
+ Math.max(unit.start, start),
4115
+ Math.min(unit.end, end),
4116
+ false
4117
+ )
3900
4118
  );
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"
4119
+ out += runXml(
4120
+ own,
4121
+ contentOf(part, unit.items, Math.max(end, unit.start), unit.end, false)
3910
4122
  );
3911
- return;
4123
+ continue;
3912
4124
  }
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
- );
4125
+ if (unit.kind === "field") {
4126
+ for (const node of unit.nodes)
4127
+ if (node.local === "r" && node.namespace === W_NS)
4128
+ out += restyledRun(
4129
+ part,
4130
+ node,
4131
+ changed(
4132
+ node.children.find(
4133
+ (child) => child.local === "rPr" && child.namespace === W_NS
4134
+ )
4135
+ )
4136
+ );
4137
+ else if (node.local === "fldSimple")
4138
+ out += startTagOf(part, node) + restyled(part, node, unit.items, start, end, change, context) + endTagOf(part, node);
4139
+ else out += sliceOf(part, node);
4140
+ continue;
4141
+ }
4142
+ const wrapper = unit.nodes[0];
4143
+ out += startTagOf(part, wrapper) + restyled(part, wrapper, unit.items, start, end, change, context) + endTagOf(part, wrapper);
4144
+ }
4145
+ return out;
4146
+ }
4147
+ var setTextStyleHandler = {
4148
+ async validate(operation3, context, issue) {
4149
+ const { style } = operation3;
4150
+ if (style.color !== void 0) {
4151
+ const problem = colorProblem(style.color);
4152
+ if (problem)
4153
+ issue("/style/color", "invalid-value", `Colour: ${problem}`);
4154
+ }
4155
+ if (style.fontFamily !== void 0) {
4156
+ const problem = attributeProblem(style.fontFamily);
4157
+ if (problem)
4158
+ issue("/style/fontFamily", "invalid-value", `Font: ${problem}`);
4159
+ }
4160
+ const target = await textTarget(operation3.target, context, issue);
4161
+ if (target)
4162
+ checkRange(target, operation3.target, operation3.range, issue, false);
3919
4163
  },
3920
4164
  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,
4165
+ const target = await textTarget(operation3.target, context, () => {
4166
+ });
4167
+ const { part, record } = target;
4168
+ const { start, end } = checkRange(
4169
+ target,
4170
+ operation3.target,
4171
+ operation3.range,
3926
4172
  () => {
3927
- }
4173
+ },
4174
+ false
3928
4175
  );
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);
4176
+ if (Object.keys(operation3.style).length === 0)
4177
+ return { createdIds: [], warnings: [] };
4178
+ const content = restyled(
4179
+ part,
4180
+ record.node,
4181
+ record.text.items,
4182
+ start,
4183
+ end,
4184
+ operation3.style,
4185
+ context
4186
+ );
4187
+ const pPr = end >= record.text.text.length ? paragraphMarkProperties(
4188
+ part,
4189
+ record.pPr,
4190
+ operation3.style,
4191
+ context.model.styles,
4192
+ context.tracked ? revisionAttributes(context) : void 0
4193
+ ) : sliceOf(part, record.pPr);
4194
+ return commitParagraph(context, target, [
4195
+ patches.replaceElement(
4196
+ part,
4197
+ record.node,
4198
+ paragraphXml(record.node, record.id, pPr, content)
4199
+ )
4200
+ ]);
4001
4201
  }
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 = {
4202
+ };
4203
+ var setParagraphStyleHandler = {
4015
4204
  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
- }
4205
+ await textTarget(operation3.target, context, issue);
4034
4206
  },
4035
4207
  async apply(operation3, context) {
4036
- const { model } = context;
4037
- const { record, side } = placement2(operation3, context, () => {
4208
+ const target = await textTarget(operation3.target, context, () => {
4038
4209
  });
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
4210
+ const { part, record } = target;
4211
+ const { style } = operation3;
4212
+ if (style.align === void 0 && (style.spacing === void 0 || Object.keys(style.spacing).length === 0))
4213
+ return { createdIds: [], warnings: [] };
4214
+ const pPr = changedParagraphProperties(
4215
+ part,
4216
+ record.pPr,
4217
+ style,
4218
+ context.tracked ? revisionAttributes(context) : void 0
4219
+ );
4220
+ const node = record.node;
4221
+ const content = node.selfClosing ? "" : part.text.slice(
4222
+ record.pPr ? record.pPr.end : node.contentStart,
4223
+ node.contentEnd
4069
4224
  );
4225
+ return commitParagraph(context, target, [
4226
+ patches.replaceElement(
4227
+ part,
4228
+ node,
4229
+ paragraphXml(node, record.id, pPr, content)
4230
+ )
4231
+ ]);
4070
4232
  }
4071
4233
  };
4072
4234
 
4073
- // src/edit/docx/text-ops.ts
4074
- function topOf(node, container) {
4235
+ // src/edit/docx/tracked.ts
4236
+ function revisionAttributes(context) {
4237
+ const tracked = context.tracked;
4238
+ return ` w:id="${context.nextRevisionId()}" w:author="${escapeAttributeValue(tracked.author)}"${tracked.date === void 0 ? "" : ` w:date="${escapeAttributeValue(tracked.date)}"`}`;
4239
+ }
4240
+ function unsupportedTracked(issue, path, what) {
4241
+ issue(
4242
+ path,
4243
+ "unsupported-change-mode",
4244
+ `${what} has no tracked form; apply it directly and review with checkpoints`
4245
+ );
4246
+ }
4247
+ function topOf2(node, container) {
4075
4248
  let current = node;
4076
4249
  while (current.parent && current.parent !== container)
4077
4250
  current = current.parent;
4078
4251
  return current;
4079
4252
  }
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;
4253
+ function trackedRangeProblem(record, start, end, issue, path) {
4254
+ for (const item of record.text.items) {
4255
+ const overlaps = item.start === item.end ? start < item.start && item.start < end : item.start < end && start < item.end;
4256
+ if (!overlaps) continue;
4257
+ const top = topOf2(item.run, record.node);
4258
+ const plainRun = item.run.local === "r" && item.run.namespace === W_NS;
4259
+ if (!plainRun || item.fieldRuns || item.kind === "field" || top !== item.run || item.wrapper !== void 0) {
4260
+ unsupportedTracked(
4261
+ issue,
4262
+ path,
4263
+ "A change touching a hyperlink, content control, field or equation"
4264
+ );
4265
+ return true;
4109
4266
  }
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
4267
  }
4117
- return units;
4268
+ return false;
4118
4269
  }
4119
- function rPrOf(part, item) {
4120
- return item ? sliceOf(part, item.rPr) : "";
4270
+ function deletedContent(content) {
4271
+ 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
4272
  }
4122
- function startTagOf(part, node) {
4123
- return part.text.slice(node.start, node.contentStart);
4273
+ function deletedRunXml(rPr, content) {
4274
+ return content.length === 0 ? "" : `<w:r>${rPr}${deletedContent(content)}</w:r>`;
4124
4275
  }
4125
- function endTagOf(part, node) {
4126
- return part.text.slice(node.contentEnd, node.end);
4276
+ function runContentOf(part, run2) {
4277
+ return run2.children.filter((child) => !(child.local === "rPr" && child.namespace === W_NS)).map((child) => sliceOf(part, child)).join("");
4127
4278
  }
4128
- function contentOf(part, items, from, to, includeStart) {
4279
+ function runPropertiesOf(part, run2) {
4280
+ const rPr = run2.children.find(
4281
+ (child) => child.local === "rPr" && child.namespace === W_NS
4282
+ );
4283
+ return sliceOf(part, rPr);
4284
+ }
4285
+ function deletedContentOf(part, items, from, to) {
4129
4286
  let out = "";
4130
4287
  for (const item of items) {
4131
4288
  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);
4289
+ if (from < item.start && item.start < to)
4290
+ out += sliceOf(part, item.child);
4134
4291
  continue;
4135
4292
  }
4136
4293
  const begin = Math.max(item.start, from);
@@ -4145,451 +4302,732 @@ function contentOf(part, items, from, to, includeStart) {
4145
4302
  }
4146
4303
  return out;
4147
4304
  }
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) {
4305
+ function trackedSplit(context, record, start, end, fallbackRPr) {
4306
+ const part = context.model.document;
4168
4307
  let before = "";
4169
4308
  let after = "";
4170
- let inserted = false;
4171
- let nested = false;
4172
- let rPr = fallbackRPr;
4309
+ const deletedRuns = [];
4310
+ let rPr;
4173
4311
  let lastRPr;
4312
+ let passed = false;
4174
4313
  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)) {
4314
+ for (const unit of unitsOf(record.node, record.text.items)) {
4181
4315
  if (unit.kind === "zero") {
4182
- if (inserted) after += raw(unit);
4316
+ if (passed) after += raw(unit);
4183
4317
  else before += raw(unit);
4184
4318
  continue;
4185
4319
  }
4186
4320
  if (unit.end <= start) {
4187
4321
  before += raw(unit);
4188
4322
  lastRPr = rPrOf(part, unit.items.at(-1));
4189
- if (!inserted && start === end && unit.end === start) place(lastRPr);
4323
+ if (start === end && unit.end === start) {
4324
+ rPr ??= lastRPr;
4325
+ passed = true;
4326
+ }
4190
4327
  continue;
4191
4328
  }
4192
4329
  if (unit.start >= end) {
4193
- if (!inserted) place(lastRPr ?? rPrOf(part, unit.items[0]));
4330
+ rPr ??= lastRPr ?? rPrOf(part, unit.items[0]);
4331
+ passed = true;
4194
4332
  after += raw(unit);
4195
4333
  continue;
4196
4334
  }
4335
+ if (unit.kind !== "run")
4336
+ throw new Error("A tracked change cannot cut a field or a wrapper");
4337
+ const run2 = unit.nodes[0];
4338
+ const style = runPropertiesOf(part, run2);
4197
4339
  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(
4340
+ deletedRuns.push(deletedRunXml(style, runContentOf(part, run2)));
4341
+ } else {
4342
+ const head = contentOf(part, unit.items, unit.start, start, true);
4343
+ const middle = deletedContentOf(
4221
4344
  part,
4222
- wrapper,
4223
4345
  unit.items,
4224
- start,
4225
- end,
4226
- insert,
4227
- fallbackRPr
4346
+ Math.max(unit.start, start),
4347
+ Math.min(unit.end, end)
4228
4348
  );
4229
- before += startTagOf(part, wrapper) + inner.before + inner.after + endTagOf(part, wrapper);
4230
- rPr = inner.rPr;
4231
- inserted = true;
4232
- nested = true;
4349
+ const tail = contentOf(part, unit.items, end, unit.end, start < end);
4350
+ if (head) before += runXml(style, head);
4351
+ if (middle) deletedRuns.push(deletedRunXml(style, middle));
4352
+ if (tail) after += runXml(style, tail);
4353
+ }
4354
+ rPr ??= style;
4355
+ if (unit.end >= end) passed = true;
4356
+ }
4357
+ const deleted = deletedRuns.length > 0 ? `<w:del${revisionAttributes(context)}>${deletedRuns.join("")}</w:del>` : "";
4358
+ return { before, deleted, rPr: rPr ?? lastRPr ?? fallbackRPr, after };
4359
+ }
4360
+ function insertedRunXml(context, rPr, text) {
4361
+ const content = runContentXml(text);
4362
+ return content.length === 0 ? "" : `<w:ins${revisionAttributes(context)}>${runXml(rPr, content)}</w:ins>`;
4363
+ }
4364
+ function markedParagraphProperties(context, pPr, mark) {
4365
+ const part = context.model.document;
4366
+ const rPr = pPr?.children.find(
4367
+ (child) => child.local === "rPr" && child.namespace === W_NS
4368
+ );
4369
+ const inner = innerPropertiesXml(part, rPr, ["ins", "del"]);
4370
+ const marked = `<w:rPr><w:${mark}${revisionAttributes(context)}/>${inner}</w:rPr>`;
4371
+ return mergedProperties(
4372
+ part,
4373
+ pPr,
4374
+ "w:pPr",
4375
+ PPR_ORDER,
4376
+ /* @__PURE__ */ new Map([
4377
+ ["rPr", marked],
4378
+ ["sectPr", null]
4379
+ ])
4380
+ );
4381
+ }
4382
+ function deletedParagraphXml(context, record) {
4383
+ const part = context.model.document;
4384
+ const pPr = markedParagraphProperties(context, record.pPr, "del");
4385
+ let content = "";
4386
+ let open = [];
4387
+ const flush = () => {
4388
+ if (open.length > 0)
4389
+ content += `<w:del${revisionAttributes(context)}>${open.join("")}</w:del>`;
4390
+ open = [];
4391
+ };
4392
+ for (const child of record.node.children) {
4393
+ if (child.local === "pPr" && child.namespace === W_NS) continue;
4394
+ if (child.local === "r" && child.namespace === W_NS) {
4395
+ const run2 = deletedRunXml(
4396
+ runPropertiesOf(part, child),
4397
+ runContentOf(part, child)
4398
+ );
4399
+ if (run2) open.push(run2);
4233
4400
  continue;
4234
4401
  }
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 };
4402
+ flush();
4403
+ content += sliceOf(part, child);
4404
+ }
4405
+ flush();
4406
+ return paragraphXml(record.node, record.id, pPr, content);
4407
+ }
4408
+ var RUN_REVISIONS = /* @__PURE__ */ new Set(["ins", "del", "moveFrom", "moveTo"]);
4409
+ function revisionsOf(part, paragraph) {
4410
+ const out = [];
4411
+ const attributes = (node) => {
4412
+ const author = part.attribute(node, "w:author");
4413
+ const date = part.attribute(node, "w:date");
4414
+ const raw = part.attribute(node, "w:id");
4415
+ return {
4416
+ id: raw !== void 0 && /^\d+$/.test(raw) ? Number(raw) : -1,
4417
+ ...author === void 0 ? {} : { author },
4418
+ ...date === void 0 ? {} : { date }
4419
+ };
4420
+ };
4421
+ const visit = (node, inRPr, inPPr) => {
4422
+ for (const child of node.children) {
4423
+ if (child.namespace !== W_NS) {
4424
+ visit(child, inRPr, inPPr);
4425
+ continue;
4426
+ }
4427
+ if (child.local === "pPr") {
4428
+ visit(child, false, true);
4429
+ continue;
4430
+ }
4431
+ if (child.local === "rPr") {
4432
+ visit(child, true, inPPr);
4433
+ continue;
4434
+ }
4435
+ if (inRPr && RUN_REVISIONS.has(child.local)) {
4436
+ out.push({
4437
+ start: child.start,
4438
+ revision: {
4439
+ kind: child.local,
4440
+ scope: inPPr ? "mark" : "runs",
4441
+ ...attributes(child),
4442
+ ...inPPr ? { text: "\n" } : {}
4443
+ }
4444
+ });
4445
+ continue;
4446
+ }
4447
+ if (inRPr && child.local === "rPrChange") {
4448
+ out.push({
4449
+ start: child.start,
4450
+ revision: {
4451
+ kind: "rPrChange",
4452
+ scope: inPPr ? "mark" : "runs",
4453
+ ...attributes(child),
4454
+ ...inPPr || !node.parent ? {} : { text: textOf(part, node.parent) }
4455
+ }
4456
+ });
4457
+ continue;
4458
+ }
4459
+ if (inPPr && child.local === "pPrChange") {
4460
+ out.push({
4461
+ start: child.start,
4462
+ revision: {
4463
+ kind: "pPrChange",
4464
+ scope: "paragraph",
4465
+ ...attributes(child)
4466
+ }
4467
+ });
4468
+ continue;
4469
+ }
4470
+ if (!inRPr && !inPPr && RUN_REVISIONS.has(child.local)) {
4471
+ out.push({
4472
+ start: child.start,
4473
+ revision: {
4474
+ kind: child.local,
4475
+ scope: "runs",
4476
+ ...attributes(child),
4477
+ text: textOf(part, child)
4478
+ }
4479
+ });
4480
+ visit(child, false, false);
4481
+ continue;
4482
+ }
4483
+ visit(child, inRPr, inPPr);
4484
+ }
4485
+ };
4486
+ visit(paragraph, false, false);
4487
+ return out.sort((a, b) => a.start - b.start).map((entry) => Object.freeze(entry.revision));
4488
+ }
4489
+ function textOf(part, node) {
4490
+ let text = "";
4491
+ const visit = (current) => {
4492
+ for (const child of current.children) {
4493
+ if (child.namespace === W_NS) {
4494
+ if (child.local === "t" || child.local === "delText") {
4495
+ text += part.textOf(child);
4496
+ continue;
4497
+ }
4498
+ if (child.local === "tab") {
4499
+ text += TAB;
4500
+ continue;
4501
+ }
4502
+ if (child.local === "br" || child.local === "cr") {
4503
+ text += LINE_BREAK;
4504
+ continue;
4505
+ }
4506
+ if (child.local === "rPr" || child.local === "pPr") continue;
4507
+ }
4508
+ visit(child);
4509
+ }
4510
+ };
4511
+ visit(node);
4512
+ return text;
4239
4513
  }
4240
- async function textTarget(id, context, issue) {
4241
- const record = context.model.byId.get(id);
4242
- if (!record || record.kind !== "paragraph") {
4514
+
4515
+ // src/edit/docx/structure-ops.ts
4516
+ var MEDIA_FOLDER = "/word/media";
4517
+ var WP_NS = "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing";
4518
+ var A_NS = "http://schemas.openxmlformats.org/drawingml/2006/main";
4519
+ var PIC_NS = "http://schemas.openxmlformats.org/drawingml/2006/picture";
4520
+ var R_NS = OFFICE_RELATIONSHIPS.slice(0, -1);
4521
+ var EMU_PER_PT = 12700;
4522
+ function blockNode(record) {
4523
+ return record.node;
4524
+ }
4525
+ function placement2(operation3, context, issue) {
4526
+ const named = [operation3.before, operation3.after].filter(
4527
+ (value) => value !== void 0
4528
+ );
4529
+ if (named.length !== 1) {
4243
4530
  issue(
4244
- "/target",
4245
- record ? "invalid-target" : "unknown-target",
4246
- record ? `Element ${id} is not a paragraph` : `No element ${id}`
4531
+ "/before",
4532
+ "invalid-value",
4533
+ "Exactly one of before and after names the reference element"
4247
4534
  );
4248
4535
  return void 0;
4249
4536
  }
4250
- if (record.readOnlyReason) {
4537
+ const side = operation3.before !== void 0 ? "before" : "after";
4538
+ const id = operation3[side];
4539
+ const record = context.model.byId.get(id);
4540
+ if (!record || record.kind !== "paragraph" && record.kind !== "table") {
4251
4541
  issue(
4252
- "/target",
4253
- "invalid-target",
4254
- `Paragraph ${id} is read-only (${record.readOnlyReason})`
4542
+ `/${side}`,
4543
+ record ? "invalid-target" : "unknown-target",
4544
+ record ? `Element ${id} is not a paragraph or a table` : `No element ${id}`
4255
4545
  );
4256
4546
  return void 0;
4257
4547
  }
4258
- return { record, part: context.model.document };
4548
+ return { record, side };
4259
4549
  }
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;
4550
+ function referenceRunProperties(part, record) {
4551
+ const paragraph = record.kind === "paragraph" ? record : record.rows[0]?.[0]?.paragraphs[0];
4552
+ if (!paragraph) return void 0;
4553
+ const first = firstTextItem(paragraph.text);
4554
+ if (first?.rPr) return first.rPr;
4555
+ return paragraph.pPr?.children.find(
4556
+ (child) => child.local === "rPr" && child.namespace === W_NS
4557
+ );
4558
+ }
4559
+ function paragraphIdsUnder(model, node) {
4560
+ const out = [];
4561
+ if (node.local === "p" && node.namespace === W_NS) {
4562
+ const own = model.paragraphIds.get(node);
4563
+ if (own) out.push(own);
4267
4564
  }
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;
4565
+ for (const paragraph of model.document.findAll("p", node)) {
4566
+ if (paragraph === node || paragraph.namespace !== W_NS) continue;
4567
+ const id = model.paragraphIds.get(paragraph);
4568
+ if (id) out.push(id);
4276
4569
  }
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;
4570
+ return out;
4571
+ }
4572
+ function elementIdsUnder(model, node) {
4573
+ const out = [];
4574
+ for (const record of model.records) {
4575
+ let current = record.node;
4576
+ while (current && current !== node) current = current.parent;
4577
+ if (current === node) out.push(record.elementId);
4578
+ }
4579
+ return out;
4580
+ }
4581
+ function stampedSlice(model, node) {
4582
+ const part = model.document;
4583
+ const targets = [
4584
+ ...node.local === "p" && node.namespace === W_NS ? [node] : [],
4585
+ ...part.findAll("p", node).filter((p) => p !== node && p.namespace === W_NS)
4586
+ ].filter((paragraph) => {
4587
+ const id = model.paragraphIds.get(paragraph);
4588
+ return id !== void 0 && model.unauthoredSet.has(id);
4589
+ }).sort((a, b) => b.start - a.start);
4590
+ let xml = sliceOf(part, node);
4591
+ const stamped = [];
4592
+ for (const paragraph of targets) {
4593
+ const id = model.paragraphIds.get(paragraph);
4594
+ if (part.attribute(paragraph, "w14:paraId")) {
4595
+ stamped.push(id);
4596
+ continue;
4306
4597
  }
4307
- return true;
4308
- };
4309
- return check(target.record.node, target.record.text.items) ? { start, end } : void 0;
4598
+ const last = paragraph.attributes.at(-1);
4599
+ const from = last ? last.end : paragraph.start + 1 + paragraph.name.length;
4600
+ const close = part.text.indexOf(">", from);
4601
+ let at = close;
4602
+ if (part.text[close - 1] === "/") at = close - 1;
4603
+ const offset = at - node.start;
4604
+ xml = `${xml.slice(0, offset)} w14:paraId="${id}"${xml.slice(offset)}`;
4605
+ stamped.push(id);
4606
+ }
4607
+ return { xml, stamped: stamped.reverse() };
4310
4608
  }
4311
- function commitParagraph(context, target, items, extra = {}) {
4312
- const { part, record } = target;
4313
- const transaction = context.pkg.transaction();
4609
+ function firstCellParagraph(record) {
4610
+ const table = record.table;
4611
+ return table && table.rows[0]?.[0]?.paragraphs[0] === record ? table : void 0;
4612
+ }
4613
+ function tableRemap(table, newFirstParagraphId) {
4614
+ return table.id === newFirstParagraphId ? void 0 : { [table.elementId]: `tbl:${newFirstParagraphId}` };
4615
+ }
4616
+ function commit(context, items, result, transaction = context.pkg.transaction()) {
4617
+ const part = context.model.document;
4314
4618
  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
- }));
4619
+ return transaction.commit().then((change) => ({ ...result, warnings: change.warnings }));
4322
4620
  }
4323
- var replaceTextHandler = {
4621
+ var insertParagraphHandler = {
4324
4622
  async validate(operation3, context, issue) {
4325
- const text = normalizeText(operation3.text);
4326
- const problem = textProblem(text);
4623
+ const problem = textProblem(normalizeText(operation3.text));
4327
4624
  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)
4625
+ if (operation3.style?.color !== void 0) {
4626
+ const colour = colorProblem(operation3.style.color);
4627
+ if (colour) issue("/style/color", "invalid-value", `Colour: ${colour}`);
4628
+ }
4629
+ if (operation3.style?.fontFamily !== void 0) {
4630
+ const font = attributeProblem(operation3.style.fontFamily);
4631
+ if (font) issue("/style/fontFamily", "invalid-value", `Font: ${font}`);
4632
+ }
4633
+ placement2(operation3, context, issue);
4634
+ },
4635
+ async apply(operation3, context) {
4636
+ const { record, side } = placement2(operation3, context, () => {
4637
+ });
4638
+ const part = context.model.document;
4639
+ const node = blockNode(record);
4640
+ const sourceRPr = referenceRunProperties(part, record);
4641
+ const rPr = operation3.style ? changedRunProperties(
4642
+ part,
4643
+ sourceRPr,
4644
+ operation3.style,
4645
+ context.model.styles
4646
+ ) : sliceOf(part, sourceRPr);
4647
+ const sourcePPr = record.kind === "paragraph" ? record.pPr : void 0;
4648
+ const pPr = paragraphPropertiesWithoutSection(part, sourcePPr);
4649
+ const segments = normalizeText(operation3.text).split("\n");
4650
+ const createdIds = [];
4651
+ const xml = segments.map((segment) => {
4652
+ const id = context.freshParagraphId();
4653
+ createdIds.push(`p:${id}`);
4654
+ return context.tracked ? paragraphXml(
4655
+ void 0,
4656
+ id,
4657
+ markedParagraphProperties(context, sourcePPr, "ins"),
4658
+ insertedRunXml(context, rPr, segment)
4659
+ ) : paragraphXml(
4660
+ void 0,
4661
+ id,
4662
+ pPr,
4663
+ runXml(rPr, runContentXml(segment))
4664
+ );
4665
+ });
4666
+ const items = side === "before" ? xml.map((paragraph) => patches.insertBefore(part, node, paragraph)) : xml.map((paragraph) => patches.insertAfter(part, node, paragraph));
4667
+ const table = side === "before" && record.kind === "paragraph" ? firstCellParagraph(record) : void 0;
4668
+ const remappedIds = table ? tableRemap(table, createdIds[0].slice(2)) : void 0;
4669
+ return commit(context, items, {
4670
+ createdIds,
4671
+ reflowFrom: record.id,
4672
+ ...remappedIds ? { remappedIds } : {}
4673
+ });
4674
+ }
4675
+ };
4676
+ function bodyTailPatch(context, remaining) {
4677
+ const { model } = context;
4678
+ const last = remaining.at(-1);
4679
+ if (last && last.kind === "paragraph") return void 0;
4680
+ const id = context.freshParagraphId();
4681
+ const xml = paragraphXml(void 0, id, "", "");
4682
+ const part = model.document;
4683
+ const patch = last ? patches.insertAfter(part, last.node, xml) : model.bodySectPr ? patches.insertBefore(part, model.bodySectPr, xml) : patches.appendChild(part, model.body, xml);
4684
+ return { patch, id };
4685
+ }
4686
+ function otherParagraphsIn(model, record) {
4687
+ if (record.table) {
4688
+ for (const row of record.table.rows)
4689
+ for (const cell of row)
4690
+ if (cell.paragraphs.includes(record))
4691
+ return cell.paragraphs.some((candidate) => candidate !== record);
4692
+ return false;
4693
+ }
4694
+ return model.blocks.some(
4695
+ (block) => block.kind === "paragraph" && block !== record
4696
+ );
4697
+ }
4698
+ var deleteElementHandler = {
4699
+ async validate(operation3, context, issue) {
4700
+ const record = context.model.byId.get(operation3.target);
4701
+ if (!record || record.kind === "other") {
4348
4702
  issue(
4349
- "/text",
4350
- "invalid-range",
4351
- "A paragraph break cannot be inserted inside a hyperlink or content control"
4703
+ "/target",
4704
+ record ? "invalid-target" : "unknown-target",
4705
+ record ? `Element ${operation3.target} cannot be deleted` : `No element ${operation3.target}`
4352
4706
  );
4707
+ return;
4708
+ }
4709
+ if (context.tracked && record.kind !== "paragraph") {
4710
+ unsupportedTracked(issue, "/target", "Deleting a table or a picture");
4711
+ return;
4712
+ }
4713
+ if (record.kind === "paragraph") {
4714
+ if (context.tracked && !record.readOnlyReason && trackedRangeProblem(
4715
+ record,
4716
+ 0,
4717
+ record.text.text.length,
4718
+ issue,
4719
+ "/target"
4720
+ ))
4721
+ return;
4722
+ if (record.sectPr)
4723
+ issue(
4724
+ "/target",
4725
+ "section-break",
4726
+ "A paragraph that ends a section cannot be deleted"
4727
+ );
4728
+ else if (record.readOnlyReason)
4729
+ issue(
4730
+ "/target",
4731
+ "invalid-target",
4732
+ `Paragraph ${operation3.target} is read-only (${record.readOnlyReason})`
4733
+ );
4734
+ else if (!otherParagraphsIn(context.model, record))
4735
+ issue(
4736
+ "/target",
4737
+ "last-paragraph",
4738
+ "The last paragraph of a body or a cell cannot be deleted"
4739
+ );
4740
+ }
4353
4741
  },
4354
4742
  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, {
4743
+ const { model } = context;
4744
+ const found = model.byId.get(operation3.target);
4745
+ const part = model.document;
4746
+ if (found.kind !== "paragraph" && found.kind !== "table")
4747
+ return deleteInline(context, found);
4748
+ const record = found;
4749
+ const node = blockNode(record);
4750
+ if (context.tracked && record.kind === "paragraph") {
4751
+ const nested = paragraphIdsUnder(model, node).filter(
4752
+ (id) => id !== record.id
4753
+ );
4754
+ return commit(
4755
+ context,
4756
+ [
4757
+ patches.replaceElement(
4758
+ part,
4759
+ node,
4760
+ deletedParagraphXml(context, record)
4761
+ )
4762
+ ],
4763
+ {
4764
+ createdIds: [],
4765
+ removedIds: record.inlines.map((inline) => inline.elementId),
4766
+ ...nested.length > 0 ? { removedParagraphIds: nested } : {},
4767
+ ...model.unauthoredSet.has(record.id) ? { stamped: [record.id] } : {},
4768
+ reflowFrom: record.id
4769
+ }
4770
+ );
4771
+ }
4772
+ const items = [patches.removeElement(part, node)];
4773
+ const createdIds = [];
4774
+ if (model.blocks.includes(record)) {
4775
+ const tail = bodyTailPatch(
4776
+ context,
4777
+ model.blocks.filter((block) => block !== record)
4778
+ );
4779
+ if (tail) {
4780
+ createdIds.push(`p:${tail.id}`);
4781
+ items.push(tail.patch);
4782
+ }
4783
+ }
4784
+ const removedParagraphIds = paragraphIdsUnder(model, node);
4785
+ const table = record.kind === "paragraph" ? firstCellParagraph(record) : void 0;
4786
+ const successor = table?.rows[0]?.[0]?.paragraphs[1];
4787
+ const remappedIds = table && successor ? tableRemap(table, successor.id) : void 0;
4788
+ return commit(context, items, {
4373
4789
  createdIds,
4374
- ...removedIds.length > 0 ? { removedIds } : {},
4375
- ...removedParagraphIds.length > 0 ? { removedParagraphIds } : {}
4790
+ removedIds: elementIdsUnder(model, node),
4791
+ removedParagraphIds,
4792
+ reflowFrom: record.id,
4793
+ ...remappedIds ? { remappedIds } : {}
4376
4794
  });
4377
4795
  }
4378
4796
  };
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
- )
4797
+ async function deleteInline(context, record) {
4798
+ const { model } = context;
4799
+ const part = model.document;
4800
+ const transaction = context.pkg.transaction();
4801
+ const blip = part.find("blip", record.node);
4802
+ const rId = blip ? part.attribute(blip, "r:embed") ?? part.attribute(blip, "r:link") : void 0;
4803
+ if (rId) {
4804
+ const uses = part.findAll("blip").filter(
4805
+ (candidate) => candidate !== blip && (part.attribute(candidate, "r:embed") === rId || part.attribute(candidate, "r:link") === rId)
4417
4806
  );
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
4807
+ const elsewhere = new RegExp(
4808
+ `r:(?:id|embed|link|pict)="${rId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`
4809
+ );
4810
+ const others = uses.length > 0 || elsewhere.test(stripNode(part, record.node));
4811
+ if (!others) transaction.removeRelationship(model.mainPart, rId);
4812
+ }
4813
+ const removedParagraphIds = paragraphIdsUnder(model, record.node);
4814
+ return commit(
4815
+ context,
4816
+ [patches.removeElement(part, record.node)],
4817
+ {
4818
+ createdIds: [],
4819
+ removedIds: [record.elementId],
4820
+ ...removedParagraphIds.length > 0 ? { removedParagraphIds } : {},
4821
+ reflowFrom: record.paragraph.id
4822
+ },
4823
+ transaction
4445
4824
  );
4446
- return sliceOf(part, rPr);
4447
4825
  }
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>`;
4826
+ function stripNode(part, node) {
4827
+ return part.text.slice(0, node.start) + part.text.slice(node.end);
4451
4828
  }
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;
4829
+ var moveElementHandler = {
4830
+ async validate(operation3, context, issue) {
4831
+ if (context.tracked) {
4832
+ unsupportedTracked(issue, "", "Moving an element");
4833
+ return;
4461
4834
  }
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)
4835
+ const record = context.model.byId.get(operation3.target);
4836
+ if (!record || record.kind !== "paragraph" && record.kind !== "table") {
4837
+ issue(
4838
+ "/target",
4839
+ record ? "invalid-target" : "unknown-target",
4840
+ record ? `Element ${operation3.target} is not a paragraph or a table` : `No element ${operation3.target}`
4489
4841
  );
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;
4842
+ return;
4508
4843
  }
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}`);
4844
+ if (record.kind === "paragraph" && record.sectPr) {
4845
+ issue(
4846
+ "/target",
4847
+ "section-break",
4848
+ "A paragraph that ends a section cannot move"
4849
+ );
4850
+ return;
4521
4851
  }
4522
- if (style.fontFamily !== void 0) {
4523
- const problem = attributeProblem(style.fontFamily);
4524
- if (problem)
4525
- issue("/style/fontFamily", "invalid-value", `Font: ${problem}`);
4852
+ if (record.kind === "paragraph" && record.readOnlyReason) {
4853
+ issue(
4854
+ "/target",
4855
+ "invalid-target",
4856
+ `Paragraph ${operation3.target} is read-only (${record.readOnlyReason})`
4857
+ );
4858
+ return;
4526
4859
  }
4527
- const target = await textTarget(operation3.target, context, issue);
4528
- if (target)
4529
- checkRange(target, operation3.target, operation3.range, issue, false);
4860
+ const reference = placement2(operation3, context, issue);
4861
+ if (!reference) return;
4862
+ if (reference.record === record) {
4863
+ issue(
4864
+ `/${reference.side}`,
4865
+ "invalid-target",
4866
+ "An element cannot move next to itself"
4867
+ );
4868
+ return;
4869
+ }
4870
+ if (blockNode(reference.record).parent !== blockNode(record).parent)
4871
+ issue(
4872
+ `/${reference.side}`,
4873
+ "invalid-target",
4874
+ "The reference must be in the same body or cell as the element"
4875
+ );
4530
4876
  },
4531
4877
  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,
4878
+ const { model } = context;
4879
+ const record = model.byId.get(operation3.target);
4880
+ const { record: reference, side } = placement2(
4881
+ operation3,
4882
+ context,
4539
4883
  () => {
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
4884
+ }
4553
4885
  );
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
- ]);
4886
+ const part = model.document;
4887
+ const node = blockNode(record);
4888
+ const target = blockNode(reference);
4889
+ const { xml, stamped } = stampedSlice(model, node);
4890
+ const items = [
4891
+ patches.removeElement(part, node),
4892
+ side === "before" ? patches.insertBefore(part, target, xml) : patches.insertAfter(part, target, xml)
4893
+ ];
4894
+ const createdIds = [];
4895
+ if (model.blocks.includes(record) && model.blocks.includes(reference)) {
4896
+ const without = model.blocks.filter((block) => block !== record);
4897
+ const at = without.indexOf(reference);
4898
+ without.splice(side === "before" ? at : at + 1, 0, record);
4899
+ const tail = bodyTailPatch(context, without);
4900
+ if (tail) {
4901
+ createdIds.push(`p:${tail.id}`);
4902
+ items.push(tail.patch);
4903
+ }
4904
+ }
4905
+ const order = (candidate) => model.records.indexOf(candidate);
4906
+ const first = order(record) < order(reference) ? record : reference;
4907
+ let remappedIds;
4908
+ if (record.kind === "paragraph" && record.table) {
4909
+ const cell = record.table.rows[0]?.[0];
4910
+ if (cell && cell.paragraphs.includes(record)) {
4911
+ const without = cell.paragraphs.filter((p) => p !== record);
4912
+ const at = without.indexOf(reference);
4913
+ const reordered = [...without];
4914
+ if (at >= 0)
4915
+ reordered.splice(side === "before" ? at : at + 1, 0, record);
4916
+ const head = reordered[0];
4917
+ if (head) remappedIds = tableRemap(record.table, head.id);
4918
+ }
4919
+ }
4920
+ return commit(context, items, {
4921
+ createdIds,
4922
+ stamped,
4923
+ reflowFrom: first.id,
4924
+ ...remappedIds ? { remappedIds } : {}
4925
+ });
4567
4926
  }
4568
4927
  };
4569
- var setParagraphStyleHandler = {
4928
+ async function existingMedia(context, bytes, mimeType) {
4929
+ const extension = `.${extensionForMime(mimeType)}`;
4930
+ for (const name of context.pkg.currentPartNames) {
4931
+ if (!name.startsWith(`${MEDIA_FOLDER}/`) || !name.endsWith(extension))
4932
+ continue;
4933
+ const entry = context.pkg.entry(name);
4934
+ if (entry && entry.uncompressedSize !== bytes.length) continue;
4935
+ const candidate = await context.pkg.part(name);
4936
+ if (candidate.length !== bytes.length) continue;
4937
+ let same = true;
4938
+ for (let index = 0; index < bytes.length; index += 1)
4939
+ if (candidate[index] !== bytes[index]) {
4940
+ same = false;
4941
+ break;
4942
+ }
4943
+ if (same) return name;
4944
+ }
4945
+ return void 0;
4946
+ }
4947
+ function matchesMime(bytes, mimeType) {
4948
+ if (mimeType === "image/png")
4949
+ return bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
4950
+ return bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255;
4951
+ }
4952
+ function nextDrawingId(part) {
4953
+ let max = 0;
4954
+ for (const node of part.findAll("docPr")) {
4955
+ if (node.namespace !== WP_NS) continue;
4956
+ const id = Number(part.attribute(node, "id") ?? NaN);
4957
+ if (Number.isInteger(id)) max = Math.max(max, id);
4958
+ }
4959
+ return max + 1;
4960
+ }
4961
+ function drawingNamespacePatches(part) {
4962
+ const root = part.root;
4963
+ const has = (name) => root.attributes.some((attribute) => attribute.name === name);
4964
+ const items = [];
4965
+ if (!has("xmlns:wp"))
4966
+ items.push(patches.setAttribute(part, root, "xmlns:wp", WP_NS));
4967
+ if (!has("xmlns:r"))
4968
+ items.push(patches.setAttribute(part, root, "xmlns:r", R_NS));
4969
+ return items;
4970
+ }
4971
+ var insertImageHandler = {
4570
4972
  async validate(operation3, context, issue) {
4571
- await textTarget(operation3.target, context, issue);
4973
+ if (context.tracked) {
4974
+ unsupportedTracked(issue, "", "Inserting a picture");
4975
+ return;
4976
+ }
4977
+ placement2(operation3, context, issue);
4978
+ try {
4979
+ const bytes = resolveBinary(operation3.data, context.assets);
4980
+ if (bytes.length === 0)
4981
+ issue("/data", "invalid-value", "The image has no bytes");
4982
+ else if (!matchesMime(bytes, operation3.mimeType))
4983
+ issue(
4984
+ "/data",
4985
+ "invalid-value",
4986
+ `The bytes are not a ${operation3.mimeType} image`
4987
+ );
4988
+ } catch {
4989
+ issue(
4990
+ "/data",
4991
+ "unknown-asset",
4992
+ "The asset is not known to the session"
4993
+ );
4994
+ }
4572
4995
  },
4573
4996
  async apply(operation3, context) {
4574
- const target = await textTarget(operation3.target, context, () => {
4997
+ const { model } = context;
4998
+ const { record, side } = placement2(operation3, context, () => {
4575
4999
  });
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
5000
+ const part = model.document;
5001
+ const bytes = resolveBinary(operation3.data, context.assets);
5002
+ const transaction = context.pkg.transaction();
5003
+ const existing = await existingMedia(context, bytes, operation3.mimeType);
5004
+ const rId = existing ? await transaction.addRelationship(
5005
+ model.mainPart,
5006
+ IMAGE_RELATIONSHIP_TYPE,
5007
+ relativeTarget(model.mainPart, existing)
5008
+ ) : (await transaction.addMedia(
5009
+ model.mainPart,
5010
+ MEDIA_FOLDER,
5011
+ bytes,
5012
+ operation3.mimeType
5013
+ )).rId;
5014
+ const cx = Math.round(operation3.size.width * EMU_PER_PT);
5015
+ const cy = Math.round(operation3.size.height * EMU_PER_PT);
5016
+ const drawingId = nextDrawingId(part);
5017
+ const id = context.freshParagraphId();
5018
+ 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>`;
5019
+ const xml = paragraphXml(void 0, id, "", drawing);
5020
+ const node = blockNode(record);
5021
+ const items = [
5022
+ ...drawingNamespacePatches(part),
5023
+ side === "before" ? patches.insertBefore(part, node, xml) : patches.insertAfter(part, node, xml)
5024
+ ];
5025
+ return commit(
5026
+ context,
5027
+ items,
5028
+ { createdIds: [`p:${id}`, `img:${id}.0`], reflowFrom: record.id },
5029
+ transaction
4585
5030
  );
4586
- return commitParagraph(context, target, [
4587
- patches.replaceElement(
4588
- part,
4589
- node,
4590
- paragraphXml(node, record.id, pPr, content)
4591
- )
4592
- ]);
4593
5031
  }
4594
5032
  };
4595
5033
 
@@ -4685,6 +5123,10 @@ function commit2(context, items, result) {
4685
5123
  }
4686
5124
  var insertTableHandler = {
4687
5125
  async validate(operation3, context, issue) {
5126
+ if (context.tracked) {
5127
+ unsupportedTracked(issue, "", "Inserting a table");
5128
+ return;
5129
+ }
4688
5130
  placement3(operation3, context, issue);
4689
5131
  const columns = operation3.rows[0]?.length ?? 0;
4690
5132
  if (operation3.rows.some((row) => row.length !== columns))
@@ -4789,6 +5231,17 @@ var setTableCellHandler = {
4789
5231
  "invalid-target",
4790
5232
  `The cell's paragraph is read-only (${first.readOnlyReason})`
4791
5233
  );
5234
+ else if (context.tracked) {
5235
+ for (const paragraph of cell.paragraphs)
5236
+ if (paragraph.readOnlyReason || trackedRangeProblem(
5237
+ paragraph,
5238
+ 0,
5239
+ paragraph.text.text.length,
5240
+ issue,
5241
+ "/target"
5242
+ ))
5243
+ return;
5244
+ }
4792
5245
  },
4793
5246
  async apply(operation3, context) {
4794
5247
  const { model } = context;
@@ -4807,13 +5260,28 @@ var setTableCellHandler = {
4807
5260
  const items = [...replaced.items];
4808
5261
  const removedIds = [...replaced.removedIds];
4809
5262
  const removedParagraphIds = [...replaced.removedParagraphIds];
5263
+ const stamped = model.unauthoredSet.has(first.id) ? [first.id] : [];
4810
5264
  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);
5265
+ if (context.tracked) {
5266
+ if (model.unauthoredSet.has(paragraph.id)) stamped.push(paragraph.id);
5267
+ items.push(
5268
+ patches.replaceElement(
5269
+ part,
5270
+ paragraph.node,
5271
+ deletedParagraphXml(context, paragraph)
5272
+ )
5273
+ );
5274
+ removedIds.push(
5275
+ ...paragraph.inlines.map((inline) => inline.elementId)
5276
+ );
5277
+ } else {
5278
+ items.push(patches.removeElement(part, paragraph.node));
5279
+ removedIds.push(
5280
+ paragraph.elementId,
5281
+ ...paragraph.inlines.map((inline) => inline.elementId)
5282
+ );
5283
+ removedParagraphIds.push(paragraph.id);
5284
+ }
4817
5285
  for (const nested of part.findAll("p", paragraph.node)) {
4818
5286
  const id = model.paragraphIds.get(nested);
4819
5287
  if (id && nested !== paragraph.node) removedParagraphIds.push(id);
@@ -4823,7 +5291,7 @@ var setTableCellHandler = {
4823
5291
  createdIds: replaced.createdIds,
4824
5292
  ...removedIds.length > 0 ? { removedIds } : {},
4825
5293
  ...removedParagraphIds.length > 0 ? { removedParagraphIds } : {},
4826
- ...model.unauthoredSet.has(first.id) ? { stamped: [first.id] } : {},
5294
+ ...stamped.length > 0 ? { stamped } : {},
4827
5295
  reflowFrom: record.id
4828
5296
  });
4829
5297
  }
@@ -4860,6 +5328,44 @@ function unauthoredIdsOf(bytes) {
4860
5328
  const text = new TextDecoder().decode(bytes);
4861
5329
  return [...text.matchAll(/<p id="([0-9A-F]{8})"\/>/g)].map((m) => m[1]);
4862
5330
  }
5331
+ function trackedOf(mode) {
5332
+ if (mode.changeMode !== "tracked") return void 0;
5333
+ return {
5334
+ author: mode.author ?? "",
5335
+ ...mode.timestamp === void 0 ? {} : { date: mode.timestamp }
5336
+ };
5337
+ }
5338
+ var DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
5339
+ function trackedModeIssues(tracked) {
5340
+ if (!tracked) return [];
5341
+ const issues = [];
5342
+ const author = attributeProblem(tracked.author);
5343
+ if (tracked.author.trim().length === 0)
5344
+ issues.push({
5345
+ operationIndex: -1,
5346
+ path: "/author",
5347
+ code: "required",
5348
+ message: "Tracked changes name their author; pass ApplyOptions.author"
5349
+ });
5350
+ else if (author)
5351
+ issues.push({
5352
+ operationIndex: -1,
5353
+ path: "/author",
5354
+ code: "invalid-value",
5355
+ message: `The author holds ${author}`
5356
+ });
5357
+ if (tracked.date !== void 0) {
5358
+ const date = attributeProblem(tracked.date);
5359
+ if (date || !DATE_TIME.test(tracked.date) || Number.isNaN(Date.parse(tracked.date)))
5360
+ issues.push({
5361
+ operationIndex: -1,
5362
+ path: "/timestamp",
5363
+ code: "invalid-value",
5364
+ message: date ? `The timestamp holds ${date}` : "The timestamp must be an ISO 8601 date-time"
5365
+ });
5366
+ }
5367
+ return issues;
5368
+ }
4863
5369
  var DocxEditEngine = class _DocxEditEngine {
4864
5370
  schemas = docxOperationSchemas;
4865
5371
  #original;
@@ -4922,9 +5428,12 @@ var DocxEditEngine = class _DocxEditEngine {
4922
5428
  });
4923
5429
  return pending;
4924
5430
  }
4925
- async validate(operations, signal) {
5431
+ async validate(operations, signal, mode = {}) {
4926
5432
  const issues = [];
4927
- const base = await this.#context(0, signal);
5433
+ const tracked = trackedOf(mode);
5434
+ const modeIssues = trackedModeIssues(tracked);
5435
+ if (modeIssues.length > 0) return modeIssues;
5436
+ const base = await this.#context(0, signal, 0, /* @__PURE__ */ new Set(), tracked);
4928
5437
  for (const [index, operation3] of operations.entries()) {
4929
5438
  const context = { ...base, operationIndex: index };
4930
5439
  const handler = docxHandlers.get(operation3.op);
@@ -4967,6 +5476,9 @@ var DocxEditEngine = class _DocxEditEngine {
4967
5476
  async apply(input, signal) {
4968
5477
  const batch = Array.isArray(input) ? { stateId: this.#nextStateId(), operations: input } : input;
4969
5478
  this.#stateId = Math.max(this.#stateId, batch.stateId);
5479
+ const tracked = trackedOf(batch);
5480
+ const modeIssues = trackedModeIssues(tracked);
5481
+ if (modeIssues.length > 0) throw invalidOperationError(modeIssues);
4970
5482
  const snapshot = this.#pkg.snapshot();
4971
5483
  const unauthored = this.#unauthored ? [...this.#unauthored] : void 0;
4972
5484
  const createdIds = [];
@@ -4990,7 +5502,8 @@ var DocxEditEngine = class _DocxEditEngine {
4990
5502
  batch.stateId,
4991
5503
  signal,
4992
5504
  index,
4993
- issued
5505
+ issued,
5506
+ tracked
4994
5507
  );
4995
5508
  const issues = [];
4996
5509
  await handler.validate(
@@ -5042,10 +5555,11 @@ var DocxEditEngine = class _DocxEditEngine {
5042
5555
  this.#stateId += 1;
5043
5556
  return this.#stateId;
5044
5557
  }
5045
- async #context(stateId, signal, operationIndex = 0, issued = /* @__PURE__ */ new Set()) {
5558
+ async #context(stateId, signal, operationIndex = 0, issued = /* @__PURE__ */ new Set(), tracked) {
5046
5559
  const model = await this.model(signal);
5047
5560
  const taken = /* @__PURE__ */ new Set([...model.takenIds, ...issued]);
5048
5561
  let count = 0;
5562
+ let revisions = 0;
5049
5563
  return {
5050
5564
  pkg: this.#pkg,
5051
5565
  model,
@@ -5059,9 +5573,21 @@ var DocxEditEngine = class _DocxEditEngine {
5059
5573
  issued.add(id);
5060
5574
  taken.add(id);
5061
5575
  return id;
5576
+ },
5577
+ ...tracked ? { tracked } : {},
5578
+ nextRevisionId: () => {
5579
+ revisions += 1;
5580
+ return model.maxRevisionId + revisions;
5062
5581
  }
5063
5582
  };
5064
5583
  }
5584
+ /** The revisions of a paragraph; none for another element or an unknown id. */
5585
+ async revisions(id, signal) {
5586
+ const model = await this.model(signal);
5587
+ const record = model.byId.get(id);
5588
+ if (!record || record.kind !== "paragraph") return [];
5589
+ return revisionsOf(model.document, record.node);
5590
+ }
5065
5591
  async materialize(purposeOrSignal = "show", options = {}, signal = new AbortController().signal) {
5066
5592
  return (await this.materializeDocument(purposeOrSignal, options, signal)).bytes;
5067
5593
  }
@@ -8497,6 +9023,16 @@ function createOoxmlEditHandler() {
8497
9023
  );
8498
9024
  return state;
8499
9025
  };
9026
+ const docx = () => {
9027
+ const current = engine();
9028
+ if (!(current instanceof DocxEditEngine))
9029
+ throw new ViewerError(
9030
+ "edit-unsupported",
9031
+ "The open document is not a Word document",
9032
+ { details: { format: "pptx", reason: "no-reads" } }
9033
+ );
9034
+ return current;
9035
+ };
8500
9036
  const pptx = () => {
8501
9037
  const current = engine();
8502
9038
  if (!(current instanceof PptxEditEngine))
@@ -8529,8 +9065,8 @@ function createOoxmlEditHandler() {
8529
9065
  return result;
8530
9066
  }
8531
9067
  case "edit-validate": {
8532
- const { operations } = payload;
8533
- return engine().validate(operations, signal);
9068
+ const { operations, mode } = payload;
9069
+ return engine().validate(operations, signal, mode);
8534
9070
  }
8535
9071
  case "edit-apply": {
8536
9072
  const { batch } = payload;
@@ -8581,6 +9117,11 @@ function createOoxmlEditHandler() {
8581
9117
  return pptx().slides(signal);
8582
9118
  case "edit-pptx-layouts":
8583
9119
  return pptx().layouts(signal);
9120
+ case "edit-docx-revisions":
9121
+ return docx().revisions(
9122
+ payload.id,
9123
+ signal
9124
+ );
8584
9125
  case "edit-dispose":
8585
9126
  await state?.dispose();
8586
9127
  state = void 0;