@stll/folio-core 0.64.1 → 0.65.0
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/dist/ai-edits/headless.js +3 -3
- package/dist/compare/reproducible-package.d.ts +5 -0
- package/dist/compare/reproducible-package.js +10 -23
- package/dist/docx/archiveInflation.d.ts +8 -103
- package/dist/docx/archiveInflation.js +8 -145
- package/dist/docx/canonicalStoryRepack.js +2 -2
- package/dist/docx/ensureParaIds.js +7 -22
- package/dist/docx/metadataPrivacy.js +38 -48
- package/dist/docx/packageParts.d.ts +4 -4
- package/dist/docx/packageParts.js +17 -24
- package/dist/docx/parser.js +4 -5
- package/dist/docx/removeHeaderFooterParts.d.ts +2 -2
- package/dist/docx/removeHeaderFooterParts.js +19 -36
- package/dist/docx/rezip.d.ts +7 -7
- package/dist/docx/rezip.js +143 -531
- package/dist/docx/selectiveSave.js +37 -41
- package/dist/docx/selectiveSaveFlags.d.ts +1 -1
- package/dist/docx/selectiveSaveTripwire.js +5 -11
- package/dist/docx/server/applyDocxXmlPatchProposal.js +20 -41
- package/dist/docx/server/boundedArchive.d.ts +1 -2
- package/dist/docx/server/boundedArchive.js +77 -98
- package/dist/docx/server/inspectDocxPackage.d.ts +1 -1
- package/dist/docx/server/inspectDocxPackage.js +1 -1
- package/dist/docx/unzip.d.ts +2 -3
- package/dist/docx/unzip.js +126 -154
- package/package.json +3 -4
package/dist/docx/unzip.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { bytesToDataUrl } from "../utils/base64.js";
|
|
2
|
-
import {
|
|
3
|
-
import { compressionRatioLimitFor, countCentralDirectoryRecords, createInflationBudget, exceedsCompressionRatio, getZipEntrySizes, inflateEntryWithinLimits, isStoredZipEntry } from "./archiveInflation.js";
|
|
2
|
+
import { DOCX_COMPRESSION_RATIO_GRACE_BYTES, isRatioBoundedPart } from "./archiveInflation.js";
|
|
4
3
|
import { DOCX_CONTAINER_TYPES, detectDocxContainerType } from "./encryption/containerFormat.js";
|
|
5
4
|
import { openDocxBuffer } from "./encryption/openEncryptedDocx.js";
|
|
6
5
|
import { RASTER_MIME_TYPES, detectRasterMimeType } from "./rasterMime.js";
|
|
7
6
|
import { decodeXmlBytes } from "./xmlEncoding.js";
|
|
8
7
|
import { FOLIO_XML_RESOURCE_LIMITS, assertXmlResourceLimits, createXmlPackageBudget } from "./xmlResourceLimits.js";
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
8
|
+
import { Result } from "better-result";
|
|
9
|
+
import { ZipArchive, ZipError } from "@stll/docx-core/zip";
|
|
11
10
|
// src/docx/unzip.ts
|
|
12
11
|
/**
|
|
13
12
|
* DOCX Unzipper
|
|
@@ -116,12 +115,10 @@ async function unzipDocx(buffer, options = {}, { verifyUnreadEntries = true } =
|
|
|
116
115
|
const zipBuffer = containerType === DOCX_CONTAINER_TYPES.ZIP ? buffer : await openDocxBuffer(buffer, { password: options.password });
|
|
117
116
|
if (zipBuffer.byteLength > limits.maxInputBytes) throw new DocxSecurityError("DOCX file exceeds the maximum allowed size");
|
|
118
117
|
const wasEncrypted = containerType === DOCX_CONTAINER_TYPES.CFB;
|
|
119
|
-
const
|
|
120
|
-
if (countCentralDirectoryRecords(new Uint8Array(zipBuffer), maxRecords) > maxRecords) throw new DocxSecurityError("DOCX file contains too many entries");
|
|
121
|
-
const loaded = await loadDocxZip(zipBuffer, limits.maxFiles);
|
|
118
|
+
const loaded = await loadDocxZip(zipBuffer, limits);
|
|
122
119
|
if (loaded.buffer.byteLength > limits.maxInputBytes) throw new DocxSecurityError("DOCX file exceeds the maximum allowed size");
|
|
123
120
|
const { zip } = loaded;
|
|
124
|
-
const entries =
|
|
121
|
+
const entries = zip.entries();
|
|
125
122
|
if (entries.length > limits.maxFiles) throw new DocxSecurityError("DOCX file contains too many entries");
|
|
126
123
|
const content = {
|
|
127
124
|
documentXml: null,
|
|
@@ -152,79 +149,46 @@ async function unzipDocx(buffer, options = {}, { verifyUnreadEntries = true } =
|
|
|
152
149
|
warnings: [],
|
|
153
150
|
wasEncrypted
|
|
154
151
|
};
|
|
155
|
-
let totalUncompressedBytes = 0;
|
|
156
|
-
const inflationBudget = createInflationBudget(limits.maxTotalUncompressedBytes);
|
|
157
152
|
const extractionTasks = [];
|
|
158
|
-
const verifyEntry = (file
|
|
159
|
-
if (!verifyUnreadEntries
|
|
153
|
+
const verifyEntry = (file) => {
|
|
154
|
+
if (!verifyUnreadEntries) return;
|
|
160
155
|
extractionTasks.push(async () => {
|
|
161
|
-
await
|
|
162
|
-
file,
|
|
163
|
-
path,
|
|
164
|
-
limits,
|
|
165
|
-
budget: inflationBudget
|
|
166
|
-
});
|
|
156
|
+
await verifyEntryData(file);
|
|
167
157
|
return null;
|
|
168
158
|
});
|
|
169
159
|
};
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
maxEntryBytes,
|
|
173
|
-
maxCompressionRatio: compressionRatioLimitFor(file.name, limits.maxCompressionRatio),
|
|
174
|
-
budget: inflationBudget
|
|
175
|
-
});
|
|
176
|
-
for (const [path, file] of entries) {
|
|
177
|
-
if (!isSafeDocxPath(path)) throw new DocxSecurityError("DOCX file contains an unsafe entry path");
|
|
178
|
-
const { compressedBytes, uncompressedBytes: declaredSize } = getZipEntrySizes(file);
|
|
179
|
-
if (declaredSize !== null) {
|
|
180
|
-
totalUncompressedBytes += declaredSize;
|
|
181
|
-
if (totalUncompressedBytes > limits.maxTotalUncompressedBytes) throw new DocxSecurityError("DOCX file expands beyond the maximum allowed size");
|
|
182
|
-
if (exceedsCompressionRatio({
|
|
183
|
-
inflatedBytes: declaredSize,
|
|
184
|
-
compressedBytes,
|
|
185
|
-
maxRatio: compressionRatioLimitFor(path, limits.maxCompressionRatio)
|
|
186
|
-
})) throw new DocxSecurityError(`DOCX entry exceeds the maximum compression ratio: ${path}`);
|
|
187
|
-
}
|
|
160
|
+
for (const file of entries) {
|
|
161
|
+
const { path, size: declaredSize } = file;
|
|
188
162
|
if (!isParsedDocxEntry(path)) {
|
|
189
|
-
verifyEntry(file
|
|
163
|
+
verifyEntry(file);
|
|
190
164
|
continue;
|
|
191
165
|
}
|
|
192
166
|
const lowerPath = path.toLowerCase();
|
|
193
167
|
if (lowerPath.endsWith(".xml") || lowerPath.endsWith(".rels")) {
|
|
194
168
|
assertEntrySize(path, declaredSize, limits.maxXmlBytes);
|
|
195
169
|
if (options.extractAllXml === false && !shouldExtractXmlPart(lowerPath)) {
|
|
196
|
-
verifyEntry(file
|
|
170
|
+
verifyEntry(file);
|
|
197
171
|
continue;
|
|
198
172
|
}
|
|
199
|
-
extractionTasks.push(async () => {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
lowerPath,
|
|
206
|
-
content: decodeXmlBytes(result.bytes)
|
|
207
|
-
};
|
|
208
|
-
});
|
|
173
|
+
extractionTasks.push(async () => ({
|
|
174
|
+
type: "xml",
|
|
175
|
+
path,
|
|
176
|
+
lowerPath,
|
|
177
|
+
content: decodeXmlBytes(await readEntry(file))
|
|
178
|
+
}));
|
|
209
179
|
} else if (lowerPath.startsWith("word/media/")) {
|
|
210
180
|
const mimeType = getMediaMimeType(path);
|
|
211
181
|
if (!limits.allowedMediaMimeTypes.has(mimeType)) {
|
|
212
|
-
verifyEntry(file
|
|
182
|
+
verifyEntry(file);
|
|
213
183
|
continue;
|
|
214
184
|
}
|
|
215
185
|
if (isEntryTooLarge(declaredSize, limits.maxMediaBytes)) {
|
|
216
186
|
content.warnings.push(`Skipped oversized media file: ${path}; original entry preserved for round-trip.`);
|
|
217
|
-
verifyEntry(file
|
|
187
|
+
verifyEntry(file);
|
|
218
188
|
continue;
|
|
219
189
|
}
|
|
220
190
|
extractionTasks.push(async () => {
|
|
221
|
-
const
|
|
222
|
-
if (!result.ok) {
|
|
223
|
-
if (result.limit !== "entry") throw inflationLimitError(result.limit, path);
|
|
224
|
-
content.warnings.push(`Skipped oversized media file: ${path}; original entry preserved for round-trip.`);
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
const binaryContent = result.bytes.buffer;
|
|
191
|
+
const binaryContent = (await readEntry(file)).buffer;
|
|
228
192
|
const effectiveMimeType = RASTER_MIME_TYPES.has(mimeType) ? detectRasterMimeType(binaryContent) : mimeType;
|
|
229
193
|
if (!effectiveMimeType || !limits.allowedMediaMimeTypes.has(effectiveMimeType) || !isMediaContentAllowed(binaryContent, effectiveMimeType)) return null;
|
|
230
194
|
return {
|
|
@@ -236,30 +200,18 @@ async function unzipDocx(buffer, options = {}, { verifyUnreadEntries = true } =
|
|
|
236
200
|
});
|
|
237
201
|
} else if (lowerPath.startsWith("word/fonts/")) {
|
|
238
202
|
if (isEntryTooLarge(declaredSize, limits.maxFontBytes)) {
|
|
239
|
-
verifyEntry(file
|
|
203
|
+
verifyEntry(file);
|
|
240
204
|
continue;
|
|
241
205
|
}
|
|
242
|
-
extractionTasks.push(async () => {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return {
|
|
249
|
-
type: "font",
|
|
250
|
-
path,
|
|
251
|
-
content: result.bytes.buffer
|
|
252
|
-
};
|
|
253
|
-
});
|
|
254
|
-
} else verifyEntry(file, path);
|
|
206
|
+
extractionTasks.push(async () => ({
|
|
207
|
+
type: "font",
|
|
208
|
+
path,
|
|
209
|
+
content: (await readEntry(file)).buffer
|
|
210
|
+
}));
|
|
211
|
+
} else verifyEntry(file);
|
|
255
212
|
}
|
|
256
|
-
if (exceedsCompressionRatio({
|
|
257
|
-
inflatedBytes: totalUncompressedBytes,
|
|
258
|
-
compressedBytes: loaded.buffer.byteLength,
|
|
259
|
-
maxRatio: limits.maxCompressionRatio
|
|
260
|
-
})) throw new DocxSecurityError("DOCX file exceeds the maximum compression ratio");
|
|
261
213
|
const xmlBudget = createXmlPackageBudget();
|
|
262
|
-
for (const extracted of await runExtractionTasks(extractionTasks
|
|
214
|
+
for (const extracted of await runExtractionTasks(extractionTasks)) {
|
|
263
215
|
if (!extracted) continue;
|
|
264
216
|
if (extracted.type === "xml") {
|
|
265
217
|
assignXmlContent(content, extracted, limits, xmlBudget);
|
|
@@ -284,18 +236,10 @@ const replaceRawDocxXmlParts = async (content, parts) => {
|
|
|
284
236
|
lowerPath: path.toLowerCase(),
|
|
285
237
|
content: xmlContent
|
|
286
238
|
});
|
|
287
|
-
|
|
288
|
-
zip: content.originalZip,
|
|
289
|
-
path,
|
|
290
|
-
data: encodeUtf8(xmlContent)
|
|
291
|
-
});
|
|
239
|
+
content.originalZip.set(path, xmlContent);
|
|
292
240
|
identitiesChanged = true;
|
|
293
241
|
}
|
|
294
|
-
if (identitiesChanged) content.originalBuffer = await content.originalZip.
|
|
295
|
-
type: "arraybuffer",
|
|
296
|
-
compression: "DEFLATE",
|
|
297
|
-
compressionOptions: { level: 1 }
|
|
298
|
-
});
|
|
242
|
+
if (identitiesChanged) content.originalBuffer = (await content.originalZip.generate({ level: 1 })).buffer;
|
|
299
243
|
};
|
|
300
244
|
/**
|
|
301
245
|
* Preflight every XML part the unzip retains, not a named few.
|
|
@@ -349,80 +293,111 @@ const indexXmlContent = (content, { path, lowerPath, content: xmlContent }) => {
|
|
|
349
293
|
content.footers.set(filename, xmlContent);
|
|
350
294
|
}
|
|
351
295
|
};
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
case "
|
|
357
|
-
case "
|
|
358
|
-
case "
|
|
296
|
+
/** The error the unzip reports for an archive or entry the reader refused. */
|
|
297
|
+
function docxErrorFor(error) {
|
|
298
|
+
const path = error.path ?? "";
|
|
299
|
+
switch (error.reason) {
|
|
300
|
+
case "declared-size-exceeded": return new DocxSecurityError(`DOCX entry inflates past its declared size: ${path}`);
|
|
301
|
+
case "compression-ratio": return new DocxSecurityError(error.path === void 0 ? "DOCX file exceeds the maximum compression ratio" : `DOCX entry exceeds the maximum compression ratio: ${path}`);
|
|
302
|
+
case "too-many-entries": return new DocxSecurityError("DOCX file contains too many entries");
|
|
303
|
+
case "entry-too-large":
|
|
304
|
+
case "total-too-large": return new DocxSecurityError("DOCX file expands beyond the maximum allowed size");
|
|
305
|
+
case "invalid-entry-name": return new DocxSecurityError("DOCX file contains an unsafe entry path");
|
|
306
|
+
case "malformed":
|
|
307
|
+
case "unsupported":
|
|
308
|
+
case "duplicate-entry":
|
|
309
|
+
case "corrupt-entry": return error;
|
|
359
310
|
}
|
|
360
311
|
}
|
|
312
|
+
const rethrowRead = (error) => {
|
|
313
|
+
throw error instanceof ZipError ? docxErrorFor(error) : error;
|
|
314
|
+
};
|
|
315
|
+
/** An extracted entry's bytes; inflation stops at its declared size. */
|
|
316
|
+
async function readEntry(file) {
|
|
317
|
+
const read = await Result.tryPromise({
|
|
318
|
+
try: async () => await file.bytes(),
|
|
319
|
+
catch: (cause) => cause
|
|
320
|
+
});
|
|
321
|
+
return Result.isOk(read) ? read.value : rethrowRead(read.error);
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Inflate an entry without keeping it, to bound what a later read of it can
|
|
325
|
+
* produce. Data past the declared size refuses the package. A corrupt entry
|
|
326
|
+
* does not: it was never going to be read here, a later read fails the same
|
|
327
|
+
* way, and refusing the whole document over a part folio does not model
|
|
328
|
+
* would be a regression.
|
|
329
|
+
*/
|
|
330
|
+
async function verifyEntryData(file) {
|
|
331
|
+
const read = await Result.tryPromise({
|
|
332
|
+
try: async () => await file.bytes(),
|
|
333
|
+
catch: (cause) => cause
|
|
334
|
+
});
|
|
335
|
+
if (Result.isOk(read)) return;
|
|
336
|
+
const error = read.error;
|
|
337
|
+
if (!(error instanceof ZipError) || error.reason === "declared-size-exceeded") rethrowRead(error);
|
|
338
|
+
}
|
|
361
339
|
/**
|
|
362
340
|
* Run extraction tasks a few at a time, in order, keeping their results in
|
|
363
|
-
* task order. The first failure
|
|
364
|
-
* the streams still running at their next chunk and leaves the queued tasks
|
|
365
|
-
* unstarted.
|
|
341
|
+
* task order. The first failure leaves the queued tasks unstarted.
|
|
366
342
|
*/
|
|
367
|
-
async function runExtractionTasks(tasks
|
|
343
|
+
async function runExtractionTasks(tasks) {
|
|
368
344
|
const results = Array.from({ length: tasks.length }, () => null);
|
|
369
345
|
let next = 0;
|
|
346
|
+
let aborted = false;
|
|
370
347
|
const worker = async () => {
|
|
371
|
-
while (next < tasks.length && !
|
|
348
|
+
while (next < tasks.length && !aborted) {
|
|
372
349
|
const index = next;
|
|
373
350
|
next += 1;
|
|
374
351
|
const task = tasks[index];
|
|
375
352
|
if (!task) return;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
353
|
+
const result = await Result.tryPromise({
|
|
354
|
+
try: async () => await task(),
|
|
355
|
+
catch: (cause) => cause
|
|
356
|
+
});
|
|
357
|
+
if (Result.isError(result)) {
|
|
358
|
+
aborted = true;
|
|
359
|
+
throw result.error;
|
|
381
360
|
}
|
|
361
|
+
results[index] = result.value;
|
|
382
362
|
}
|
|
383
363
|
};
|
|
384
364
|
await Promise.all(Array.from({ length: Math.min(EXTRACTION_CONCURRENCY, tasks.length) }, async () => await worker()));
|
|
385
365
|
return results;
|
|
386
366
|
}
|
|
387
|
-
/**
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
};
|
|
422
|
-
} catch {
|
|
423
|
-
throw error;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
367
|
+
/** Package limits as the archive reader enforces them. */
|
|
368
|
+
const zipReadOptionsFor = (limits) => ({
|
|
369
|
+
limits: {
|
|
370
|
+
maxEntries: limits.maxFiles * 2,
|
|
371
|
+
maxEntryBytes: limits.maxTotalUncompressedBytes,
|
|
372
|
+
maxTotalBytes: limits.maxTotalUncompressedBytes,
|
|
373
|
+
maxCompressionRatio: limits.maxCompressionRatio,
|
|
374
|
+
compressionRatioGraceBytes: DOCX_COMPRESSION_RATIO_GRACE_BYTES
|
|
375
|
+
},
|
|
376
|
+
isCompressionRatioExempt: (path) => !isRatioBoundedPart(path)
|
|
377
|
+
});
|
|
378
|
+
async function loadDocxZip(buffer, limits) {
|
|
379
|
+
const options = zipReadOptionsFor(limits);
|
|
380
|
+
const read = await Result.tryPromise({
|
|
381
|
+
try: async () => await ZipArchive.read(buffer, options),
|
|
382
|
+
catch: (cause) => cause
|
|
383
|
+
});
|
|
384
|
+
if (Result.isOk(read)) return {
|
|
385
|
+
zip: read.value,
|
|
386
|
+
buffer
|
|
387
|
+
};
|
|
388
|
+
const error = read.error;
|
|
389
|
+
if (!(error instanceof ZipError) || error.reason !== "malformed") return rethrowRead(error);
|
|
390
|
+
const repairedBuffer = repairTruncatedEndOfCentralDirectory(buffer, limits.maxFiles);
|
|
391
|
+
if (!repairedBuffer) throw error;
|
|
392
|
+
const repaired = await Result.tryPromise({
|
|
393
|
+
try: async () => await ZipArchive.read(repairedBuffer, options),
|
|
394
|
+
catch: (cause) => cause
|
|
395
|
+
});
|
|
396
|
+
if (Result.isOk(repaired)) return {
|
|
397
|
+
zip: repaired.value,
|
|
398
|
+
buffer: repairedBuffer
|
|
399
|
+
};
|
|
400
|
+
throw error;
|
|
426
401
|
}
|
|
427
402
|
function repairTruncatedEndOfCentralDirectory(buffer, maxFiles) {
|
|
428
403
|
const bytes = new Uint8Array(buffer);
|
|
@@ -520,14 +495,11 @@ function isParsedDocxEntry(path) {
|
|
|
520
495
|
if (!(lowerPath.endsWith(".xml") || lowerPath.endsWith(".rels"))) return false;
|
|
521
496
|
return lowerPath === "[content_types].xml" || lowerPath.startsWith("_rels/") || lowerPath.startsWith("docprops/") || lowerPath.startsWith("word/") || lowerPath.startsWith("customxml/");
|
|
522
497
|
}
|
|
523
|
-
function getEntryUncompressedSize(file) {
|
|
524
|
-
return getZipEntrySizes(file).uncompressedBytes;
|
|
525
|
-
}
|
|
526
498
|
function assertEntrySize(path, declaredSize, maxBytes) {
|
|
527
499
|
if (isEntryTooLarge(declaredSize, maxBytes)) throw new DocxSecurityError(`DOCX entry exceeds maximum size: ${path}`);
|
|
528
500
|
}
|
|
529
501
|
function isEntryTooLarge(declaredSize, maxBytes) {
|
|
530
|
-
return declaredSize
|
|
502
|
+
return declaredSize > maxBytes;
|
|
531
503
|
}
|
|
532
504
|
function isMediaContentAllowed(data, mimeType) {
|
|
533
505
|
if (RASTER_MIME_TYPES.has(mimeType)) return detectRasterMimeType(data) === mimeType;
|
|
@@ -548,7 +520,7 @@ function isMediaContentAllowed(data, mimeType) {
|
|
|
548
520
|
*/
|
|
549
521
|
function getFileList(content) {
|
|
550
522
|
const files = [];
|
|
551
|
-
for (const path of
|
|
523
|
+
for (const path of content.originalZip.paths()) if (isParsedDocxEntry(path)) files.push(path);
|
|
552
524
|
return files.toSorted();
|
|
553
525
|
}
|
|
554
526
|
/**
|
|
@@ -591,11 +563,11 @@ function mediaToDataUrl(data, mimeType) {
|
|
|
591
563
|
* @returns File content as string or ArrayBuffer, or null if not found
|
|
592
564
|
*/
|
|
593
565
|
function extractFile(content, path) {
|
|
594
|
-
const file = content.originalZip.
|
|
566
|
+
const file = content.originalZip.entry(path);
|
|
595
567
|
if (!file || !isParsedDocxEntry(path)) return Promise.resolve(null);
|
|
596
568
|
const lowerPath = path.toLowerCase();
|
|
597
|
-
if (lowerPath.endsWith(".xml") || lowerPath.endsWith(".rels")) return file.
|
|
598
|
-
return file.
|
|
569
|
+
if (lowerPath.endsWith(".xml") || lowerPath.endsWith(".rels")) return file.bytes().then(decodeXmlBytes);
|
|
570
|
+
return file.arrayBuffer();
|
|
599
571
|
}
|
|
600
572
|
/**
|
|
601
573
|
* Check if a file exists in the DOCX
|
|
@@ -605,7 +577,7 @@ function extractFile(content, path) {
|
|
|
605
577
|
* @returns true if file exists
|
|
606
578
|
*/
|
|
607
579
|
function hasFile(content, path) {
|
|
608
|
-
return content.originalZip.
|
|
580
|
+
return content.originalZip.has(path);
|
|
609
581
|
}
|
|
610
582
|
/**
|
|
611
583
|
* Get summary of DOCX content
|
|
@@ -627,7 +599,7 @@ function getContentSummary(content) {
|
|
|
627
599
|
footerCount: content.footers.size,
|
|
628
600
|
mediaCount: content.media.size,
|
|
629
601
|
fontCount: content.fonts.size,
|
|
630
|
-
totalFiles:
|
|
602
|
+
totalFiles: content.originalZip.paths().length
|
|
631
603
|
};
|
|
632
604
|
}
|
|
633
|
-
export { DocxSecurityError, extractFile, getContentSummary,
|
|
605
|
+
export { DocxSecurityError, extractFile, getContentSummary, getFileList, getMediaMimeType, hasFile, mediaToDataUrl, replaceRawDocxXmlParts, unzipDocx };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/folio-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.65.0",
|
|
4
4
|
"description": "Headless, framework-neutral core of folio: the OOXML (.docx) parser, document model, ProseMirror integration, and page-layout engine. No React.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"document-model",
|
|
@@ -171,16 +171,15 @@
|
|
|
171
171
|
"perf": "bun scripts/profile-editor.ts"
|
|
172
172
|
},
|
|
173
173
|
"dependencies": {
|
|
174
|
-
"@stll/docx-core": "^0.
|
|
174
|
+
"@stll/docx-core": "^0.40.0",
|
|
175
175
|
"@stll/docx-utils": "^0.2.0",
|
|
176
176
|
"@stll/template-conditions": "^0.6.2",
|
|
177
|
-
"@stll/utf8-boundaries": "0.
|
|
177
|
+
"@stll/utf8-boundaries": "0.2.0",
|
|
178
178
|
"better-result": "3.0.1",
|
|
179
179
|
"csstype": "^3.1.3",
|
|
180
180
|
"dompurify": "^3.4.17",
|
|
181
181
|
"fast-xml-parser": "^5.11.2",
|
|
182
182
|
"hyphen": "1.14.1",
|
|
183
|
-
"jszip": "3.10.2",
|
|
184
183
|
"utif2": "^4.1.0",
|
|
185
184
|
"valibot": "1.5.0"
|
|
186
185
|
},
|