@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.
- package/CHANGELOG.md +108 -70
- package/LICENSE +21 -21
- package/README.md +286 -247
- package/dist/canvas/box-clipboard.js +238 -238
- package/dist/canvas/box-selection.js +80 -80
- package/dist/canvas/canvas-editor.js +502 -502
- package/dist/canvas/controller.js +717 -717
- package/dist/canvas/geometry.js +131 -131
- package/dist/canvas/gesture.d.ts +0 -1
- package/dist/canvas/gesture.js +289 -289
- package/dist/canvas/handles.js +212 -212
- package/dist/canvas/menu.js +82 -82
- package/dist/canvas/mode-controller.js +81 -81
- package/dist/canvas/nudge.js +108 -108
- package/dist/canvas/overlay.js +149 -149
- package/dist/canvas/snap.js +132 -132
- package/dist/canvas/viewport.js +112 -112
- package/dist/canvas/z-order.js +171 -171
- package/dist/commands/alignment.js +150 -150
- package/dist/commands/box.js +272 -272
- package/dist/commands/clipboard.js +93 -93
- package/dist/commands/graphemes.js +112 -112
- package/dist/commands/marks.js +153 -153
- package/dist/commands/paragraph-props.js +176 -176
- package/dist/commands/size.js +368 -368
- package/dist/commands/text.js +291 -291
- package/dist/docx.d.ts +1 -0
- package/dist/docx.js +15 -13
- package/dist/editor/editing-host.js +434 -434
- package/dist/element.js +41 -41
- package/dist/fonts/OFL-Carlito.txt +94 -0
- package/dist/fonts/calibri-metrics.d.ts +27 -0
- package/dist/fonts/calibri-metrics.js +88 -0
- package/dist/history/history.js +433 -433
- package/dist/index.js +63 -63
- package/dist/input/beforeinput.js +224 -224
- package/dist/input/clipboard.js +107 -107
- package/dist/input/composition.js +341 -341
- package/dist/input/keymap.js +54 -54
- package/dist/io/ooxml/custgeom.js +72 -72
- package/dist/io/ooxml/drawingml.js +232 -232
- package/dist/io/ooxml/edits.js +67 -67
- package/dist/io/ooxml/export-splice.js +880 -880
- package/dist/io/ooxml/export.js +365 -365
- package/dist/io/ooxml/fill.d.ts +93 -8
- package/dist/io/ooxml/fill.js +618 -562
- package/dist/io/ooxml/fit.d.ts +252 -0
- package/dist/io/ooxml/fit.js +598 -0
- package/dist/io/ooxml/import-gate.js +200 -200
- package/dist/io/ooxml/import.js +691 -691
- package/dist/io/ooxml/inflate.js +126 -126
- package/dist/io/ooxml/media.js +87 -87
- package/dist/io/ooxml/ns.d.ts +2 -0
- package/dist/io/ooxml/ns.js +26 -21
- package/dist/io/ooxml/numbering.js +290 -290
- package/dist/io/ooxml/opc-write.js +171 -171
- package/dist/io/ooxml/provenance.js +93 -93
- package/dist/io/ooxml/recipe.js +708 -708
- package/dist/io/ooxml/relationships.js +59 -59
- package/dist/io/ooxml/run-fonts.d.ts +89 -0
- package/dist/io/ooxml/run-fonts.js +378 -0
- package/dist/io/ooxml/slots.d.ts +4 -0
- package/dist/io/ooxml/slots.js +582 -581
- package/dist/io/ooxml/styles-resolve.js +257 -257
- package/dist/io/ooxml/units.js +43 -43
- package/dist/io/ooxml/value.js +120 -120
- package/dist/io/ooxml/xml-scan.js +616 -616
- package/dist/io/ooxml/zip-container.js +228 -228
- package/dist/io/ooxml/zip-reject.js +106 -106
- package/dist/measure/placement.js +83 -83
- package/dist/measure/report.js +75 -75
- package/dist/model/ids.js +70 -70
- package/dist/model/normalize.js +146 -146
- package/dist/model/position.js +283 -283
- package/dist/model/transaction.js +209 -209
- package/dist/platform/capabilities.js +25 -25
- package/dist/react/index.js +21 -21
- package/dist/selection/controller.js +315 -315
- package/dist/selection/dom-map.js +334 -334
- package/dist/state/editor-state.js +109 -109
- package/dist/styles.css +66 -66
- 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
|
+
}
|