@pixerate/editor 0.24.0 → 0.25.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.
- package/README.md +71 -1
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/agent/index.cjs +10 -9
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +1 -1
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +4 -3
- package/dist/agent/index.js.map +1 -1
- package/dist/canvas/index.cjs +3173 -340
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.d.cts +617 -4
- package/dist/canvas/index.d.ts +617 -4
- package/dist/canvas/index.js +3105 -272
- package/dist/canvas/index.js.map +1 -1
- package/dist/chunk-4BVP64JJ.cjs +1406 -0
- package/dist/chunk-4BVP64JJ.cjs.map +1 -0
- package/dist/{chunk-XNORY7M6.cjs → chunk-7WISYZFO.cjs} +105 -50
- package/dist/chunk-7WISYZFO.cjs.map +1 -0
- package/dist/{chunk-OLZJXL4R.cjs → chunk-AL6JMGWW.cjs} +59 -25
- package/dist/chunk-AL6JMGWW.cjs.map +1 -0
- package/dist/chunk-BVP3TSLH.cjs +69 -0
- package/dist/chunk-BVP3TSLH.cjs.map +1 -0
- package/dist/{chunk-MCSJFWAO.cjs → chunk-C7P22VIP.cjs} +670 -96
- package/dist/chunk-C7P22VIP.cjs.map +1 -0
- package/dist/{chunk-PGNZKFQS.js → chunk-ECCCYKGL.js} +76 -21
- package/dist/chunk-ECCCYKGL.js.map +1 -0
- package/dist/{chunk-3CN2WD3X.js → chunk-FH6F67DN.js} +646 -72
- package/dist/chunk-FH6F67DN.js.map +1 -0
- package/dist/chunk-HRMEDT2L.js +1406 -0
- package/dist/chunk-HRMEDT2L.js.map +1 -0
- package/dist/{chunk-ITBSKI26.js → chunk-IXHSYOVY.js} +941 -9
- package/dist/chunk-IXHSYOVY.js.map +1 -0
- package/dist/{chunk-FODNLX5N.js → chunk-NAXVVS5I.js} +38 -4
- package/dist/chunk-NAXVVS5I.js.map +1 -0
- package/dist/chunk-O5RYH2PG.js +69 -0
- package/dist/chunk-O5RYH2PG.js.map +1 -0
- package/dist/{chunk-SBCZ6RN5.cjs → chunk-PO6A6QQH.cjs} +1039 -107
- package/dist/chunk-PO6A6QQH.cjs.map +1 -0
- package/dist/{chunk-3LV6RRPH.js → chunk-QGYDWPRM.js} +2 -2
- package/dist/{chunk-U5KWZU4X.js → chunk-RR7F5GUM.js} +2632 -129
- package/dist/chunk-RR7F5GUM.js.map +1 -0
- package/dist/{chunk-5S3Z4PPF.js → chunk-UAKDG62Z.js} +232 -25
- package/dist/chunk-UAKDG62Z.js.map +1 -0
- package/dist/{chunk-SML64VJG.cjs → chunk-V3CHHWOE.cjs} +4 -4
- package/dist/{chunk-SML64VJG.cjs.map → chunk-V3CHHWOE.cjs.map} +1 -1
- package/dist/chunk-WSQKHXW7.cjs +5726 -0
- package/dist/chunk-WSQKHXW7.cjs.map +1 -0
- package/dist/{chunk-D27YFC46.cjs → chunk-X4N4WUKB.cjs} +267 -60
- package/dist/chunk-X4N4WUKB.cjs.map +1 -0
- package/dist/commands/index.cjs +74 -1
- package/dist/commands/index.cjs.map +1 -1
- package/dist/commands/index.d.cts +34 -1
- package/dist/commands/index.d.ts +34 -1
- package/dist/commands/index.js +73 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/compare/index.cjs +3 -3
- package/dist/compare/index.d.cts +1 -1
- package/dist/compare/index.d.ts +1 -1
- package/dist/compare/index.js +2 -2
- package/dist/controller-BIJiNoVs.d.cts +1842 -0
- package/dist/controller-BIJiNoVs.d.ts +1842 -0
- package/dist/{documents-BQjzJNe2.d.ts → documents-CHT1U5Vs.d.ts} +10 -0
- package/dist/{documents-KRTPhvst.d.cts → documents-Gp7wb0Me.d.cts} +10 -0
- package/dist/extensions/index.cjs +53 -6
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +12 -4
- package/dist/extensions/index.d.ts +12 -4
- package/dist/extensions/index.js +53 -6
- package/dist/generation/index.cjs +1013 -69
- package/dist/generation/index.cjs.map +1 -1
- package/dist/generation/index.d.cts +474 -13
- package/dist/generation/index.d.ts +474 -13
- package/dist/generation/index.js +976 -32
- package/dist/generation/index.js.map +1 -1
- package/dist/image-editor/index.cjs +3385 -1275
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +693 -128
- package/dist/image-editor/index.d.ts +693 -128
- package/dist/image-editor/index.js +3325 -1215
- package/dist/image-editor/index.js.map +1 -1
- package/dist/{index-fJrlkJCd.d.ts → index-7a6bHTLy.d.ts} +494 -5
- package/dist/{index-BhJBSm2d.d.cts → index-Csd5qI8u.d.cts} +494 -5
- package/dist/index.cjs +1491 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +110 -19
- package/dist/index.d.ts +110 -19
- package/dist/index.js +1464 -18
- package/dist/index.js.map +1 -1
- package/dist/mention-text-CDnCFd3c.d.cts +53 -0
- package/dist/mention-text-CDnCFd3c.d.ts +53 -0
- package/dist/model-C1zApQM8.d.cts +219 -0
- package/dist/model-C1zApQM8.d.ts +219 -0
- package/dist/persistence/index.d.cts +3 -120
- package/dist/persistence/index.d.ts +3 -120
- package/dist/{registry-GJI4gToU.d.cts → registry-CEgwXED1.d.cts} +12 -1
- package/dist/{registry-GJI4gToU.d.ts → registry-CEgwXED1.d.ts} +12 -1
- package/dist/serializers/index.cjs +12 -4
- package/dist/serializers/index.cjs.map +1 -1
- package/dist/serializers/index.d.cts +39 -12
- package/dist/serializers/index.d.ts +39 -12
- package/dist/serializers/index.js +12 -4
- package/dist/spreadsheet/index.cjs +127 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.d.cts +431 -11
- package/dist/spreadsheet/index.d.ts +431 -11
- package/dist/spreadsheet/index.js +127 -3
- package/dist/storage-BpFfspHm.d.cts +120 -0
- package/dist/storage-BpFfspHm.d.ts +120 -0
- package/dist/tokens/index.cjs +115 -0
- package/dist/tokens/index.cjs.map +1 -0
- package/dist/tokens/index.d.cts +212 -0
- package/dist/tokens/index.d.ts +212 -0
- package/dist/tokens/index.js +115 -0
- package/dist/tokens/index.js.map +1 -0
- package/dist/{track-changes-BK5sVf4_.d.cts → track-changes-D_VAPELi.d.cts} +95 -11
- package/dist/{track-changes-CDjCrwKe.d.ts → track-changes-Db_fGEvx.d.ts} +95 -11
- package/package.json +6 -1
- package/dist/chunk-3CN2WD3X.js.map +0 -1
- package/dist/chunk-5S3Z4PPF.js.map +0 -1
- package/dist/chunk-D27YFC46.cjs.map +0 -1
- package/dist/chunk-FODNLX5N.js.map +0 -1
- package/dist/chunk-ITBSKI26.js.map +0 -1
- package/dist/chunk-MA2F5KHF.cjs +0 -3223
- package/dist/chunk-MA2F5KHF.cjs.map +0 -1
- package/dist/chunk-MCSJFWAO.cjs.map +0 -1
- package/dist/chunk-OLZJXL4R.cjs.map +0 -1
- package/dist/chunk-PGNZKFQS.js.map +0 -1
- package/dist/chunk-SBCZ6RN5.cjs.map +0 -1
- package/dist/chunk-U5KWZU4X.js.map +0 -1
- package/dist/chunk-XNORY7M6.cjs.map +0 -1
- package/dist/controller-BhLLAoi8.d.cts +0 -1000
- package/dist/controller-BhLLAoi8.d.ts +0 -1000
- /package/dist/{chunk-3LV6RRPH.js.map → chunk-QGYDWPRM.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-X4N4WUKB.cjs","../src/extensions/comment-store.ts","../src/extensions/track-changes.ts"],"names":[],"mappings":"AAAA;ACgFA,IAAI,UAAA,EAAY,CAAA;AAKT,SAAS,cAAA,CAAe,OAAA,EAAS,GAAA,EAAa;AACnD,EAAA,UAAA,EAAA,CAAa,UAAA,EAAY,CAAA,EAAA,EAAK,GAAA;AAC9B,EAAA,OAAO,CAAA,EAAA;AAGT;AAES;AACH,EAAA;AACC,EAAA;AACE,EAAA;AACT;AAES;AACA,EAAA;AACD,IAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACM,IAAA;AACF,IAAA;AACJ,IAAA;AACA,IAAA;AACI,IAAA;AACA,IAAA;AACJ,IAAA;AACI,IAAA;AACN,EAAA;AACF;AAWa;AACH,iBAAA;AACA,kBAAA;AACA,EAAA;AACA,kBAAA;AAA0C;AAE1C,kBAAA;AACA,EAAA;AACA,EAAA;AAER,EAAA;AACO,IAAA;AACA,IAAA;AACD,IAAA;AACF,MAAA;AACF,IAAA;AACK,IAAA;AACP,EAAA;AAAA;AAGO,EAAA;AACC,IAAA;AACA,IAAA;AACD,IAAA;AACL,IAAA;AACF,EAAA;AAEA,kBAAA;AACO,IAAA;AACL,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AAEA,kBAAA;AAEA,EAAA;AACE,IAAA;AACF,EAAA;AAEA,EAAA;AACQ,IAAA;AACA,MAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACK,IAAA;AACA,IAAA;AACA,IAAA;AACL,IAAA;AACF,EAAA;AAAA;AAGA,EAAA;AACQ,IAAA;AACN,IAAA;AACM,MAAA;AACA,MAAA;AACJ,MAAA;AACE,QAAA;AACA,QAAA;AACF,MAAA;AACD,IAAA;AACH,EAAA;AAEA,EAAA;AAIQ,IAAA;AACA,MAAA;AACJ,MAAA;AACA,MAAA;AACA,MAAA;AACF,IAAA;AACM,IAAA;AACD,MAAA;AACH,MAAA;AAA6B;AAE7B,MAAA;AACA,MAAA;AACA,MAAA;AACA,IAAA;AACF,IAAA;AACF,EAAA;AAEA,EAAA;AACE,IAAA;AAAyB,MAAA;AAAW,MAAA;AAIpC,IAAA;AACF,EAAA;AAEA,EAAA;AACE,IAAA;AAAyB,MAAA;AAAK,MAAA;AAM9B,IAAA;AACF,EAAA;AAEA,EAAA;AACQ,IAAA;AACD,IAAA;AACA,IAAA;AACA,IAAA;AACD,IAAA;AACC,IAAA;AACL,IAAA;AACF,EAAA;AAAA;AAGA,EAAA;AACQ,IAAA;AACF,IAAA;AACA,IAAA;AACC,IAAA;AACD,IAAA;AACF,MAAA;AACF,IAAA;AACK,IAAA;AACL,IAAA;AACF,EAAA;AAEA,EAAA;AACQ,IAAA;AACF,IAAA;AACC,IAAA;AACA,IAAA;AACP,EAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOA,EAAA;AACM,IAAA;AACE,IAAA;AACN,IAAA;AACM,MAAA;AACF,QAAA;AACA,QAAA;AACF,MAAA;AACD,IAAA;AACK,IAAA;AACJ,MAAA;AACA,MAAA;AACI,MAAA;AACJ,MAAA;AAGA,MAAA;AAGD,IAAA;AACI,IAAA;AACA,IAAA;AACA,IAAA;AACL,IAAA;AACF,EAAA;AAAA;AAGK,EAAA;AACE,IAAA;AACA,IAAA;AACD,IAAA;AACF,MAAA;AACF,IAAA;AACK,IAAA;AACP,EAAA;AAEc,EAAA;AACP,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACP,EAAA;AAEA,EAAA;AACE,IAAA;AACE,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AAEQ,EAAA;AACA,IAAA;AACF,IAAA;AACE,IAAA;AACF,IAAA;AACC,IAAA;AACA,IAAA;AACL,IAAA;AACF,EAAA;AAEQ,EAAA;AACA,IAAA;AACA,MAAA;AACF,QAAA;AACF,MAAA;AACI,MAAA;AACA,MAAA;AACJ,MAAA;AACD,IAAA;AACD,IAAA;AACF,EAAA;AAEqB,EAAA;AACd,IAAA;AACC,IAAA;AACD,IAAA;AACC,MAAA;AACF,QAAA;AACF,MAAA;AACE,QAAA;AACF,MAAA;AACD,IAAA;AACH,EAAA;AACF;AAEgB;AACP,EAAA;AACT;AAMgB;AACR,EAAA;AACF,EAAA;AACE,EAAA;AACF,EAAA;AACE,EAAA;AACF,EAAA;AACG,EAAA;AACT;ADrIU;AACA;AE7OD;AACT;AACE;AACA;AACA;AAGK;AACP;AACE;AACQ;AACR;AACA;AAMK;AACP;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEK;AAIM;AACA;AAEA;AAEA;AAMA;AAOP;AACJ,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACD;AAMY;AA6KA;AACH,EAAA;AACA,kBAAA;AAER,EAAA;AACO,IAAA;AACP,EAAA;AAEA,kBAAA;AACO,IAAA;AACL,IAAA;AACA,IAAA;AACE,MAAA;AACF,IAAA;AACF,EAAA;AAEA,kBAAA;AAA+C;AAG3C,EAAA;AACG,IAAA;AACA,IAAA;AACC,MAAA;AACF,QAAA;AACF,MAAA;AACE,QAAA;AACF,MAAA;AACD,IAAA;AACH,EAAA;AACF;AAea;AA4BJ;AACA,EAAA;AACT;AAES;AACF,EAAA;AACD,EAAA;AACF,IAAA;AACM,EAAA;AACN,IAAA;AACF,EAAA;AACF;AAEM;AACJ,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AAEF,EAAA;AACA,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACF;AAEM;AACH,EAAA;AACC,IAAA;AACA,IAAA;AACA,IAAA;AAEF,EAAA;AACF;AASa;AAEA;AAOA;AACL,EAAA;AACN,EAAA;AACA,EAAA;AACE,IAAA;AACE,MAAA;AACE,QAAA;AAEF,MAAA;AACA,MAAA;AACF,IAAA;AACF,EAAA;AACA,EAAA;AAEE,IAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACE,IAAA;AACF,EAAA;AACA,EAAA;AACQ,IAAA;AAGN,IAAA;AACE,MAAA;AACA,MAAA;AACE,QAAA;AACD,MAAA;AACD,MAAA;AACF,IAAA;AACF,EAAA;AACD;AAOY;AACL,EAAA;AACN,EAAA;AACA,EAAA;AACE,IAAA;A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AA;AACA,EAAA;AACI,IAAA;AACF,IAAA;AACN,EAAA;AACQ,EAAA;AACV;AAEgB;AACR,EAAA;AACD,EAAA;AACE,EAAA;AACT;AAGgB;AACN,EAAA;AACV;AAMS;AACD,EAAA;AACL,EAAA;AACK,IAAA;AACE,MAAA;AACN,IAAA;AAEA,IAAA;AACD,EAAA;AACM,EAAA;AACT;AAES;AAKD,EAAA;AACA,EAAA;AACD,EAAA;AACC,EAAA;AACA,EAAA;AACE,EAAA;AAAS,IAAA;AACU,MAAA;AAErB,QAAA;AACA,QAAA;AACA,QAAA;AACE,UAAA;AACF,QAAA;AACA,QAAA;AACD,MAAA;AACH,IAAA;AACF,EAAA;AACA,EAAA;AACK,IAAA;AACC,IAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACD,IAAA;AACC,MAAA;AACL,IAAA;AACD,IAAA;AACD,EAAA;AACH;AAEM;AAMG;AAID,EAAA;AACA,EAAA;AACF,EAAA;AACC,EAAA;AACH,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAGS;AACD,EAAA;AACD,EAAA;AACG,EAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACJ,IAAA;AACA,IAAA;AACA,IAAA;AACF,EAAA;AACM,EAAA;AACA,EAAA;AACH,EAAA;AACG,IAAA;AACN,EAAA;AACO,EAAA;AACT;AAMS;AACD,EAAA;AACD,EAAA;AACG,EAAA;AACF,EAAA;AACF,EAAA;AACE,EAAA;AACF,EAAA;AACC,IAAA;AACA,IAAA;AACH,IAAA;AACF,EAAA;AACM,EAAA;AACA,EAAA;AACF,EAAA;AACC,IAAA;AACG,MAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACA,QAAA;AACF,MAAA;AACF,IAAA;AACA,IAAA;AACF,EAAA;AACM,EAAA;AACF,EAAA;AACI,EAAA;AACD,EAAA;AACT;AAGS;AAKD,EAAA;AAEA,EAAA;AACE,IAAA;AACA,IAAA;AACN,IAAA;AACA,IAAA;AACF,EAAA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AACE,YAAA;AACA,YAAA;AACA,YAAA;AACF,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACA,UAAA;AACE,YAAA;AAEA,YAAA;AACA,YAAA;AACE,cAAA;AAAyE,YAAA;AAG3E,YAAA;AACF,UAAA;AACA,UAAA;AAA2D,YAAA;AACzD,YAAA;AACa,YAAA;AACO,YAAA;AACgC,YAAA;AACF,YAAA;AAI5C,YAAA;AAC+B,YAAA;AACrC,YAAA;AACA,YAAA;AAEF,UAAA;AACA,UAAA;AACA,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AAAiB,YAAA;AAEb,cAAA;AACA,cAAA;AAAO,YAAA;AAEX,UAAA;AACF,QAAA;AACA,QAAA;AACE,UAAA;AACA,UAAA;AACA,UAAA;AAAO,YAAA;AAEH,cAAA;AAME,gBAAA;AAAmD,cAAA;AAErD,cAAA;AAA8C,YAAA;AAChD,YAAA;AAEE,cAAA;AACA,cAAA;AACA,cAAA;AAAuC,YAAA;AAE3C,UAAA;AACF,QAAA;AACD,MAAA;AACH,IAAA;AACF,EAAA;AACD;AF5iBS;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-X4N4WUKB.cjs","sourcesContent":[null,"/**\n * Who made a comment, reply or tracked change.\n */\nexport interface ReviewAuthor {\n id: string;\n name: string;\n color?: string;\n avatarUrl?: string;\n}\n\n/**\n * Where a comment thread is attached in the document. `from`/`to` are\n * ProseMirror positions and `quote` is the commented text at the time the\n * anchor was last synced. Positions are only meaningful for the current\n * document; the `comment` mark in the document is the source of truth.\n */\nexport interface CommentAnchor {\n from: number;\n to: number;\n quote: string;\n}\n\nexport interface CommentReply {\n id: string;\n author: ReviewAuthor;\n body: string;\n createdAt: number;\n editedAt?: number;\n}\n\nexport interface CommentThread {\n id: string;\n /** `null` while the thread has no text in the document (see `orphaned`). */\n anchor: CommentAnchor | null;\n author: ReviewAuthor;\n createdAt: number;\n /** An empty body marks a draft that has not been posted yet. */\n body: string;\n editedAt?: number;\n replies: CommentReply[];\n resolved: boolean;\n resolvedAt?: number;\n resolvedBy?: ReviewAuthor;\n /**\n * True when the commented text was deleted from the document. Undoing the\n * deletion re-attaches the thread.\n */\n orphaned: boolean;\n /** The quote the thread had before it was orphaned. */\n lastQuote?: string;\n}\n\nexport interface CommentStoreSnapshot {\n /** Threads in document order (anchored first), then orphaned threads. */\n threads: readonly CommentThread[];\n activeThreadId: string | null;\n}\n\n/** JSON shape produced by `CommentStore.toJSON()`. */\nexport interface SerializedComments {\n version: 1;\n threads: CommentThread[];\n}\n\nexport interface CommentStoreOptions {\n threads?: CommentThread[] | SerializedComments;\n generateId?: () => string;\n now?: () => number;\n}\n\nexport interface CreateThreadInput {\n id?: string;\n body: string;\n author: ReviewAuthor;\n anchor?: CommentAnchor | null;\n createdAt?: number;\n}\n\nexport type CommentStoreListener = (snapshot: CommentStoreSnapshot) => void;\n\nlet idCounter = 0;\n\n/**\n * Generates a reasonably unique id without depending on `crypto`.\n */\nexport function createReviewId(prefix = \"c\"): string {\n idCounter = (idCounter + 1) % 1e6;\n return `${prefix}-${Date.now().toString(36)}-${idCounter.toString(36)}-${Math.random()\n .toString(36)\n .slice(2, 8)}`;\n}\n\nfunction sameAnchor(a: CommentAnchor | null, b: CommentAnchor | null): boolean {\n if (a === b) return true;\n if (!a || !b) return false;\n return a.from === b.from && a.to === b.to && a.quote === b.quote;\n}\n\nfunction normalizeThread(raw: any): CommentThread {\n return {\n id: String(raw.id),\n anchor: raw.anchor ?? null,\n author: raw.author ?? { id: \"unknown\", name: \"Unknown\" },\n createdAt: Number(raw.createdAt ?? 0),\n body: String(raw.body ?? \"\"),\n ...(raw.editedAt !== undefined ? { editedAt: raw.editedAt } : {}),\n replies: Array.isArray(raw.replies) ? raw.replies.map((r: any) => ({ ...r })) : [],\n resolved: !!raw.resolved,\n ...(raw.resolvedAt !== undefined ? { resolvedAt: raw.resolvedAt } : {}),\n ...(raw.resolvedBy !== undefined ? { resolvedBy: raw.resolvedBy } : {}),\n orphaned: !!raw.orphaned,\n ...(raw.lastQuote !== undefined ? { lastQuote: String(raw.lastQuote) } : {}),\n };\n}\n\n/**\n * Headless, framework-agnostic store of comment threads.\n *\n * The store never talks to a server: persist `toJSON()` yourself and pass it\n * back via `new CommentStore({ threads })` or `load()`. Snapshots are\n * immutable and only replaced when something changes, so `getSnapshot` is safe\n * to use with React's `useSyncExternalStore`. `subscribe` calls the listener\n * immediately (Svelte store contract).\n */\nexport class CommentStore {\n private threads: CommentThread[] = [];\n private activeThreadId: string | null = null;\n private snapshot: CommentStoreSnapshot;\n private listeners = new Set<CommentStoreListener>();\n /** Deleted threads, kept so undoing a \"Delete Comment\" can restore them. */\n private deleted = new Map<string, CommentThread>();\n private generateId: () => string;\n private now: () => number;\n\n constructor(options: CommentStoreOptions = {}) {\n this.generateId = options.generateId ?? (() => createReviewId(\"comment\"));\n this.now = options.now ?? (() => Date.now());\n if (options.threads) {\n this.threads = CommentStore.parse(options.threads);\n }\n this.snapshot = this.computeSnapshot();\n }\n\n /** Parses `toJSON()` output (or a plain thread array). */\n static parse(json: string | CommentThread[] | SerializedComments): CommentThread[] {\n const value = typeof json === \"string\" ? JSON.parse(json) : json;\n const list = Array.isArray(value) ? value : value?.threads;\n if (!Array.isArray(list)) return [];\n return list.filter((t: any) => t && t.id != null).map(normalizeThread);\n }\n\n subscribe = (listener: CommentStoreListener): (() => void) => {\n this.listeners.add(listener);\n listener(this.snapshot);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = (): CommentStoreSnapshot => this.snapshot;\n\n getThread(id: string): CommentThread | undefined {\n return this.threads.find((t) => t.id === id);\n }\n\n createThread(input: CreateThreadInput): CommentThread {\n const thread: CommentThread = {\n id: input.id ?? this.generateId(),\n anchor: input.anchor ?? null,\n author: input.author,\n createdAt: input.createdAt ?? this.now(),\n body: input.body,\n replies: [],\n resolved: false,\n orphaned: false,\n };\n this.threads = [...this.threads.filter((t) => t.id !== thread.id), thread];\n this.deleted.delete(thread.id);\n this.emit();\n return thread;\n }\n\n /** Edits the thread's body, or one of its replies when `replyId` is given. */\n editComment(threadId: string, body: string, replyId?: string): boolean {\n const editedAt = this.now();\n return this.updateThread(threadId, (t) => {\n if (!replyId) return { ...t, body, editedAt };\n if (!t.replies.some((r) => r.id === replyId)) return t;\n return {\n ...t,\n replies: t.replies.map((r) => (r.id === replyId ? { ...r, body, editedAt } : r)),\n };\n });\n }\n\n addReply(\n threadId: string,\n input: { body: string; author: ReviewAuthor; id?: string },\n ): CommentReply | null {\n const reply: CommentReply = {\n id: input.id ?? this.generateId(),\n author: input.author,\n body: input.body,\n createdAt: this.now(),\n };\n const ok = this.updateThread(threadId, (t) => ({\n ...t,\n replies: [...t.replies, reply],\n // Replying to a resolved thread re-opens it, as in Word and Pages.\n resolved: false,\n resolvedAt: undefined,\n resolvedBy: undefined,\n }));\n return ok ? reply : null;\n }\n\n deleteReply(threadId: string, replyId: string): boolean {\n return this.updateThread(threadId, (t) =>\n t.replies.some((r) => r.id === replyId)\n ? { ...t, replies: t.replies.filter((r) => r.id !== replyId) }\n : t,\n );\n }\n\n resolveThread(id: string, resolved = true, by?: ReviewAuthor): boolean {\n return this.updateThread(id, (t) =>\n t.resolved === resolved\n ? t\n : resolved\n ? { ...t, resolved, resolvedAt: this.now(), ...(by ? { resolvedBy: by } : {}) }\n : { ...t, resolved, resolvedAt: undefined, resolvedBy: undefined },\n );\n }\n\n deleteThread(id: string): boolean {\n const thread = this.getThread(id);\n if (!thread) return false;\n this.deleted.set(id, thread);\n this.threads = this.threads.filter((t) => t.id !== id);\n if (this.activeThreadId === id) this.activeThreadId = null;\n this.emit();\n return true;\n }\n\n /** Deletes every thread whose text is no longer in the document. */\n removeOrphanedThreads(): string[] {\n const ids = this.threads.filter((t) => t.orphaned).map((t) => t.id);\n if (ids.length === 0) return ids;\n ids.forEach((id) => this.deleted.set(id, this.getThread(id)!));\n this.threads = this.threads.filter((t) => !t.orphaned);\n if (this.activeThreadId && ids.includes(this.activeThreadId)) {\n this.activeThreadId = null;\n }\n this.emit();\n return ids;\n }\n\n setActiveThread(id: string | null): void {\n const next = id && this.getThread(id) ? id : null;\n if (next === this.activeThreadId) return;\n this.activeThreadId = next;\n this.emit();\n }\n\n /**\n * Updates anchors from the document. Threads missing from `anchors` become\n * orphaned. Threads that were deleted but whose mark re-appeared (e.g. after\n * undoing \"Delete Comment\") are restored. Returns true if anything changed.\n */\n syncAnchors(anchors: Map<string, CommentAnchor>): boolean {\n let changed = false;\n const restored: CommentThread[] = [];\n anchors.forEach((_anchor, id) => {\n if (!this.getThread(id) && this.deleted.has(id)) {\n restored.push(this.deleted.get(id)!);\n this.deleted.delete(id);\n }\n });\n const next = [...this.threads, ...restored].map((t) => {\n const anchor = anchors.get(t.id) ?? null;\n const orphaned = anchor === null;\n if (sameAnchor(t.anchor, anchor) && t.orphaned === orphaned) return t;\n changed = true;\n // Keep the last known anchor quote for orphaned threads so the UI can\n // still show what was commented on.\n return orphaned\n ? { ...t, anchor: null, orphaned, ...(t.anchor ? { lastQuote: t.anchor.quote } : {}) }\n : { ...t, anchor, orphaned };\n });\n if (!changed && restored.length === 0) return false;\n this.threads = next;\n this.emit();\n return true;\n }\n\n /** Replaces all threads, e.g. with data loaded from a server. */\n load(json: string | CommentThread[] | SerializedComments): void {\n this.threads = CommentStore.parse(json);\n this.deleted.clear();\n if (this.activeThreadId && !this.getThread(this.activeThreadId)) {\n this.activeThreadId = null;\n }\n this.emit();\n }\n\n clear(): void {\n this.threads = [];\n this.deleted.clear();\n this.activeThreadId = null;\n this.emit();\n }\n\n toJSON(): SerializedComments {\n return {\n version: 1,\n threads: this.threads.map((t) => ({ ...t, replies: t.replies.map((r) => ({ ...r })) })),\n };\n }\n\n private updateThread(id: string, fn: (t: CommentThread) => CommentThread): boolean {\n const index = this.threads.findIndex((t) => t.id === id);\n if (index === -1) return false;\n const updated = fn(this.threads[index]);\n if (updated === this.threads[index]) return true;\n this.threads = this.threads.map((t, i) => (i === index ? updated : t));\n this.emit();\n return true;\n }\n\n private computeSnapshot(): CommentStoreSnapshot {\n const threads = [...this.threads].sort((a, b) => {\n if (a.anchor && b.anchor) {\n return a.anchor.from - b.anchor.from || a.createdAt - b.createdAt;\n }\n if (a.anchor) return -1;\n if (b.anchor) return 1;\n return a.createdAt - b.createdAt;\n });\n return { threads, activeThreadId: this.activeThreadId };\n }\n\n private emit(): void {\n this.snapshot = this.computeSnapshot();\n const snapshot = this.snapshot;\n this.listeners.forEach((listener) => {\n try {\n listener(snapshot);\n } catch (err) {\n console.error(\"[CommentStore] Error in listener:\", err);\n }\n });\n }\n}\n\nexport function createCommentStore(options?: CommentStoreOptions): CommentStore {\n return new CommentStore(options);\n}\n\n/**\n * Short, human-readable timestamp for review UIs (\"just now\", \"5m ago\",\n * \"3h ago\", then a locale date). Shared by the React and Svelte components.\n */\nexport function formatReviewTime(timestamp: number, now: number = Date.now()): string {\n const seconds = Math.round((now - timestamp) / 1000);\n if (seconds < 45) return \"just now\";\n const minutes = Math.round(seconds / 60);\n if (minutes < 60) return `${minutes}m ago`;\n const hours = Math.round(minutes / 60);\n if (hours < 24) return `${hours}h ago`;\n return new Date(timestamp).toLocaleDateString();\n}\n","// Portions ported from wordcraft/crates/engine/src/cmd/moves.rs and wordcraft/crates/engine/src/cmd/review.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\nimport { Extension, Mark, mergeAttributes, type Editor } from \"@tiptap/core\";\nimport {\n Plugin,\n PluginKey,\n TextSelection,\n type EditorState,\n type Transaction,\n} from \"@tiptap/pm/state\";\nimport {\n Fragment,\n Mark as ProsemirrorMark,\n NodeRange,\n Slice,\n type ResolvedPos,\n type MarkType,\n type Node as ProsemirrorNode,\n type NodeType,\n type Schema,\n} from \"@tiptap/pm/model\";\nimport {\n AddMarkStep,\n AttrStep,\n Mapping,\n RemoveMarkStep,\n ReplaceAroundStep,\n ReplaceStep,\n Transform,\n canJoin,\n liftTarget,\n type StepMap,\n} from \"@tiptap/pm/transform\";\nimport type { EditorView } from \"@tiptap/pm/view\";\nimport { createReviewId, type ReviewAuthor } from \"./comment-store\";\n\nexport const INSERTION_MARK_NAME = \"insertion\";\nexport const DELETION_MARK_NAME = \"deletion\";\n/** Mark recording a tracked formatting change (mark added/removed on existing text). */\nexport const FORMAT_CHANGE_MARK_NAME = \"formatChange\";\n/** Node attribute recording a tracked block change (type, attributes or wrapping). */\nexport const BLOCK_CHANGE_ATTRIBUTE = \"blockChange\";\n/**\n * Attribute of the insertion and deletion marks linking the two ends of a\n * tracked move: the deletion is where the text moved from, the insertion\n * where it moved to. Serialized as `data-move-id`.\n */\nexport const MOVE_ID_ATTRIBUTE = \"moveId\";\n\n/**\n * Marks that are never treated as formatting: the review marks themselves,\n * comments (`comment`) and bookmarks (`bookmark`). Names are used instead of\n * imports to avoid module cycles.\n */\nconst REVIEW_MARK_NAMES = new Set([\n INSERTION_MARK_NAME,\n DELETION_MARK_NAME,\n FORMAT_CHANGE_MARK_NAME,\n \"comment\",\n \"bookmark\",\n]);\n\n/**\n * Set this transaction meta to `true` to apply a change without tracking it,\n * e.g. `editor.chain().command(({ tr }) => { tr.setMeta(TRACK_CHANGES_SKIP_META, true); return true; }).setContent(…).run()`.\n */\nexport const TRACK_CHANGES_SKIP_META = \"trackChangesSkip\";\n\nexport type TrackedChangeType = \"insertion\" | \"deletion\" | \"format\" | \"block\" | \"move\";\n\n/**\n * How tracked content is rendered by serializers:\n * - `accepted` (default): as if every change were accepted (deleted text is\n * dropped, inserted text kept, new formatting and block types kept).\n * - `original`: as if every change were rejected.\n * - `all`: both inserted and deleted text, current formatting and blocks.\n */\nexport type TrackedChangesView = \"accepted\" | \"original\" | \"all\";\n\nexport interface TrackedChangeAttributes {\n changeId: string;\n authorId: string;\n authorName: string;\n authorColor: string | null;\n createdAt: number;\n /**\n * Insertion/deletion marks only: links a deletion (moved from) and an\n * insertion (moved to) into one tracked move.\n */\n moveId?: string | null;\n}\n\n/** A serializable mark (`mark.toJSON()`). */\nexport interface TrackedMark {\n type: string;\n attrs?: Record<string, any>;\n}\n\n/** Attributes of the `formatChange` mark. */\nexport interface TrackedFormatChangeAttributes extends TrackedChangeAttributes {\n /** Formatting marks before the change. */\n before: TrackedMark[];\n /** Formatting marks after the change. */\n after: TrackedMark[];\n}\n\n/** A textblock's type, attributes and enclosing wrappers (lists, quotes). */\nexport interface TrackedBlockSnapshot {\n type: string;\n attrs: Record<string, any>;\n /** Ancestors from the outermost (below the document) to the parent. */\n wrappers: { type: string; attrs: Record<string, any> }[];\n}\n\n/**\n * Whole-block operation of a `blockChange` record: the block was inserted\n * (rejecting removes it) or deleted (accepting removes it). Produced by\n * `compareDocuments`; absent for type/attribute/wrapping changes.\n */\nexport type TrackedBlockOperation = \"insert\" | \"delete\";\n\n/** Value of the `blockChange` node attribute. */\nexport interface TrackedBlockChangeAttributes extends TrackedChangeAttributes {\n previous: TrackedBlockSnapshot;\n operation?: TrackedBlockOperation;\n}\n\nexport interface TrackedFormatSummary {\n /** Labels of added formatting, e.g. `[\"Bold\"]`. */\n added: string[];\n /** Labels of removed formatting. */\n removed: string[];\n /** Labels of changed formatting (same mark, different attributes), e.g. `[\"Font size\"]`. */\n changed: string[];\n}\n\nexport interface TrackedChange {\n id: string;\n type: TrackedChangeType;\n author: ReviewAuthor;\n createdAt: number;\n /** Start of the first range. */\n from: number;\n /** End of the last range. */\n to: number;\n /**\n * A change can be split into several ranges by other marks or blocks.\n * Block changes cover whole textblock nodes.\n */\n ranges: { from: number; to: number }[];\n text: string;\n /** Human-readable summary, e.g. `Formatted: Bold` or `Changed Paragraph to Heading 2`. */\n description: string;\n /** Formatting changes (`type: \"format\"`). */\n format?: TrackedFormatSummary;\n /**\n * Block changes (`type: \"block\"`): the first block's previous and current\n * state, and the whole-block operation for inserted/deleted blocks.\n */\n block?: {\n previous: TrackedBlockSnapshot;\n current: TrackedBlockSnapshot;\n operation?: TrackedBlockOperation;\n };\n /**\n * Moves (`type: \"move\"`): where the text was moved from (struck through\n * until accepted) and where it was moved to. `ranges` holds both.\n */\n move?: {\n from: { from: number; to: number }[];\n to: { from: number; to: number }[];\n /** The moved text at its source. */\n fromText: string;\n /** The moved text at its destination. */\n toText: string;\n };\n}\n\nexport interface TrackChangesSnapshot {\n enabled: boolean;\n author: ReviewAuthor;\n /** Changes in document order. */\n changes: readonly TrackedChange[];\n /** The change at the selection, if any. */\n currentChangeId: string | null;\n}\n\nexport interface TrackChangesOptions {\n /** Whether tracking is on when the editor is created. */\n enabled: boolean;\n author: ReviewAuthor;\n now: () => number;\n generateId: () => string;\n insertionHTMLAttributes: Record<string, any>;\n deletionHTMLAttributes: Record<string, any>;\n /** HTML attributes (e.g. `class`) of tracked formatting. Replaces the default dotted underline. */\n formatChangeHTMLAttributes: Record<string, any>;\n /** Class added to blocks with a tracked block change (margin bar by default). */\n blockChangeClass: string;\n /** Extra class of wholly inserted blocks (`blockChange.operation: \"insert\"`, from comparisons). */\n blockInsertedClass: string;\n /** Extra class of wholly deleted blocks (`blockChange.operation: \"delete\"`, from comparisons). */\n blockDeletedClass: string;\n /** Track mark changes (bold, links, font size, …) on existing text. Default `true`. */\n trackFormatting: boolean;\n /** Track textblock type/attribute and wrapping (list, quote) changes. Default `true`. */\n trackBlocks: boolean;\n /**\n * Track insertions and deletions inside code blocks by allowing the\n * insertion/deletion marks there. Default `true`.\n */\n trackCodeBlocks: boolean;\n /** Textblock node types that can record block changes. */\n blockTypes: string[];\n /** Extra mark names not tracked as formatting. */\n ignoredMarks: string[];\n /** Node attributes ignored when comparing blocks (e.g. generated ids). */\n ignoredBlockAttributes: string[];\n /**\n * Record a cut and paste (or a drag) of the same text as one tracked move\n * instead of a deletion and an insertion. Default `true`.\n */\n trackMoves: boolean;\n /**\n * Decides whether moved text is big enough to be a move. Default\n * `isMoveSized` (a paragraph, or at least a sentence of two words, as in\n * Word); pass `() => true` to record every cut/paste or drag as a move.\n */\n isMove: (text: string) => boolean;\n /** HTML attributes (e.g. `class`) of moved text, at both ends. Replaces the default green double line. */\n moveHTMLAttributes: Record<string, any>;\n}\n\n/**\n * External store over the editor's tracked-change state. Snapshots only\n * change when the document, selection or tracking state changes, so they can\n * be used with `useSyncExternalStore`. `subscribe` calls the listener\n * immediately (Svelte store contract).\n */\nexport class TrackChangesStore {\n private snapshot: TrackChangesSnapshot;\n private listeners = new Set<(snapshot: TrackChangesSnapshot) => void>();\n\n constructor(initial: TrackChangesSnapshot) {\n this.snapshot = initial;\n }\n\n subscribe = (listener: (snapshot: TrackChangesSnapshot) => void): (() => void) => {\n this.listeners.add(listener);\n listener(this.snapshot);\n return () => {\n this.listeners.delete(listener);\n };\n };\n\n getSnapshot = (): TrackChangesSnapshot => this.snapshot;\n\n /** @internal Called by the extension's plugin view. */\n set(snapshot: TrackChangesSnapshot): void {\n this.snapshot = snapshot;\n this.listeners.forEach((listener) => {\n try {\n listener(snapshot);\n } catch (err) {\n console.error(\"[TrackChangesStore] Error in listener:\", err);\n }\n });\n }\n}\n\nexport interface TrackChangesStorage {\n store: TrackChangesStore;\n}\n\ninterface TrackChangesPluginState {\n enabled: boolean;\n author: ReviewAuthor;\n}\n\ntype TrackChangesMeta =\n | { type: \"setEnabled\"; enabled: boolean }\n | { type: \"setAuthor\"; author: ReviewAuthor };\n\nexport const trackChangesPluginKey = new PluginKey<TrackChangesPluginState>(\"trackChanges\");\n\ndeclare module \"@tiptap/core\" {\n interface Commands<ReturnType> {\n trackChanges: {\n setTrackChanges: (enabled: boolean) => ReturnType;\n toggleTrackChanges: () => ReturnType;\n setTrackChangesAuthor: (author: ReviewAuthor) => ReturnType;\n /** Accepts one change (single undo step). */\n acceptChange: (id: string) => ReturnType;\n /** Rejects one change (single undo step). */\n rejectChange: (id: string) => ReturnType;\n acceptAllChanges: () => ReturnType;\n /** Accepts a tracked move: removes the source, keeps the destination. */\n acceptMove: (id: string) => ReturnType;\n /** Rejects a tracked move: restores the source, removes the destination. */\n rejectMove: (id: string) => ReturnType;\n rejectAllChanges: () => ReturnType;\n /** Selects the next change after the selection, wrapping around. */\n selectNextChange: () => ReturnType;\n /** Selects the previous change before the selection, wrapping around. */\n selectPreviousChange: () => ReturnType;\n /** Selects a change (a move's destination, a block change's content). */\n selectChange: (id: string) => ReturnType;\n };\n }\n}\n\nfunction changeAttributes(mark: { attrs: Record<string, any> }): TrackedChangeAttributes {\n return mark.attrs as TrackedChangeAttributes;\n}\n\nfunction parseJSON<T>(value: string | null, fallback: T): T {\n if (!value) return fallback;\n try {\n return JSON.parse(value) as T;\n } catch {\n return fallback;\n }\n}\n\nconst sharedAttributes = () => ({\n changeId: {\n default: null,\n parseHTML: (el: HTMLElement) => el.getAttribute(\"data-change-id\"),\n renderHTML: (attrs: any) => ({ \"data-change-id\": attrs.changeId }),\n },\n authorId: {\n default: \"\",\n parseHTML: (el: HTMLElement) => el.getAttribute(\"data-author-id\") ?? \"\",\n renderHTML: (attrs: any) => ({ \"data-author-id\": attrs.authorId }),\n },\n authorName: {\n default: \"\",\n parseHTML: (el: HTMLElement) => el.getAttribute(\"data-author-name\") ?? \"\",\n renderHTML: (attrs: any) => ({ \"data-author-name\": attrs.authorName }),\n },\n authorColor: {\n default: null,\n parseHTML: (el: HTMLElement) => el.getAttribute(\"data-author-color\"),\n renderHTML: (attrs: any) =>\n attrs.authorColor ? { \"data-author-color\": attrs.authorColor } : {},\n },\n createdAt: {\n default: 0,\n parseHTML: (el: HTMLElement) => Number(el.getAttribute(\"data-created-at\") ?? 0),\n renderHTML: (attrs: any) => ({ \"data-created-at\": String(attrs.createdAt) }),\n },\n});\n\nconst moveAttribute = () => ({\n [MOVE_ID_ATTRIBUTE]: {\n default: null,\n parseHTML: (el: HTMLElement) => el.getAttribute(\"data-move-id\"),\n renderHTML: (attrs: any) =>\n attrs[MOVE_ID_ATTRIBUTE] ? { \"data-move-id\": attrs[MOVE_ID_ATTRIBUTE] } : {},\n },\n});\n\ninterface TrackedMarkOptions {\n HTMLAttributes: Record<string, any>;\n /** Attributes used instead of `HTMLAttributes` when the mark is one end of a move. */\n moveHTMLAttributes: Record<string, any>;\n}\n\n/** Default classes of moved text: green double underline (destination) / strike (source), as in Word. */\nexport const MOVE_TO_CLASS =\n \"pixerate-move-to underline decoration-double decoration-emerald-600 text-emerald-700 dark:text-emerald-400\";\nexport const MOVE_FROM_CLASS =\n \"pixerate-move-from line-through decoration-double decoration-emerald-600 text-emerald-700 dark:text-emerald-400\";\n\n/**\n * Text inserted while tracking was on. Rendered as `<ins>`. With a `moveId`\n * it is the destination of a tracked move.\n */\nexport const TrackedInsertion = Mark.create<TrackedMarkOptions>({\n name: INSERTION_MARK_NAME,\n inclusive: false,\n addOptions() {\n return {\n HTMLAttributes: {\n class:\n \"pixerate-insertion underline decoration-emerald-500 text-emerald-700 dark:text-emerald-400\",\n },\n moveHTMLAttributes: { class: MOVE_TO_CLASS },\n };\n },\n addStorage() {\n // tiptap-markdown: inserted text is exported as plain text.\n return { markdown: { serialize: { open: \"\", close: \"\" }, parse: {} } };\n },\n addAttributes: () => ({ ...sharedAttributes(), ...moveAttribute() }),\n parseHTML() {\n return [{ tag: 'ins[data-change-type=\"insertion\"]', priority: 100 }];\n },\n renderHTML({ mark, HTMLAttributes }) {\n const base = mark.attrs[MOVE_ID_ATTRIBUTE]\n ? this.options.moveHTMLAttributes\n : this.options.HTMLAttributes;\n return [\n \"ins\",\n mergeAttributes(base, HTMLAttributes, {\n \"data-change-type\": \"insertion\",\n }),\n 0,\n ];\n },\n});\n\n/**\n * Text deleted while tracking was on. The text stays in the document until\n * the change is accepted. Rendered as `<del>` (struck through). With a\n * `moveId` it is the source of a tracked move.\n */\nexport const TrackedDeletion = Mark.create<TrackedMarkOptions>({\n name: DELETION_MARK_NAME,\n inclusive: false,\n addOptions() {\n return {\n HTMLAttributes: {\n class:\n \"pixerate-deletion line-through decoration-red-500 text-red-600 dark:text-red-400\",\n },\n moveHTMLAttributes: { class: MOVE_FROM_CLASS },\n };\n },\n addStorage() {\n // Only reached when serializing a document that still has deletions\n // (`getEditorMarkdown(editor, { trackedChanges: \"all\" })`).\n return {\n markdown: {\n serialize: { open: \"~~\", close: \"~~\", mixable: true, expelEnclosingWhitespace: true },\n parse: {},\n },\n };\n },\n addAttributes: () => ({ ...sharedAttributes(), ...moveAttribute() }),\n parseHTML() {\n // Higher priority than Strike, which also parses `<del>`.\n return [{ tag: 'del[data-change-type=\"deletion\"]', priority: 100 }];\n },\n renderHTML({ mark, HTMLAttributes }) {\n const base = mark.attrs[MOVE_ID_ATTRIBUTE]\n ? this.options.moveHTMLAttributes\n : this.options.HTMLAttributes;\n return [\n \"del\",\n mergeAttributes(base, HTMLAttributes, {\n \"data-change-type\": \"deletion\",\n }),\n 0,\n ];\n },\n});\n\n/**\n * Existing text whose formatting changed while tracking was on. Stores the\n * formatting before and after the change, rendered as\n * `<span data-change-type=\"format\">` (dotted underline by default).\n */\nexport const TrackedFormatChange = Mark.create<{ HTMLAttributes: Record<string, any> }>({\n name: FORMAT_CHANGE_MARK_NAME,\n inclusive: false,\n addOptions() {\n return {\n HTMLAttributes: {\n class:\n \"pixerate-format-change underline decoration-dotted decoration-2 decoration-sky-500 underline-offset-4\",\n },\n };\n },\n addStorage() {\n return { markdown: { serialize: { open: \"\", close: \"\" }, parse: {} } };\n },\n addAttributes() {\n return {\n ...sharedAttributes(),\n before: {\n default: [],\n parseHTML: (el: HTMLElement) => parseJSON(el.getAttribute(\"data-format-before\"), []),\n renderHTML: (attrs: any) => ({ \"data-format-before\": JSON.stringify(attrs.before ?? []) }),\n },\n after: {\n default: [],\n parseHTML: (el: HTMLElement) => parseJSON(el.getAttribute(\"data-format-after\"), []),\n renderHTML: (attrs: any) => ({ \"data-format-after\": JSON.stringify(attrs.after ?? []) }),\n },\n };\n },\n parseHTML() {\n return [{ tag: 'span[data-change-type=\"format\"]', priority: 100 }];\n },\n renderHTML({ HTMLAttributes }) {\n return [\n \"span\",\n mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {\n \"data-change-type\": \"format\",\n }),\n 0,\n ];\n },\n});\n\n// ---------------------------------------------------------------------------\n// Descriptions\n// ---------------------------------------------------------------------------\n\nconst MARK_LABELS: Record<string, string> = {\n bold: \"Bold\",\n italic: \"Italic\",\n underline: \"Underline\",\n strike: \"Strikethrough\",\n code: \"Code\",\n link: \"Link\",\n highlight: \"Highlight\",\n subscript: \"Subscript\",\n superscript: \"Superscript\",\n textStyle: \"Text style\",\n};\n\nconst TEXT_STYLE_LABELS: Record<string, string> = {\n fontSize: \"Font size\",\n fontFamily: \"Font\",\n color: \"Color\",\n backgroundColor: \"Background color\",\n lineHeight: \"Line height\",\n};\n\nconst BLOCK_LABELS: Record<string, string> = {\n paragraph: \"Paragraph\",\n codeBlock: \"Code block\",\n blockquote: \"Quote\",\n bulletList: \"Bulleted list\",\n orderedList: \"Numbered list\",\n taskList: \"Task list\",\n listItem: \"List item\",\n taskItem: \"Task item\",\n};\n\nfunction humanize(name: string): string {\n const words = name\n .replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n .replace(/[-_]+/g, \" \")\n .toLowerCase();\n return words.charAt(0).toUpperCase() + words.slice(1);\n}\n\nfunction stableStringify(value: unknown): string {\n if (value === undefined) return \"null\";\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n const obj = value as Record<string, unknown>;\n return `{${Object.keys(obj)\n .filter((k) => obj[k] !== undefined)\n .sort()\n .map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`)\n .join(\",\")}}`;\n}\n\nconst sameValue = (a: unknown, b: unknown) => stableStringify(a) === stableStringify(b);\n\nfunction markLabels(mark: TrackedMark, other?: TrackedMark): string[] {\n if (mark.type === \"textStyle\") {\n const attrs = mark.attrs ?? {};\n const otherAttrs = other?.attrs ?? {};\n const keys = Array.from(new Set([...Object.keys(attrs), ...Object.keys(otherAttrs)])).filter(\n (key) => (other ? !sameValue(attrs[key], otherAttrs[key]) : attrs[key] != null),\n );\n return keys.length ? keys.map((key) => TEXT_STYLE_LABELS[key] ?? humanize(key)) : [\"Text style\"];\n }\n return [MARK_LABELS[mark.type] ?? humanize(mark.type)];\n}\n\nfunction diffFormatting(before: TrackedMark[], after: TrackedMark[]): TrackedFormatSummary {\n const summary: TrackedFormatSummary = { added: [], removed: [], changed: [] };\n const push = (list: string[], labels: string[]) =>\n labels.forEach((label) => {\n if (!list.includes(label)) list.push(label);\n });\n after.forEach((mark) => {\n const old = before.find((m) => m.type === mark.type);\n if (!old) push(summary.added, markLabels(mark));\n else if (!sameValue(old.attrs ?? {}, mark.attrs ?? {})) push(summary.changed, markLabels(mark, old));\n });\n before.forEach((mark) => {\n if (!after.some((m) => m.type === mark.type)) push(summary.removed, markLabels(mark));\n });\n return summary;\n}\n\nfunction mergeSummary(target: TrackedFormatSummary, source: TrackedFormatSummary): void {\n ([\"added\", \"removed\", \"changed\"] as const).forEach((key) =>\n source[key].forEach((label) => {\n if (!target[key].includes(label)) target[key].push(label);\n }),\n );\n}\n\n/** Describes a formatting change, e.g. `Formatted: Bold; Removed formatting: Italic`. */\nexport function describeFormatChange(summary: TrackedFormatSummary): string {\n const parts: string[] = [];\n if (summary.added.length) parts.push(`Formatted: ${summary.added.join(\", \")}`);\n if (summary.removed.length) parts.push(`Removed formatting: ${summary.removed.join(\", \")}`);\n if (summary.changed.length) parts.push(`Changed formatting: ${summary.changed.join(\", \")}`);\n return parts.join(\"; \") || \"Formatted\";\n}\n\nfunction blockLabel(type: string, attrs?: Record<string, any>): string {\n if (type === \"heading\" && attrs?.level) return `Heading ${attrs.level}`;\n return BLOCK_LABELS[type] ?? humanize(type);\n}\n\nconst containerLabels = (wrappers: TrackedBlockSnapshot[\"wrappers\"]) =>\n wrappers\n .filter((w) => w.type !== \"listItem\" && w.type !== \"taskItem\")\n .map((w) => blockLabel(w.type, w.attrs));\n\nconst isPrefix = (a: string[], b: string[]) => a.every((label, i) => b[i] === label);\n\n/** Describes a block change, e.g. `Changed Paragraph to Heading 2`. */\nexport function describeBlockChange(\n previous: TrackedBlockSnapshot,\n current: TrackedBlockSnapshot,\n ignoredAttributes: readonly string[] = [\"id\"],\n): string {\n const parts: string[] = [];\n const before = blockLabel(previous.type, previous.attrs);\n const after = blockLabel(current.type, current.attrs);\n if (before !== after) {\n parts.push(`Changed ${before} to ${after}`);\n } else {\n const keys = Array.from(\n new Set([...Object.keys(previous.attrs ?? {}), ...Object.keys(current.attrs ?? {})]),\n ).filter(\n (key) =>\n key !== BLOCK_CHANGE_ATTRIBUTE &&\n !ignoredAttributes.includes(key) &&\n !(current.type === \"heading\" && key === \"level\") &&\n !sameValue(previous.attrs?.[key], current.attrs?.[key]),\n );\n keys.forEach((key) => {\n if (key === \"textAlign\") {\n parts.push(`Changed alignment to ${humanize(String(current.attrs?.textAlign ?? \"default\"))}`);\n } else if (key === \"styleId\") {\n // Paragraph styles (`ParagraphStyles`): no id means the block's default style.\n parts.push(`Changed style to ${current.attrs?.styleId ?? \"default\"}`);\n } else {\n parts.push(`Changed ${humanize(key).toLowerCase()} of ${after}`);\n }\n });\n }\n const oldTypes = previous.wrappers.map((w) => w.type).join(\"/\");\n const newTypes = current.wrappers.map((w) => w.type).join(\"/\");\n if (oldTypes !== newTypes) {\n const was = containerLabels(previous.wrappers);\n const now = containerLabels(current.wrappers);\n const last = (list: string[]) => list[list.length - 1];\n if (was.length === 0 && now.length > 0) parts.push(`Wrapped in ${last(now)}`);\n else if (now.length === 0 && was.length > 0) parts.push(`Removed from ${last(was)}`);\n else if (now.length > was.length && isPrefix(was, now)) parts.push(`Indented in ${last(now)}`);\n else if (now.length < was.length && isPrefix(now, was)) parts.push(`Outdented from ${last(was)}`);\n else if (was.length && now.length) parts.push(`Changed ${last(was)} to ${last(now)}`);\n else parts.push(\"Changed nesting\");\n }\n return parts.join(\"; \") || `Changed ${after}`;\n}\n\n/** Short label of a change's type for review UIs (`Moved` for a move). */\nexport function trackedChangeLabel(change: Pick<TrackedChange, \"type\">): string {\n switch (change.type) {\n case \"insertion\":\n return \"Inserted\";\n case \"deletion\":\n return \"Deleted\";\n case \"format\":\n return \"Formatted\";\n case \"move\":\n return \"Moved\";\n default:\n return \"Changed\";\n }\n}\n\nfunction truncate(text: string, max = 40): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\n// ---------------------------------------------------------------------------\n// Reading changes\n// ---------------------------------------------------------------------------\n\nfunction toDoc(source: any): ProsemirrorNode | null {\n if (!source) return null;\n if (source.state?.doc) return source.state.doc;\n if (source.doc && typeof source.doc.descendants === \"function\") return source.doc;\n if (typeof source.descendants === \"function\") return source;\n return null;\n}\n\nfunction stripBlockChange(attrs: Record<string, any>): Record<string, any> {\n const copy = { ...attrs };\n delete copy[BLOCK_CHANGE_ATTRIBUTE];\n return copy;\n}\n\n/** Snapshot of the textblock at `pos` (position before the node). */\nfunction blockSnapshot(doc: ProsemirrorNode, pos: number): TrackedBlockSnapshot {\n const $pos = doc.resolve(pos);\n const node = $pos.nodeAfter!;\n const wrappers: TrackedBlockSnapshot[\"wrappers\"] = [];\n for (let depth = 1; depth <= $pos.depth; depth++) {\n const wrapper = $pos.node(depth);\n wrappers.push({ type: wrapper.type.name, attrs: stripBlockChange(wrapper.attrs) });\n }\n return { type: node.type.name, attrs: stripBlockChange(node.attrs), wrappers };\n}\n\nfunction sameBlock(\n a: TrackedBlockSnapshot,\n b: TrackedBlockSnapshot,\n ignored: ReadonlySet<string>,\n): boolean {\n if (a.type !== b.type) return false;\n const filter = (attrs: Record<string, any>) =>\n Object.fromEntries(\n Object.entries(attrs ?? {}).filter(\n ([key]) => key !== BLOCK_CHANGE_ATTRIBUTE && !ignored.has(key),\n ),\n );\n if (!sameValue(filter(a.attrs), filter(b.attrs))) return false;\n if (a.wrappers.length !== b.wrappers.length) return false;\n return a.wrappers.every((w, i) => w.type === b.wrappers[i].type);\n}\n\nconst isMarkChange = (type: string) =>\n type === INSERTION_MARK_NAME || type === DELETION_MARK_NAME || type === FORMAT_CHANGE_MARK_NAME;\n\n/**\n * Lists tracked changes in document order. Accepts an editor, an editor\n * state or a ProseMirror document.\n */\nexport function getTrackedChanges(\n source: Editor | EditorState | ProsemirrorNode | null | undefined,\n): TrackedChange[] {\n const doc = toDoc(source);\n if (!doc) return [];\n const byKey = new Map<string, TrackedChange>();\n\n const add = (\n key: string,\n type: TrackedChangeType,\n attrs: TrackedChangeAttributes,\n from: number,\n to: number,\n text: string,\n ): TrackedChange => {\n let change = byKey.get(key);\n if (!change) {\n change = {\n id: attrs.changeId,\n type,\n author: {\n id: attrs.authorId,\n name: attrs.authorName,\n ...(attrs.authorColor ? { color: attrs.authorColor } : {}),\n },\n createdAt: attrs.createdAt,\n from,\n to,\n ranges: [],\n text: \"\",\n description: \"\",\n };\n byKey.set(key, change);\n }\n const last = change.ranges[change.ranges.length - 1];\n if (last && last.to === from) {\n last.to = to;\n } else {\n change.ranges.push({ from, to });\n }\n change.to = to;\n change.text += text;\n return change;\n };\n\n const blockCounts = new Map<string, number>();\n doc.descendants((node, pos) => {\n const block = node.attrs?.[BLOCK_CHANGE_ATTRIBUTE] as TrackedBlockChangeAttributes | null;\n if (block?.changeId && node.isTextblock) {\n blockCounts.set(block.changeId, (blockCounts.get(block.changeId) ?? 0) + 1);\n const change = add(\n `block:${block.changeId}`,\n \"block\",\n block,\n pos,\n pos + node.nodeSize,\n node.textContent,\n );\n if (!change.block) {\n change.block = {\n previous: block.previous,\n current: blockSnapshot(doc, pos),\n ...(block.operation ? { operation: block.operation } : {}),\n };\n }\n }\n if (!node.isInline) return;\n node.marks.forEach((mark) => {\n const markType = mark.type.name;\n if (!isMarkChange(markType)) return;\n const attrs = changeAttributes(mark);\n if (!attrs.changeId) return;\n const text = node.isText ? node.text ?? \"\" : \"\";\n const moveId = markType !== FORMAT_CHANGE_MARK_NAME ? attrs[MOVE_ID_ATTRIBUTE] : null;\n if (moveId) {\n const change = add(`move:${moveId}`, \"move\", { ...attrs, changeId: moveId }, pos, pos + node.nodeSize, text);\n if (!change.move) change.move = { from: [], to: [], fromText: \"\", toText: \"\" };\n const source = markType === DELETION_MARK_NAME;\n const list = source ? change.move.from : change.move.to;\n const last = list[list.length - 1];\n if (last && last.to === pos) last.to = pos + node.nodeSize;\n else list.push({ from: pos, to: pos + node.nodeSize });\n if (source) change.move.fromText += text;\n else change.move.toText += text;\n return;\n }\n const type: TrackedChangeType = markType === FORMAT_CHANGE_MARK_NAME ? \"format\" : (markType as TrackedChangeType);\n const change = add(\n `${type}:${attrs.changeId}`,\n type,\n attrs,\n pos,\n pos + node.nodeSize,\n text,\n );\n if (type === \"format\") {\n const summary = diffFormatting(mark.attrs.before ?? [], mark.attrs.after ?? []);\n if (!change.format) change.format = { added: [], removed: [], changed: [] };\n mergeSummary(change.format, summary);\n }\n });\n });\n\n const changes = Array.from(byKey.values());\n changes.forEach((change) => {\n switch (change.type) {\n case \"insertion\":\n change.description = change.text ? `Inserted \"${truncate(change.text)}\"` : \"Inserted content\";\n break;\n case \"deletion\":\n change.description = change.text ? `Deleted \"${truncate(change.text)}\"` : \"Deleted content\";\n break;\n case \"format\":\n change.description = describeFormatChange(change.format!);\n break;\n case \"move\": {\n const move = change.move!;\n change.text = move.toText || move.fromText;\n change.description = change.text ? `Moved \"${truncate(change.text)}\"` : \"Moved content\";\n break;\n }\n case \"block\": {\n const { previous, current, operation } = change.block!;\n if (operation) {\n const count = blockCounts.get(change.id) ?? 1;\n const verb = operation === \"insert\" ? \"Inserted\" : \"Deleted\";\n change.description =\n count > 1 ? `${verb} ${count} blocks` : `${verb} ${blockLabel(current.type, current.attrs)}`;\n } else {\n change.description = describeBlockChange(previous, current);\n }\n break;\n }\n }\n });\n return changes.sort((a, b) => a.from - b.from || a.to - b.to);\n}\n\n/** The change containing `pos` (defaults to the selection start). Inline changes win over block changes. */\nexport function getTrackedChangeAt(\n state: EditorState,\n pos: number = state.selection.from,\n changes: readonly TrackedChange[] = getTrackedChanges(state.doc),\n): TrackedChange | null {\n const { from, to } = state.selection;\n // A selection exactly covering a change (e.g. after selectNextChange).\n const exact = changes.find((c) => {\n const [start, end] = selectionRange(c);\n return start === from && end === to;\n });\n if (exact) return exact;\n const inline = changes.filter((c) => c.type !== \"block\");\n const blocks = changes.filter((c) => c.type === \"block\");\n return (\n inline.find((c) => c.ranges.some((r) => r.from < pos && pos < r.to)) ??\n inline.find((c) => c.ranges.some((r) => r.from <= pos && pos <= r.to)) ??\n blocks.find((c) => c.ranges.some((r) => r.from < pos && pos < r.to)) ??\n null\n );\n}\n\n/**\n * Text positions selected for a change (block changes select their content,\n * moves their destination).\n */\nfunction selectionRange(change: TrackedChange): [number, number] {\n if (change.type === \"block\") return [change.from + 1, change.to - 1];\n if (change.type === \"move\" && change.move) {\n const ranges = change.move.to.length ? change.move.to : change.move.from;\n if (ranges.length) return [ranges[0].from, ranges[ranges.length - 1].to];\n }\n return [change.from, change.to];\n}\n\nexport function isTrackChangesEnabled(source: Editor | EditorState | null | undefined): boolean {\n const state: EditorState | undefined = (source as any)?.state ?? (source as any);\n if (!state || typeof (state as any).plugins === \"undefined\") return false;\n return trackChangesPluginKey.getState(state)?.enabled ?? false;\n}\n\n/** Returns the tracked-changes store of an editor using `TrackChanges`. */\nexport function getTrackChangesStore(editor: Editor | null | undefined): TrackChangesStore | null {\n return (editor?.storage as any)?.trackChanges?.store ?? null;\n}\n\n// ---------------------------------------------------------------------------\n// Accept / reject\n// ---------------------------------------------------------------------------\n\nfunction marksFromJSON(schema: Schema, marks: TrackedMark[] | undefined): ProsemirrorMark[] {\n const result: ProsemirrorMark[] = [];\n (marks ?? []).forEach((json) => {\n try {\n if (schema.marks[json.type]) result.push(schema.markFromJSON(json));\n } catch {\n // Unknown or invalid mark: skip.\n }\n });\n return result;\n}\n\nfunction decideFormat(\n tr: Transform,\n changes: readonly TrackedChange[],\n decision: \"accept\" | \"reject\",\n): void {\n const schema = tr.doc.type.schema;\n const formatType = schema.marks[FORMAT_CHANGE_MARK_NAME];\n if (!formatType) return;\n const ids = new Set(changes.map((c) => c.id));\n const segments: { from: number; to: number; node: ProsemirrorNode; mark: ProsemirrorMark }[] = [];\n changes.forEach((change) =>\n change.ranges.forEach(({ from, to }) =>\n tr.doc.nodesBetween(from, to, (node, pos) => {\n if (!node.isInline) return true;\n const mark = formatType.isInSet(node.marks);\n if (mark && ids.has(mark.attrs.changeId)) {\n segments.push({ from: Math.max(pos, from), to: Math.min(pos + node.nodeSize, to), node, mark });\n }\n return false;\n }),\n ),\n );\n segments.forEach(({ from, to, node, mark }) => {\n tr.removeMark(from, to, formatType);\n if (decision === \"accept\") return;\n const before: TrackedMark[] = mark.attrs.before ?? [];\n const after: TrackedMark[] = mark.attrs.after ?? [];\n const involved = new Set([...before, ...after].map((m) => m.type));\n node.marks.forEach((current) => {\n if (involved.has(current.type.name)) tr.removeMark(from, to, current);\n });\n marksFromJSON(schema, before).forEach((restored) => tr.addMark(from, to, restored));\n });\n}\n\nconst LIST_ITEM_TYPES = new Set([\"listItem\", \"taskItem\"]);\n\n/**\n * The textblock at `contentPos` when it is the first child of a list item:\n * its resolved position and the item's depth.\n */\nfunction listItemAt(\n tr: Transform,\n contentPos: number,\n): { $pos: ResolvedPos; itemDepth: number } | null {\n const $pos = tr.doc.resolve(contentPos);\n const itemDepth = $pos.depth - 1;\n if (itemDepth < 2) return null;\n if (!LIST_ITEM_TYPES.has($pos.node(itemDepth).type.name) || $pos.index(itemDepth) !== 0) {\n return null;\n }\n return { $pos, itemDepth };\n}\n\n/** Nests a list item into its previous sibling (prosemirror-schema-list `sinkListItem`). */\nfunction sinkItem(tr: Transform, contentPos: number): boolean {\n const info = listItemAt(tr, contentPos);\n if (!info) return false;\n const { $pos, itemDepth } = info;\n const list = $pos.node(itemDepth - 1);\n const index = $pos.index(itemDepth - 1);\n const itemType = $pos.node(itemDepth).type;\n if (index === 0 || list.child(index - 1).type !== itemType) return false;\n const nodeBefore = list.child(index - 1);\n const nestedBefore = nodeBefore.lastChild?.type === list.type;\n const inner = Fragment.from(nestedBefore ? itemType.create() : null);\n const slice = new Slice(\n Fragment.from(itemType.create(null, Fragment.from(list.type.create(null, inner)))),\n nestedBefore ? 3 : 1,\n 0,\n );\n const before = $pos.before(itemDepth);\n const after = $pos.after(itemDepth);\n tr.step(\n new ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after, before, after, slice, 1, true),\n );\n return true;\n}\n\n/**\n * Moves a nested list item out to the enclosing list (prosemirror-schema-list\n * `liftToOuterList`); following siblings become its children.\n */\nfunction outdentItem(tr: Transform, contentPos: number): boolean {\n const info = listItemAt(tr, contentPos);\n if (!info) return false;\n const { $pos, itemDepth } = info;\n const listDepth = itemDepth - 1;\n if (listDepth < 2 || !LIST_ITEM_TYPES.has($pos.node(listDepth - 1).type.name)) return false;\n const itemType = $pos.node(itemDepth).type;\n let range = new NodeRange(\n tr.doc.resolve($pos.before(itemDepth)),\n tr.doc.resolve($pos.after(itemDepth)),\n listDepth,\n );\n const end = range.end;\n const endOfList = range.$to.end(range.depth);\n if (end < endOfList) {\n tr.step(\n new ReplaceAroundStep(\n end - 1,\n endOfList,\n end,\n endOfList,\n new Slice(Fragment.from(itemType.create(null, range.parent.copy())), 1, 0),\n 1,\n true,\n ),\n );\n range = new NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);\n }\n const target = liftTarget(range);\n if (target == null) return false;\n tr.lift(range, target);\n return true;\n}\n\n/** Lifts/wraps the textblock whose content starts at `contentPos` to match `wrappers`. */\nfunction restoreWrappers(\n tr: Transform,\n contentPos: number,\n wrappers: TrackedBlockSnapshot[\"wrappers\"],\n): void {\n const schema = tr.doc.type.schema;\n /** Runs `fn`, maps `contentPos` and reports whether it added steps. */\n const step = (fn: () => unknown): boolean => {\n const index = tr.steps.length;\n const result = fn();\n contentPos = tr.mapping.slice(index).map(contentPos);\n return result !== false && tr.steps.length > index;\n };\n const chain = () => {\n const $pos = tr.doc.resolve(contentPos);\n const types: string[] = [];\n for (let depth = 1; depth < $pos.depth; depth++) types.push($pos.node(depth).type.name);\n return types;\n };\n const target = wrappers.map((w) => w.type);\n\n for (let guard = 0; guard < 24; guard++) {\n const current = chain();\n if (current.join(\"/\") === target.join(\"/\")) return;\n\n // Same nesting, different wrapper types (e.g. bulleted → numbered list).\n if (current.length === target.length) {\n const index = current.findIndex((type, i) => type !== target[i]);\n const type = schema.nodes[target[index]];\n if (type && LIST_ITEM_TYPES.has(type.name) === LIST_ITEM_TYPES.has(current[index])) {\n const $pos = tr.doc.resolve(contentPos);\n if (step(() => tr.setNodeMarkup($pos.before(index + 1), type, wrappers[index].attrs))) {\n continue;\n }\n }\n }\n\n let common = 0;\n while (common < current.length && common < target.length && current[common] === target[common]) {\n common++;\n }\n\n if (current.length > common) {\n // Leave wrappers that were not there before: list items move to the\n // outer list, anything else is lifted.\n if (step(() => outdentItem(tr, contentPos))) continue;\n const $from = tr.doc.resolve(contentPos);\n const range = $from.blockRange(tr.doc.resolve($from.end()));\n const lift = range ? liftTarget(range) : null;\n if (!range || lift == null || !step(() => tr.lift(range, lift))) return;\n continue;\n }\n\n // Enter the wrappers that were removed: a list item nests into its\n // previous sibling, anything else is wrapped.\n const missing = wrappers.slice(current.length);\n if (\n missing.length >= 2 &&\n LIST_ITEM_TYPES.has(missing[1].type) &&\n step(() => sinkItem(tr, contentPos))\n ) {\n continue;\n }\n const types = missing.map((w) => ({ type: schema.nodes[w.type], attrs: w.attrs }));\n if (types.some((t) => !t.type)) return;\n const $from = tr.doc.resolve(contentPos);\n const range = $from.blockRange(tr.doc.resolve($from.end()));\n if (!range) return;\n const start = range.start;\n step(() => tr.wrap(range, types as { type: NodeType; attrs: Record<string, any> }[]));\n // Re-join with neighbouring wrappers of the same type (restoring one list).\n const wrapper = tr.doc.nodeAt(start);\n if (!wrapper) return;\n const end = start + wrapper.nodeSize;\n if (tr.doc.resolve(end).nodeAfter?.type === wrapper.type && canJoin(tr.doc, end)) {\n step(() => tr.join(end));\n }\n if (tr.doc.resolve(start).nodeBefore?.type === wrapper.type && canJoin(tr.doc, start)) {\n step(() => tr.join(start));\n }\n return;\n }\n}\n\n/**\n * Removes the blocks of whole-block operations that the decision drops\n * (accepted deletions, rejected insertions). Containers whose textblocks are\n * all removed (list items, lists, quotes, table rows) are removed with them.\n */\nfunction removeBlocks(tr: Transform, ids: ReadonlySet<string>, decision: \"accept\" | \"reject\"): void {\n const removed: TrackedBlockOperation = decision === \"accept\" ? \"delete\" : \"insert\";\n const memo = new Map<ProsemirrorNode, boolean>();\n const removable = (node: ProsemirrorNode): boolean => {\n const cached = memo.get(node);\n if (cached !== undefined) return cached;\n let result: boolean;\n if (node.isTextblock) {\n const record = node.attrs?.[BLOCK_CHANGE_ATTRIBUTE] as TrackedBlockChangeAttributes | null;\n result = !!record?.changeId && record.operation === removed && ids.has(record.changeId);\n } else if (node.isLeaf || node.childCount === 0) {\n result = false;\n } else {\n result = true;\n for (let i = 0; i < node.childCount && result; i++) result = removable(node.child(i));\n }\n memo.set(node, result);\n return result;\n };\n\n const doc = tr.doc;\n if (removable(doc)) {\n const filler = doc.type.contentMatch.defaultType?.createAndFill();\n if (filler) tr.replaceWith(0, doc.content.size, filler);\n return;\n }\n const ranges: { from: number; to: number }[] = [];\n const visit = (node: ProsemirrorNode, contentStart: number) => {\n node.forEach((child, offset) => {\n const from = contentStart + offset;\n if (removable(child)) {\n ranges.push({ from, to: from + child.nodeSize });\n } else if (!child.isTextblock && !child.isLeaf) {\n visit(child, from + 1);\n }\n });\n };\n visit(doc, 0);\n if (ranges.length === 0) return;\n const startIndex = tr.steps.length;\n ranges.reverse().forEach(({ from, to }) => {\n const mapping = tr.mapping.slice(startIndex);\n try {\n tr.delete(mapping.map(from, 1), mapping.map(to, -1));\n } catch {\n // The parent cannot lose this block (e.g. a list item's first paragraph): keep it.\n }\n });\n}\n\nfunction decideBlocks(\n tr: Transform,\n changes: readonly TrackedChange[],\n decision: \"accept\" | \"reject\",\n): void {\n const ids = new Set(changes.map((c) => c.id));\n removeBlocks(tr, ids, decision);\n const items: { contentPos: number; record: TrackedBlockChangeAttributes }[] = [];\n tr.doc.descendants((node, pos) => {\n const record = node.attrs?.[BLOCK_CHANGE_ATTRIBUTE] as TrackedBlockChangeAttributes | null;\n if (node.isTextblock && record?.changeId && ids.has(record.changeId)) {\n items.push({ contentPos: pos + 1, record });\n }\n return !node.isTextblock;\n });\n const schema = tr.doc.type.schema;\n const startIndex = tr.steps.length;\n // Last block first, so structural changes do not shift earlier blocks.\n items.reverse().forEach(({ contentPos: original, record }) => {\n const contentPos = tr.mapping.slice(startIndex).map(original);\n const $pos = tr.doc.resolve(contentPos);\n if (!$pos.parent.isTextblock || $pos.parentOffset !== 0) return;\n const pos = $pos.before();\n if (decision === \"accept\" || record.operation) {\n tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, null);\n return;\n }\n const previous = record.previous;\n const type = previous && schema.nodes[previous.type];\n if (!type) {\n tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, null);\n return;\n }\n const attrs = { ...previous.attrs, [BLOCK_CHANGE_ATTRIBUTE]: null };\n try {\n tr.setNodeMarkup(pos, type, attrs);\n } catch {\n // The content does not fit the previous type (e.g. marks in a code\n // block): convert while dropping incompatible content.\n try {\n tr.setBlockType(pos + 1, pos + 1, type, attrs);\n } catch {\n tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, null);\n }\n }\n try {\n restoreWrappers(tr, tr.mapping.slice(startIndex).map(original), previous.wrappers ?? []);\n } catch {\n // Best effort: the block keeps its current wrappers.\n }\n });\n}\n\n/**\n * Moved text that fills a whole top-level textblock takes the block with it\n * when it is removed, so moving a paragraph and accepting (or rejecting) the\n * move leaves no empty paragraph behind. Returns `false` when the range is\n * not such a block or the block cannot be removed.\n */\nfunction deleteWholeBlock(tr: Transform, from: number, to: number): boolean {\n const $from = tr.doc.resolve(from);\n const block = $from.parent;\n if (\n $from.depth !== 1 ||\n !block.isTextblock ||\n from !== $from.start() ||\n to !== $from.end() ||\n tr.doc.childCount < 2\n ) {\n return false;\n }\n try {\n tr.delete($from.before(), $from.after());\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Accepts or rejects changes on a transform. Formatting is resolved first,\n * then inline marks are removed and text is deleted from the end of the\n * document backwards so positions stay valid; block changes come last.\n */\nfunction applyDecision(\n tr: Transform,\n changes: readonly TrackedChange[],\n decision: \"accept\" | \"reject\",\n): void {\n const schema = tr.doc.type.schema;\n const format = changes.filter((c) => c.type === \"format\");\n if (format.length) decideFormat(tr, format, decision);\n\n const deletions: { from: number; to: number; wholeBlocks?: boolean }[] = [];\n changes.forEach((change) => {\n if (change.type === \"move\" && change.move) {\n // Accepting keeps the destination and removes the source; rejecting\n // restores the source and removes the destination.\n const accept = decision === \"accept\";\n const keepType = schema.marks[accept ? INSERTION_MARK_NAME : DELETION_MARK_NAME];\n (accept ? change.move.to : change.move.from).forEach(({ from, to }) => {\n if (keepType) tr.removeMark(from, to, keepType);\n });\n (accept ? change.move.from : change.move.to).forEach(({ from, to }) =>\n deletions.push({ from, to, wholeBlocks: true }),\n );\n return;\n }\n if (change.type !== \"insertion\" && change.type !== \"deletion\") return;\n const markType = schema.marks[change.type];\n if (!markType) return;\n const keep = (change.type === \"insertion\") === (decision === \"accept\");\n change.ranges.forEach(({ from, to }) => {\n if (keep) {\n tr.removeMark(from, to, markType);\n } else {\n deletions.push({ from, to });\n }\n });\n });\n deletions\n .sort((a, b) => b.from - a.from)\n .forEach(({ from, to, wholeBlocks }) => {\n const start = tr.mapping.map(from);\n const end = tr.mapping.map(to);\n if (wholeBlocks && deleteWholeBlock(tr, start, end)) return;\n tr.delete(start, end);\n });\n\n const blocks = changes.filter((c) => c.type === \"block\");\n if (blocks.length) decideBlocks(tr, blocks, decision);\n}\n\n/**\n * Returns a copy of `doc` with tracked changes resolved for `view`\n * (`accepted` by default). The input document is not modified.\n */\nexport function resolveTrackedChanges(\n doc: ProsemirrorNode,\n view: TrackedChangesView = \"accepted\",\n): ProsemirrorNode {\n if (view === \"all\") return doc;\n const changes = getTrackedChanges(doc);\n if (changes.length === 0) return doc;\n const tr = new Transform(doc);\n applyDecision(tr, changes, view === \"accepted\" ? \"accept\" : \"reject\");\n return tr.doc;\n}\n\n// ---------------------------------------------------------------------------\n// Tracking\n// ---------------------------------------------------------------------------\n\nfunction hasInlineContent(fragment: Fragment): boolean {\n let found = false;\n fragment.descendants((node) => {\n if (found) return false;\n if (node.isInline) found = true;\n return !found;\n });\n return found;\n}\n\n/**\n * Removes deleted text and review marks/attributes from a slice (used for\n * paste), so pasting tracked content pastes it as if all changes were\n * accepted.\n */\nfunction acceptSlice(slice: Slice, schema: Schema): Slice {\n const insertion = schema.marks[INSERTION_MARK_NAME];\n const deletion = schema.marks[DELETION_MARK_NAME];\n const format = schema.marks[FORMAT_CHANGE_MARK_NAME];\n const mapFragment = (fragment: Fragment): Fragment => {\n const children: ProsemirrorNode[] = [];\n fragment.forEach((child) => {\n if (child.isInline) {\n if (deletion && deletion.isInSet(child.marks)) return;\n let marks = child.marks;\n if (insertion) marks = insertion.removeFromSet(marks);\n if (format) marks = format.removeFromSet(marks);\n children.push(marks === child.marks ? child : child.mark(marks));\n } else if (child.attrs?.[BLOCK_CHANGE_ATTRIBUTE]) {\n children.push(\n child.type.create(\n { ...child.attrs, [BLOCK_CHANGE_ATTRIBUTE]: null },\n mapFragment(child.content),\n child.marks,\n ),\n );\n } else {\n children.push(child.copy(mapFragment(child.content)));\n }\n });\n return Fragment.from(children);\n };\n return new Slice(mapFragment(slice.content), slice.openStart, slice.openEnd);\n}\n\n/** Copy of `fragment` without inline marks (adjacent text is merged). */\nfunction stripMarks(fragment: Fragment): Fragment {\n const children: ProsemirrorNode[] = [];\n fragment.forEach((child) => {\n children.push(child.isInline ? child.mark(ProsemirrorMark.none) : child.copy(stripMarks(child.content)));\n });\n return Fragment.fromArray(children);\n}\n\ninterface TrackContext {\n author: ReviewAuthor;\n now: () => number;\n generateId: () => string;\n insertion: MarkType;\n deletion: MarkType;\n /** Present when formatting is tracked. */\n format?: MarkType;\n trackBlocks: boolean;\n ignoredMarks: ReadonlySet<string>;\n ignoredAttributes: ReadonlySet<string>;\n /** Move detection for this batch of transactions, when moves are tracked. */\n moves?: MoveContext;\n}\n\n/** A tracked cut that the next paste can turn into the source of a move. */\ninterface PendingMove {\n /** Change id of the cut's deletion (becomes the move id). */\n id: string;\n /** The cut text (normalized). */\n text: string;\n authorId: string;\n}\n\ninterface MoveContext {\n /** The user action of the batch (`uiEvent` meta). */\n event: \"cut\" | \"paste\" | \"drop\" | null;\n /** The last tracked cut, for a paste. */\n pending: PendingMove | null;\n isMove: (text: string) => boolean;\n /** Set by `trackTransactions` when the batch is a cut that can become a move. */\n cut?: PendingMove;\n}\n\nconst SENTENCE_END = /[.!?…]$/;\nconst TRAILING_CLOSERS = /[\"'”’)]+$/;\nconst TRAILING_SPACES = /[ ]+$/;\n\n/**\n * Whether moved text is big enough to be recorded as a move rather than a\n * deletion and an insertion, as in Word: a paragraph (the text holds a\n * paragraph break) or at least a sentence of two or more words.\n */\nexport function isMoveSized(text: string): boolean {\n const trimmed = text.replace(TRAILING_SPACES, \"\");\n if (trimmed.includes(\"\\n\")) return true;\n const sentence = trimmed.replace(TRAILING_CLOSERS, \"\");\n return SENTENCE_END.test(sentence) && sentence.trim().split(/\\s+/).length >= 2;\n}\n\n/**\n * Drops white space so moved text can be compared across block boundaries\n * and mark splits.\n */\nconst normalizeMoveText = (text: string) => text.replace(/\\s+/g, \"\");\n\n/**\n * The text of a deleted slice that the deletion mark will record: neither\n * the author's own insertions (really deleted) nor text deleted before.\n * Blocks are separated by `\\n`.\n */\nfunction deletedText(slice: Slice, ctx: TrackContext): string {\n let text = \"\";\n let blocks = 0;\n slice.content.descendants((node) => {\n if (node.isTextblock) {\n if (blocks++ > 0) text += \"\\n\";\n return true;\n }\n if (!node.isText) return true;\n const insertion = ctx.insertion.isInSet(node.marks);\n if (insertion && insertion.attrs.authorId === ctx.author.id) return false;\n if (ctx.deletion.isInSet(node.marks)) return false;\n text += node.text ?? \"\";\n return false;\n });\n return text;\n}\n\n/** Text of a document range with blocks separated by `\\n`. */\nconst rangeText = (doc: ProsemirrorNode, from: number, to: number) =>\n doc.textBetween(from, to, \"\\n\", \"\");\n\n/**\n * Adds the move id to the source deletion of a pending cut. Returns `false`\n * when that deletion is no longer in the document.\n */\nfunction linkMoveSource(tr: Transform, pending: PendingMove, ctx: TrackContext): boolean {\n const segments: { from: number; to: number; mark: ProsemirrorMark }[] = [];\n tr.doc.descendants((node, pos) => {\n if (!node.isInline) return true;\n const mark = ctx.deletion.isInSet(node.marks);\n if (mark && mark.attrs.changeId === pending.id && !mark.attrs[MOVE_ID_ATTRIBUTE]) {\n segments.push({ from: pos, to: pos + node.nodeSize, mark });\n }\n return false;\n });\n if (segments.length === 0) return false;\n const text = segments.map((s) => rangeText(tr.doc, s.from, s.to)).join(\"\");\n if (normalizeMoveText(text) !== pending.text) return false;\n segments.forEach(({ from, to, mark }) =>\n tr.addMark(from, to, ctx.deletion.create({ ...mark.attrs, [MOVE_ID_ATTRIBUTE]: pending.id })),\n );\n return true;\n}\n\nfunction newAttributes(ctx: TrackContext): TrackedChangeAttributes {\n return {\n changeId: ctx.generateId(),\n authorId: ctx.author.id,\n authorName: ctx.author.name,\n authorColor: ctx.author.color ?? null,\n createdAt: ctx.now(),\n };\n}\n\nfunction baseAttributes(attrs: TrackedChangeAttributes): TrackedChangeAttributes {\n return {\n changeId: attrs.changeId,\n authorId: attrs.authorId,\n authorName: attrs.authorName,\n authorColor: attrs.authorColor ?? null,\n createdAt: attrs.createdAt,\n };\n}\n\n/**\n * Reuses the attributes of an adjacent change of the same type by the same\n * author, so typing (or backspacing) a word produces one change, not one per\n * character.\n */\nfunction adjacentAttributes(\n doc: ProsemirrorNode,\n from: number,\n to: number,\n type: MarkType,\n ctx: TrackContext,\n): TrackedChangeAttributes | null {\n const candidates = [doc.resolve(from).nodeBefore, doc.resolve(to).nodeAfter];\n for (const node of candidates) {\n const mark = node && type.isInSet(node.marks);\n // Typing next to moved text is not part of the move.\n if (\n mark &&\n mark.attrs.authorId === ctx.author.id &&\n mark.attrs.changeId &&\n !mark.attrs[MOVE_ID_ATTRIBUTE]\n ) {\n return changeAttributes(mark);\n }\n }\n return null;\n}\n\n/**\n * Marks deleted content with the deletion mark so it can be put back.\n * Text inserted by the same author (an untracked-in-effect draft) is dropped,\n * i.e. really deleted. Returns `null` if the slice holds content that cannot\n * carry the mark, in which case the deletion is not tracked.\n */\nfunction markDeletedSlice(\n slice: Slice,\n parentType: NodeType,\n attrs: TrackedChangeAttributes,\n ctx: TrackContext,\n): { slice: Slice; hasContent: boolean } | null {\n let ok = true;\n let hasContent = false;\n const deletionMark = ctx.deletion.create(attrs);\n const mapFragment = (fragment: Fragment, parent: NodeType): Fragment => {\n const children: ProsemirrorNode[] = [];\n fragment.forEach((child) => {\n if (child.isInline) {\n const insertion = ctx.insertion.isInSet(child.marks);\n if (insertion && insertion.attrs.authorId === ctx.author.id) return;\n hasContent = true;\n if (ctx.deletion.isInSet(child.marks)) {\n children.push(child);\n return;\n }\n if (!parent.allowsMarkType(ctx.deletion)) {\n ok = false;\n children.push(child);\n return;\n }\n children.push(child.mark(deletionMark.addToSet(child.marks)));\n } else {\n children.push(child.copy(mapFragment(child.content, child.type)));\n }\n });\n return Fragment.from(children);\n };\n const content = mapFragment(slice.content, parentType);\n if (!ok) return null;\n return { slice: new Slice(content, slice.openStart, slice.openEnd), hasContent };\n}\n\nfunction shouldSkip(tr: Transaction): boolean {\n return (\n !tr.docChanged ||\n tr.getMeta(TRACK_CHANGES_SKIP_META) === true ||\n !!tr.getMeta(trackChangesPluginKey) ||\n // Undo/redo replays already-tracked history.\n !!tr.getMeta(\"history$\") ||\n // Remote collaborative changes were tracked by their author.\n !!tr.getMeta(\"y-sync$\") ||\n // Housekeeping appended by other plugins (e.g. trailing nodes).\n !!tr.getMeta(\"appendedTransaction\")\n );\n}\n\ninterface PendingRestore {\n pos: number;\n slice: Slice;\n parentType: NodeType;\n /** Nothing was inserted in place of the deleted content. */\n pureDelete: boolean;\n backspace: boolean;\n}\n\ninterface PendingBlock {\n previous: TrackedBlockSnapshot;\n existing: TrackedBlockChangeAttributes | null;\n}\n\n/** Formatting marks of a node, as JSON (review and ignored marks excluded). */\nfunction formattingOf(marks: readonly ProsemirrorMark[], ctx: TrackContext): TrackedMark[] {\n return marks\n .filter((m) => !REVIEW_MARK_NAMES.has(m.type.name) && !ctx.ignoredMarks.has(m.type.name))\n .map((m) => m.toJSON() as TrackedMark);\n}\n\nfunction mergeRanges(ranges: { from: number; to: number }[]): { from: number; to: number }[] {\n const sorted = ranges.filter((r) => r.to > r.from).sort((a, b) => a.from - b.from);\n const merged: { from: number; to: number }[] = [];\n sorted.forEach((range) => {\n const last = merged[merged.length - 1];\n if (last && range.from <= last.to) last.to = Math.max(last.to, range.to);\n else merged.push({ ...range });\n });\n return merged;\n}\n\n/**\n * Records formatting changes on `ranges` (final-document positions) by\n * comparing each piece of text with the same text in `oldDoc`.\n */\nfunction trackFormatting(\n tr: Transaction,\n ranges: { from: number; to: number }[],\n oldDoc: ProsemirrorNode,\n back: Mapping,\n inserted: { from: number; to: number }[],\n ctx: TrackContext,\n): boolean {\n const format = ctx.format!;\n const doc = tr.doc;\n let changed = false;\n let batch: TrackedChangeAttributes | null = null;\n\n mergeRanges(ranges).forEach(({ from: a, to: b }) => {\n const segments: { from: number; to: number; oldNode: ProsemirrorNode; node: ProsemirrorNode }[] = [];\n doc.nodesBetween(a, b, (node, pos, parent) => {\n if (!node.isInline) return true;\n const s = Math.max(a, pos);\n const e = Math.min(b, pos + node.nodeSize);\n if (e <= s || !parent || !parent.type.allowsMarkType(format)) return false;\n // Formatting a tracked insertion is part of the insertion.\n if (ctx.insertion.isInSet(node.marks)) return false;\n if (inserted.some((r) => s < r.to && e > r.from)) return false;\n const os = back.map(s, 1);\n const oe = back.map(e, -1);\n if (oe - os !== e - s) return false;\n oldDoc.nodesBetween(os, oe, (oldNode, oldPos) => {\n if (!oldNode.isInline) return true;\n const ss = Math.max(os, oldPos);\n const ee = Math.min(oe, oldPos + oldNode.nodeSize);\n if (ee > ss && !ctx.insertion.isInSet(oldNode.marks)) {\n segments.push({ from: s + (ss - os), to: s + (ee - os), oldNode, node });\n }\n return false;\n });\n return false;\n });\n\n segments.forEach(({ from, to, oldNode, node }) => {\n const existing = format.isInSet(oldNode.marks);\n const original: TrackedMark[] = existing\n ? (existing.attrs.before as TrackedMark[]) ?? []\n : formattingOf(oldNode.marks, ctx);\n const current = formattingOf(node.marks, ctx);\n if (sameValue(original, current)) {\n if (existing || format.isInSet(node.marks)) {\n tr.removeMark(from, to, format);\n changed = true;\n }\n return;\n }\n let attrs: TrackedChangeAttributes;\n if (existing && existing.attrs.authorId === ctx.author.id && existing.attrs.changeId) {\n attrs = baseAttributes(changeAttributes(existing));\n } else {\n if (!batch) {\n const description = describeFormatChange(diffFormatting(original, current));\n const neighbours = [doc.resolve(from).nodeBefore, doc.resolve(to).nodeAfter];\n for (const neighbour of neighbours) {\n const mark = neighbour && format.isInSet(neighbour.marks);\n if (\n mark &&\n mark.attrs.authorId === ctx.author.id &&\n mark.attrs.changeId &&\n describeFormatChange(diffFormatting(mark.attrs.before ?? [], mark.attrs.after ?? [])) ===\n description\n ) {\n batch = baseAttributes(changeAttributes(mark));\n break;\n }\n }\n batch = batch ?? newAttributes(ctx);\n }\n attrs = batch;\n }\n tr.addMark(from, to, format.create({ ...attrs, before: original, after: current }));\n changed = true;\n });\n });\n return changed;\n}\n\nfunction trackBlocks(\n tr: Transaction,\n pending: Map<number, PendingBlock>,\n ctx: TrackContext,\n): boolean {\n let changed = false;\n let batch: TrackedChangeAttributes | null = null;\n pending.forEach(({ previous, existing }, contentPos) => {\n if (contentPos < 0 || contentPos > tr.doc.content.size) return;\n const $pos = tr.doc.resolve(contentPos);\n const node = $pos.parent;\n if (!node.isTextblock || $pos.parentOffset !== 0 || $pos.depth === 0) return;\n if (!node.type.spec.attrs || !(BLOCK_CHANGE_ATTRIBUTE in node.type.spec.attrs)) return;\n const pos = $pos.before();\n // Inserted/deleted blocks (from a comparison) keep their record.\n if (existing?.operation || node.attrs[BLOCK_CHANGE_ATTRIBUTE]?.operation) return;\n const original = existing?.previous ?? previous;\n const current = blockSnapshot(tr.doc, pos);\n if (sameBlock(original, current, ctx.ignoredAttributes)) {\n if (node.attrs[BLOCK_CHANGE_ATTRIBUTE]) {\n tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, null);\n changed = true;\n }\n return;\n }\n const base =\n existing && existing.authorId === ctx.author.id && existing.changeId\n ? baseAttributes(existing)\n : (batch = batch ?? newAttributes(ctx));\n const value: TrackedBlockChangeAttributes = { ...base, previous: original };\n if (!sameValue(node.attrs[BLOCK_CHANGE_ATTRIBUTE], value)) {\n tr.setNodeAttribute(pos, BLOCK_CHANGE_ATTRIBUTE, value);\n changed = true;\n }\n });\n return changed;\n}\n\n/** Records textblocks whose type, attributes or wrappers changed in a step. */\nfunction collectBlockChanges(\n step: ReplaceAroundStep | AttrStep,\n docBefore: ProsemirrorNode,\n docAfter: ProsemirrorNode,\n rest: Mapping,\n pending: Map<number, PendingBlock>,\n ctx: TrackContext,\n): void {\n const record = (oldPos: number, newPos: number) => {\n const key = rest.map(newPos + 1);\n if (pending.has(key)) return;\n const oldNode = docBefore.nodeAt(oldPos)!;\n const previous = blockSnapshot(docBefore, oldPos);\n if (sameBlock(previous, blockSnapshot(docAfter, newPos), ctx.ignoredAttributes)) return;\n pending.set(key, {\n previous,\n existing: (oldNode.attrs[BLOCK_CHANGE_ATTRIBUTE] as TrackedBlockChangeAttributes) ?? null,\n });\n };\n\n if (step instanceof AttrStep) {\n const node = docBefore.nodeAt(step.pos);\n if (\n node?.isTextblock &&\n step.attr !== BLOCK_CHANGE_ATTRIBUTE &&\n !ctx.ignoredAttributes.has(step.attr)\n ) {\n record(step.pos, step.pos);\n }\n return;\n }\n\n const map = step.getMap();\n const back = map.invert();\n const start = map.map(step.from, -1);\n const end = map.map(step.to, 1);\n docAfter.nodesBetween(start, end, (node, pos) => {\n if (!node.isTextblock) return true;\n const oldContent = back.map(pos + 1);\n if (oldContent < 1 || oldContent > docBefore.content.size) return false;\n const $old = docBefore.resolve(oldContent);\n if (\n $old.parent.isTextblock &&\n $old.parentOffset === 0 &&\n $old.parent.content.size === node.content.size\n ) {\n record($old.before(), pos);\n }\n return false;\n });\n}\n\n/**\n * Builds a transaction that turns the user's raw edits into tracked changes:\n * inserted content gets the insertion mark, deleted content is put back with\n * the deletion mark, formatting changes on existing text get the\n * `formatChange` mark and textblock type/wrapping changes get the\n * `blockChange` attribute. Paragraph splits and joins are not tracked.\n */\nfunction trackTransactions(\n transactions: readonly Transaction[],\n oldState: EditorState,\n newState: EditorState,\n ctx: TrackContext,\n): Transaction | null {\n const maps: StepMap[] = [];\n transactions.forEach((t) => maps.push(...(t.mapping.maps as StepMap[])));\n\n const inserted: { from: number; to: number }[] = [];\n const restores: PendingRestore[] = [];\n const formatRanges: { from: number; to: number }[] = [];\n const blocks = new Map<number, PendingBlock>();\n let mapIndex = 0;\n const single =\n transactions.length === 1 && transactions[0].steps.length === 1;\n\n transactions.forEach((t) => {\n const skip = shouldSkip(t);\n t.steps.forEach((step, i) => {\n const index = mapIndex + i;\n if (skip) return;\n const rest = new Mapping(maps.slice(index + 1));\n const docBefore = t.docs[i];\n\n if (step instanceof AddMarkStep || step instanceof RemoveMarkStep) {\n const name = step.mark.type.name;\n if (ctx.format && !REVIEW_MARK_NAMES.has(name) && !ctx.ignoredMarks.has(name)) {\n formatRanges.push({ from: rest.map(step.from, 1), to: rest.map(step.to, -1) });\n }\n return;\n }\n if (step instanceof ReplaceAroundStep || step instanceof AttrStep) {\n if (ctx.trackBlocks) {\n const docAfter = t.docs[i + 1] ?? t.doc;\n collectBlockChanges(step, docBefore, docAfter, rest, blocks, ctx);\n }\n return;\n }\n if (!(step instanceof ReplaceStep)) return;\n const { from, to, slice } = step as unknown as { from: number; to: number; slice: Slice };\n\n // Replacing content with the same content: only marks may differ.\n if (to > from && slice.size === to - from) {\n const deleted = docBefore.slice(from, to);\n if (\n slice.openStart === deleted.openStart &&\n slice.openEnd === deleted.openEnd &&\n stripMarks(slice.content).eq(stripMarks(deleted.content))\n ) {\n if (ctx.format && !slice.content.eq(deleted.content)) {\n formatRanges.push({ from: rest.map(from, 1), to: rest.map(to, -1) });\n }\n return;\n }\n }\n\n if (slice.size > 0 && hasInlineContent(slice.content)) {\n const start = rest.map(from, 1);\n const end = rest.map(from + slice.size, -1);\n if (end > start) inserted.push({ from: start, to: end });\n }\n if (to > from) {\n const deleted = docBefore.slice(from, to);\n if (!hasInlineContent(deleted.content)) return;\n const $from = docBefore.resolve(from);\n const before = i === 0 ? t.before : null;\n const sel = before === oldState.doc ? oldState.selection : null;\n restores.push({\n pos: rest.map(from, -1),\n slice: deleted,\n parentType: $from.node(Math.max(0, $from.depth - deleted.openStart)).type,\n pureDelete: slice.size === 0,\n backspace:\n single && slice.size === 0 && !!sel && sel.empty && sel.from === to,\n });\n }\n });\n mapIndex += t.steps.length;\n });\n\n if (\n inserted.length === 0 &&\n restores.length === 0 &&\n formatRanges.length === 0 &&\n blocks.size === 0\n ) {\n return null;\n }\n\n const tr = newState.tr;\n let changed = false;\n\n // Formatting and block attributes first: they do not move positions.\n if (formatRanges.length && ctx.format) {\n const back = new Mapping(maps).invert();\n changed = trackFormatting(tr, formatRanges, oldState.doc, back, inserted, ctx) || changed;\n }\n if (blocks.size) {\n changed = trackBlocks(tr, blocks, ctx) || changed;\n }\n\n // Moves: a paste of the text the last tracked cut deleted, or a drag of a\n // range within the document, links the deletion and the insertion.\n const moves = ctx.moves;\n let insertAttrs: TrackedChangeAttributes | null = null;\n let deleteAttrs: TrackedChangeAttributes | null = null;\n const moveAttrs = (id: string): TrackedChangeAttributes => ({\n changeId: id,\n authorId: ctx.author.id,\n authorName: ctx.author.name,\n authorColor: ctx.author.color ?? null,\n createdAt: ctx.now(),\n [MOVE_ID_ATTRIBUTE]: id,\n });\n if (moves && inserted.length) {\n const insertedText = normalizeMoveText(\n inserted.map((r) => rangeText(tr.doc, r.from, r.to)).join(\"\"),\n );\n const pending = moves.pending;\n if (\n moves.event === \"paste\" &&\n pending &&\n pending.authorId === ctx.author.id &&\n insertedText === pending.text &&\n linkMoveSource(tr, pending, ctx)\n ) {\n insertAttrs = moveAttrs(pending.id);\n } else if (moves.event === \"drop\" && restores.length) {\n const raw = restores.map((r) => deletedText(r.slice, ctx)).join(\"\\n\");\n if (insertedText && normalizeMoveText(raw) === insertedText && moves.isMove(raw)) {\n insertAttrs = deleteAttrs = moveAttrs(ctx.generateId());\n }\n }\n }\n if (moves?.event === \"cut\" && restores.length) {\n // A cut gets a change of its own, so the paste after it moves exactly it.\n deleteAttrs = newAttributes(ctx);\n const raw = restores.map((r) => deletedText(r.slice, ctx)).join(\"\\n\");\n if (normalizeMoveText(raw) && moves.isMove(raw)) {\n moves.cut = { id: deleteAttrs.changeId, text: normalizeMoveText(raw), authorId: ctx.author.id };\n }\n }\n\n inserted.forEach(({ from, to }) => {\n tr.removeMark(from, to, ctx.deletion);\n tr.removeMark(from, to, ctx.insertion);\n if (ctx.format) tr.removeMark(from, to, ctx.format);\n const attrs =\n insertAttrs ?? adjacentAttributes(tr.doc, from, to, ctx.insertion, ctx) ?? newAttributes(ctx);\n tr.addMark(from, to, ctx.insertion.create(attrs));\n changed = true;\n });\n if (insertAttrs) changed = true;\n\n let cursor: number | null = null;\n restores\n .sort((a, b) => b.pos - a.pos)\n .forEach((restore) => {\n const pos = tr.mapping.map(restore.pos, -1);\n const attrs =\n deleteAttrs ?? adjacentAttributes(tr.doc, pos, pos, ctx.deletion, ctx) ?? newAttributes(ctx);\n const marked = markDeletedSlice(restore.slice, restore.parentType, attrs, ctx);\n if (!marked || !marked.hasContent) return;\n const sizeBefore = tr.doc.content.size;\n try {\n tr.replace(pos, pos, marked.slice);\n } catch {\n return;\n }\n changed = true;\n // Backspace leaves the caret before the struck-through text so the\n // next Backspace continues backwards; Delete leaves it after.\n if (restores.length === 1 && restore.pureDelete && newState.selection.empty) {\n cursor = restore.backspace ? pos : pos + (tr.doc.content.size - sizeBefore);\n }\n });\n\n if (!changed) return null;\n if (cursor !== null) {\n try {\n tr.setSelection(TextSelection.create(tr.doc, cursor));\n } catch {\n // Keep the mapped selection.\n }\n }\n tr.setMeta(TRACK_CHANGES_SKIP_META, true);\n return tr;\n}\n\nfunction buildSnapshot(state: EditorState, author: ReviewAuthor): TrackChangesSnapshot {\n const plugin = trackChangesPluginKey.getState(state);\n const changes = getTrackedChanges(state.doc);\n return {\n enabled: plugin?.enabled ?? false,\n author: plugin?.author ?? author,\n changes,\n currentChangeId: getTrackedChangeAt(state, state.selection.from, changes)?.id ?? null,\n };\n}\n\n/**\n * Lets code blocks (`marks: \"\"`) carry the insertion and deletion marks.\n * The node's own `marks` spec wins over `extendNodeSchema`, so the allowed\n * mark set of the built schema is extended instead.\n */\nfunction allowTrackingInCodeBlocks(schema: Schema): void {\n const marks = [schema.marks[INSERTION_MARK_NAME], schema.marks[DELETION_MARK_NAME]].filter(\n Boolean,\n ) as MarkType[];\n Object.values(schema.nodes).forEach((type) => {\n const markSet = type.markSet;\n if (!type.isTextblock || !type.spec.code || markSet === null) return;\n const missing = marks.filter((mark) => !markSet.includes(mark));\n if (missing.length) {\n (type as { markSet: readonly MarkType[] | null }).markSet = [...markSet, ...missing];\n }\n });\n}\n\n/**\n * Suggestion mode / track changes. When enabled, typed and pasted content is\n * marked as an insertion and deleted content is kept but marked as a\n * deletion (struck through) until the change is accepted or rejected.\n * Formatting changes on existing text are recorded with the `formatChange`\n * mark, and textblock type/attribute/wrapping changes with the `blockChange`\n * node attribute. A cut and paste (or a drag) of a sentence or paragraph is\n * recorded as a move: a deletion and an insertion linked by `moveId`, listed\n * as one change and accepted or rejected together.\n *\n * Deleting your own insertion really deletes it. Paragraph splits and joins\n * are not tracked. IME composition is tracked once the composition ends.\n */\nexport const TrackChanges = Extension.create<TrackChangesOptions, TrackChangesStorage>({\n name: \"trackChanges\",\n\n addOptions() {\n return {\n enabled: false,\n author: { id: \"anonymous\", name: \"Anonymous\" },\n now: () => Date.now(),\n generateId: () => createReviewId(\"change\"),\n insertionHTMLAttributes: {},\n deletionHTMLAttributes: {},\n formatChangeHTMLAttributes: {},\n blockChangeClass: \"pixerate-block-change border-l-2 border-amber-400 pl-2 dark:border-amber-500\",\n blockInsertedClass:\n \"pixerate-block-inserted underline decoration-emerald-500 text-emerald-700 dark:text-emerald-400\",\n blockDeletedClass:\n \"pixerate-block-deleted line-through decoration-red-500 text-red-600 dark:text-red-400\",\n trackFormatting: true,\n trackBlocks: true,\n trackCodeBlocks: true,\n blockTypes: [\"paragraph\", \"heading\", \"codeBlock\"],\n ignoredMarks: [],\n ignoredBlockAttributes: [\"id\"],\n trackMoves: true,\n isMove: isMoveSized,\n moveHTMLAttributes: {},\n };\n },\n\n addStorage() {\n return {\n store: new TrackChangesStore({\n enabled: this.options.enabled,\n author: this.options.author,\n changes: [],\n currentChangeId: null,\n }),\n };\n },\n\n addGlobalAttributes() {\n if (this.options?.trackBlocks === false) return [];\n const blockClass = this.options?.blockChangeClass ?? \"\";\n const insertedClass = this.options?.blockInsertedClass ?? \"\";\n const deletedClass = this.options?.blockDeletedClass ?? \"\";\n return [\n {\n types: this.options?.blockTypes ?? [\"paragraph\", \"heading\", \"codeBlock\"],\n attributes: {\n [BLOCK_CHANGE_ATTRIBUTE]: {\n default: null,\n keepOnSplit: false,\n parseHTML: (el: HTMLElement) => parseJSON(el.getAttribute(\"data-block-change\"), null),\n renderHTML: (attrs: Record<string, any>) => {\n const value = attrs[BLOCK_CHANGE_ATTRIBUTE];\n if (!value) return {};\n const operation = (value as TrackedBlockChangeAttributes).operation;\n const extra = operation === \"insert\" ? insertedClass : operation === \"delete\" ? deletedClass : \"\";\n const className = [blockClass, extra].filter(Boolean).join(\" \");\n return {\n \"data-block-change\": JSON.stringify(value),\n ...(operation ? { \"data-block-operation\": operation } : {}),\n ...(className ? { class: className } : {}),\n };\n },\n },\n },\n },\n ];\n },\n\n addExtensions() {\n const insertionAttrs = this.options?.insertionHTMLAttributes ?? {};\n const deletionAttrs = this.options?.deletionHTMLAttributes ?? {};\n const formatAttrs = this.options?.formatChangeHTMLAttributes ?? {};\n const moveAttrs = this.options?.moveHTMLAttributes ?? {};\n const markOptions = (attrs: Record<string, any>) => ({\n ...(Object.keys(attrs).length ? { HTMLAttributes: attrs } : {}),\n ...(Object.keys(moveAttrs).length ? { moveHTMLAttributes: moveAttrs } : {}),\n });\n const configured = <T extends typeof TrackedInsertion>(mark: T, attrs: Record<string, any>) => {\n const options = markOptions(attrs);\n return Object.keys(options).length ? mark.configure(options) : mark;\n };\n return [\n configured(TrackedInsertion, insertionAttrs),\n configured(TrackedDeletion, deletionAttrs),\n Object.keys(formatAttrs).length\n ? TrackedFormatChange.configure({ HTMLAttributes: formatAttrs })\n : TrackedFormatChange,\n ];\n },\n\n onBeforeCreate() {\n if (this.options.trackCodeBlocks) allowTrackingInCodeBlocks(this.editor.schema);\n },\n\n addCommands() {\n const decide =\n (decision: \"accept\" | \"reject\", id: string | null, onlyMoves = false) =>\n ({ state, tr, dispatch }: { state: EditorState; tr: Transaction; dispatch?: any }) => {\n const all = getTrackedChanges(state.doc);\n const changes =\n id === null ? all : all.filter((c) => c.id === id && (!onlyMoves || c.type === \"move\"));\n if (changes.length === 0) return false;\n if (dispatch) {\n applyDecision(tr, changes, decision);\n tr.setMeta(TRACK_CHANGES_SKIP_META, true);\n }\n return true;\n };\n\n const select =\n (direction: 1 | -1) =>\n ({ state, tr, dispatch }: { state: EditorState; tr: Transaction; dispatch?: any }) => {\n const changes = getTrackedChanges(state.doc);\n if (changes.length === 0) return false;\n const { from } = state.selection;\n const start = (c: TrackedChange) => selectionRange(c)[0];\n const target =\n direction === 1\n ? changes.find((c) => start(c) > from) ?? changes[0]\n : [...changes].reverse().find((c) => start(c) < from) ?? changes[changes.length - 1];\n if (dispatch) {\n const [anchor, head] = selectionRange(target);\n tr.setSelection(TextSelection.create(tr.doc, anchor, head)).scrollIntoView();\n }\n return true;\n };\n\n return {\n setTrackChanges:\n (enabled) =>\n ({ tr, dispatch }) => {\n if (dispatch) {\n tr.setMeta(trackChangesPluginKey, { type: \"setEnabled\", enabled });\n tr.setMeta(\"addToHistory\", false);\n }\n return true;\n },\n toggleTrackChanges:\n () =>\n ({ state, commands }) =>\n commands.setTrackChanges(!isTrackChangesEnabled(state)),\n setTrackChangesAuthor:\n (author) =>\n ({ tr, dispatch }) => {\n if (dispatch) {\n tr.setMeta(trackChangesPluginKey, { type: \"setAuthor\", author });\n tr.setMeta(\"addToHistory\", false);\n }\n return true;\n },\n acceptChange: (id) => decide(\"accept\", id),\n acceptMove: (id) => decide(\"accept\", id, true),\n rejectMove: (id) => decide(\"reject\", id, true),\n rejectChange: (id) => decide(\"reject\", id),\n acceptAllChanges: () => decide(\"accept\", null),\n rejectAllChanges: () => decide(\"reject\", null),\n selectNextChange: () => select(1),\n selectPreviousChange: () => select(-1),\n selectChange:\n (id) =>\n ({ state, tr, dispatch }) => {\n const change = getTrackedChanges(state.doc).find((c) => c.id === id);\n if (!change) return false;\n if (dispatch) {\n const [anchor, head] = selectionRange(change);\n tr.setSelection(TextSelection.create(tr.doc, anchor, head)).scrollIntoView();\n }\n return true;\n },\n };\n },\n\n addProseMirrorPlugins() {\n const options = this.options;\n const store = this.storage.store;\n const ignoredMarks = new Set(options.ignoredMarks ?? []);\n const ignoredAttributes = new Set(options.ignoredBlockAttributes ?? []);\n let editorView: EditorView | null = null;\n // Transactions applied during IME composition. Changing the document\n // around the composition would break the IME, so they are tracked\n // together once it ends.\n let pending: { oldState: EditorState; transactions: Transaction[] } | null = null;\n let flushTimer: ReturnType<typeof setTimeout> | null = null;\n // The last tracked cut, which the paste right after it turns into a move.\n let pendingMove: PendingMove | null = null;\n const isMove = options.isMove ?? isMoveSized;\n\n const scheduleFlush = () => {\n if (flushTimer) clearTimeout(flushTimer);\n flushTimer = setTimeout(() => {\n flushTimer = null;\n const view = editorView;\n if (!view || !pending) return;\n if (view.composing) {\n scheduleFlush();\n return;\n }\n view.dispatch(view.state.tr.setMeta(\"trackChangesFlush\", true));\n }, 0);\n };\n\n return [\n new Plugin<TrackChangesPluginState>({\n key: trackChangesPluginKey,\n state: {\n init: () => ({ enabled: options.enabled, author: options.author }),\n apply: (tr, value) => {\n const meta = tr.getMeta(trackChangesPluginKey) as TrackChangesMeta | undefined;\n if (!meta) return value;\n if (meta.type === \"setEnabled\") {\n return meta.enabled === value.enabled ? value : { ...value, enabled: meta.enabled };\n }\n return { ...value, author: meta.author };\n },\n },\n appendTransaction: (transactions, oldState, newState) => {\n const plugin = trackChangesPluginKey.getState(oldState);\n const tracking = !!plugin?.enabled;\n if (!tracking && !pending) {\n if (transactions.some((t) => t.docChanged)) pendingMove = null;\n return null;\n }\n\n if (editorView?.composing && transactions.some((t) => t.docChanged)) {\n if (tracking) {\n if (!pending) pending = { oldState, transactions: [] };\n pending.transactions.push(...transactions);\n }\n return null;\n }\n\n let trs: readonly Transaction[] = transactions;\n let fromState = oldState;\n if (pending) {\n trs = [...pending.transactions, ...transactions];\n fromState = pending.oldState;\n pending = null;\n }\n if (!tracking && trs === transactions) return null;\n const author = plugin?.author ?? options.author;\n const userTransactions = trs.filter((t) => !shouldSkip(t));\n let moves: MoveContext | undefined;\n if (userTransactions.length) {\n const uiEvent =\n userTransactions.length === 1 ? userTransactions[0].getMeta(\"uiEvent\") : null;\n const event = uiEvent === \"cut\" || uiEvent === \"paste\" || uiEvent === \"drop\" ? uiEvent : null;\n if (options.trackMoves !== false) {\n moves = { event, pending: event === \"paste\" ? pendingMove : null, isMove };\n }\n // Any other edit ends the cut-and-paste sequence.\n pendingMove = null;\n }\n const result = trackTransactions(trs, fromState, newState, {\n author,\n now: options.now,\n generateId: options.generateId,\n insertion: newState.schema.marks[INSERTION_MARK_NAME],\n deletion: newState.schema.marks[DELETION_MARK_NAME],\n format:\n options.trackFormatting !== false\n ? newState.schema.marks[FORMAT_CHANGE_MARK_NAME]\n : undefined,\n trackBlocks: options.trackBlocks !== false,\n ignoredMarks,\n ignoredAttributes,\n moves,\n });\n if (moves?.cut) pendingMove = moves.cut;\n return result;\n },\n props: {\n transformPasted: (slice, view) => acceptSlice(slice, view.state.schema),\n handleDOMEvents: {\n compositionend: () => {\n if (pending) scheduleFlush();\n return false;\n },\n },\n },\n view: (view) => {\n editorView = view;\n store.set(buildSnapshot(view.state, options.author));\n return {\n update(view, prevState) {\n if (\n prevState.doc !== view.state.doc ||\n !prevState.selection.eq(view.state.selection) ||\n trackChangesPluginKey.getState(prevState) !==\n trackChangesPluginKey.getState(view.state)\n ) {\n store.set(buildSnapshot(view.state, options.author));\n }\n if (pending && !view.composing) scheduleFlush();\n },\n destroy() {\n editorView = null;\n pending = null;\n if (flushTimer) clearTimeout(flushTimer);\n },\n };\n },\n }),\n ];\n },\n});\n"]}
|
package/dist/commands/index.cjs
CHANGED
|
@@ -800,6 +800,78 @@ function createArrangeCommands(controller, options = {}) {
|
|
|
800
800
|
};
|
|
801
801
|
});
|
|
802
802
|
}
|
|
803
|
+
var SHAPE_COMMANDS = [
|
|
804
|
+
{ op: "unite", label: "Unite Shapes", keywords: ["union", "merge", "boolean", "combine"] },
|
|
805
|
+
{ op: "intersect", label: "Intersect Shapes", keywords: ["intersection", "boolean", "combine"] },
|
|
806
|
+
{ op: "subtract", label: "Subtract Shapes", keywords: ["difference", "minus front", "boolean", "combine"] },
|
|
807
|
+
{ op: "exclude", label: "Exclude Shapes", keywords: ["xor", "boolean", "combine"] },
|
|
808
|
+
{ op: "divide", label: "Divide Shapes", keywords: ["split", "pathfinder", "boolean"] }
|
|
809
|
+
];
|
|
810
|
+
var JOIN_PARAM = { type: "string", enum: ["miter", "round", "bevel"], description: "Corner style (default miter)." };
|
|
811
|
+
function createShapeCommands(controller, options = {}) {
|
|
812
|
+
const prefix = _nullishCoalesce(options.prefix, () => ( "canvas."));
|
|
813
|
+
const group = _nullishCoalesce(options.group, () => ( "Shape"));
|
|
814
|
+
const scope = options.scope === null ? void 0 : _nullishCoalesce(options.scope, () => ( "canvas"));
|
|
815
|
+
const base = { group, ...scope ? { scope } : {} };
|
|
816
|
+
const combine = SHAPE_COMMANDS.map(
|
|
817
|
+
({ op, label, keywords }) => ({
|
|
818
|
+
...base,
|
|
819
|
+
id: `${prefix}${op}`,
|
|
820
|
+
label,
|
|
821
|
+
keywords: [...keywords],
|
|
822
|
+
description: `${label.split(" ")[0]}s the selected shapes into a new path (curves are kept).`,
|
|
823
|
+
params: IDS_PARAMS,
|
|
824
|
+
enabled: (_ctx, params) => controller.canCombineShapes(_optionalChain([params, 'optionalAccess', _40 => _40.ids])) ? true : "Select at least two unlocked shapes.",
|
|
825
|
+
run: (_ctx, params) => controller.combineShapes(op, _optionalChain([params, 'optionalAccess', _41 => _41.ids]))
|
|
826
|
+
})
|
|
827
|
+
);
|
|
828
|
+
return [
|
|
829
|
+
...combine,
|
|
830
|
+
{
|
|
831
|
+
...base,
|
|
832
|
+
id: `${prefix}offsetPath`,
|
|
833
|
+
label: "Offset Path\u2026",
|
|
834
|
+
keywords: ["inset", "outset", "grow", "shrink", "expand", "contract"],
|
|
835
|
+
description: "Grows (positive distance) or shrinks (negative) the selected shapes.",
|
|
836
|
+
params: {
|
|
837
|
+
type: "object",
|
|
838
|
+
properties: {
|
|
839
|
+
distance: { type: "number", description: `Offset in canvas units (default ${_nullishCoalesce(options.defaultOffset, () => ( 10))}).` },
|
|
840
|
+
join: JOIN_PARAM,
|
|
841
|
+
miterLimit: { type: "number", description: "Miter limit (default 4)." },
|
|
842
|
+
ids: IDS_PARAMS.properties.ids
|
|
843
|
+
}
|
|
844
|
+
},
|
|
845
|
+
enabled: (_ctx, params) => controller.canOffsetShapes(_optionalChain([params, 'optionalAccess', _42 => _42.ids])) ? true : "Select an unlocked shape.",
|
|
846
|
+
run: (_ctx, params) => controller.offsetShapes(_nullishCoalesce(_nullishCoalesce(_optionalChain([params, 'optionalAccess', _43 => _43.distance]), () => ( options.defaultOffset)), () => ( 10)), _optionalChain([params, 'optionalAccess', _44 => _44.ids]), {
|
|
847
|
+
..._optionalChain([params, 'optionalAccess', _45 => _45.join]) ? { join: params.join } : {},
|
|
848
|
+
..._optionalChain([params, 'optionalAccess', _46 => _46.miterLimit]) !== void 0 ? { miterLimit: params.miterLimit } : {}
|
|
849
|
+
})
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
...base,
|
|
853
|
+
id: `${prefix}outlineStroke`,
|
|
854
|
+
label: "Outline Stroke",
|
|
855
|
+
keywords: ["expand stroke", "stroke to path", "convert stroke"],
|
|
856
|
+
description: "Converts the strokes of the selected shapes to filled shapes.",
|
|
857
|
+
params: {
|
|
858
|
+
type: "object",
|
|
859
|
+
properties: {
|
|
860
|
+
join: JOIN_PARAM,
|
|
861
|
+
cap: { type: "string", enum: ["butt", "round", "square"], description: "Line cap (default butt)." },
|
|
862
|
+
miterLimit: { type: "number", description: "Miter limit (default 4)." },
|
|
863
|
+
ids: IDS_PARAMS.properties.ids
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
enabled: (_ctx, params) => controller.canOutlineStroke(_optionalChain([params, 'optionalAccess', _47 => _47.ids])) ? true : "Select an unlocked shape with a stroke.",
|
|
867
|
+
run: (_ctx, params) => controller.outlineStroke(_optionalChain([params, 'optionalAccess', _48 => _48.ids]), {
|
|
868
|
+
..._optionalChain([params, 'optionalAccess', _49 => _49.join]) ? { join: params.join } : {},
|
|
869
|
+
..._optionalChain([params, 'optionalAccess', _50 => _50.cap]) ? { cap: params.cap } : {},
|
|
870
|
+
..._optionalChain([params, 'optionalAccess', _51 => _51.miterLimit]) !== void 0 ? { miterLimit: params.miterLimit } : {}
|
|
871
|
+
})
|
|
872
|
+
}
|
|
873
|
+
];
|
|
874
|
+
}
|
|
803
875
|
function createSpreadsheetCommands(controller, options = {}) {
|
|
804
876
|
const prefix = _nullishCoalesce(options.prefix, () => ( "spreadsheet."));
|
|
805
877
|
const scope = options.scope === null ? void 0 : _nullishCoalesce(options.scope, () => ( "spreadsheet"));
|
|
@@ -856,5 +928,6 @@ function createSpreadsheetCommands(controller, options = {}) {
|
|
|
856
928
|
|
|
857
929
|
|
|
858
930
|
|
|
859
|
-
|
|
931
|
+
|
|
932
|
+
exports.CommandRegistry = CommandRegistry; exports.CommandRegistryError = CommandRegistryError; exports.DEFAULT_HISTORY_SHORTCUTS = DEFAULT_HISTORY_SHORTCUTS; exports.DEFAULT_RECENT_LIMIT = DEFAULT_RECENT_LIMIT; exports.PLATFORMS = _chunk26BVFKHUcjs.PLATFORMS; exports.ShortcutMap = _chunk26BVFKHUcjs.ShortcutMap; exports.chordToString = _chunk26BVFKHUcjs.chordToString; exports.codeFromKey = _chunk26BVFKHUcjs.codeFromKey; exports.codeToKeyText = _chunk26BVFKHUcjs.codeToKeyText; exports.createArrangeCommands = createArrangeCommands; exports.createCommandPaletteController = createCommandPaletteController; exports.createCommandRegistry = createCommandRegistry; exports.createCommandToolProvider = createCommandToolProvider; exports.createHistoryCommands = createHistoryCommands; exports.createShapeCommands = createShapeCommands; exports.createSpreadsheetCommands = createSpreadsheetCommands; exports.defineCommand = defineCommand; exports.detectPlatform = _chunk26BVFKHUcjs.detectPlatform; exports.effectiveChord = _chunk26BVFKHUcjs.effectiveChord; exports.effectiveChordKey = _chunk26BVFKHUcjs.effectiveChordKey; exports.eventCode = _chunk26BVFKHUcjs.eventCode; exports.exactBonus = exactBonus; exports.formatShortcut = _chunk26BVFKHUcjs.formatShortcut; exports.fuzzyMatch = fuzzyMatch; exports.fuzzyScore = fuzzyScore; exports.isEditableEventTarget = _chunk26BVFKHUcjs.isEditableEventTarget; exports.keyToCode = _chunk26BVFKHUcjs.keyToCode; exports.matchHistoryShortcut = matchHistoryShortcut; exports.matchPaletteHotkey = matchPaletteHotkey; exports.normalizeChord = _chunk26BVFKHUcjs.normalizeChord; exports.parseChord = _chunk26BVFKHUcjs.parseChord; exports.pushRecent = pushRecent; exports.rankCommands = rankCommands; exports.shortcutCode = _chunk26BVFKHUcjs.shortcutCode; exports.toShortcutBindings = _chunk26BVFKHUcjs.toShortcutBindings;
|
|
860
933
|
//# sourceMappingURL=index.cjs.map
|