@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,228 +1,228 @@
1
- // End of central directory -> central directory -> local headers, in one
2
- // pass. Pure and synchronous — no decompression happens here (that is
3
- // inflate.js's job, the one async file in this directory) and no XML is
4
- // read (that is a later parça's job). This file only answers "which zip
5
- // members exist, and where are their compressed bytes" — ported from
6
- // scripts/opc-bytes.py's read_package(), same field offsets, same
7
- // rejections (via zip-reject.js), same physical-order sort at the end.
8
- //
9
- // Every multi-byte field in a zip file is little-endian — DataView's
10
- // `littleEndian=true` argument on every read is not optional decoration,
11
- // it is the one thing that makes these offsets mean what the zip spec says
12
- // they mean.
13
-
14
- import {
15
- rejectZip64Locator,
16
- rejectZip64EocdSentinel,
17
- rejectEntryZip64Sentinel,
18
- rejectEntryEncrypted,
19
- rejectEntryDataDescriptor,
20
- rejectEntryUnknownMethod,
21
- } from "./zip-reject.js";
22
-
23
- const EOCD_SIG = [0x50, 0x4b, 0x05, 0x06]; // "PK\x05\x06"
24
- const CENTRAL_SIG = [0x50, 0x4b, 0x01, 0x02]; // "PK\x01\x02"
25
- const LOCAL_SIG = [0x50, 0x4b, 0x03, 0x04]; // "PK\x03\x04"
26
-
27
- const LOC_CRC = 14;
28
- const LOC_CSIZE = 18;
29
- const LOC_USIZE = 22;
30
- const LOC_NAMELEN = 26;
31
- const LOC_EXTRALEN = 28;
32
- const LOC_FIXED = 30;
33
-
34
- const CEN_FLAGS = 8;
35
- const CEN_METHOD = 10;
36
- const CEN_CRC = 16;
37
- const CEN_CSIZE = 20;
38
- const CEN_USIZE = 24;
39
- const CEN_NAMELEN = 28;
40
- const CEN_EXTRALEN = 30;
41
- const CEN_COMMENTLEN = 32;
42
- const CEN_LOCAL_OFFSET = 42;
43
- const CEN_FIXED = 46;
44
-
45
- const EOCD_TOTAL_ENTRIES = 10;
46
- const EOCD_CD_SIZE = 12;
47
- const EOCD_CD_OFFSET = 16;
48
-
49
- /**
50
- * @typedef {object} ZipEntry
51
- * @property {string} name
52
- * @property {number} method 0 = store, 8 = deflate (already checked, only these two survive)
53
- * @property {number} compressedSize
54
- * @property {number} uncompressedSize
55
- * @property {number} crc32 the RECORDED crc, not yet verified against decompressed content — inflate.js does that
56
- * @property {number} localHeaderOffset the local header starts here; it ends at
57
- * `compressedRange.start`, so the local-header bytes are that slice
58
- * @property {{start: number, end: number}} compressedRange byte offsets into the ORIGINAL buffer passed to parseZipContainer
59
- * @property {{start: number, end: number}} centralEntryRange this entry's own
60
- * central-directory record's byte range — the repack writer (opc-write.js)
61
- * re-emits it verbatim, patching only the numeric fields (opc-bytes.py parity)
62
- */
63
-
64
- /**
65
- * @param {Uint8Array} bytes
66
- * @param {number} offset
67
- * @param {number[]} sig
68
- * @returns {boolean}
69
- */
70
- function matchSig(bytes, offset, sig) {
71
- for (let i = 0; i < sig.length; i++) {
72
- if (bytes[offset + i] !== sig[i]) return false;
73
- }
74
- return true;
75
- }
76
-
77
- /**
78
- * @param {Uint8Array} bytes
79
- * @param {number[]} sig
80
- * @param {number} searchStart
81
- * @returns {number}
82
- */
83
- function findSigBackwards(bytes, sig, searchStart) {
84
- outer: for (let i = searchStart; i >= 0; i--) {
85
- for (let j = 0; j < sig.length; j++) {
86
- if (bytes[i + j] !== sig[j]) continue outer;
87
- }
88
- return i;
89
- }
90
- return -1;
91
- }
92
-
93
- /**
94
- * The EOCD sits at the end, but a zip comment (up to 65535 bytes) may follow
95
- * it — scan backwards over the largest legal window, matching
96
- * opc-bytes.py's `_find_eocd`.
97
- * @param {Uint8Array} bytes
98
- * @returns {number}
99
- */
100
- function findEocd(bytes) {
101
- const window = Math.min(bytes.length, 22 + 0xffff);
102
- const searchStart = bytes.length - window;
103
- const idx = findSigBackwards(bytes, EOCD_SIG, bytes.length - 4);
104
- if (idx < 0 || idx < searchStart) {
105
- throw new Error("zip: no end-of-central-directory record — not a zip file");
106
- }
107
- return idx;
108
- }
109
-
110
- /**
111
- * @param {Uint8Array} bytes the full, undecompressed .docx/.zip file contents
112
- * @returns {ZipEntry[]} in PHYSICAL order (by local header offset), matching opc-bytes.py's own final sort
113
- */
114
- export function parseZipContainer(bytes) {
115
- const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
116
- const eocdOffset = findEocd(bytes);
117
-
118
- rejectZip64Locator(bytes, eocdOffset);
119
-
120
- const totalEntries = view.getUint16(eocdOffset + EOCD_TOTAL_ENTRIES, true);
121
- const cdSize = view.getUint32(eocdOffset + EOCD_CD_SIZE, true);
122
- const cdOffset = view.getUint32(eocdOffset + EOCD_CD_OFFSET, true);
123
- const thisDisk = view.getUint16(eocdOffset + 4, true);
124
- const diskWithCd = view.getUint16(eocdOffset + 6, true);
125
- rejectZip64EocdSentinel({ diskWithCd, thisDisk, cdOffset, cdSize });
126
-
127
- /** @type {ZipEntry[]} */
128
- const entries = [];
129
- let pos = cdOffset;
130
- for (let i = 0; i < totalEntries; i++) {
131
- if (!matchSig(bytes, pos, CENTRAL_SIG)) {
132
- throw new Error(`zip: central directory entry ${i} has a bad signature at offset ${pos}`);
133
- }
134
- const nameLen = view.getUint16(pos + CEN_NAMELEN, true);
135
- const extraLen = view.getUint16(pos + CEN_EXTRALEN, true);
136
- const commentLen = view.getUint16(pos + CEN_COMMENTLEN, true);
137
-
138
- const name = new TextDecoder("utf-8", { fatal: true }).decode(
139
- bytes.subarray(pos + CEN_FIXED, pos + CEN_FIXED + nameLen)
140
- );
141
- const flags = view.getUint16(pos + CEN_FLAGS, true);
142
- const method = view.getUint16(pos + CEN_METHOD, true);
143
- const crc32 = view.getUint32(pos + CEN_CRC, true);
144
- const csize = view.getUint32(pos + CEN_CSIZE, true);
145
- const usize = view.getUint32(pos + CEN_USIZE, true);
146
- const localOffset = view.getUint32(pos + CEN_LOCAL_OFFSET, true);
147
-
148
- rejectEntryEncrypted(name, flags);
149
- rejectEntryDataDescriptor(name, flags);
150
- rejectEntryUnknownMethod(name, method);
151
- rejectEntryZip64Sentinel(name, csize, usize, localOffset);
152
-
153
- if (!matchSig(bytes, localOffset, LOCAL_SIG)) {
154
- throw new Error(`zip: ${JSON.stringify(name)}: bad local header signature at offset ${localOffset}`);
155
- }
156
- const locNameLen = view.getUint16(localOffset + LOC_NAMELEN, true);
157
- const locExtraLen = view.getUint16(localOffset + LOC_EXTRALEN, true);
158
- const headerLen = LOC_FIXED + locNameLen + locExtraLen;
159
- const dataStart = localOffset + headerLen;
160
- const dataEnd = dataStart + csize;
161
- if (dataEnd > bytes.length) {
162
- throw new Error(`zip: ${JSON.stringify(name)}: truncated compressed data (need ${csize} bytes at ${dataStart}, file ends at ${bytes.length})`);
163
- }
164
-
165
- const centralEntryEnd = pos + CEN_FIXED + nameLen + extraLen + commentLen;
166
- entries.push({
167
- name,
168
- method,
169
- compressedSize: csize,
170
- uncompressedSize: usize,
171
- crc32,
172
- localHeaderOffset: localOffset,
173
- compressedRange: { start: dataStart, end: dataEnd },
174
- centralEntryRange: { start: pos, end: centralEntryEnd },
175
- });
176
-
177
- pos = centralEntryEnd;
178
- }
179
-
180
- entries.sort((a, b) => a.localHeaderOffset - b.localHeaderOffset);
181
- return entries;
182
- }
183
-
184
- /**
185
- * ADR-0020 does not design real OPC relationship resolution yet ([Content_
186
- * Types].xml / _rels/.rels parsing is XML parsing, out of scope for the zip
187
- * layer). Measured 2026-08-27: all 46 corpus documents' _rels/.rels resolve
188
- * their officeDocument relationship to exactly "word/document.xml" — so
189
- * that fixed path is used AS A NAMED, VISIBLE ASSUMPTION, not silently. A
190
- * document that does not use it is REJECTED with its own specific message
191
- * (ADR-0020 DOĞRULANMALI: real _rels resolution belongs in import.js, the
192
- * orchestrator that will have XML-reading capability once xml-scan.js
193
- * exists — this function is a placeholder for that, not a substitute).
194
- * @param {ZipEntry[]} entries
195
- * @returns {ZipEntry}
196
- */
197
- export function resolveMainDocumentPart(entries) {
198
- const found = entries.find((e) => e.name === "word/document.xml");
199
- if (found === undefined) {
200
- throw new Error(
201
- "zip: this package does not use the fixed path \"word/document.xml\" for its main document part — " +
202
- "real _rels/.rels relationship resolution is not implemented yet (ADR-0020 DOĞRULANMALI, deferred " +
203
- "to import.js). Measured 46/46 real corpus documents use this fixed path; this one does not."
204
- );
205
- }
206
- return found;
207
- }
208
-
209
- /**
210
- * Same fixed-path assumption as `resolveMainDocumentPart`, for the styles
211
- * part. Measured 2026-08-27: all 46 real corpus documents use exactly
212
- * "word/styles.xml" — real `word/document.xml`'s own `_rels` would point to
213
- * it via the styles relationship type, but that resolution is the same
214
- * deferred XML-reading work (ADR-0020 DOĞRULANMALI #16, `import.js`).
215
- * @param {ZipEntry[]} entries
216
- * @returns {ZipEntry}
217
- */
218
- export function resolveStylesPart(entries) {
219
- const found = entries.find((e) => e.name === "word/styles.xml");
220
- if (found === undefined) {
221
- throw new Error(
222
- "zip: this package does not use the fixed path \"word/styles.xml\" for its styles part — " +
223
- "real _rels/.rels relationship resolution is not implemented yet (ADR-0020 DOĞRULANMALI, deferred " +
224
- "to import.js). Measured 46/46 real corpus documents use this fixed path; this one does not."
225
- );
226
- }
227
- return found;
228
- }
1
+ // End of central directory -> central directory -> local headers, in one
2
+ // pass. Pure and synchronous — no decompression happens here (that is
3
+ // inflate.js's job, the one async file in this directory) and no XML is
4
+ // read (that is a later parça's job). This file only answers "which zip
5
+ // members exist, and where are their compressed bytes" — ported from
6
+ // scripts/opc-bytes.py's read_package(), same field offsets, same
7
+ // rejections (via zip-reject.js), same physical-order sort at the end.
8
+ //
9
+ // Every multi-byte field in a zip file is little-endian — DataView's
10
+ // `littleEndian=true` argument on every read is not optional decoration,
11
+ // it is the one thing that makes these offsets mean what the zip spec says
12
+ // they mean.
13
+
14
+ import {
15
+ rejectZip64Locator,
16
+ rejectZip64EocdSentinel,
17
+ rejectEntryZip64Sentinel,
18
+ rejectEntryEncrypted,
19
+ rejectEntryDataDescriptor,
20
+ rejectEntryUnknownMethod,
21
+ } from "./zip-reject.js";
22
+
23
+ const EOCD_SIG = [0x50, 0x4b, 0x05, 0x06]; // "PK\x05\x06"
24
+ const CENTRAL_SIG = [0x50, 0x4b, 0x01, 0x02]; // "PK\x01\x02"
25
+ const LOCAL_SIG = [0x50, 0x4b, 0x03, 0x04]; // "PK\x03\x04"
26
+
27
+ const LOC_CRC = 14;
28
+ const LOC_CSIZE = 18;
29
+ const LOC_USIZE = 22;
30
+ const LOC_NAMELEN = 26;
31
+ const LOC_EXTRALEN = 28;
32
+ const LOC_FIXED = 30;
33
+
34
+ const CEN_FLAGS = 8;
35
+ const CEN_METHOD = 10;
36
+ const CEN_CRC = 16;
37
+ const CEN_CSIZE = 20;
38
+ const CEN_USIZE = 24;
39
+ const CEN_NAMELEN = 28;
40
+ const CEN_EXTRALEN = 30;
41
+ const CEN_COMMENTLEN = 32;
42
+ const CEN_LOCAL_OFFSET = 42;
43
+ const CEN_FIXED = 46;
44
+
45
+ const EOCD_TOTAL_ENTRIES = 10;
46
+ const EOCD_CD_SIZE = 12;
47
+ const EOCD_CD_OFFSET = 16;
48
+
49
+ /**
50
+ * @typedef {object} ZipEntry
51
+ * @property {string} name
52
+ * @property {number} method 0 = store, 8 = deflate (already checked, only these two survive)
53
+ * @property {number} compressedSize
54
+ * @property {number} uncompressedSize
55
+ * @property {number} crc32 the RECORDED crc, not yet verified against decompressed content — inflate.js does that
56
+ * @property {number} localHeaderOffset the local header starts here; it ends at
57
+ * `compressedRange.start`, so the local-header bytes are that slice
58
+ * @property {{start: number, end: number}} compressedRange byte offsets into the ORIGINAL buffer passed to parseZipContainer
59
+ * @property {{start: number, end: number}} centralEntryRange this entry's own
60
+ * central-directory record's byte range — the repack writer (opc-write.js)
61
+ * re-emits it verbatim, patching only the numeric fields (opc-bytes.py parity)
62
+ */
63
+
64
+ /**
65
+ * @param {Uint8Array} bytes
66
+ * @param {number} offset
67
+ * @param {number[]} sig
68
+ * @returns {boolean}
69
+ */
70
+ function matchSig(bytes, offset, sig) {
71
+ for (let i = 0; i < sig.length; i++) {
72
+ if (bytes[offset + i] !== sig[i]) return false;
73
+ }
74
+ return true;
75
+ }
76
+
77
+ /**
78
+ * @param {Uint8Array} bytes
79
+ * @param {number[]} sig
80
+ * @param {number} searchStart
81
+ * @returns {number}
82
+ */
83
+ function findSigBackwards(bytes, sig, searchStart) {
84
+ outer: for (let i = searchStart; i >= 0; i--) {
85
+ for (let j = 0; j < sig.length; j++) {
86
+ if (bytes[i + j] !== sig[j]) continue outer;
87
+ }
88
+ return i;
89
+ }
90
+ return -1;
91
+ }
92
+
93
+ /**
94
+ * The EOCD sits at the end, but a zip comment (up to 65535 bytes) may follow
95
+ * it — scan backwards over the largest legal window, matching
96
+ * opc-bytes.py's `_find_eocd`.
97
+ * @param {Uint8Array} bytes
98
+ * @returns {number}
99
+ */
100
+ function findEocd(bytes) {
101
+ const window = Math.min(bytes.length, 22 + 0xffff);
102
+ const searchStart = bytes.length - window;
103
+ const idx = findSigBackwards(bytes, EOCD_SIG, bytes.length - 4);
104
+ if (idx < 0 || idx < searchStart) {
105
+ throw new Error("zip: no end-of-central-directory record — not a zip file");
106
+ }
107
+ return idx;
108
+ }
109
+
110
+ /**
111
+ * @param {Uint8Array} bytes the full, undecompressed .docx/.zip file contents
112
+ * @returns {ZipEntry[]} in PHYSICAL order (by local header offset), matching opc-bytes.py's own final sort
113
+ */
114
+ export function parseZipContainer(bytes) {
115
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
116
+ const eocdOffset = findEocd(bytes);
117
+
118
+ rejectZip64Locator(bytes, eocdOffset);
119
+
120
+ const totalEntries = view.getUint16(eocdOffset + EOCD_TOTAL_ENTRIES, true);
121
+ const cdSize = view.getUint32(eocdOffset + EOCD_CD_SIZE, true);
122
+ const cdOffset = view.getUint32(eocdOffset + EOCD_CD_OFFSET, true);
123
+ const thisDisk = view.getUint16(eocdOffset + 4, true);
124
+ const diskWithCd = view.getUint16(eocdOffset + 6, true);
125
+ rejectZip64EocdSentinel({ diskWithCd, thisDisk, cdOffset, cdSize });
126
+
127
+ /** @type {ZipEntry[]} */
128
+ const entries = [];
129
+ let pos = cdOffset;
130
+ for (let i = 0; i < totalEntries; i++) {
131
+ if (!matchSig(bytes, pos, CENTRAL_SIG)) {
132
+ throw new Error(`zip: central directory entry ${i} has a bad signature at offset ${pos}`);
133
+ }
134
+ const nameLen = view.getUint16(pos + CEN_NAMELEN, true);
135
+ const extraLen = view.getUint16(pos + CEN_EXTRALEN, true);
136
+ const commentLen = view.getUint16(pos + CEN_COMMENTLEN, true);
137
+
138
+ const name = new TextDecoder("utf-8", { fatal: true }).decode(
139
+ bytes.subarray(pos + CEN_FIXED, pos + CEN_FIXED + nameLen)
140
+ );
141
+ const flags = view.getUint16(pos + CEN_FLAGS, true);
142
+ const method = view.getUint16(pos + CEN_METHOD, true);
143
+ const crc32 = view.getUint32(pos + CEN_CRC, true);
144
+ const csize = view.getUint32(pos + CEN_CSIZE, true);
145
+ const usize = view.getUint32(pos + CEN_USIZE, true);
146
+ const localOffset = view.getUint32(pos + CEN_LOCAL_OFFSET, true);
147
+
148
+ rejectEntryEncrypted(name, flags);
149
+ rejectEntryDataDescriptor(name, flags);
150
+ rejectEntryUnknownMethod(name, method);
151
+ rejectEntryZip64Sentinel(name, csize, usize, localOffset);
152
+
153
+ if (!matchSig(bytes, localOffset, LOCAL_SIG)) {
154
+ throw new Error(`zip: ${JSON.stringify(name)}: bad local header signature at offset ${localOffset}`);
155
+ }
156
+ const locNameLen = view.getUint16(localOffset + LOC_NAMELEN, true);
157
+ const locExtraLen = view.getUint16(localOffset + LOC_EXTRALEN, true);
158
+ const headerLen = LOC_FIXED + locNameLen + locExtraLen;
159
+ const dataStart = localOffset + headerLen;
160
+ const dataEnd = dataStart + csize;
161
+ if (dataEnd > bytes.length) {
162
+ throw new Error(`zip: ${JSON.stringify(name)}: truncated compressed data (need ${csize} bytes at ${dataStart}, file ends at ${bytes.length})`);
163
+ }
164
+
165
+ const centralEntryEnd = pos + CEN_FIXED + nameLen + extraLen + commentLen;
166
+ entries.push({
167
+ name,
168
+ method,
169
+ compressedSize: csize,
170
+ uncompressedSize: usize,
171
+ crc32,
172
+ localHeaderOffset: localOffset,
173
+ compressedRange: { start: dataStart, end: dataEnd },
174
+ centralEntryRange: { start: pos, end: centralEntryEnd },
175
+ });
176
+
177
+ pos = centralEntryEnd;
178
+ }
179
+
180
+ entries.sort((a, b) => a.localHeaderOffset - b.localHeaderOffset);
181
+ return entries;
182
+ }
183
+
184
+ /**
185
+ * ADR-0020 does not design real OPC relationship resolution yet ([Content_
186
+ * Types].xml / _rels/.rels parsing is XML parsing, out of scope for the zip
187
+ * layer). Measured 2026-08-27: all 46 corpus documents' _rels/.rels resolve
188
+ * their officeDocument relationship to exactly "word/document.xml" — so
189
+ * that fixed path is used AS A NAMED, VISIBLE ASSUMPTION, not silently. A
190
+ * document that does not use it is REJECTED with its own specific message
191
+ * (ADR-0020 DOĞRULANMALI: real _rels resolution belongs in import.js, the
192
+ * orchestrator that will have XML-reading capability once xml-scan.js
193
+ * exists — this function is a placeholder for that, not a substitute).
194
+ * @param {ZipEntry[]} entries
195
+ * @returns {ZipEntry}
196
+ */
197
+ export function resolveMainDocumentPart(entries) {
198
+ const found = entries.find((e) => e.name === "word/document.xml");
199
+ if (found === undefined) {
200
+ throw new Error(
201
+ "zip: this package does not use the fixed path \"word/document.xml\" for its main document part — " +
202
+ "real _rels/.rels relationship resolution is not implemented yet (ADR-0020 DOĞRULANMALI, deferred " +
203
+ "to import.js). Measured 46/46 real corpus documents use this fixed path; this one does not."
204
+ );
205
+ }
206
+ return found;
207
+ }
208
+
209
+ /**
210
+ * Same fixed-path assumption as `resolveMainDocumentPart`, for the styles
211
+ * part. Measured 2026-08-27: all 46 real corpus documents use exactly
212
+ * "word/styles.xml" — real `word/document.xml`'s own `_rels` would point to
213
+ * it via the styles relationship type, but that resolution is the same
214
+ * deferred XML-reading work (ADR-0020 DOĞRULANMALI #16, `import.js`).
215
+ * @param {ZipEntry[]} entries
216
+ * @returns {ZipEntry}
217
+ */
218
+ export function resolveStylesPart(entries) {
219
+ const found = entries.find((e) => e.name === "word/styles.xml");
220
+ if (found === undefined) {
221
+ throw new Error(
222
+ "zip: this package does not use the fixed path \"word/styles.xml\" for its styles part — " +
223
+ "real _rels/.rels relationship resolution is not implemented yet (ADR-0020 DOĞRULANMALI, deferred " +
224
+ "to import.js). Measured 46/46 real corpus documents use this fixed path; this one does not."
225
+ );
226
+ }
227
+ return found;
228
+ }