@stll/folio-core 0.3.1 → 0.5.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/apply.d.ts +3 -9
- package/dist/ai-edits/apply.js +94 -14
- package/dist/ai-edits/blockRange.d.ts +33 -0
- package/dist/ai-edits/blockRange.js +58 -0
- package/dist/ai-edits/headless.d.ts +35 -5
- package/dist/ai-edits/headless.js +92 -22
- package/dist/ai-edits/index.d.ts +5 -3
- package/dist/ai-edits/index.js +3 -2
- package/dist/ai-edits/snapshot.d.ts +14 -2
- package/dist/ai-edits/snapshot.js +12 -1
- package/dist/ai-edits/types.d.ts +42 -3
- package/dist/compat/eigenpal.d.ts +3 -2
- package/dist/compat/eigenpal.js +6 -5
- package/dist/controller/layoutPipeline.d.ts +1 -0
- package/dist/controller/layoutPipeline.js +35 -115
- package/dist/controller/layoutScheduler.d.ts +2 -1
- package/dist/controller/layoutScheduler.js +6 -0
- package/dist/document-operations.d.ts +122 -0
- package/dist/document-operations.js +575 -0
- package/dist/docx/blockContentParser.js +17 -7
- package/dist/docx/encryption/agileDecryption.d.ts +6 -0
- package/dist/docx/encryption/agileDecryption.js +209 -0
- package/dist/docx/encryption/compoundFile.d.ts +20 -0
- package/dist/docx/encryption/compoundFile.js +251 -0
- package/dist/docx/encryption/containerFormat.d.ts +15 -0
- package/dist/docx/encryption/containerFormat.js +40 -0
- package/dist/docx/encryption/cryptoBytes.d.ts +11 -0
- package/dist/docx/encryption/cryptoBytes.js +55 -0
- package/dist/docx/encryption/encryptionInfo.d.ts +32 -0
- package/dist/docx/encryption/encryptionInfo.js +128 -0
- package/dist/docx/encryption/errors.d.ts +18 -0
- package/dist/docx/encryption/errors.js +13 -0
- package/dist/docx/encryption/index.d.ts +4 -0
- package/dist/docx/encryption/index.js +4 -0
- package/dist/docx/encryption/openEncryptedDocx.d.ts +28 -0
- package/dist/docx/encryption/openEncryptedDocx.js +65 -0
- package/dist/docx/footnoteParser.d.ts +1 -17
- package/dist/docx/footnoteParser.js +1 -27
- package/dist/docx/groupDrawingParser.d.ts +8 -0
- package/dist/docx/groupDrawingParser.js +131 -0
- package/dist/docx/index.d.ts +3 -1
- package/dist/docx/index.js +3 -1
- package/dist/docx/numberingParser.d.ts +3 -1
- package/dist/docx/numberingParser.js +8 -2
- package/dist/docx/paragraphParser.js +9 -3
- package/dist/docx/parser.d.ts +4 -5
- package/dist/docx/parser.js +8 -2
- package/dist/docx/runParser.js +8 -1
- package/dist/docx/selectiveXmlPatch.js +4 -10
- package/dist/docx/serializer/numberingSerializer.js +3 -1
- package/dist/docx/settingsParser.d.ts +6 -3
- package/dist/docx/settingsParser.js +2 -0
- package/dist/docx/unzip.d.ts +7 -5
- package/dist/docx/unzip.js +8 -2
- package/dist/docx/xmlParser.js +11 -4
- package/dist/index.d.ts +3 -2
- package/dist/index.js +6 -5
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +37 -1
- package/dist/layout-bridge/convert/footnoteLayout.js +138 -14
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +6 -8
- package/dist/layout-bridge/convert/headerFooterLayout.js +21 -13
- package/dist/layout-bridge/convert/toFlowBlocks.js +58 -19
- package/dist/layout-engine/index.d.ts +6 -5
- package/dist/layout-engine/index.js +116 -27
- package/dist/layout-engine/keep-together.d.ts +13 -8
- package/dist/layout-engine/keep-together.js +40 -25
- package/dist/layout-engine/layoutInstrumentation.d.ts +11 -1
- package/dist/layout-engine/layoutInstrumentation.js +10 -1
- package/dist/layout-engine/measure/measureBlocks.js +43 -19
- package/dist/layout-engine/measure/measureParagraph.js +91 -14
- package/dist/layout-engine/paginator.d.ts +2 -1
- package/dist/layout-engine/paginator.js +9 -3
- package/dist/layout-engine/textBoxGroup.d.ts +7 -0
- package/dist/layout-engine/textBoxGroup.js +8 -0
- package/dist/layout-engine/types.d.ts +33 -9
- package/dist/layout-engine/types.js +20 -2
- package/dist/layout-painter/renderPage.d.ts +15 -1
- package/dist/layout-painter/renderPage.js +29 -12
- package/dist/layout-painter/renderParagraph.js +44 -9
- package/dist/layout-painter/renderTable.js +27 -7
- package/dist/managers/DocumentLoaderManager.d.ts +3 -1
- package/dist/managers/DocumentLoaderManager.js +13 -5
- package/dist/managers/TableSelectionManager.js +1 -1
- package/dist/model.js +1 -1
- package/dist/paged-layout/pageText.d.ts +14 -0
- package/dist/paged-layout/pageText.js +21 -0
- package/dist/prosemirror/attrs/index.js +3 -0
- package/dist/prosemirror/commands/comments.js +75 -5
- package/dist/prosemirror/commands/hyperlink.js +4 -2
- package/dist/prosemirror/commands/image.js +2 -1
- package/dist/prosemirror/commands/index.js +1 -1
- package/dist/prosemirror/commands/propertyChangeScope.d.ts +68 -0
- package/dist/prosemirror/commands/propertyChangeScope.js +242 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +21 -5
- package/dist/prosemirror/conversion/toProseDoc.js +43 -15
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +3 -0
- package/dist/prosemirror/extensions/features/ListExtension.js +6 -0
- package/dist/prosemirror/extensions/nodes/TableExtension.js +12 -11
- package/dist/prosemirror/findReplaceSelection.js +2 -1
- package/dist/prosemirror/index.js +1 -1
- package/dist/prosemirror/insertOperations.js +1 -1
- package/dist/prosemirror/schema/nodes.d.ts +14 -7
- package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
- package/dist/prosemirror/utils/tabCalculator.js +4 -2
- package/dist/redline.d.ts +20 -0
- package/dist/redline.js +127 -0
- package/dist/server.d.ts +7 -3
- package/dist/server.js +6 -2
- package/dist/symbols.d.ts +35 -0
- package/dist/symbols.js +512 -0
- package/dist/utils/clipboard.js +3 -3
- package/dist/utils/fontLoader.js +3 -0
- package/dist/utils/fontResolver.js +65 -1
- package/dist/utils/zoom.d.ts +21 -0
- package/dist/utils/zoom.js +21 -0
- package/dist/version-comparison.d.ts +92 -0
- package/dist/version-comparison.js +436 -0
- package/package.json +7 -3
- package/dist/paged-layout/headerFooterMargins.d.ts +0 -73
- package/dist/paged-layout/headerFooterMargins.js +0 -120
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { decodeBase64 } from "./cryptoBytes.js";
|
|
2
|
+
import { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError } from "./errors.js";
|
|
3
|
+
//#region src/docx/encryption/encryptionInfo.ts
|
|
4
|
+
/**
|
|
5
|
+
* Parse the EncryptionInfo stream for Agile Encryption (Office 2010+).
|
|
6
|
+
*
|
|
7
|
+
* @see https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/87020a34-e73f-4139-99bc-bbdf6cf6fa55
|
|
8
|
+
*/
|
|
9
|
+
const AGILE_STREAM_VERSION = 4;
|
|
10
|
+
const AGILE_STREAM_KIND = 4;
|
|
11
|
+
const STANDARD_STREAM_KIND = 3;
|
|
12
|
+
const STREAM_PREFIX_BYTES = 8;
|
|
13
|
+
const readUint16Le = (bytes, offset) => bytes[offset] | bytes[offset + 1] << 8;
|
|
14
|
+
const findElement = (xml, localName) => {
|
|
15
|
+
let index = 0;
|
|
16
|
+
while (index < xml.length) {
|
|
17
|
+
const hit = xml.indexOf(localName, index);
|
|
18
|
+
if (hit === -1) return null;
|
|
19
|
+
const tagStart = xml.lastIndexOf("<", hit);
|
|
20
|
+
if (tagStart === -1 || hit - tagStart > 24) {
|
|
21
|
+
index = hit + localName.length;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const tagEnd = xml.indexOf(">", hit);
|
|
25
|
+
if (tagEnd === -1) return null;
|
|
26
|
+
const tag = xml.slice(tagStart, tagEnd + 1);
|
|
27
|
+
if (tag.includes(localName)) return tag;
|
|
28
|
+
index = hit + localName.length;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
const parseAttributes = (tag) => {
|
|
33
|
+
const attrs = {};
|
|
34
|
+
let i = tag.indexOf(" ");
|
|
35
|
+
if (i === -1) return attrs;
|
|
36
|
+
while (i < tag.length) {
|
|
37
|
+
while (i < tag.length && (tag[i] === " " || tag[i] === "\n" || tag[i] === "\r" || tag[i] === " ")) i++;
|
|
38
|
+
const nameStart = i;
|
|
39
|
+
while (i < tag.length && tag[i] !== "=" && tag[i] !== ">" && tag[i] !== "/") i++;
|
|
40
|
+
if (i >= tag.length || tag[i] !== "=") break;
|
|
41
|
+
const name = tag.slice(nameStart, i).trim();
|
|
42
|
+
i++;
|
|
43
|
+
const quote = tag[i];
|
|
44
|
+
if (quote !== "\"" && quote !== "'") break;
|
|
45
|
+
i++;
|
|
46
|
+
const valueStart = i;
|
|
47
|
+
while (i < tag.length && tag[i] !== quote) i++;
|
|
48
|
+
if (i >= tag.length) break;
|
|
49
|
+
attrs[name] = tag.slice(valueStart, i);
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
return attrs;
|
|
53
|
+
};
|
|
54
|
+
const requireAttr = (attrs, name, element) => {
|
|
55
|
+
const value = attrs[name];
|
|
56
|
+
if (value === void 0) throw new DocxEncryptionError({
|
|
57
|
+
code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
|
|
58
|
+
message: `EncryptionInfo <${element}> missing attribute "${name}"`
|
|
59
|
+
});
|
|
60
|
+
return value;
|
|
61
|
+
};
|
|
62
|
+
const requireInt = (attrs, name, element) => {
|
|
63
|
+
const parsed = Number.parseInt(requireAttr(attrs, name, element), 10);
|
|
64
|
+
if (Number.isNaN(parsed)) throw new DocxEncryptionError({
|
|
65
|
+
code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
|
|
66
|
+
message: `EncryptionInfo <${element}> attribute "${name}" is not an integer`
|
|
67
|
+
});
|
|
68
|
+
return parsed;
|
|
69
|
+
};
|
|
70
|
+
const requireBase64 = (attrs, name, element) => decodeBase64(requireAttr(attrs, name, element));
|
|
71
|
+
const parseAgileXml = (xmlBytes) => {
|
|
72
|
+
const xml = new TextDecoder("utf-8").decode(xmlBytes);
|
|
73
|
+
const keyDataTag = findElement(xml, "keyData");
|
|
74
|
+
const integrityTag = findElement(xml, "dataIntegrity");
|
|
75
|
+
const encryptedKeyTag = findElement(xml, "encryptedKey");
|
|
76
|
+
if (!keyDataTag || !integrityTag || !encryptedKeyTag) throw new DocxEncryptionError({
|
|
77
|
+
code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
|
|
78
|
+
message: "EncryptionInfo XML is missing required agile encryption elements"
|
|
79
|
+
});
|
|
80
|
+
const keyData = parseAttributes(keyDataTag);
|
|
81
|
+
const integrity = parseAttributes(integrityTag);
|
|
82
|
+
const encryptedKey = parseAttributes(encryptedKeyTag);
|
|
83
|
+
return {
|
|
84
|
+
packageSalt: requireBase64(keyData, "saltValue", "keyData"),
|
|
85
|
+
packageKeyBits: requireInt(keyData, "keyBits", "keyData"),
|
|
86
|
+
packageHash: requireAttr(keyData, "hashAlgorithm", "keyData"),
|
|
87
|
+
packageHashBytes: requireInt(keyData, "hashSize", "keyData"),
|
|
88
|
+
cipherBlockBytes: requireInt(keyData, "blockSize", "keyData"),
|
|
89
|
+
cipherName: requireAttr(keyData, "cipherAlgorithm", "keyData"),
|
|
90
|
+
cipherMode: requireAttr(keyData, "cipherChaining", "keyData"),
|
|
91
|
+
passwordIterations: requireInt(encryptedKey, "spinCount", "encryptedKey"),
|
|
92
|
+
passwordSalt: requireBase64(encryptedKey, "saltValue", "encryptedKey"),
|
|
93
|
+
passwordHash: requireAttr(encryptedKey, "hashAlgorithm", "encryptedKey"),
|
|
94
|
+
passwordKeyBits: requireInt(encryptedKey, "keyBits", "encryptedKey"),
|
|
95
|
+
passwordBlockBytes: requireInt(encryptedKey, "blockSize", "encryptedKey"),
|
|
96
|
+
encryptedVerifierInput: requireBase64(encryptedKey, "encryptedVerifierHashInput", "encryptedKey"),
|
|
97
|
+
encryptedVerifierDigest: requireBase64(encryptedKey, "encryptedVerifierHashValue", "encryptedKey"),
|
|
98
|
+
encryptedIntermediateKey: requireBase64(encryptedKey, "encryptedKeyValue", "encryptedKey"),
|
|
99
|
+
encryptedIntegrityKey: requireBase64(integrity, "encryptedHmacKey", "dataIntegrity"),
|
|
100
|
+
encryptedIntegrityDigest: requireBase64(integrity, "encryptedHmacValue", "dataIntegrity")
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const parseAgileEncryptionInfo = (stream) => {
|
|
104
|
+
if (stream.length < STREAM_PREFIX_BYTES) throw new DocxEncryptionError({
|
|
105
|
+
code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
|
|
106
|
+
message: `EncryptionInfo stream too short (${stream.length} bytes)`
|
|
107
|
+
});
|
|
108
|
+
const version = readUint16Le(stream, 0);
|
|
109
|
+
const kind = readUint16Le(stream, 2);
|
|
110
|
+
if (version === AGILE_STREAM_VERSION && kind === AGILE_STREAM_KIND) return {
|
|
111
|
+
scheme: "agile",
|
|
112
|
+
material: parseAgileXml(stream.subarray(STREAM_PREFIX_BYTES))
|
|
113
|
+
};
|
|
114
|
+
if ((version === 3 || version === AGILE_STREAM_VERSION) && kind === STANDARD_STREAM_KIND) throw new DocxEncryptionError({
|
|
115
|
+
code: DOCX_ENCRYPTION_ERROR_CODES.ENCRYPTION_UNSUPPORTED,
|
|
116
|
+
message: "Standard Encryption (Office 2007) is not supported. Only Agile Encryption (Office 2010+) is supported."
|
|
117
|
+
});
|
|
118
|
+
if (version <= 2) throw new DocxEncryptionError({
|
|
119
|
+
code: DOCX_ENCRYPTION_ERROR_CODES.ENCRYPTION_UNSUPPORTED,
|
|
120
|
+
message: "Legacy RC4 encryption is not supported. Only Agile Encryption (Office 2010+) is supported."
|
|
121
|
+
});
|
|
122
|
+
throw new DocxEncryptionError({
|
|
123
|
+
code: DOCX_ENCRYPTION_ERROR_CODES.ENCRYPTION_UNSUPPORTED,
|
|
124
|
+
message: `Unrecognized EncryptionInfo header version=${version} kind=${kind}`
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
//#endregion
|
|
128
|
+
export { parseAgileEncryptionInfo };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/docx/encryption/errors.d.ts
|
|
2
|
+
/** Machine-readable codes for password-protected OOXML failures. */
|
|
3
|
+
declare const DOCX_ENCRYPTION_ERROR_CODES: {
|
|
4
|
+
readonly PASSWORD_REQUIRED: "DOCX_PASSWORD_REQUIRED";
|
|
5
|
+
readonly PASSWORD_INVALID: "DOCX_PASSWORD_INVALID";
|
|
6
|
+
readonly ENCRYPTION_UNSUPPORTED: "DOCX_ENCRYPTION_UNSUPPORTED";
|
|
7
|
+
readonly DECRYPTION_FAILED: "DOCX_DECRYPTION_FAILED";
|
|
8
|
+
};
|
|
9
|
+
type DocxEncryptionErrorCode = (typeof DOCX_ENCRYPTION_ERROR_CODES)[keyof typeof DOCX_ENCRYPTION_ERROR_CODES];
|
|
10
|
+
declare const DocxEncryptionError_base: import("better-result").TaggedErrorClass<"DocxEncryptionError", {
|
|
11
|
+
message: string;
|
|
12
|
+
code: DocxEncryptionErrorCode;
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
}>;
|
|
15
|
+
declare class DocxEncryptionError extends DocxEncryptionError_base {}
|
|
16
|
+
declare const isDocxEncryptionError: (error: unknown) => error is DocxEncryptionError;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError, DocxEncryptionErrorCode, isDocxEncryptionError };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TaggedError } from "better-result";
|
|
2
|
+
//#region src/docx/encryption/errors.ts
|
|
3
|
+
/** Machine-readable codes for password-protected OOXML failures. */
|
|
4
|
+
const DOCX_ENCRYPTION_ERROR_CODES = {
|
|
5
|
+
PASSWORD_REQUIRED: "DOCX_PASSWORD_REQUIRED",
|
|
6
|
+
PASSWORD_INVALID: "DOCX_PASSWORD_INVALID",
|
|
7
|
+
ENCRYPTION_UNSUPPORTED: "DOCX_ENCRYPTION_UNSUPPORTED",
|
|
8
|
+
DECRYPTION_FAILED: "DOCX_DECRYPTION_FAILED"
|
|
9
|
+
};
|
|
10
|
+
var DocxEncryptionError = class extends TaggedError("DocxEncryptionError")() {};
|
|
11
|
+
const isDocxEncryptionError = (error) => error instanceof DocxEncryptionError;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError, isDocxEncryptionError };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { DOCX_CONTAINER_TYPES, DocxContainerType, detectDocxContainerType } from "./containerFormat.js";
|
|
2
|
+
import { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError, DocxEncryptionErrorCode, isDocxEncryptionError } from "./errors.js";
|
|
3
|
+
import { DecryptDocxOptions, DecryptDocxResult, decryptDocxIfNeeded, openDocxBuffer } from "./openEncryptedDocx.js";
|
|
4
|
+
export { DOCX_CONTAINER_TYPES, DOCX_ENCRYPTION_ERROR_CODES, type DecryptDocxOptions, type DecryptDocxResult, type DocxContainerType, DocxEncryptionError, type DocxEncryptionErrorCode, decryptDocxIfNeeded, detectDocxContainerType, isDocxEncryptionError, openDocxBuffer };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError, isDocxEncryptionError } from "./errors.js";
|
|
2
|
+
import { DOCX_CONTAINER_TYPES, detectDocxContainerType } from "./containerFormat.js";
|
|
3
|
+
import { decryptDocxIfNeeded, openDocxBuffer } from "./openEncryptedDocx.js";
|
|
4
|
+
export { DOCX_CONTAINER_TYPES, DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError, decryptDocxIfNeeded, detectDocxContainerType, isDocxEncryptionError, openDocxBuffer };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/docx/encryption/openEncryptedDocx.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Decrypt password-protected OOXML before ZIP extraction.
|
|
4
|
+
*
|
|
5
|
+
* Implementation is derived from [MS-OFFCRYPTO] and [MS-CFB] only.
|
|
6
|
+
*
|
|
7
|
+
* @see https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/
|
|
8
|
+
*/
|
|
9
|
+
type DecryptDocxOptions = {
|
|
10
|
+
/** Password for Agile-encrypted .docx files (Office 2010+). */password?: string | undefined;
|
|
11
|
+
};
|
|
12
|
+
type DecryptDocxResult = {
|
|
13
|
+
/** Plaintext OOXML ZIP bytes, or the original buffer when unencrypted. */data: ArrayBuffer; /** True when the input was an encrypted CFB container and decryption ran. */
|
|
14
|
+
wasEncrypted: boolean;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Decrypt a password-protected `.docx` when needed, or return the input unchanged.
|
|
18
|
+
*
|
|
19
|
+
* Supports Agile Encryption (Office 2010+). Standard Encryption and legacy RC4
|
|
20
|
+
* are rejected with `DOCX_ENCRYPTION_UNSUPPORTED`.
|
|
21
|
+
*
|
|
22
|
+
* Save produces a standard unencrypted OOXML ZIP; folio does not re-encrypt on export.
|
|
23
|
+
*/
|
|
24
|
+
declare const decryptDocxIfNeeded: (data: ArrayBuffer | Uint8Array, options?: DecryptDocxOptions) => Promise<DecryptDocxResult>;
|
|
25
|
+
/** Open a `.docx` buffer for parsing: decrypt when encrypted, then return the ZIP bytes. */
|
|
26
|
+
declare const openDocxBuffer: (data: ArrayBuffer | Uint8Array, options?: DecryptDocxOptions) => Promise<ArrayBuffer>;
|
|
27
|
+
//#endregion
|
|
28
|
+
export { DecryptDocxOptions, DecryptDocxResult, decryptDocxIfNeeded, openDocxBuffer };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { toArrayBuffer } from "./cryptoBytes.js";
|
|
2
|
+
import { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError } from "./errors.js";
|
|
3
|
+
import { decryptAgileEncryptedPackage } from "./agileDecryption.js";
|
|
4
|
+
import { readEncryptedPackageStreams } from "./compoundFile.js";
|
|
5
|
+
import { DOCX_CONTAINER_TYPES, detectDocxContainerType } from "./containerFormat.js";
|
|
6
|
+
import { parseAgileEncryptionInfo } from "./encryptionInfo.js";
|
|
7
|
+
//#region src/docx/encryption/openEncryptedDocx.ts
|
|
8
|
+
/**
|
|
9
|
+
* Decrypt password-protected OOXML before ZIP extraction.
|
|
10
|
+
*
|
|
11
|
+
* Implementation is derived from [MS-OFFCRYPTO] and [MS-CFB] only.
|
|
12
|
+
*
|
|
13
|
+
* @see https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/
|
|
14
|
+
*/
|
|
15
|
+
const assertZipPayload = (bytes) => {
|
|
16
|
+
if (detectDocxContainerType(bytes) !== DOCX_CONTAINER_TYPES.ZIP) throw new DocxEncryptionError({
|
|
17
|
+
code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
|
|
18
|
+
message: "Decrypted output is not a valid ZIP archive — the file may be corrupt"
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Decrypt a password-protected `.docx` when needed, or return the input unchanged.
|
|
23
|
+
*
|
|
24
|
+
* Supports Agile Encryption (Office 2010+). Standard Encryption and legacy RC4
|
|
25
|
+
* are rejected with `DOCX_ENCRYPTION_UNSUPPORTED`.
|
|
26
|
+
*
|
|
27
|
+
* Save produces a standard unencrypted OOXML ZIP; folio does not re-encrypt on export.
|
|
28
|
+
*/
|
|
29
|
+
const decryptDocxIfNeeded = async (data, options = {}) => {
|
|
30
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
31
|
+
const container = detectDocxContainerType(bytes);
|
|
32
|
+
if (container === DOCX_CONTAINER_TYPES.ZIP) return {
|
|
33
|
+
data: toArrayBuffer(bytes),
|
|
34
|
+
wasEncrypted: false
|
|
35
|
+
};
|
|
36
|
+
if (container !== DOCX_CONTAINER_TYPES.CFB) throw new DocxEncryptionError({
|
|
37
|
+
code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
|
|
38
|
+
message: "Unrecognized file format — expected a .docx (ZIP) or encrypted .docx (OLE/CFB)"
|
|
39
|
+
});
|
|
40
|
+
if (options.password == null) throw new DocxEncryptionError({
|
|
41
|
+
code: DOCX_ENCRYPTION_ERROR_CODES.PASSWORD_REQUIRED,
|
|
42
|
+
message: "This document is password-protected. A password is required to open it."
|
|
43
|
+
});
|
|
44
|
+
let streams;
|
|
45
|
+
try {
|
|
46
|
+
streams = readEncryptedPackageStreams(bytes);
|
|
47
|
+
} catch (cause) {
|
|
48
|
+
throw new DocxEncryptionError({
|
|
49
|
+
code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
|
|
50
|
+
message: "Failed to read encrypted OOXML streams",
|
|
51
|
+
cause
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
const descriptor = parseAgileEncryptionInfo(streams.encryptionInfo);
|
|
55
|
+
const decryptedZip = await decryptAgileEncryptedPackage(options.password, descriptor.material, streams.encryptedPackage);
|
|
56
|
+
assertZipPayload(decryptedZip);
|
|
57
|
+
return {
|
|
58
|
+
data: toArrayBuffer(decryptedZip),
|
|
59
|
+
wasEncrypted: true
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
/** Open a `.docx` buffer for parsing: decrypt when encrypted, then return the ZIP bytes. */
|
|
63
|
+
const openDocxBuffer = async (data, options = {}) => (await decryptDocxIfNeeded(data, options)).data;
|
|
64
|
+
//#endregion
|
|
65
|
+
export { decryptDocxIfNeeded, openDocxBuffer };
|
|
@@ -73,22 +73,6 @@ declare function isSeparatorFootnote(footnote: document_d_exports.Footnote): boo
|
|
|
73
73
|
* Check if an endnote is a separator (not regular content)
|
|
74
74
|
*/
|
|
75
75
|
declare function isSeparatorEndnote(endnote: document_d_exports.Endnote): boolean;
|
|
76
|
-
/**
|
|
77
|
-
* Get footnote number for display (excluding separators)
|
|
78
|
-
* @param footnote - The footnote to get the number for
|
|
79
|
-
* @param footnoteMap - The footnote map
|
|
80
|
-
* @param startNumber - Starting number (default 1)
|
|
81
|
-
* @returns The display number, or null for separator footnotes
|
|
82
|
-
*/
|
|
83
|
-
declare function getFootnoteDisplayNumber(footnote: document_d_exports.Footnote, footnoteMap: FootnoteMap, startNumber?: number): number | null;
|
|
84
|
-
/**
|
|
85
|
-
* Get endnote number for display (excluding separators)
|
|
86
|
-
* @param endnote - The endnote to get the number for
|
|
87
|
-
* @param endnoteMap - The endnote map
|
|
88
|
-
* @param startNumber - Starting number (default 1)
|
|
89
|
-
* @returns The display number, or null for separator endnotes
|
|
90
|
-
*/
|
|
91
|
-
declare function getEndnoteDisplayNumber(endnote: document_d_exports.Endnote, endnoteMap: EndnoteMap, startNumber?: number): number | null;
|
|
92
76
|
/**
|
|
93
77
|
* Create an empty footnote map
|
|
94
78
|
*/
|
|
@@ -106,4 +90,4 @@ declare function mergeFootnoteMaps(...maps: FootnoteMap[]): FootnoteMap;
|
|
|
106
90
|
*/
|
|
107
91
|
declare function mergeEndnoteMaps(...maps: EndnoteMap[]): EndnoteMap;
|
|
108
92
|
//#endregion
|
|
109
|
-
export { EndnoteMap, FootnoteMap, createEmptyEndnoteMap, createEmptyFootnoteMap,
|
|
93
|
+
export { EndnoteMap, FootnoteMap, createEmptyEndnoteMap, createEmptyFootnoteMap, getEndnoteText, getFootnoteText, isSeparatorEndnote, isSeparatorFootnote, mergeEndnoteMaps, mergeFootnoteMaps, parseEndnoteProperties, parseEndnotes, parseFootnoteProperties, parseFootnotes };
|
|
@@ -205,32 +205,6 @@ function isSeparatorEndnote(endnote) {
|
|
|
205
205
|
return endnote.noteType === "separator" || endnote.noteType === "continuationSeparator" || endnote.noteType === "continuationNotice";
|
|
206
206
|
}
|
|
207
207
|
/**
|
|
208
|
-
* Get footnote number for display (excluding separators)
|
|
209
|
-
* @param footnote - The footnote to get the number for
|
|
210
|
-
* @param footnoteMap - The footnote map
|
|
211
|
-
* @param startNumber - Starting number (default 1)
|
|
212
|
-
* @returns The display number, or null for separator footnotes
|
|
213
|
-
*/
|
|
214
|
-
function getFootnoteDisplayNumber(footnote, footnoteMap, startNumber = 1) {
|
|
215
|
-
if (isSeparatorFootnote(footnote)) return null;
|
|
216
|
-
const index = footnoteMap.getNormalFootnotes().findIndex((fn) => fn.id === footnote.id);
|
|
217
|
-
if (index === -1) return null;
|
|
218
|
-
return startNumber + index;
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Get endnote number for display (excluding separators)
|
|
222
|
-
* @param endnote - The endnote to get the number for
|
|
223
|
-
* @param endnoteMap - The endnote map
|
|
224
|
-
* @param startNumber - Starting number (default 1)
|
|
225
|
-
* @returns The display number, or null for separator endnotes
|
|
226
|
-
*/
|
|
227
|
-
function getEndnoteDisplayNumber(endnote, endnoteMap, startNumber = 1) {
|
|
228
|
-
if (isSeparatorEndnote(endnote)) return null;
|
|
229
|
-
const index = endnoteMap.getNormalEndnotes().findIndex((en) => en.id === endnote.id);
|
|
230
|
-
if (index === -1) return null;
|
|
231
|
-
return startNumber + index;
|
|
232
|
-
}
|
|
233
|
-
/**
|
|
234
208
|
* Create an empty footnote map
|
|
235
209
|
*/
|
|
236
210
|
function createEmptyFootnoteMap() {
|
|
@@ -267,4 +241,4 @@ function mergeEndnoteMaps(...maps) {
|
|
|
267
241
|
return createEndnoteMap(byId, endnotes);
|
|
268
242
|
}
|
|
269
243
|
//#endregion
|
|
270
|
-
export { createEmptyEndnoteMap, createEmptyFootnoteMap,
|
|
244
|
+
export { createEmptyEndnoteMap, createEmptyFootnoteMap, getEndnoteText, getFootnoteText, isSeparatorEndnote, isSeparatorFootnote, mergeEndnoteMaps, mergeFootnoteMaps, parseEndnoteProperties, parseEndnotes, parseFootnoteProperties, parseFootnotes };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
import { XmlElement } from "./xmlParser.js";
|
|
3
|
+
|
|
4
|
+
//#region src/docx/groupDrawingParser.d.ts
|
|
5
|
+
/** Parse a WordprocessingGroup drawing into a safe SVG-backed image preview. */
|
|
6
|
+
declare const parseGroupDrawing: (drawing: XmlElement) => document_d_exports.Image | null;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { parseGroupDrawing };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { findAllDeep, findChildByLocalName, findChildrenByLocalName, getAttribute, getLocalName, getTextContent, parseNumericAttribute } from "./xmlParser.js";
|
|
2
|
+
import { emuToPixels } from "../utils/units.js";
|
|
3
|
+
import { parseImage } from "./imageParser.js";
|
|
4
|
+
//#region src/docx/groupDrawingParser.ts
|
|
5
|
+
const HEX_COLOR = /^[0-9A-Fa-f]{6}$/u;
|
|
6
|
+
const DEFAULT_TEXT_COLOR = "000000";
|
|
7
|
+
const DEFAULT_FONT_HALF_POINTS = 22;
|
|
8
|
+
const DEFAULT_LINE_WIDTH_EMU = 9525;
|
|
9
|
+
const HALF_POINT_TO_EMU = 6350;
|
|
10
|
+
const MAX_GROUP_SHAPES = 256;
|
|
11
|
+
const MAX_PATH_COMMANDS = 1e4;
|
|
12
|
+
const MAX_TEXT_CHARACTERS = 2e4;
|
|
13
|
+
const MAX_SVG_CHARACTERS = 1e6;
|
|
14
|
+
const escapeXml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
15
|
+
const numericAttr = (element, name) => {
|
|
16
|
+
const direct = parseNumericAttribute(element, null, name);
|
|
17
|
+
if (direct !== void 0) return direct;
|
|
18
|
+
const wordValue = getAttribute(element, "w", name);
|
|
19
|
+
if (!wordValue) return 0;
|
|
20
|
+
const parsed = Number.parseInt(wordValue, 10);
|
|
21
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
22
|
+
};
|
|
23
|
+
const childTransform = (wsp) => {
|
|
24
|
+
const xfrm = findChildByLocalName(findChildByLocalName(wsp, "spPr"), "xfrm");
|
|
25
|
+
const off = findChildByLocalName(xfrm, "off");
|
|
26
|
+
const ext = findChildByLocalName(xfrm, "ext");
|
|
27
|
+
return {
|
|
28
|
+
x: numericAttr(off, "x"),
|
|
29
|
+
y: numericAttr(off, "y"),
|
|
30
|
+
width: numericAttr(ext, "cx"),
|
|
31
|
+
height: numericAttr(ext, "cy")
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
const colorFrom = (parent, fallback) => {
|
|
35
|
+
if (parent && getLocalName(parent.name ?? "") === "color") {
|
|
36
|
+
const value = getAttribute(parent, "w", "val");
|
|
37
|
+
if (value && HEX_COLOR.test(value)) return value.toUpperCase();
|
|
38
|
+
}
|
|
39
|
+
const value = getAttribute(findChildByLocalName(findChildByLocalName(parent, "solidFill"), "srgbClr"), null, "val");
|
|
40
|
+
if (value && HEX_COLOR.test(value)) return value.toUpperCase();
|
|
41
|
+
return fallback;
|
|
42
|
+
};
|
|
43
|
+
const wrapLine = (line, maxCharacters) => {
|
|
44
|
+
const words = line.trim().split(/\s+/u);
|
|
45
|
+
const lines = [];
|
|
46
|
+
let current = "";
|
|
47
|
+
for (const word of words) {
|
|
48
|
+
const candidate = current ? `${current} ${word}` : word;
|
|
49
|
+
if (current && candidate.length > maxCharacters) {
|
|
50
|
+
lines.push(current);
|
|
51
|
+
current = word;
|
|
52
|
+
} else current = candidate;
|
|
53
|
+
}
|
|
54
|
+
if (current) lines.push(current);
|
|
55
|
+
return lines;
|
|
56
|
+
};
|
|
57
|
+
const pathData = (path) => {
|
|
58
|
+
const commands = [];
|
|
59
|
+
for (const command of path.elements ?? []) {
|
|
60
|
+
if (commands.length >= MAX_PATH_COMMANDS) break;
|
|
61
|
+
if (command.type !== "element") continue;
|
|
62
|
+
const point = findChildByLocalName(command, "pt");
|
|
63
|
+
const x = numericAttr(point, "x");
|
|
64
|
+
const y = numericAttr(point, "y");
|
|
65
|
+
const name = command.name?.split(":").at(-1);
|
|
66
|
+
if (name === "moveTo") commands.push(`M ${x} ${y}`);
|
|
67
|
+
else if (name === "lnTo") commands.push(`L ${x} ${y}`);
|
|
68
|
+
else if (name === "quadBezTo") {
|
|
69
|
+
const points = findChildrenByLocalName(command, "pt");
|
|
70
|
+
if (points.length >= 2) commands.push(`Q ${numericAttr(points[0] ?? null, "x")} ${numericAttr(points[0] ?? null, "y")} ${numericAttr(points[1] ?? null, "x")} ${numericAttr(points[1] ?? null, "y")}`);
|
|
71
|
+
} else if (name === "cubicBezTo") {
|
|
72
|
+
const points = findChildrenByLocalName(command, "pt");
|
|
73
|
+
if (points.length >= 3) commands.push(`C ${numericAttr(points[0] ?? null, "x")} ${numericAttr(points[0] ?? null, "y")} ${numericAttr(points[1] ?? null, "x")} ${numericAttr(points[1] ?? null, "y")} ${numericAttr(points[2] ?? null, "x")} ${numericAttr(points[2] ?? null, "y")}`);
|
|
74
|
+
} else if (name === "close") commands.push("Z");
|
|
75
|
+
}
|
|
76
|
+
return commands.join(" ");
|
|
77
|
+
};
|
|
78
|
+
const renderGeometry = (wsp) => {
|
|
79
|
+
const { x, y, width, height } = childTransform(wsp);
|
|
80
|
+
if (width <= 0 || height <= 0) return "";
|
|
81
|
+
const spPr = findChildByLocalName(wsp, "spPr");
|
|
82
|
+
const fill = colorFrom(spPr, "none");
|
|
83
|
+
const line = findChildByLocalName(spPr, "ln");
|
|
84
|
+
const stroke = colorFrom(line, "none");
|
|
85
|
+
const strokeWidth = line ? parseNumericAttribute(line, null, "w") ?? DEFAULT_LINE_WIDTH_EMU : 0;
|
|
86
|
+
const paths = findAllDeep(findChildByLocalName(spPr, "custGeom"), "a", "path");
|
|
87
|
+
if (paths.length === 0) return `<rect x="${x}" y="${y}" width="${width}" height="${height}" fill="${fill === "none" ? "none" : `#${fill}`}" stroke="${stroke === "none" ? "none" : `#${stroke}`}" stroke-width="${strokeWidth}"/>`;
|
|
88
|
+
return paths.map((path) => {
|
|
89
|
+
const viewWidth = numericAttr(path, "w") || width;
|
|
90
|
+
const viewHeight = numericAttr(path, "h") || height;
|
|
91
|
+
const d = pathData(path);
|
|
92
|
+
if (!d) return "";
|
|
93
|
+
return `<path d="${d}" transform="translate(${x} ${y}) scale(${width / viewWidth} ${height / viewHeight})" fill="${fill === "none" ? "none" : `#${fill}`}" stroke="${stroke === "none" ? "none" : `#${stroke}`}" stroke-width="${strokeWidth}"/>`;
|
|
94
|
+
}).join("");
|
|
95
|
+
};
|
|
96
|
+
const renderTextBox = (wsp) => {
|
|
97
|
+
const { x, y, width, height } = childTransform(wsp);
|
|
98
|
+
if (width <= 0 || height <= 0) return "";
|
|
99
|
+
const paragraphs = findAllDeep(wsp, "w", "p");
|
|
100
|
+
const firstSize = findAllDeep(wsp, "w", "sz").at(0);
|
|
101
|
+
const halfPoints = numericAttr(firstSize ?? null, "val") || DEFAULT_FONT_HALF_POINTS;
|
|
102
|
+
const color = colorFrom(findAllDeep(wsp, "w", "color").at(0) ?? null, DEFAULT_TEXT_COLOR);
|
|
103
|
+
const fontSize = halfPoints * HALF_POINT_TO_EMU;
|
|
104
|
+
const maxCharacters = Math.max(1, Math.floor(width / (fontSize * .38)));
|
|
105
|
+
const lines = paragraphs.flatMap((paragraph) => wrapLine(getTextContent(paragraph).slice(0, MAX_TEXT_CHARACTERS), maxCharacters)).map(escapeXml);
|
|
106
|
+
if (lines.length === 0) return "";
|
|
107
|
+
const lineHeight = fontSize * 1.15;
|
|
108
|
+
const svgFontSize = 1e3;
|
|
109
|
+
const scale = fontSize / svgFontSize;
|
|
110
|
+
const lineStep = lineHeight / fontSize * svgFontSize;
|
|
111
|
+
return `<text x="0" y="${svgFontSize}" transform="translate(${x} ${y}) scale(${scale})" font-family="Arial, sans-serif" font-size="${svgFontSize}" fill="#${color}">${lines.map((line, index) => `<tspan x="0" dy="${index === 0 ? 0 : lineStep}">${line}</tspan>`).join("")}</text>`;
|
|
112
|
+
};
|
|
113
|
+
const createSvg = (group, width, height) => {
|
|
114
|
+
const content = findChildrenByLocalName(group, "wsp").slice(0, MAX_GROUP_SHAPES).map((wsp) => findChildByLocalName(wsp, "txbx") ? renderTextBox(wsp) : renderGeometry(wsp)).join("");
|
|
115
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${emuToPixels(width)}" height="${emuToPixels(height)}">${content}</svg>`;
|
|
116
|
+
};
|
|
117
|
+
/** Parse a WordprocessingGroup drawing into a safe SVG-backed image preview. */
|
|
118
|
+
const parseGroupDrawing = (drawing) => {
|
|
119
|
+
const group = findChildByLocalName(findAllDeep(drawing, "a", "graphicData").at(0) ?? null, "wgp");
|
|
120
|
+
if (!group) return null;
|
|
121
|
+
const image = parseImage(drawing, void 0, void 0);
|
|
122
|
+
if (!image || image.size.width <= 0 || image.size.height <= 0) return null;
|
|
123
|
+
const svg = createSvg(group, image.size.width, image.size.height);
|
|
124
|
+
if (svg.length > MAX_SVG_CHARACTERS) return null;
|
|
125
|
+
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
126
|
+
image.mimeType = "image/svg+xml";
|
|
127
|
+
image.filename = "wordprocessing-group.svg";
|
|
128
|
+
return image;
|
|
129
|
+
};
|
|
130
|
+
//#endregion
|
|
131
|
+
export { parseGroupDrawing };
|
package/dist/docx/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import { getCachedNumberingMap } from "./numberingParser.js";
|
|
2
|
-
|
|
2
|
+
import { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError, DocxEncryptionErrorCode, isDocxEncryptionError } from "./encryption/errors.js";
|
|
3
|
+
import { DecryptDocxOptions, DecryptDocxResult, decryptDocxIfNeeded, openDocxBuffer } from "./encryption/openEncryptedDocx.js";
|
|
4
|
+
export { DOCX_ENCRYPTION_ERROR_CODES, type DecryptDocxOptions, type DecryptDocxResult, DocxEncryptionError, type DocxEncryptionErrorCode, decryptDocxIfNeeded, getCachedNumberingMap, isDocxEncryptionError, openDocxBuffer };
|
package/dist/docx/index.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import { getCachedNumberingMap } from "./numberingParser.js";
|
|
2
|
-
|
|
2
|
+
import { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError, isDocxEncryptionError } from "./encryption/errors.js";
|
|
3
|
+
import { decryptDocxIfNeeded, openDocxBuffer } from "./encryption/openEncryptedDocx.js";
|
|
4
|
+
export { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError, decryptDocxIfNeeded, getCachedNumberingMap, isDocxEncryptionError, openDocxBuffer };
|
|
@@ -39,7 +39,9 @@ declare function createNumberingMap(definitions: document_d_exports.NumberingDef
|
|
|
39
39
|
declare function computeListRendering(numPr: {
|
|
40
40
|
numId?: number;
|
|
41
41
|
ilvl?: number;
|
|
42
|
-
}, numbering: NumberingMap): document_d_exports.ListRendering
|
|
42
|
+
}, numbering: NumberingMap): (document_d_exports.ListRendering & {
|
|
43
|
+
levelStarts: number[];
|
|
44
|
+
}) | null;
|
|
43
45
|
/**
|
|
44
46
|
* Format a number according to the specified format
|
|
45
47
|
*
|
|
@@ -581,7 +581,12 @@ function computeListRendering(numPr, numbering) {
|
|
|
581
581
|
const level = numbering.getLevel(numId, ilvl);
|
|
582
582
|
if (!level) return null;
|
|
583
583
|
const levelNumFmts = [];
|
|
584
|
-
|
|
584
|
+
const levelStarts = [];
|
|
585
|
+
for (let i = 0; i <= ilvl; i += 1) {
|
|
586
|
+
const listLevel = numbering.getLevel(numId, i);
|
|
587
|
+
levelNumFmts.push(level.isLgl ? "decimal" : listLevel?.numFmt ?? "decimal");
|
|
588
|
+
levelStarts.push(listLevel?.start ?? 1);
|
|
589
|
+
}
|
|
585
590
|
const instance = numbering.getInstance(numId);
|
|
586
591
|
const overrideForLevel = instance?.levelOverrides?.find((override) => override.ilvl === ilvl);
|
|
587
592
|
const rendering = {
|
|
@@ -590,7 +595,8 @@ function computeListRendering(numPr, numbering) {
|
|
|
590
595
|
marker: level.lvlText,
|
|
591
596
|
isBullet: level.numFmt === "bullet",
|
|
592
597
|
numFmt: level.isLgl ? "decimal" : level.numFmt,
|
|
593
|
-
levelNumFmts
|
|
598
|
+
levelNumFmts,
|
|
599
|
+
levelStarts
|
|
594
600
|
};
|
|
595
601
|
if (level.isLgl) rendering.isLegal = true;
|
|
596
602
|
if (level.rPr?.hidden) rendering.markerHidden = true;
|
|
@@ -436,7 +436,7 @@ function paragraphStartsWithRenderedPageBreak(node) {
|
|
|
436
436
|
return "continue";
|
|
437
437
|
};
|
|
438
438
|
const outcome = visit(node);
|
|
439
|
-
if (outcome === "forced") return
|
|
439
|
+
if (outcome === "forced") return sawRenderedPageBreak;
|
|
440
440
|
return outcome === "visible" && sawRenderedPageBreak;
|
|
441
441
|
}
|
|
442
442
|
function replaceLocalName(name, localName) {
|
|
@@ -949,13 +949,19 @@ function parseParagraph(node, styles, theme, numbering, rels = null, media = nul
|
|
|
949
949
|
const level = numbering.getLevel(numId, ilvl);
|
|
950
950
|
if (level) {
|
|
951
951
|
const levelNumFmts = [];
|
|
952
|
-
|
|
952
|
+
const levelStarts = [];
|
|
953
|
+
for (let levelIndex = 0; levelIndex <= ilvl; levelIndex += 1) {
|
|
954
|
+
const listLevel = numbering.getLevel(numId, levelIndex);
|
|
955
|
+
levelNumFmts.push(level.isLgl ? "decimal" : listLevel?.numFmt ?? "decimal");
|
|
956
|
+
levelStarts.push(listLevel?.start ?? 1);
|
|
957
|
+
}
|
|
953
958
|
const listRendering = {
|
|
954
959
|
level: ilvl,
|
|
955
960
|
numId,
|
|
956
961
|
marker: level.lvlText,
|
|
957
962
|
isBullet: level.numFmt === "bullet",
|
|
958
|
-
levelNumFmts
|
|
963
|
+
levelNumFmts,
|
|
964
|
+
levelStarts
|
|
959
965
|
};
|
|
960
966
|
const instance = numbering.getInstance(numId);
|
|
961
967
|
const overrideForLevel = instance?.levelOverrides?.find((override) => override.ilvl === ilvl);
|
package/dist/docx/parser.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { document_d_exports } from "../types/document.js";
|
|
2
|
-
import {
|
|
2
|
+
import { DocxUnzipOptions } from "./unzip.js";
|
|
3
3
|
import { DocxInput } from "../utils/docxInput.js";
|
|
4
4
|
|
|
5
5
|
//#region src/docx/parser.d.ts
|
|
@@ -24,10 +24,9 @@ type ParseOptions = {
|
|
|
24
24
|
preloadFonts?: boolean; /** Whether to parse headers/footers (default: true) */
|
|
25
25
|
parseHeadersFooters?: boolean; /** Whether to parse footnotes/endnotes (default: true) */
|
|
26
26
|
parseNotes?: boolean; /** Whether to detect template variables (default: true) */
|
|
27
|
-
detectVariables?: boolean; /**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}; /** Optional async hook to override display URLs for non-browser media. */
|
|
27
|
+
detectVariables?: boolean; /** Password for Agile-encrypted .docx files (Office 2010+). */
|
|
28
|
+
password?: string | undefined; /** Security limits for DOCX ZIP extraction */
|
|
29
|
+
unzipLimits?: DocxUnzipOptions; /** Optional async hook to override display URLs for non-browser media. */
|
|
31
30
|
mediaResolver?: MediaResolver;
|
|
32
31
|
};
|
|
33
32
|
/**
|
package/dist/docx/parser.js
CHANGED
|
@@ -3,6 +3,7 @@ import { applyThemeFontLang, parseTheme } from "./themeParser.js";
|
|
|
3
3
|
import { parseEndnotes, parseFootnotes } from "./footnoteParser.js";
|
|
4
4
|
import { DocxModelValidationError, formatDocumentModelIssues, validateFolioDocumentModel } from "./modelValidation.js";
|
|
5
5
|
import { parseNumbering } from "./numberingParser.js";
|
|
6
|
+
import { DocxEncryptionError } from "./encryption/errors.js";
|
|
6
7
|
import { getMediaMimeType, mediaToDataUrl, unzipDocx } from "./unzip.js";
|
|
7
8
|
import { parseComments } from "./commentParser.js";
|
|
8
9
|
import { parseFooter, parseHeader } from "./headerFooterParser.js";
|
|
@@ -48,13 +49,17 @@ import { TaggedError } from "better-result";
|
|
|
48
49
|
*/
|
|
49
50
|
async function parseDocx(input, options = {}) {
|
|
50
51
|
const buffer = input instanceof ArrayBuffer ? input : await toArrayBuffer(input);
|
|
51
|
-
const { onProgress = () => {}, preloadFonts = true, parseHeadersFooters = true, parseNotes = true, detectVariables = true, unzipLimits, mediaResolver } = options;
|
|
52
|
+
const { onProgress = () => {}, preloadFonts = true, parseHeadersFooters = true, parseNotes = true, detectVariables = true, password, unzipLimits, mediaResolver } = options;
|
|
52
53
|
const warnings = [];
|
|
53
54
|
try {
|
|
54
55
|
const timeStage = (_name, fn) => fn();
|
|
55
56
|
const timeStageAsync = async (_name, fn) => await fn();
|
|
56
57
|
onProgress("Extracting DOCX...", 0);
|
|
57
|
-
const raw = await timeStageAsync("unzip", () => unzipDocx(buffer,
|
|
58
|
+
const raw = await timeStageAsync("unzip", () => unzipDocx(buffer, {
|
|
59
|
+
...unzipLimits,
|
|
60
|
+
password
|
|
61
|
+
}));
|
|
62
|
+
if (raw.wasEncrypted) warnings.push("Document was opened from password-protected storage; saving writes an unencrypted .docx file.");
|
|
58
63
|
warnings.push(...raw.warnings);
|
|
59
64
|
onProgress("Extracted DOCX", 10);
|
|
60
65
|
onProgress("Parsing relationships...", 10);
|
|
@@ -186,6 +191,7 @@ async function parseDocx(input, options = {}) {
|
|
|
186
191
|
onProgress("Complete", 100);
|
|
187
192
|
return document;
|
|
188
193
|
} catch (error) {
|
|
194
|
+
if (error instanceof DocxEncryptionError) throw error;
|
|
189
195
|
throw new DocxParseError({
|
|
190
196
|
message: `Failed to parse DOCX: ${error instanceof Error ? error.message : String(error)}`,
|
|
191
197
|
cause: error
|