@sametalmdr/word-editor 0.2.0-alpha → 0.3.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +108 -70
  2. package/LICENSE +21 -21
  3. package/README.md +286 -247
  4. package/dist/canvas/box-clipboard.js +238 -238
  5. package/dist/canvas/box-selection.js +80 -80
  6. package/dist/canvas/canvas-editor.js +502 -502
  7. package/dist/canvas/controller.js +717 -717
  8. package/dist/canvas/geometry.js +131 -131
  9. package/dist/canvas/gesture.d.ts +0 -1
  10. package/dist/canvas/gesture.js +289 -289
  11. package/dist/canvas/handles.js +212 -212
  12. package/dist/canvas/menu.js +82 -82
  13. package/dist/canvas/mode-controller.js +81 -81
  14. package/dist/canvas/nudge.js +108 -108
  15. package/dist/canvas/overlay.js +149 -149
  16. package/dist/canvas/snap.js +132 -132
  17. package/dist/canvas/viewport.js +112 -112
  18. package/dist/canvas/z-order.js +171 -171
  19. package/dist/commands/alignment.js +150 -150
  20. package/dist/commands/box.js +272 -272
  21. package/dist/commands/clipboard.js +93 -93
  22. package/dist/commands/graphemes.js +112 -112
  23. package/dist/commands/marks.js +153 -153
  24. package/dist/commands/paragraph-props.js +176 -176
  25. package/dist/commands/size.js +368 -368
  26. package/dist/commands/text.js +291 -291
  27. package/dist/docx.d.ts +1 -0
  28. package/dist/docx.js +15 -13
  29. package/dist/editor/editing-host.js +434 -434
  30. package/dist/element.js +41 -41
  31. package/dist/fonts/OFL-Carlito.txt +94 -0
  32. package/dist/fonts/calibri-metrics.d.ts +27 -0
  33. package/dist/fonts/calibri-metrics.js +88 -0
  34. package/dist/history/history.js +433 -433
  35. package/dist/index.js +63 -63
  36. package/dist/input/beforeinput.js +224 -224
  37. package/dist/input/clipboard.js +107 -107
  38. package/dist/input/composition.js +341 -341
  39. package/dist/input/keymap.js +54 -54
  40. package/dist/io/ooxml/custgeom.js +72 -72
  41. package/dist/io/ooxml/drawingml.js +232 -232
  42. package/dist/io/ooxml/edits.js +67 -67
  43. package/dist/io/ooxml/export-splice.js +880 -880
  44. package/dist/io/ooxml/export.js +365 -365
  45. package/dist/io/ooxml/fill.d.ts +93 -8
  46. package/dist/io/ooxml/fill.js +618 -562
  47. package/dist/io/ooxml/fit.d.ts +252 -0
  48. package/dist/io/ooxml/fit.js +598 -0
  49. package/dist/io/ooxml/import-gate.js +200 -200
  50. package/dist/io/ooxml/import.js +691 -691
  51. package/dist/io/ooxml/inflate.js +126 -126
  52. package/dist/io/ooxml/media.js +87 -87
  53. package/dist/io/ooxml/ns.d.ts +2 -0
  54. package/dist/io/ooxml/ns.js +26 -21
  55. package/dist/io/ooxml/numbering.js +290 -290
  56. package/dist/io/ooxml/opc-write.js +171 -171
  57. package/dist/io/ooxml/provenance.js +93 -93
  58. package/dist/io/ooxml/recipe.js +708 -708
  59. package/dist/io/ooxml/relationships.js +59 -59
  60. package/dist/io/ooxml/run-fonts.d.ts +89 -0
  61. package/dist/io/ooxml/run-fonts.js +378 -0
  62. package/dist/io/ooxml/slots.d.ts +4 -0
  63. package/dist/io/ooxml/slots.js +582 -581
  64. package/dist/io/ooxml/styles-resolve.js +257 -257
  65. package/dist/io/ooxml/units.js +43 -43
  66. package/dist/io/ooxml/value.js +120 -120
  67. package/dist/io/ooxml/xml-scan.js +616 -616
  68. package/dist/io/ooxml/zip-container.js +228 -228
  69. package/dist/io/ooxml/zip-reject.js +106 -106
  70. package/dist/measure/placement.js +83 -83
  71. package/dist/measure/report.js +75 -75
  72. package/dist/model/ids.js +70 -70
  73. package/dist/model/normalize.js +146 -146
  74. package/dist/model/position.js +283 -283
  75. package/dist/model/transaction.js +209 -209
  76. package/dist/platform/capabilities.js +25 -25
  77. package/dist/react/index.js +21 -21
  78. package/dist/selection/controller.js +315 -315
  79. package/dist/selection/dom-map.js +334 -334
  80. package/dist/state/editor-state.js +109 -109
  81. package/dist/styles.css +66 -66
  82. package/package.json +94 -93
@@ -1,171 +1,171 @@
1
- // P6 piece (2): package writer (repack). A field-for-field JS port of
2
- // scripts/opc-bytes.py's write_package, which was measured 46/46 byte-identical
3
- // on a no-op round-trip across the real corpus. Untouched parts are copied
4
- // VERBATIM — original local header AND compressed bytes, never re-derived — so
5
- // Word's own zip signature (compress method, flag bits, versions, one shared
6
- // timestamp) survives; only the CHANGED part is re-compressed and only CRC-32,
7
- // the two size fields, local-header offsets and the EOCD's cd-size/offset are
8
- // rewritten. That verbatim copy is §3.5 at the package level: N parts changed =
9
- // exactly N parts differ.
10
- //
11
- // The changed part is re-compressed with deflate-raw via CompressionStream —
12
- // M5 (examples/static/m5-deflate-probe.html) confirmed CompressionStream
13
- // ("deflate-raw") exists, round-trips and is run-to-run DETERMINISTIC in
14
- // Chrome AND Firefox 154 (2026-08-30, user-run, ADR-0023). Its output is NOT
15
- // byte-identical to Python zlib level 9 (different encoder), but §3.5
16
- // compares the INFLATED content, not the compressed bytes, so that is fine.
17
- //
18
- // This piece turns bytes into a valid .docx; it does NOT orchestrate the full
19
- // export (finding dirty parts, running the splice) — that is a later piece. Its
20
- // input is the original package bytes, its parsed entries (piece 1), and
21
- // { partName -> new UNCOMPRESSED bytes }.
22
-
23
- import { crc32 } from "./inflate.js";
24
-
25
- // Field offsets — verbatim from scripts/opc-bytes.py's constants block.
26
- const LOC_CRC = 14;
27
- const LOC_CSIZE = 18;
28
- const LOC_USIZE = 22;
29
- const CEN_CRC = 16;
30
- const CEN_CSIZE = 20;
31
- const CEN_USIZE = 24;
32
- const CEN_LOCAL_OFFSET = 42;
33
- const EOCD_CD_SIZE = 12;
34
- const EOCD_CD_OFFSET = 16;
35
- const EOCD_SIG = [0x50, 0x4b, 0x05, 0x06]; // "PK\x05\x06"
36
- const METHOD_STORE = 0;
37
-
38
- /**
39
- * The EOCD's offset — scanned backwards over the largest legal window (a zip
40
- * comment up to 65535 bytes may follow it). Mirrors opc-bytes.py's _find_eocd.
41
- * @param {Uint8Array} bytes
42
- * @returns {number}
43
- */
44
- function findEocd(bytes) {
45
- const window = Math.min(bytes.length, 22 + 0xffff);
46
- for (let i = bytes.length - 4; i >= bytes.length - window; i -= 1) {
47
- if (bytes[i] === EOCD_SIG[0] && bytes[i + 1] === EOCD_SIG[1] && bytes[i + 2] === EOCD_SIG[2] && bytes[i + 3] === EOCD_SIG[3]) {
48
- return i;
49
- }
50
- }
51
- throw new Error("opc-write: no end-of-central-directory record — not a zip file");
52
- }
53
-
54
- /**
55
- * Raw DEFLATE of `bytes` via the platform CompressionStream (M5). Async.
56
- * @param {Uint8Array} bytes
57
- * @returns {Promise<Uint8Array>}
58
- */
59
- async function deflateRaw(bytes) {
60
- // ReadableStream + pipeThrough (as inflate.js does for DecompressionStream) —
61
- // avoids the writer.write chunk-type friction and collects via Response.
62
- const source = new ReadableStream({
63
- start(controller) {
64
- controller.enqueue(bytes);
65
- controller.close();
66
- },
67
- });
68
- const compressed = source.pipeThrough(new CompressionStream("deflate-raw"));
69
- return new Uint8Array(await new Response(compressed).arrayBuffer());
70
- }
71
-
72
- /**
73
- * LE u32 write in place.
74
- * @param {Uint8Array} buf
75
- * @param {number} off
76
- * @param {number} value
77
- * @returns {void}
78
- */
79
- function patchU32(buf, off, value) {
80
- buf[off] = value & 0xff;
81
- buf[off + 1] = (value >>> 8) & 0xff;
82
- buf[off + 2] = (value >>> 16) & 0xff;
83
- buf[off + 3] = (value >>> 24) & 0xff;
84
- }
85
-
86
- /** @param {Uint8Array[]} parts @param {number} total @returns {Uint8Array} */
87
- function concatBytes(parts, total) {
88
- const out = new Uint8Array(total);
89
- let at = 0;
90
- for (const p of parts) {
91
- out.set(p, at);
92
- at += p.length;
93
- }
94
- return out;
95
- }
96
-
97
- /**
98
- * Serialise the package, substituting new UNCOMPRESSED content for the part
99
- * names in `replacements`. Parts not named there are emitted as the exact
100
- * original bytes. `entries` must be the `parseZipContainer(packageBytes)` output
101
- * (piece 1's provenance), and `packageBytes` the buffer those offsets address.
102
- *
103
- * @param {Uint8Array} packageBytes the original .docx bytes
104
- * @param {import("./zip-container.js").ZipEntry[]} entries physical order (parseZipContainer sorts them)
105
- * @param {Record<string, Uint8Array>} replacements partName -> new uncompressed bytes
106
- * @returns {Promise<Uint8Array>}
107
- */
108
- export async function writePackage(packageBytes, entries, replacements) {
109
- const names = new Set(entries.map((e) => e.name));
110
- for (const name of Object.keys(replacements)) {
111
- if (!names.has(name)) {
112
- throw new Error(`opc-write: replacement names a part not in the package: ${JSON.stringify(name)}`);
113
- }
114
- }
115
-
116
- const eocdOffset = findEocd(packageBytes);
117
- const eocdBytes = packageBytes.subarray(eocdOffset); // to the end (any zip comment included)
118
-
119
- /** @type {Uint8Array[]} */
120
- const outParts = [];
121
- let outLen = 0;
122
- /** @type {Map<string, number>} */
123
- const newOffsets = new Map();
124
- /** @type {Map<string, [number, number, number]>} */
125
- const newMeta = new Map();
126
-
127
- for (const entry of entries) {
128
- newOffsets.set(entry.name, outLen);
129
- const localHeader = packageBytes.subarray(entry.localHeaderOffset, entry.compressedRange.start);
130
-
131
- if (Object.prototype.hasOwnProperty.call(replacements, entry.name)) {
132
- const content = replacements[entry.name];
133
- const blob = entry.method === METHOD_STORE ? content : await deflateRaw(content);
134
- const header = new Uint8Array(localHeader); // OWN copy — Buffer.slice() shares memory in Node, so patch a real copy
135
- const crc = crc32(content) >>> 0;
136
- patchU32(header, LOC_CRC, crc);
137
- patchU32(header, LOC_CSIZE, blob.length);
138
- patchU32(header, LOC_USIZE, content.length);
139
- outParts.push(header, blob);
140
- outLen += header.length + blob.length;
141
- newMeta.set(entry.name, [crc, blob.length, content.length]);
142
- } else {
143
- // Verbatim — the whole point (opc-bytes.py:267-269).
144
- const compressed = packageBytes.subarray(entry.compressedRange.start, entry.compressedRange.end);
145
- outParts.push(localHeader, compressed);
146
- outLen += localHeader.length + compressed.length;
147
- newMeta.set(entry.name, [entry.crc32 >>> 0, entry.compressedSize, entry.uncompressedSize]);
148
- }
149
- }
150
-
151
- const cdOffset = outLen;
152
- for (const entry of entries) {
153
- const meta = /** @type {[number, number, number]} */ (newMeta.get(entry.name));
154
- const central = new Uint8Array(packageBytes.subarray(entry.centralEntryRange.start, entry.centralEntryRange.end));
155
- patchU32(central, CEN_CRC, meta[0]);
156
- patchU32(central, CEN_CSIZE, meta[1]);
157
- patchU32(central, CEN_USIZE, meta[2]);
158
- patchU32(central, CEN_LOCAL_OFFSET, /** @type {number} */ (newOffsets.get(entry.name)));
159
- outParts.push(central);
160
- outLen += central.length;
161
- }
162
- const cdSize = outLen - cdOffset;
163
-
164
- const eocd = new Uint8Array(eocdBytes); // real copy (see header note — Buffer.slice() aliases)
165
- patchU32(eocd, EOCD_CD_SIZE, cdSize);
166
- patchU32(eocd, EOCD_CD_OFFSET, cdOffset);
167
- outParts.push(eocd);
168
- outLen += eocd.length;
169
-
170
- return concatBytes(outParts, outLen);
171
- }
1
+ // P6 piece (2): package writer (repack). A field-for-field JS port of
2
+ // scripts/opc-bytes.py's write_package, which was measured 46/46 byte-identical
3
+ // on a no-op round-trip across the real corpus. Untouched parts are copied
4
+ // VERBATIM — original local header AND compressed bytes, never re-derived — so
5
+ // Word's own zip signature (compress method, flag bits, versions, one shared
6
+ // timestamp) survives; only the CHANGED part is re-compressed and only CRC-32,
7
+ // the two size fields, local-header offsets and the EOCD's cd-size/offset are
8
+ // rewritten. That verbatim copy is §3.5 at the package level: N parts changed =
9
+ // exactly N parts differ.
10
+ //
11
+ // The changed part is re-compressed with deflate-raw via CompressionStream —
12
+ // M5 (examples/static/m5-deflate-probe.html) confirmed CompressionStream
13
+ // ("deflate-raw") exists, round-trips and is run-to-run DETERMINISTIC in
14
+ // Chrome AND Firefox 154 (2026-08-30, user-run, ADR-0023). Its output is NOT
15
+ // byte-identical to Python zlib level 9 (different encoder), but §3.5
16
+ // compares the INFLATED content, not the compressed bytes, so that is fine.
17
+ //
18
+ // This piece turns bytes into a valid .docx; it does NOT orchestrate the full
19
+ // export (finding dirty parts, running the splice) — that is a later piece. Its
20
+ // input is the original package bytes, its parsed entries (piece 1), and
21
+ // { partName -> new UNCOMPRESSED bytes }.
22
+
23
+ import { crc32 } from "./inflate.js";
24
+
25
+ // Field offsets — verbatim from scripts/opc-bytes.py's constants block.
26
+ const LOC_CRC = 14;
27
+ const LOC_CSIZE = 18;
28
+ const LOC_USIZE = 22;
29
+ const CEN_CRC = 16;
30
+ const CEN_CSIZE = 20;
31
+ const CEN_USIZE = 24;
32
+ const CEN_LOCAL_OFFSET = 42;
33
+ const EOCD_CD_SIZE = 12;
34
+ const EOCD_CD_OFFSET = 16;
35
+ const EOCD_SIG = [0x50, 0x4b, 0x05, 0x06]; // "PK\x05\x06"
36
+ const METHOD_STORE = 0;
37
+
38
+ /**
39
+ * The EOCD's offset — scanned backwards over the largest legal window (a zip
40
+ * comment up to 65535 bytes may follow it). Mirrors opc-bytes.py's _find_eocd.
41
+ * @param {Uint8Array} bytes
42
+ * @returns {number}
43
+ */
44
+ function findEocd(bytes) {
45
+ const window = Math.min(bytes.length, 22 + 0xffff);
46
+ for (let i = bytes.length - 4; i >= bytes.length - window; i -= 1) {
47
+ if (bytes[i] === EOCD_SIG[0] && bytes[i + 1] === EOCD_SIG[1] && bytes[i + 2] === EOCD_SIG[2] && bytes[i + 3] === EOCD_SIG[3]) {
48
+ return i;
49
+ }
50
+ }
51
+ throw new Error("opc-write: no end-of-central-directory record — not a zip file");
52
+ }
53
+
54
+ /**
55
+ * Raw DEFLATE of `bytes` via the platform CompressionStream (M5). Async.
56
+ * @param {Uint8Array} bytes
57
+ * @returns {Promise<Uint8Array>}
58
+ */
59
+ async function deflateRaw(bytes) {
60
+ // ReadableStream + pipeThrough (as inflate.js does for DecompressionStream) —
61
+ // avoids the writer.write chunk-type friction and collects via Response.
62
+ const source = new ReadableStream({
63
+ start(controller) {
64
+ controller.enqueue(bytes);
65
+ controller.close();
66
+ },
67
+ });
68
+ const compressed = source.pipeThrough(new CompressionStream("deflate-raw"));
69
+ return new Uint8Array(await new Response(compressed).arrayBuffer());
70
+ }
71
+
72
+ /**
73
+ * LE u32 write in place.
74
+ * @param {Uint8Array} buf
75
+ * @param {number} off
76
+ * @param {number} value
77
+ * @returns {void}
78
+ */
79
+ function patchU32(buf, off, value) {
80
+ buf[off] = value & 0xff;
81
+ buf[off + 1] = (value >>> 8) & 0xff;
82
+ buf[off + 2] = (value >>> 16) & 0xff;
83
+ buf[off + 3] = (value >>> 24) & 0xff;
84
+ }
85
+
86
+ /** @param {Uint8Array[]} parts @param {number} total @returns {Uint8Array} */
87
+ function concatBytes(parts, total) {
88
+ const out = new Uint8Array(total);
89
+ let at = 0;
90
+ for (const p of parts) {
91
+ out.set(p, at);
92
+ at += p.length;
93
+ }
94
+ return out;
95
+ }
96
+
97
+ /**
98
+ * Serialise the package, substituting new UNCOMPRESSED content for the part
99
+ * names in `replacements`. Parts not named there are emitted as the exact
100
+ * original bytes. `entries` must be the `parseZipContainer(packageBytes)` output
101
+ * (piece 1's provenance), and `packageBytes` the buffer those offsets address.
102
+ *
103
+ * @param {Uint8Array} packageBytes the original .docx bytes
104
+ * @param {import("./zip-container.js").ZipEntry[]} entries physical order (parseZipContainer sorts them)
105
+ * @param {Record<string, Uint8Array>} replacements partName -> new uncompressed bytes
106
+ * @returns {Promise<Uint8Array>}
107
+ */
108
+ export async function writePackage(packageBytes, entries, replacements) {
109
+ const names = new Set(entries.map((e) => e.name));
110
+ for (const name of Object.keys(replacements)) {
111
+ if (!names.has(name)) {
112
+ throw new Error(`opc-write: replacement names a part not in the package: ${JSON.stringify(name)}`);
113
+ }
114
+ }
115
+
116
+ const eocdOffset = findEocd(packageBytes);
117
+ const eocdBytes = packageBytes.subarray(eocdOffset); // to the end (any zip comment included)
118
+
119
+ /** @type {Uint8Array[]} */
120
+ const outParts = [];
121
+ let outLen = 0;
122
+ /** @type {Map<string, number>} */
123
+ const newOffsets = new Map();
124
+ /** @type {Map<string, [number, number, number]>} */
125
+ const newMeta = new Map();
126
+
127
+ for (const entry of entries) {
128
+ newOffsets.set(entry.name, outLen);
129
+ const localHeader = packageBytes.subarray(entry.localHeaderOffset, entry.compressedRange.start);
130
+
131
+ if (Object.prototype.hasOwnProperty.call(replacements, entry.name)) {
132
+ const content = replacements[entry.name];
133
+ const blob = entry.method === METHOD_STORE ? content : await deflateRaw(content);
134
+ const header = new Uint8Array(localHeader); // OWN copy — Buffer.slice() shares memory in Node, so patch a real copy
135
+ const crc = crc32(content) >>> 0;
136
+ patchU32(header, LOC_CRC, crc);
137
+ patchU32(header, LOC_CSIZE, blob.length);
138
+ patchU32(header, LOC_USIZE, content.length);
139
+ outParts.push(header, blob);
140
+ outLen += header.length + blob.length;
141
+ newMeta.set(entry.name, [crc, blob.length, content.length]);
142
+ } else {
143
+ // Verbatim — the whole point (opc-bytes.py:267-269).
144
+ const compressed = packageBytes.subarray(entry.compressedRange.start, entry.compressedRange.end);
145
+ outParts.push(localHeader, compressed);
146
+ outLen += localHeader.length + compressed.length;
147
+ newMeta.set(entry.name, [entry.crc32 >>> 0, entry.compressedSize, entry.uncompressedSize]);
148
+ }
149
+ }
150
+
151
+ const cdOffset = outLen;
152
+ for (const entry of entries) {
153
+ const meta = /** @type {[number, number, number]} */ (newMeta.get(entry.name));
154
+ const central = new Uint8Array(packageBytes.subarray(entry.centralEntryRange.start, entry.centralEntryRange.end));
155
+ patchU32(central, CEN_CRC, meta[0]);
156
+ patchU32(central, CEN_CSIZE, meta[1]);
157
+ patchU32(central, CEN_USIZE, meta[2]);
158
+ patchU32(central, CEN_LOCAL_OFFSET, /** @type {number} */ (newOffsets.get(entry.name)));
159
+ outParts.push(central);
160
+ outLen += central.length;
161
+ }
162
+ const cdSize = outLen - cdOffset;
163
+
164
+ const eocd = new Uint8Array(eocdBytes); // real copy (see header note — Buffer.slice() aliases)
165
+ patchU32(eocd, EOCD_CD_SIZE, cdSize);
166
+ patchU32(eocd, EOCD_CD_OFFSET, cdOffset);
167
+ outParts.push(eocd);
168
+ outLen += eocd.length;
169
+
170
+ return concatBytes(outParts, outLen);
171
+ }
@@ -1,93 +1,93 @@
1
- // anchorRanges: ContainerId/DecorationId -> the anchor's own byte range in
2
- // the untouched document.xml (ADR-0020 §3.5 / Karar 2). The wrapping
3
- // `w:drawing`/`mc:AlternateContent` is deliberately EXCLUDED — "N touched
4
- // boxes = exactly N anchor ranges" (§3.5), and including the wrapper would
5
- // pull `mc:Fallback`'s VML duplicate into the range too (ADR-0017
6
- // DOĞRULANMALI #5, still open — P6 must confirm this before relying on it).
7
- //
8
- // This module is intentionally thin: xml-scan.js's own elements ALREADY
9
- // carry `start`/`end`, so there is nothing to compute here beyond recording
10
- // which id owns which anchor's own range.
11
-
12
- /**
13
- * @typedef {Map<number, {start: number, end: number}>} AnchorRanges
14
- */
15
-
16
- /**
17
- * @returns {AnchorRanges}
18
- */
19
- export function createAnchorRanges() {
20
- return new Map();
21
- }
22
-
23
- /**
24
- * @param {AnchorRanges} ranges
25
- * @param {number} id
26
- * @param {import("./xml-scan.js").XmlElement} anchor
27
- * @returns {void}
28
- */
29
- export function recordAnchorRange(ranges, id, anchor) {
30
- ranges.set(id, { start: anchor.start, end: anchor.end });
31
- }
32
-
33
- /**
34
- * `ContainerId -> (PropRef -> the pPr/rPr byte range in the untouched
35
- * document.xml)`. ADR-0017's opaque blobs: the model stores only the opaque
36
- * `pPrRef`/`rPrRef` token, so export (P6) needs this to know WHERE to copy the
37
- * original bytes back from. Same `start`/`end` discipline as `anchorRanges` —
38
- * the elements already carry offsets, nothing is computed here.
39
- * @typedef {Map<number, Map<string, {start: number, end: number}>>} PropBlobs
40
- */
41
-
42
- /**
43
- * The §3.5 provenance an export must have surviving import. `packageBytes`/
44
- * `entries` come only from `importDocx` (the zip entry point); `convertDocument`
45
- * is handed an already-inflated document.xml and never sees the package, so it
46
- * omits them (both optional).
47
- *
48
- * 🔴 `dirty` stays UNUSED (2026-08-29, P6 parça 5 / Dilim B): it was reserved
49
- * for a dirty set carried on provenance itself, but both dirty sources that
50
- * actually shipped compute their own — `dirtyContainers`/`dirtyGeometry`
51
- * (value/identity compare, export.js) and the history layer's
52
- * `recordForwardDirty`/`verifyInverseDirty` (also export.js; the first pass
53
- * called it `accumulateHistoryDirty`, split in 0c67481) — neither reads or
54
- * writes this field.
55
- * Left in place rather than removed: changing `Provenance`'s shape is its own
56
- * decision, not a side effect of closing HIST7.
57
- * @typedef {object} Provenance
58
- * @property {Uint8Array} documentXmlBytes inflated word/document.xml — piece 3 splices this
59
- * @property {AnchorRanges} anchorRanges the same Map as the top-level `anchorRanges` field
60
- * @property {PropBlobs} propBlobs ADR-0017 opaque pPr/rPr blob ranges
61
- * @property {Set<number>} dirty UNUSED — see note above
62
- * @property {Uint8Array} [packageBytes] original .docx bytes — piece 2 repack byte-copies untouched parts (§3.5 step 1)
63
- * @property {import("./zip-container.js").ZipEntry[]} [entries] parsed zip entries (re-derivable from packageBytes)
64
- */
65
-
66
- /**
67
- * @returns {PropBlobs}
68
- */
69
- export function createPropBlobs() {
70
- return new Map();
71
- }
72
-
73
- /**
74
- * Records `containerId`'s `ref` -> the element's byte range. FIRST WINS: a ref
75
- * recurs iff its byte content is identical (interning is by content), so every
76
- * occurrence's bytes reproduce the same blob — the first range is as good as
77
- * any, and keeping it makes the table deterministic.
78
- * @param {PropBlobs} propBlobs
79
- * @param {number} containerId
80
- * @param {string} ref
81
- * @param {import("./xml-scan.js").XmlElement} el
82
- * @returns {void}
83
- */
84
- export function recordPropBlob(propBlobs, containerId, ref, el) {
85
- let byRef = propBlobs.get(containerId);
86
- if (byRef === undefined) {
87
- byRef = new Map();
88
- propBlobs.set(containerId, byRef);
89
- }
90
- if (!byRef.has(ref)) {
91
- byRef.set(ref, { start: el.start, end: el.end });
92
- }
93
- }
1
+ // anchorRanges: ContainerId/DecorationId -> the anchor's own byte range in
2
+ // the untouched document.xml (ADR-0020 §3.5 / Karar 2). The wrapping
3
+ // `w:drawing`/`mc:AlternateContent` is deliberately EXCLUDED — "N touched
4
+ // boxes = exactly N anchor ranges" (§3.5), and including the wrapper would
5
+ // pull `mc:Fallback`'s VML duplicate into the range too (ADR-0017
6
+ // DOĞRULANMALI #5, still open — P6 must confirm this before relying on it).
7
+ //
8
+ // This module is intentionally thin: xml-scan.js's own elements ALREADY
9
+ // carry `start`/`end`, so there is nothing to compute here beyond recording
10
+ // which id owns which anchor's own range.
11
+
12
+ /**
13
+ * @typedef {Map<number, {start: number, end: number}>} AnchorRanges
14
+ */
15
+
16
+ /**
17
+ * @returns {AnchorRanges}
18
+ */
19
+ export function createAnchorRanges() {
20
+ return new Map();
21
+ }
22
+
23
+ /**
24
+ * @param {AnchorRanges} ranges
25
+ * @param {number} id
26
+ * @param {import("./xml-scan.js").XmlElement} anchor
27
+ * @returns {void}
28
+ */
29
+ export function recordAnchorRange(ranges, id, anchor) {
30
+ ranges.set(id, { start: anchor.start, end: anchor.end });
31
+ }
32
+
33
+ /**
34
+ * `ContainerId -> (PropRef -> the pPr/rPr byte range in the untouched
35
+ * document.xml)`. ADR-0017's opaque blobs: the model stores only the opaque
36
+ * `pPrRef`/`rPrRef` token, so export (P6) needs this to know WHERE to copy the
37
+ * original bytes back from. Same `start`/`end` discipline as `anchorRanges` —
38
+ * the elements already carry offsets, nothing is computed here.
39
+ * @typedef {Map<number, Map<string, {start: number, end: number}>>} PropBlobs
40
+ */
41
+
42
+ /**
43
+ * The §3.5 provenance an export must have surviving import. `packageBytes`/
44
+ * `entries` come only from `importDocx` (the zip entry point); `convertDocument`
45
+ * is handed an already-inflated document.xml and never sees the package, so it
46
+ * omits them (both optional).
47
+ *
48
+ * 🔴 `dirty` stays UNUSED (2026-08-29, P6 parça 5 / Dilim B): it was reserved
49
+ * for a dirty set carried on provenance itself, but both dirty sources that
50
+ * actually shipped compute their own — `dirtyContainers`/`dirtyGeometry`
51
+ * (value/identity compare, export.js) and the history layer's
52
+ * `recordForwardDirty`/`verifyInverseDirty` (also export.js; the first pass
53
+ * called it `accumulateHistoryDirty`, split in 0c67481) — neither reads or
54
+ * writes this field.
55
+ * Left in place rather than removed: changing `Provenance`'s shape is its own
56
+ * decision, not a side effect of closing HIST7.
57
+ * @typedef {object} Provenance
58
+ * @property {Uint8Array} documentXmlBytes inflated word/document.xml — piece 3 splices this
59
+ * @property {AnchorRanges} anchorRanges the same Map as the top-level `anchorRanges` field
60
+ * @property {PropBlobs} propBlobs ADR-0017 opaque pPr/rPr blob ranges
61
+ * @property {Set<number>} dirty UNUSED — see note above
62
+ * @property {Uint8Array} [packageBytes] original .docx bytes — piece 2 repack byte-copies untouched parts (§3.5 step 1)
63
+ * @property {import("./zip-container.js").ZipEntry[]} [entries] parsed zip entries (re-derivable from packageBytes)
64
+ */
65
+
66
+ /**
67
+ * @returns {PropBlobs}
68
+ */
69
+ export function createPropBlobs() {
70
+ return new Map();
71
+ }
72
+
73
+ /**
74
+ * Records `containerId`'s `ref` -> the element's byte range. FIRST WINS: a ref
75
+ * recurs iff its byte content is identical (interning is by content), so every
76
+ * occurrence's bytes reproduce the same blob — the first range is as good as
77
+ * any, and keeping it makes the table deterministic.
78
+ * @param {PropBlobs} propBlobs
79
+ * @param {number} containerId
80
+ * @param {string} ref
81
+ * @param {import("./xml-scan.js").XmlElement} el
82
+ * @returns {void}
83
+ */
84
+ export function recordPropBlob(propBlobs, containerId, ref, el) {
85
+ let byRef = propBlobs.get(containerId);
86
+ if (byRef === undefined) {
87
+ byRef = new Map();
88
+ propBlobs.set(containerId, byRef);
89
+ }
90
+ if (!byRef.has(ref)) {
91
+ byRef.set(ref, { start: el.start, end: el.end });
92
+ }
93
+ }