@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,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
|
+
}
|