@file-viewer/doc 2.1.10
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/LICENSE +21 -0
- package/README.en.md +34 -0
- package/README.md +263 -0
- package/dist/core/binary.d.ts +31 -0
- package/dist/core/binary.js +102 -0
- package/dist/core/cfb.d.ts +7 -0
- package/dist/core/cfb.js +286 -0
- package/dist/core/utils.d.ts +19 -0
- package/dist/core/utils.js +129 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/msdoc/clx.d.ts +18 -0
- package/dist/msdoc/clx.js +172 -0
- package/dist/msdoc/constants.d.ts +73 -0
- package/dist/msdoc/constants.js +73 -0
- package/dist/msdoc/fib.d.ts +4 -0
- package/dist/msdoc/fib.js +125 -0
- package/dist/msdoc/fkp.d.ts +19 -0
- package/dist/msdoc/fkp.js +184 -0
- package/dist/msdoc/fonts.d.ts +2 -0
- package/dist/msdoc/fonts.js +87 -0
- package/dist/msdoc/objects.d.ts +15 -0
- package/dist/msdoc/objects.js +192 -0
- package/dist/msdoc/parser.d.ts +8 -0
- package/dist/msdoc/parser.js +549 -0
- package/dist/msdoc/properties.d.ts +18 -0
- package/dist/msdoc/properties.js +405 -0
- package/dist/msdoc/sprm.d.ts +153 -0
- package/dist/msdoc/sprm.js +500 -0
- package/dist/msdoc/styles.d.ts +10 -0
- package/dist/msdoc/styles.js +214 -0
- package/dist/render/html.d.ts +8 -0
- package/dist/render/html.js +311 -0
- package/dist/types.d.ts +635 -0
- package/dist/types.js +1 -0
- package/dist/viewer.d.ts +8 -0
- package/dist/viewer.js +71 -0
- package/dist/worker-client.d.ts +19 -0
- package/dist/worker-client.js +71 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +36 -0
- package/package.json +73 -0
package/dist/core/cfb.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { BinaryReader, toUint8Array } from './binary.js';
|
|
2
|
+
import { pushWarning } from './utils.js';
|
|
3
|
+
const CFB_SIGNATURE = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
|
|
4
|
+
const FREESECT = 0xffffffff;
|
|
5
|
+
const ENDOFCHAIN = 0xfffffffe;
|
|
6
|
+
const FATSECT = 0xfffffffd;
|
|
7
|
+
const DIFSECT = 0xfffffffc;
|
|
8
|
+
const MAX_CHAIN_SECTORS = 1 << 20;
|
|
9
|
+
const MINI_STREAM_CUTOFF = 4096;
|
|
10
|
+
function isSpecialSectorId(sid) {
|
|
11
|
+
return sid === FREESECT || sid === ENDOFCHAIN || sid === FATSECT || sid === DIFSECT;
|
|
12
|
+
}
|
|
13
|
+
function readSectorAbsolute(bytes, sectorSize, sid) {
|
|
14
|
+
const start = 512 + sid * sectorSize;
|
|
15
|
+
const end = start + sectorSize;
|
|
16
|
+
if (start < 0 || start >= bytes.length) {
|
|
17
|
+
throw new Error(`Sector ${sid} is out of bounds`);
|
|
18
|
+
}
|
|
19
|
+
if (end <= bytes.length)
|
|
20
|
+
return bytes.subarray(start, end);
|
|
21
|
+
const sector = new Uint8Array(sectorSize);
|
|
22
|
+
sector.set(bytes.subarray(start));
|
|
23
|
+
return sector;
|
|
24
|
+
}
|
|
25
|
+
function collectChain(startSid, nextSectorFn, limit = MAX_CHAIN_SECTORS) {
|
|
26
|
+
const chain = [];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
let sid = startSid;
|
|
29
|
+
let guard = 0;
|
|
30
|
+
while (sid !== ENDOFCHAIN && sid !== FREESECT && sid >= 0) {
|
|
31
|
+
if (seen.has(sid))
|
|
32
|
+
throw new Error(`Detected sector loop at ${sid}`);
|
|
33
|
+
seen.add(sid);
|
|
34
|
+
chain.push(sid);
|
|
35
|
+
sid = nextSectorFn(sid);
|
|
36
|
+
guard += 1;
|
|
37
|
+
if (guard > limit)
|
|
38
|
+
throw new Error('Sector chain exceeds safe limit');
|
|
39
|
+
}
|
|
40
|
+
return chain;
|
|
41
|
+
}
|
|
42
|
+
function readChainBytes(bytes, sectorSize, startSid, fat, expectedSize = null) {
|
|
43
|
+
if (startSid === ENDOFCHAIN || startSid === FREESECT || startSid < 0) {
|
|
44
|
+
return new Uint8Array(0);
|
|
45
|
+
}
|
|
46
|
+
const chain = collectChain(startSid, (sid) => fat[sid] ?? ENDOFCHAIN);
|
|
47
|
+
const out = new Uint8Array(chain.length * sectorSize);
|
|
48
|
+
let offset = 0;
|
|
49
|
+
for (const sid of chain) {
|
|
50
|
+
out.set(readSectorAbsolute(bytes, sectorSize, sid), offset);
|
|
51
|
+
offset += sectorSize;
|
|
52
|
+
}
|
|
53
|
+
return expectedSize == null ? out : out.subarray(0, Math.min(expectedSize, out.length));
|
|
54
|
+
}
|
|
55
|
+
function parseDirectoryTree(entries) {
|
|
56
|
+
function walkSiblings(entryId, out) {
|
|
57
|
+
if (entryId < 0 || entryId >= entries.length)
|
|
58
|
+
return;
|
|
59
|
+
const entry = entries[entryId];
|
|
60
|
+
if (!entry)
|
|
61
|
+
return;
|
|
62
|
+
walkSiblings(entry.leftSiblingId, out);
|
|
63
|
+
out.push(entryId);
|
|
64
|
+
walkSiblings(entry.rightSiblingId, out);
|
|
65
|
+
}
|
|
66
|
+
function attachChildren(storageId) {
|
|
67
|
+
const storage = entries[storageId];
|
|
68
|
+
if (!storage || storage.childId < 0)
|
|
69
|
+
return;
|
|
70
|
+
const childIds = [];
|
|
71
|
+
walkSiblings(storage.childId, childIds);
|
|
72
|
+
storage.children = childIds;
|
|
73
|
+
for (const childId of childIds) {
|
|
74
|
+
const child = entries[childId];
|
|
75
|
+
if (!child)
|
|
76
|
+
continue;
|
|
77
|
+
child.parentId = storageId;
|
|
78
|
+
attachChildren(childId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const root = entries.find((entry) => entry.objectType === 5);
|
|
82
|
+
if (!root)
|
|
83
|
+
throw new Error('CFB root storage not found');
|
|
84
|
+
attachChildren(root.id);
|
|
85
|
+
return root;
|
|
86
|
+
}
|
|
87
|
+
function buildPathMap(entries) {
|
|
88
|
+
const map = new Map();
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (!entry || !entry.name)
|
|
91
|
+
continue;
|
|
92
|
+
const parts = [];
|
|
93
|
+
let current = entry;
|
|
94
|
+
while (current) {
|
|
95
|
+
if (current.objectType !== 5)
|
|
96
|
+
parts.push(current.name);
|
|
97
|
+
current = current.parentId != null ? entries[current.parentId] ?? null : null;
|
|
98
|
+
}
|
|
99
|
+
const path = `/${parts.reverse().join('/')}`;
|
|
100
|
+
entry.path = path === '/' ? `/${entry.name}` : path;
|
|
101
|
+
map.set(entry.path, entry);
|
|
102
|
+
}
|
|
103
|
+
return map;
|
|
104
|
+
}
|
|
105
|
+
function normalizeName(name) {
|
|
106
|
+
return name.replace(/\u0000+$/, '');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Parses the CFB/OLE container that wraps a legacy `.doc` file.
|
|
110
|
+
* The returned object exposes stream helpers so higher layers can focus on
|
|
111
|
+
* Word-specific structures instead of low-level sector navigation.
|
|
112
|
+
*/
|
|
113
|
+
export function parseCFB(input, _options = {}) {
|
|
114
|
+
const bytes = toUint8Array(input);
|
|
115
|
+
const reader = new BinaryReader(bytes);
|
|
116
|
+
const warnings = [];
|
|
117
|
+
for (let i = 0; i < CFB_SIGNATURE.length; i += 1) {
|
|
118
|
+
if (reader.u8(i) !== CFB_SIGNATURE[i]) {
|
|
119
|
+
throw new Error('Not a Compound File Binary document');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const majorVersion = reader.u16(26);
|
|
123
|
+
const sectorShift = reader.u16(30);
|
|
124
|
+
const miniSectorShift = reader.u16(32);
|
|
125
|
+
const sectorSize = 1 << sectorShift;
|
|
126
|
+
const miniSectorSize = 1 << miniSectorShift;
|
|
127
|
+
const numDirSectors = reader.u32(40);
|
|
128
|
+
const numFatSectors = reader.u32(44);
|
|
129
|
+
const firstDirSector = reader.i32(48);
|
|
130
|
+
const transactionSignature = reader.u32(52);
|
|
131
|
+
const miniStreamCutoffSize = reader.u32(56);
|
|
132
|
+
const firstMiniFatSector = reader.i32(60);
|
|
133
|
+
const numMiniFatSectors = reader.u32(64);
|
|
134
|
+
const firstDifatSector = reader.i32(68);
|
|
135
|
+
const numDifatSectors = reader.u32(72);
|
|
136
|
+
if (miniStreamCutoffSize !== MINI_STREAM_CUTOFF) {
|
|
137
|
+
pushWarning(warnings, `Unexpected mini stream cutoff size ${miniStreamCutoffSize}`);
|
|
138
|
+
}
|
|
139
|
+
const headerDifat = [];
|
|
140
|
+
for (let i = 0; i < 109; i += 1) {
|
|
141
|
+
const sid = reader.i32(76 + i * 4);
|
|
142
|
+
if (!isSpecialSectorId(sid) && sid >= 0)
|
|
143
|
+
headerDifat.push(sid);
|
|
144
|
+
}
|
|
145
|
+
const difat = [...headerDifat];
|
|
146
|
+
let difatSid = firstDifatSector;
|
|
147
|
+
let difatCount = 0;
|
|
148
|
+
while (difatSid !== ENDOFCHAIN && difatSid !== FREESECT && difatSid >= 0) {
|
|
149
|
+
const sector = readSectorAbsolute(bytes, sectorSize, difatSid);
|
|
150
|
+
const sectorReader = new BinaryReader(sector);
|
|
151
|
+
const entriesPerDifatSector = sectorSize / 4 - 1;
|
|
152
|
+
for (let i = 0; i < entriesPerDifatSector; i += 1) {
|
|
153
|
+
const sid = sectorReader.i32(i * 4);
|
|
154
|
+
if (!isSpecialSectorId(sid) && sid >= 0)
|
|
155
|
+
difat.push(sid);
|
|
156
|
+
}
|
|
157
|
+
difatSid = sectorReader.i32(sectorSize - 4);
|
|
158
|
+
difatCount += 1;
|
|
159
|
+
if (difatCount > numDifatSectors + 4) {
|
|
160
|
+
pushWarning(warnings, 'DIFAT chain exceeded declared sector count; stopping early');
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (numFatSectors && difat.length < numFatSectors) {
|
|
165
|
+
pushWarning(warnings, `FAT sector count mismatch: header says ${numFatSectors}, found ${difat.length}`);
|
|
166
|
+
}
|
|
167
|
+
const fat = [];
|
|
168
|
+
for (const fatSid of difat) {
|
|
169
|
+
const fatSector = readSectorAbsolute(bytes, sectorSize, fatSid);
|
|
170
|
+
const fatReader = new BinaryReader(fatSector);
|
|
171
|
+
for (let i = 0; i < sectorSize / 4; i += 1) {
|
|
172
|
+
fat.push(fatReader.i32(i * 4));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const directoryBytes = readChainBytes(bytes, sectorSize, firstDirSector, fat);
|
|
176
|
+
const directoryReader = new BinaryReader(directoryBytes);
|
|
177
|
+
const entries = [];
|
|
178
|
+
for (let offset = 0, id = 0; offset + 128 <= directoryBytes.length; offset += 128, id += 1) {
|
|
179
|
+
const nameLength = directoryReader.u16(offset + 64);
|
|
180
|
+
const name = normalizeName(directoryReader.utf16le(offset, Math.max(0, nameLength - 2)));
|
|
181
|
+
if (!name && directoryReader.u8(offset + 66) === 0)
|
|
182
|
+
continue;
|
|
183
|
+
entries.push({
|
|
184
|
+
id,
|
|
185
|
+
name,
|
|
186
|
+
objectType: directoryReader.u8(offset + 66),
|
|
187
|
+
colorFlag: directoryReader.u8(offset + 67),
|
|
188
|
+
leftSiblingId: directoryReader.i32(offset + 68),
|
|
189
|
+
rightSiblingId: directoryReader.i32(offset + 72),
|
|
190
|
+
childId: directoryReader.i32(offset + 76),
|
|
191
|
+
clsid: directoryReader.slice(offset + 80, 16),
|
|
192
|
+
stateBits: directoryReader.u32(offset + 96),
|
|
193
|
+
creationTime: directoryReader.u64(offset + 100),
|
|
194
|
+
modifiedTime: directoryReader.u64(offset + 108),
|
|
195
|
+
startSector: directoryReader.i32(offset + 116),
|
|
196
|
+
streamSize: majorVersion === 3 ? directoryReader.u32(offset + 120) : directoryReader.u64(offset + 120),
|
|
197
|
+
children: [],
|
|
198
|
+
parentId: null,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const root = parseDirectoryTree(entries);
|
|
202
|
+
const pathMap = buildPathMap(entries);
|
|
203
|
+
const miniFat = [];
|
|
204
|
+
if (numMiniFatSectors && firstMiniFatSector >= 0) {
|
|
205
|
+
const miniFatBytes = readChainBytes(bytes, sectorSize, firstMiniFatSector, fat);
|
|
206
|
+
const miniFatReader = new BinaryReader(miniFatBytes);
|
|
207
|
+
for (let i = 0; i + 4 <= miniFatBytes.length; i += 4) {
|
|
208
|
+
miniFat.push(miniFatReader.i32(i));
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const rootStreamBytes = readChainBytes(bytes, sectorSize, root.startSector, fat, root.streamSize);
|
|
212
|
+
function readMiniStream(entry) {
|
|
213
|
+
if (entry.startSector < 0)
|
|
214
|
+
return new Uint8Array(0);
|
|
215
|
+
const chain = collectChain(entry.startSector, (sid) => miniFat[sid] ?? ENDOFCHAIN);
|
|
216
|
+
const out = new Uint8Array(chain.length * miniSectorSize);
|
|
217
|
+
let offset = 0;
|
|
218
|
+
for (const miniSid of chain) {
|
|
219
|
+
const start = miniSid * miniSectorSize;
|
|
220
|
+
const end = start + miniSectorSize;
|
|
221
|
+
if (end > rootStreamBytes.length)
|
|
222
|
+
throw new Error(`Mini sector ${miniSid} is out of bounds`);
|
|
223
|
+
out.set(rootStreamBytes.subarray(start, end), offset);
|
|
224
|
+
offset += miniSectorSize;
|
|
225
|
+
}
|
|
226
|
+
return out.subarray(0, Math.min(entry.streamSize, out.length));
|
|
227
|
+
}
|
|
228
|
+
function getStream(identifier) {
|
|
229
|
+
const entry = typeof identifier === 'string' ? pathMap.get(identifier) ?? null : identifier;
|
|
230
|
+
if (!entry)
|
|
231
|
+
return null;
|
|
232
|
+
if (entry.objectType !== 2 && entry.objectType !== 5)
|
|
233
|
+
return null;
|
|
234
|
+
if (entry.objectType === 5)
|
|
235
|
+
return rootStreamBytes;
|
|
236
|
+
if (entry.streamSize < miniStreamCutoffSize && entry.startSector >= 0 && miniFat.length) {
|
|
237
|
+
return readMiniStream(entry);
|
|
238
|
+
}
|
|
239
|
+
return readChainBytes(bytes, sectorSize, entry.startSector, fat, entry.streamSize);
|
|
240
|
+
}
|
|
241
|
+
function listChildren(path) {
|
|
242
|
+
const entry = typeof path === 'string' ? pathMap.get(path) ?? null : path;
|
|
243
|
+
if (!entry)
|
|
244
|
+
return [];
|
|
245
|
+
return (entry.children || []).map((id) => entries[id]).filter((child) => Boolean(child));
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
bytes,
|
|
249
|
+
majorVersion,
|
|
250
|
+
sectorSize,
|
|
251
|
+
miniSectorSize,
|
|
252
|
+
numDirSectors,
|
|
253
|
+
numFatSectors,
|
|
254
|
+
firstDirSector,
|
|
255
|
+
transactionSignature,
|
|
256
|
+
miniStreamCutoffSize,
|
|
257
|
+
warnings,
|
|
258
|
+
entries,
|
|
259
|
+
root,
|
|
260
|
+
pathMap,
|
|
261
|
+
getEntry(path) {
|
|
262
|
+
return pathMap.get(path) ?? null;
|
|
263
|
+
},
|
|
264
|
+
getStream,
|
|
265
|
+
listChildren,
|
|
266
|
+
findByName(name, startPath = '/') {
|
|
267
|
+
const start = startPath === '/' ? root : pathMap.get(startPath) ?? null;
|
|
268
|
+
if (!start)
|
|
269
|
+
return null;
|
|
270
|
+
const stack = [start];
|
|
271
|
+
while (stack.length) {
|
|
272
|
+
const entry = stack.pop();
|
|
273
|
+
if (!entry)
|
|
274
|
+
continue;
|
|
275
|
+
if (entry.name === name)
|
|
276
|
+
return entry;
|
|
277
|
+
for (const childId of entry.children || []) {
|
|
278
|
+
const child = entries[childId];
|
|
279
|
+
if (child)
|
|
280
|
+
stack.push(child);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BinaryInput, CssStyleObject, MsDocWarning } from '../types.js';
|
|
2
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
3
|
+
export declare function escapeHtml(text: unknown): string;
|
|
4
|
+
export declare function uniqueId(prefix?: string): string;
|
|
5
|
+
export declare function alignEven(value: number): number;
|
|
6
|
+
export declare function bytesToHex(bytes: BinaryInput, max?: number): string;
|
|
7
|
+
export declare function bytesToBase64(bytes: BinaryInput): string;
|
|
8
|
+
export declare function dataUrlFromBytes(bytes: BinaryInput, mime?: string): string;
|
|
9
|
+
export declare function slugify(text: unknown): string;
|
|
10
|
+
export declare function shallowEqual<T extends Record<string, unknown>>(a: T | null | undefined, b: T | null | undefined): boolean;
|
|
11
|
+
export declare function mergeObjects<T extends Record<string, unknown>>(...parts: Array<Partial<T> | null | undefined>): Partial<T>;
|
|
12
|
+
export declare function mapObject<TInput, TOutput>(obj: Record<string, TInput> | null | undefined, mapper: (value: TInput, key: string) => TOutput): Record<string, TOutput>;
|
|
13
|
+
export declare function pushWarning<TExtra extends Record<string, unknown> | undefined>(target: MsDocWarning[], message: string, extra?: TExtra): MsDocWarning;
|
|
14
|
+
export declare function twipsToPx(twips: number | null | undefined, pxPerInch?: number): number | undefined;
|
|
15
|
+
export declare function pointsToPx(points: number | null | undefined, pxPerInch?: number): number | undefined;
|
|
16
|
+
export declare function cleanTextControlChars(text: unknown): string;
|
|
17
|
+
export declare function groupConsecutive<T>(items: Iterable<T>, belongsTogether: (left: T, right: T) => boolean): T[][];
|
|
18
|
+
export declare function textDecoder(label?: string): TextDecoder;
|
|
19
|
+
export type { CssStyleObject };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const globalBuffer = globalThis.Buffer;
|
|
2
|
+
let uniqueIdCounter = 0;
|
|
3
|
+
export function clamp(value, min, max) {
|
|
4
|
+
return Math.max(min, Math.min(max, value));
|
|
5
|
+
}
|
|
6
|
+
export function escapeHtml(text) {
|
|
7
|
+
return String(text)
|
|
8
|
+
.replace(/&/g, '&')
|
|
9
|
+
.replace(/</g, '<')
|
|
10
|
+
.replace(/>/g, '>')
|
|
11
|
+
.replace(/"/g, '"')
|
|
12
|
+
.replace(/'/g, ''');
|
|
13
|
+
}
|
|
14
|
+
export function uniqueId(prefix = 'id') {
|
|
15
|
+
uniqueIdCounter += 1;
|
|
16
|
+
return `${prefix}-${uniqueIdCounter}`;
|
|
17
|
+
}
|
|
18
|
+
export function alignEven(value) {
|
|
19
|
+
return value % 2 === 0 ? value : value + 1;
|
|
20
|
+
}
|
|
21
|
+
function asUint8Array(input) {
|
|
22
|
+
if (input instanceof Uint8Array)
|
|
23
|
+
return input;
|
|
24
|
+
if (input instanceof ArrayBuffer)
|
|
25
|
+
return new Uint8Array(input);
|
|
26
|
+
return new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
27
|
+
}
|
|
28
|
+
export function bytesToHex(bytes, max = 32) {
|
|
29
|
+
const slice = asUint8Array(bytes).subarray(0, max);
|
|
30
|
+
return Array.from(slice, (b) => b.toString(16).padStart(2, '0')).join(' ');
|
|
31
|
+
}
|
|
32
|
+
export function bytesToBase64(bytes) {
|
|
33
|
+
const view = bytes instanceof Uint8Array ? bytes : bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
34
|
+
if (globalBuffer) {
|
|
35
|
+
return globalBuffer.from(view).toString('base64');
|
|
36
|
+
}
|
|
37
|
+
let binary = '';
|
|
38
|
+
const chunk = 0x8000;
|
|
39
|
+
for (let i = 0; i < view.length; i += chunk) {
|
|
40
|
+
binary += String.fromCharCode(...view.subarray(i, Math.min(i + chunk, view.length)));
|
|
41
|
+
}
|
|
42
|
+
return btoa(binary);
|
|
43
|
+
}
|
|
44
|
+
export function dataUrlFromBytes(bytes, mime = 'application/octet-stream') {
|
|
45
|
+
return `data:${mime};base64,${bytesToBase64(bytes)}`;
|
|
46
|
+
}
|
|
47
|
+
export function slugify(text) {
|
|
48
|
+
return String(text ?? '')
|
|
49
|
+
.toLowerCase()
|
|
50
|
+
.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, '-')
|
|
51
|
+
.replace(/^-+|-+$/g, '') || 'item';
|
|
52
|
+
}
|
|
53
|
+
export function shallowEqual(a, b) {
|
|
54
|
+
if (a === b)
|
|
55
|
+
return true;
|
|
56
|
+
if (!a || !b)
|
|
57
|
+
return false;
|
|
58
|
+
const aKeys = Object.keys(a);
|
|
59
|
+
const bKeys = Object.keys(b);
|
|
60
|
+
if (aKeys.length !== bKeys.length)
|
|
61
|
+
return false;
|
|
62
|
+
for (const key of aKeys) {
|
|
63
|
+
if (a[key] !== b[key])
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
export function mergeObjects(...parts) {
|
|
69
|
+
const out = {};
|
|
70
|
+
for (const part of parts) {
|
|
71
|
+
if (!part || typeof part !== 'object')
|
|
72
|
+
continue;
|
|
73
|
+
for (const [key, value] of Object.entries(part)) {
|
|
74
|
+
if (value === undefined)
|
|
75
|
+
continue;
|
|
76
|
+
out[key] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
export function mapObject(obj, mapper) {
|
|
82
|
+
const out = {};
|
|
83
|
+
for (const [key, value] of Object.entries(obj ?? {})) {
|
|
84
|
+
out[key] = mapper(value, key);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
export function pushWarning(target, message, extra) {
|
|
89
|
+
const warning = extra ? { message, ...extra } : { message };
|
|
90
|
+
target.push(warning);
|
|
91
|
+
return warning;
|
|
92
|
+
}
|
|
93
|
+
export function twipsToPx(twips, pxPerInch = 96) {
|
|
94
|
+
if (twips == null || Number.isNaN(twips))
|
|
95
|
+
return undefined;
|
|
96
|
+
return (twips / 1440) * pxPerInch;
|
|
97
|
+
}
|
|
98
|
+
export function pointsToPx(points, pxPerInch = 96) {
|
|
99
|
+
if (points == null || Number.isNaN(points))
|
|
100
|
+
return undefined;
|
|
101
|
+
return (points / 72) * pxPerInch;
|
|
102
|
+
}
|
|
103
|
+
export function cleanTextControlChars(text) {
|
|
104
|
+
return String(text ?? '')
|
|
105
|
+
.replace(/\u000b/g, '\n')
|
|
106
|
+
.replace(/\u000c/g, '\n');
|
|
107
|
+
}
|
|
108
|
+
export function groupConsecutive(items, belongsTogether) {
|
|
109
|
+
const groups = [];
|
|
110
|
+
let current = [];
|
|
111
|
+
for (const item of items) {
|
|
112
|
+
if (current.length === 0) {
|
|
113
|
+
current.push(item);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (belongsTogether(current[current.length - 1], item)) {
|
|
117
|
+
current.push(item);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
groups.push(current);
|
|
121
|
+
current = [item];
|
|
122
|
+
}
|
|
123
|
+
if (current.length)
|
|
124
|
+
groups.push(current);
|
|
125
|
+
return groups;
|
|
126
|
+
}
|
|
127
|
+
export function textDecoder(label = 'utf-8') {
|
|
128
|
+
return new TextDecoder(label, { fatal: false });
|
|
129
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { parseMsDoc } from './msdoc/parser.js';
|
|
2
|
+
export { renderMsDoc, defaultMsDocCss } from './render/html.js';
|
|
3
|
+
export { createMsDocViewer, mountMsDoc, parseMsDocToHtml } from './viewer.js';
|
|
4
|
+
export { MsDocWorkerClient } from './worker-client.js';
|
|
5
|
+
export type * from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FibRgFcLcb, ParsedClx, PieceDescriptor, PieceTable } from '../types.js';
|
|
2
|
+
export declare function parseClx(tableBytes: Uint8Array, fibRgFcLcb: FibRgFcLcb): ParsedClx;
|
|
3
|
+
export declare function parsePlcPcd(bytes: Uint8Array): PieceTable;
|
|
4
|
+
export declare function extractDocumentText(wordBytes: Uint8Array, clx: ParsedClx): string;
|
|
5
|
+
export declare function extractPieceText(wordBytes: Uint8Array, piece: PieceDescriptor): string;
|
|
6
|
+
export declare function buildPieceTextCache(wordBytes: Uint8Array, clx: ParsedClx): string[];
|
|
7
|
+
export declare function cpToFileOffset(clx: ParsedClx, cp: number): {
|
|
8
|
+
piece: PieceDescriptor;
|
|
9
|
+
offset: number;
|
|
10
|
+
compressed: boolean;
|
|
11
|
+
} | null;
|
|
12
|
+
export declare function fcToCp(clx: ParsedClx, fcRawOrActual: number, explicitCompressed?: boolean | null): number | null;
|
|
13
|
+
export declare function getTextByCp(_wordBytes: Uint8Array, clx: ParsedClx, pieceTexts: string[], cpStart: number, cpEnd: number): string;
|
|
14
|
+
export declare function splitParagraphRanges(documentText: string): Array<{
|
|
15
|
+
cpStart: number;
|
|
16
|
+
cpEnd: number;
|
|
17
|
+
terminator: string;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { BinaryReader } from '../core/binary.js';
|
|
2
|
+
const REPLACEMENTS = {
|
|
3
|
+
'\x82': '\u201A', '\x83': '\u0192', '\x84': '\u201E', '\x85': '\u2026', '\x86': '\u2020', '\x87': '\u2021',
|
|
4
|
+
'\x88': '\u02C6', '\x89': '\u2030', '\x8A': '\u0160', '\x8B': '\u2039', '\x8C': '\u0152', '\x91': '\u2018',
|
|
5
|
+
'\x92': '\u2019', '\x93': '\u201C', '\x94': '\u201D', '\x95': '\u2022', '\x96': '\u2013', '\x97': '\u2014',
|
|
6
|
+
'\x98': '\u02DC', '\x99': '\u2122', '\x9A': '\u0161', '\x9B': '\u203A', '\x9C': '\u0153', '\x9F': '\u0178',
|
|
7
|
+
};
|
|
8
|
+
function fixCompressedText(str) {
|
|
9
|
+
return str.replace(/[\x82-\x8C\x91-\x9C\x9F]/g, (ch) => REPLACEMENTS[ch] || ch);
|
|
10
|
+
}
|
|
11
|
+
function decodeCompressed(bytes) {
|
|
12
|
+
let text = '';
|
|
13
|
+
for (let i = 0; i < bytes.length; i += 1)
|
|
14
|
+
text += String.fromCharCode(bytes[i] ?? 0);
|
|
15
|
+
return fixCompressedText(text);
|
|
16
|
+
}
|
|
17
|
+
function decodeUnicode(bytes) {
|
|
18
|
+
return new TextDecoder('utf-16le').decode(bytes);
|
|
19
|
+
}
|
|
20
|
+
export function parseClx(tableBytes, fibRgFcLcb) {
|
|
21
|
+
const fcClx = fibRgFcLcb.fcClx;
|
|
22
|
+
const lcbClx = fibRgFcLcb.lcbClx;
|
|
23
|
+
if (fcClx == null || lcbClx == null || lcbClx <= 0) {
|
|
24
|
+
throw new Error('FIB does not point to a CLX structure');
|
|
25
|
+
}
|
|
26
|
+
const clxBytes = tableBytes.subarray(fcClx, fcClx + lcbClx);
|
|
27
|
+
const reader = new BinaryReader(clxBytes);
|
|
28
|
+
let offset = 0;
|
|
29
|
+
const prcs = [];
|
|
30
|
+
while (offset < clxBytes.length && reader.u8(offset) === 0x01) {
|
|
31
|
+
const cbGrpprl = reader.u16(offset + 1);
|
|
32
|
+
prcs.push({ type: 1, cbGrpprl, bytes: reader.slice(offset + 3, cbGrpprl) });
|
|
33
|
+
offset += 3 + cbGrpprl;
|
|
34
|
+
}
|
|
35
|
+
if (reader.u8(offset) !== 0x02) {
|
|
36
|
+
throw new Error('CLX does not contain a Pcdt marker');
|
|
37
|
+
}
|
|
38
|
+
const lcb = reader.u32(offset + 1);
|
|
39
|
+
const plcPcdBytes = reader.slice(offset + 5, lcb);
|
|
40
|
+
const pieceTable = parsePlcPcd(plcPcdBytes);
|
|
41
|
+
return { prcs, pcdt: { lcb, pieceTable } };
|
|
42
|
+
}
|
|
43
|
+
export function parsePlcPcd(bytes) {
|
|
44
|
+
if (!bytes.length)
|
|
45
|
+
throw new Error('Empty PlcPcd');
|
|
46
|
+
const reader = new BinaryReader(bytes);
|
|
47
|
+
const pcdCount = (bytes.length - 4) / 12;
|
|
48
|
+
if (!Number.isInteger(pcdCount) || pcdCount < 0) {
|
|
49
|
+
throw new Error('Invalid PlcPcd size');
|
|
50
|
+
}
|
|
51
|
+
const cps = [];
|
|
52
|
+
for (let i = 0; i < pcdCount + 1; i += 1)
|
|
53
|
+
cps.push(reader.u32(i * 4));
|
|
54
|
+
const pieces = [];
|
|
55
|
+
let offset = (pcdCount + 1) * 4;
|
|
56
|
+
for (let i = 0; i < pcdCount; i += 1) {
|
|
57
|
+
const descriptor = reader.u16(offset);
|
|
58
|
+
const fcRaw = reader.u32(offset + 2) >>> 0;
|
|
59
|
+
const prm = reader.u16(offset + 6);
|
|
60
|
+
const compressed = Boolean(fcRaw & 0x40000000);
|
|
61
|
+
const fc = fcRaw & 0x3fffffff;
|
|
62
|
+
const cpStart = cps[i] ?? 0;
|
|
63
|
+
const cpEnd = cps[i + 1] ?? cpStart;
|
|
64
|
+
const actualByteStart = compressed ? Math.floor(fc / 2) : fc;
|
|
65
|
+
const byteLength = compressed ? (cpEnd - cpStart) : (cpEnd - cpStart) * 2;
|
|
66
|
+
pieces.push({
|
|
67
|
+
index: i,
|
|
68
|
+
descriptor,
|
|
69
|
+
fNoParaLast: Boolean(descriptor & 0x0001),
|
|
70
|
+
fRaw: Boolean(descriptor & 0x0002),
|
|
71
|
+
prm,
|
|
72
|
+
compressed,
|
|
73
|
+
fcRaw,
|
|
74
|
+
fc,
|
|
75
|
+
cpStart,
|
|
76
|
+
cpEnd,
|
|
77
|
+
actualByteStart,
|
|
78
|
+
byteLength,
|
|
79
|
+
actualByteEnd: actualByteStart + byteLength,
|
|
80
|
+
});
|
|
81
|
+
offset += 8;
|
|
82
|
+
}
|
|
83
|
+
return { cps, pieces };
|
|
84
|
+
}
|
|
85
|
+
export function extractDocumentText(wordBytes, clx) {
|
|
86
|
+
return clx.pcdt.pieceTable.pieces.map((piece) => extractPieceText(wordBytes, piece)).join('');
|
|
87
|
+
}
|
|
88
|
+
export function extractPieceText(wordBytes, piece) {
|
|
89
|
+
const bytes = wordBytes.subarray(piece.actualByteStart, piece.actualByteEnd);
|
|
90
|
+
return piece.compressed ? decodeCompressed(bytes) : decodeUnicode(bytes);
|
|
91
|
+
}
|
|
92
|
+
export function buildPieceTextCache(wordBytes, clx) {
|
|
93
|
+
return clx.pcdt.pieceTable.pieces.map((piece) => extractPieceText(wordBytes, piece));
|
|
94
|
+
}
|
|
95
|
+
export function cpToFileOffset(clx, cp) {
|
|
96
|
+
const pieces = clx.pcdt.pieceTable.pieces;
|
|
97
|
+
let lo = 0;
|
|
98
|
+
let hi = pieces.length - 1;
|
|
99
|
+
while (lo <= hi) {
|
|
100
|
+
const mid = (lo + hi) >> 1;
|
|
101
|
+
const piece = pieces[mid];
|
|
102
|
+
if (cp < piece.cpStart)
|
|
103
|
+
hi = mid - 1;
|
|
104
|
+
else if (cp >= piece.cpEnd)
|
|
105
|
+
lo = mid + 1;
|
|
106
|
+
else {
|
|
107
|
+
return {
|
|
108
|
+
piece,
|
|
109
|
+
offset: piece.actualByteStart + (cp - piece.cpStart) * (piece.compressed ? 1 : 2),
|
|
110
|
+
compressed: piece.compressed,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
export function fcToCp(clx, fcRawOrActual, explicitCompressed = null) {
|
|
117
|
+
const pieces = clx.pcdt.pieceTable.pieces;
|
|
118
|
+
const hintedCompressed = explicitCompressed == null ? Boolean(fcRawOrActual & 0x40000000) : explicitCompressed;
|
|
119
|
+
const fc = explicitCompressed == null ? (fcRawOrActual & 0x3fffffff) : fcRawOrActual;
|
|
120
|
+
const actual = hintedCompressed ? Math.floor(fc / 2) : fc;
|
|
121
|
+
for (const piece of pieces) {
|
|
122
|
+
if (actual < piece.actualByteStart)
|
|
123
|
+
continue;
|
|
124
|
+
if (actual > piece.actualByteEnd)
|
|
125
|
+
continue;
|
|
126
|
+
const delta = actual - piece.actualByteStart;
|
|
127
|
+
const cp = piece.cpStart + Math.floor(delta / (piece.compressed ? 1 : 2));
|
|
128
|
+
if (cp >= piece.cpStart && cp <= piece.cpEnd)
|
|
129
|
+
return cp;
|
|
130
|
+
}
|
|
131
|
+
for (const piece of pieces) {
|
|
132
|
+
if (fcRawOrActual < piece.actualByteStart)
|
|
133
|
+
continue;
|
|
134
|
+
if (fcRawOrActual > piece.actualByteEnd)
|
|
135
|
+
continue;
|
|
136
|
+
const delta = fcRawOrActual - piece.actualByteStart;
|
|
137
|
+
const cp = piece.cpStart + Math.floor(delta / (piece.compressed ? 1 : 2));
|
|
138
|
+
if (cp >= piece.cpStart && cp <= piece.cpEnd)
|
|
139
|
+
return cp;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
export function getTextByCp(_wordBytes, clx, pieceTexts, cpStart, cpEnd) {
|
|
144
|
+
if (cpEnd <= cpStart)
|
|
145
|
+
return '';
|
|
146
|
+
const out = [];
|
|
147
|
+
for (let i = 0; i < clx.pcdt.pieceTable.pieces.length; i += 1) {
|
|
148
|
+
const piece = clx.pcdt.pieceTable.pieces[i];
|
|
149
|
+
if (piece.cpEnd <= cpStart)
|
|
150
|
+
continue;
|
|
151
|
+
if (piece.cpStart >= cpEnd)
|
|
152
|
+
break;
|
|
153
|
+
const localStart = Math.max(cpStart, piece.cpStart) - piece.cpStart;
|
|
154
|
+
const localEnd = Math.min(cpEnd, piece.cpEnd) - piece.cpStart;
|
|
155
|
+
out.push((pieceTexts[i] ?? '').slice(localStart, localEnd));
|
|
156
|
+
}
|
|
157
|
+
return out.join('');
|
|
158
|
+
}
|
|
159
|
+
export function splitParagraphRanges(documentText) {
|
|
160
|
+
const ranges = [];
|
|
161
|
+
let start = 0;
|
|
162
|
+
for (let i = 0; i < documentText.length; i += 1) {
|
|
163
|
+
const ch = documentText[i];
|
|
164
|
+
if (ch === '\r' || ch === '\u0007') {
|
|
165
|
+
ranges.push({ cpStart: start, cpEnd: i + 1, terminator: ch });
|
|
166
|
+
start = i + 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (start < documentText.length)
|
|
170
|
+
ranges.push({ cpStart: start, cpEnd: documentText.length, terminator: '' });
|
|
171
|
+
return ranges;
|
|
172
|
+
}
|