@stll/folio-core 0.3.1 → 0.4.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.d.ts +2 -1
- package/dist/ai-edits/headless.js +9 -7
- package/dist/controller/layoutPipeline.js +22 -0
- package/dist/docx/encryption/agileDecryption.d.ts +6 -0
- package/dist/docx/encryption/agileDecryption.js +208 -0
- package/dist/docx/encryption/compoundFile.d.ts +20 -0
- package/dist/docx/encryption/compoundFile.js +249 -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/index.d.ts +3 -1
- package/dist/docx/index.js +3 -1
- package/dist/docx/parser.d.ts +4 -5
- package/dist/docx/parser.js +8 -2
- package/dist/docx/unzip.d.ts +7 -5
- package/dist/docx/unzip.js +8 -2
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -1
- package/dist/layout-bridge/convert/headerFooterLayout.js +16 -6
- package/dist/layout-bridge/convert/toFlowBlocks.js +2 -0
- package/dist/layout-engine/index.js +27 -1
- package/dist/layout-engine/measure/measureBlocks.js +15 -1
- package/dist/layout-engine/measure/measureParagraph.js +37 -6
- package/dist/layout-engine/types.d.ts +3 -0
- package/dist/layout-painter/renderPage.js +2 -1
- package/dist/layout-painter/renderParagraph.js +30 -5
- package/dist/layout-painter/renderTable.js +17 -0
- package/dist/managers/DocumentLoaderManager.d.ts +3 -1
- package/dist/managers/DocumentLoaderManager.js +3 -2
- package/dist/paged-layout/headerFooterMargins.d.ts +3 -1
- package/dist/paged-layout/headerFooterMargins.js +5 -3
- package/dist/prosemirror/attrs/index.js +1 -0
- package/dist/prosemirror/conversion/fromProseDoc.js +7 -2
- package/dist/prosemirror/conversion/toProseDoc.js +14 -7
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +1 -0
- package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -0
- package/dist/prosemirror/schema/nodes.d.ts +4 -2
- package/dist/utils/fontResolver.js +17 -0
- package/package.json +5 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//#region src/docx/encryption/cryptoBytes.ts
|
|
2
|
+
/** Byte helpers shared by MS-CFB and MS-OFFCRYPTO agile decryption. */
|
|
3
|
+
const joinBytes = (parts) => {
|
|
4
|
+
let total = 0;
|
|
5
|
+
for (const part of parts) total += part.length;
|
|
6
|
+
const out = new Uint8Array(total);
|
|
7
|
+
let offset = 0;
|
|
8
|
+
for (const part of parts) {
|
|
9
|
+
out.set(part, offset);
|
|
10
|
+
offset += part.length;
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
};
|
|
14
|
+
const writeUint32Le = (value) => {
|
|
15
|
+
const out = /* @__PURE__ */ new Uint8Array(4);
|
|
16
|
+
out[0] = value & 255;
|
|
17
|
+
out[1] = value >>> 8 & 255;
|
|
18
|
+
out[2] = value >>> 16 & 255;
|
|
19
|
+
out[3] = value >>> 24 & 255;
|
|
20
|
+
return out;
|
|
21
|
+
};
|
|
22
|
+
const passwordToUtf16Le = (password) => {
|
|
23
|
+
const out = new Uint8Array(password.length * 2);
|
|
24
|
+
for (let i = 0; i < password.length; i++) {
|
|
25
|
+
const code = password.charCodeAt(i);
|
|
26
|
+
out[i * 2] = code & 255;
|
|
27
|
+
out[i * 2 + 1] = code >> 8;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
};
|
|
31
|
+
const bytesEqual = (left, right) => {
|
|
32
|
+
if (left.length !== right.length) return false;
|
|
33
|
+
let mismatch = 0;
|
|
34
|
+
for (let i = 0; i < left.length; i++) mismatch |= left[i] ^ right[i];
|
|
35
|
+
return mismatch === 0;
|
|
36
|
+
};
|
|
37
|
+
const toArrayBuffer = (bytes) => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
38
|
+
const decodeBase64 = (encoded) => {
|
|
39
|
+
if (typeof atob === "function") {
|
|
40
|
+
const binary = atob(encoded);
|
|
41
|
+
const out = new Uint8Array(binary.length);
|
|
42
|
+
for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
return new Uint8Array(Buffer.from(encoded, "base64"));
|
|
46
|
+
};
|
|
47
|
+
const padToBlock = (bytes, blockSize, padByte = 54) => {
|
|
48
|
+
if (bytes.length >= blockSize) return bytes.subarray(0, blockSize);
|
|
49
|
+
const out = new Uint8Array(blockSize);
|
|
50
|
+
out.set(bytes);
|
|
51
|
+
out.fill(padByte, bytes.length);
|
|
52
|
+
return out;
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { bytesEqual, decodeBase64, joinBytes, padToBlock, passwordToUtf16Le, toArrayBuffer, writeUint32Le };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/docx/encryption/encryptionInfo.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Parse the EncryptionInfo stream for Agile Encryption (Office 2010+).
|
|
4
|
+
*
|
|
5
|
+
* @see https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/87020a34-e73f-4139-99bc-bbdf6cf6fa55
|
|
6
|
+
*/
|
|
7
|
+
type AgileKeyMaterial = {
|
|
8
|
+
packageSalt: Uint8Array;
|
|
9
|
+
packageKeyBits: number;
|
|
10
|
+
packageHash: string;
|
|
11
|
+
packageHashBytes: number;
|
|
12
|
+
cipherBlockBytes: number;
|
|
13
|
+
cipherName: string;
|
|
14
|
+
cipherMode: string;
|
|
15
|
+
passwordIterations: number;
|
|
16
|
+
passwordSalt: Uint8Array;
|
|
17
|
+
passwordHash: string;
|
|
18
|
+
passwordKeyBits: number;
|
|
19
|
+
passwordBlockBytes: number;
|
|
20
|
+
encryptedVerifierInput: Uint8Array;
|
|
21
|
+
encryptedVerifierDigest: Uint8Array;
|
|
22
|
+
encryptedIntermediateKey: Uint8Array;
|
|
23
|
+
encryptedIntegrityKey: Uint8Array;
|
|
24
|
+
encryptedIntegrityDigest: Uint8Array;
|
|
25
|
+
};
|
|
26
|
+
type AgileEncryptionDescriptor = {
|
|
27
|
+
scheme: "agile";
|
|
28
|
+
material: AgileKeyMaterial;
|
|
29
|
+
};
|
|
30
|
+
declare const parseAgileEncryptionInfo: (stream: Uint8Array) => AgileEncryptionDescriptor;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { AgileEncryptionDescriptor, AgileKeyMaterial, parseAgileEncryptionInfo };
|
|
@@ -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 };
|
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 };
|
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
|
package/dist/docx/unzip.d.ts
CHANGED
|
@@ -13,8 +13,9 @@ type DocxUnzipLimits = {
|
|
|
13
13
|
maxTotalUncompressedBytes: number;
|
|
14
14
|
allowedMediaMimeTypes: ReadonlySet<string>;
|
|
15
15
|
};
|
|
16
|
-
type
|
|
17
|
-
allowedMediaMimeTypes?: Iterable<string>;
|
|
16
|
+
type DocxUnzipOptions = Partial<Omit<DocxUnzipLimits, "allowedMediaMimeTypes">> & {
|
|
17
|
+
allowedMediaMimeTypes?: Iterable<string>; /** Password for Agile-encrypted .docx files (Office 2010+). */
|
|
18
|
+
password?: string | undefined;
|
|
18
19
|
};
|
|
19
20
|
/**
|
|
20
21
|
* Raw extracted content from a DOCX file
|
|
@@ -45,7 +46,8 @@ type RawDocxContent = {
|
|
|
45
46
|
allXml: Map<string, string>;
|
|
46
47
|
originalZip: JSZip;
|
|
47
48
|
originalBuffer: ArrayBuffer;
|
|
48
|
-
warnings: string[];
|
|
49
|
+
warnings: string[]; /** True when the input was a password-protected CFB container. */
|
|
50
|
+
wasEncrypted: boolean;
|
|
49
51
|
};
|
|
50
52
|
/**
|
|
51
53
|
* Extract all content from a DOCX file
|
|
@@ -53,7 +55,7 @@ type RawDocxContent = {
|
|
|
53
55
|
* @param buffer - DOCX file as ArrayBuffer
|
|
54
56
|
* @returns Promise resolving to extracted content
|
|
55
57
|
*/
|
|
56
|
-
declare function unzipDocx(buffer: ArrayBuffer, options?:
|
|
58
|
+
declare function unzipDocx(buffer: ArrayBuffer, options?: DocxUnzipOptions): Promise<RawDocxContent>;
|
|
57
59
|
declare function isPreservableDocxEntry(path: string): boolean;
|
|
58
60
|
/**
|
|
59
61
|
* Get a list of all files in the DOCX
|
|
@@ -115,4 +117,4 @@ declare function getContentSummary(content: RawDocxContent): {
|
|
|
115
117
|
totalFiles: number;
|
|
116
118
|
};
|
|
117
119
|
//#endregion
|
|
118
|
-
export { DocxSecurityError, DocxUnzipLimits, RawDocxContent, extractFile, getContentSummary, getFileList, getMediaMimeType, hasFile, isPreservableDocxEntry, mediaToDataUrl, unzipDocx };
|
|
120
|
+
export { DocxSecurityError, DocxUnzipLimits, DocxUnzipOptions, RawDocxContent, extractFile, getContentSummary, getFileList, getMediaMimeType, hasFile, isPreservableDocxEntry, mediaToDataUrl, unzipDocx };
|
package/dist/docx/unzip.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DOCX_CONTAINER_TYPES, detectDocxContainerType } from "./encryption/containerFormat.js";
|
|
2
|
+
import { openDocxBuffer } from "./encryption/openEncryptedDocx.js";
|
|
1
3
|
import JSZip from "jszip";
|
|
2
4
|
//#region src/docx/unzip.ts
|
|
3
5
|
/**
|
|
@@ -71,7 +73,10 @@ const ZIP_CENTRAL_DIRECTORY_FILE_HEADER_SIZE = 46;
|
|
|
71
73
|
async function unzipDocx(buffer, options = {}) {
|
|
72
74
|
const limits = createUnzipLimits(options);
|
|
73
75
|
if (buffer.byteLength > limits.maxInputBytes) throw new DocxSecurityError("DOCX file exceeds the maximum allowed size");
|
|
74
|
-
const
|
|
76
|
+
const zipBuffer = await openDocxBuffer(buffer, { password: options.password });
|
|
77
|
+
if (zipBuffer.byteLength > limits.maxInputBytes) throw new DocxSecurityError("DOCX file exceeds the maximum allowed size");
|
|
78
|
+
const wasEncrypted = detectDocxContainerType(buffer) === DOCX_CONTAINER_TYPES.CFB;
|
|
79
|
+
const loaded = await loadDocxZip(zipBuffer, limits.maxFiles);
|
|
75
80
|
if (loaded.buffer.byteLength > limits.maxInputBytes) throw new DocxSecurityError("DOCX file exceeds the maximum allowed size");
|
|
76
81
|
const { zip } = loaded;
|
|
77
82
|
const entries = Object.entries(zip.files).filter(([, file]) => !file.dir);
|
|
@@ -102,7 +107,8 @@ async function unzipDocx(buffer, options = {}) {
|
|
|
102
107
|
allXml: /* @__PURE__ */ new Map(),
|
|
103
108
|
originalZip: zip,
|
|
104
109
|
originalBuffer: loaded.buffer,
|
|
105
|
-
warnings: []
|
|
110
|
+
warnings: [],
|
|
111
|
+
wasEncrypted
|
|
106
112
|
};
|
|
107
113
|
let totalUncompressedBytes = 0;
|
|
108
114
|
for (const [path, file] of entries) {
|
|
@@ -12,7 +12,7 @@ type HeaderFooterMetrics = {
|
|
|
12
12
|
};
|
|
13
13
|
margins: PageMargins;
|
|
14
14
|
};
|
|
15
|
-
declare function normalizeHeaderFooterMeasureBlocks(blocks: FlowBlock[]): FlowBlock[];
|
|
15
|
+
declare function normalizeHeaderFooterMeasureBlocks(blocks: FlowBlock[], section?: HeaderFooterMetrics["section"]): FlowBlock[];
|
|
16
16
|
declare function resolveHeaderFooterPositionedVisualTop(position: ImageRunPosition | undefined, elementHeight: number, sourceY: number, flowHeight: number, metrics: HeaderFooterMetrics): number;
|
|
17
17
|
declare function resolveHeaderFooterVisualTop(run: ImageRun, paragraphY: number, flowHeight: number, metrics: HeaderFooterMetrics): number;
|
|
18
18
|
declare function calculateHeaderFooterVisualBounds(blocks: FlowBlock[], measures: Measure[], flowHeight: number, metrics: HeaderFooterMetrics): {
|
|
@@ -16,13 +16,13 @@ function hasAuthoredVisualContent(block) {
|
|
|
16
16
|
if (attrs.spacingExplicit?.before || attrs.spacingExplicit?.after) return true;
|
|
17
17
|
return false;
|
|
18
18
|
}
|
|
19
|
-
function normalizeHeaderFooterMeasureBlocks(blocks) {
|
|
20
|
-
return normalizeFlowBlockArray(blocks);
|
|
19
|
+
function normalizeHeaderFooterMeasureBlocks(blocks, section = "header") {
|
|
20
|
+
return normalizeFlowBlockArray(blocks, { suppressTrailingEmptyAfterTable: section === "header" });
|
|
21
21
|
}
|
|
22
|
-
function normalizeFlowBlockArray(blocks) {
|
|
22
|
+
function normalizeFlowBlockArray(blocks, normalization) {
|
|
23
23
|
const trailingEmptyAfterTable = /* @__PURE__ */ new Set();
|
|
24
24
|
const lastIndex = blocks.length - 1;
|
|
25
|
-
if (lastIndex > 0) {
|
|
25
|
+
if (normalization.suppressTrailingEmptyAfterTable && lastIndex > 0) {
|
|
26
26
|
const cur = blocks[lastIndex];
|
|
27
27
|
if (blocks[lastIndex - 1]?.kind === "table" && cur?.kind === "paragraph" && cur.runs.length === 0 && !hasAuthoredVisualContent(cur)) trailingEmptyAfterTable.add(lastIndex);
|
|
28
28
|
}
|
|
@@ -70,12 +70,21 @@ function normalizeFlowBlockArray(blocks) {
|
|
|
70
70
|
};
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
+
function isCanonicalTrailingEmptyParagraphAfterTable(blocks, index) {
|
|
74
|
+
const cur = blocks[index];
|
|
75
|
+
const prev = blocks[index - 1];
|
|
76
|
+
return index === blocks.length - 1 && prev?.kind === "table" && cur?.kind === "paragraph" && isVisuallyEmptyParagraph(cur) && !hasAuthoredVisualContent(cur);
|
|
77
|
+
}
|
|
78
|
+
function isVisuallyEmptyParagraph(block) {
|
|
79
|
+
if (block.kind !== "paragraph") return false;
|
|
80
|
+
return block.runs.every((run) => run.kind === "text" && run.text.trim().length === 0);
|
|
81
|
+
}
|
|
73
82
|
function normalizeTableBlock(block) {
|
|
74
83
|
const blockState = { changed: false };
|
|
75
84
|
const rows = block.rows.map((row) => {
|
|
76
85
|
const rowState = { changed: false };
|
|
77
86
|
const cells = row.cells.map((cell) => {
|
|
78
|
-
const normalizedBlocks = normalizeFlowBlockArray(cell.blocks);
|
|
87
|
+
const normalizedBlocks = normalizeFlowBlockArray(cell.blocks, { suppressTrailingEmptyAfterTable: true });
|
|
79
88
|
if (!normalizedBlocks.some((normalizedBlock, idx) => normalizedBlock !== cell.blocks[idx])) return cell;
|
|
80
89
|
rowState.changed = true;
|
|
81
90
|
return {
|
|
@@ -232,6 +241,7 @@ function calculateHeaderFooterMarginPushBounds(blocks, measures, flowHeight, met
|
|
|
232
241
|
const block = blocks[i];
|
|
233
242
|
const measure = measures[i];
|
|
234
243
|
if (!block || !measure) continue;
|
|
244
|
+
if (isCanonicalTrailingEmptyParagraphAfterTable(blocks, i)) continue;
|
|
235
245
|
if (block.kind === "paragraph" && measure.kind === "paragraph") {
|
|
236
246
|
const paragraphBottomY = cursorY + measure.totalHeight;
|
|
237
247
|
top = Math.min(top, cursorY);
|
|
@@ -312,7 +322,7 @@ function convertHeaderFooterPmDocToContent(pmDoc, contentWidth, metrics, options
|
|
|
312
322
|
}
|
|
313
323
|
function finalizeHeaderFooterContent(blocks, contentWidth, metrics, options) {
|
|
314
324
|
if (blocks.length === 0) return;
|
|
315
|
-
const blocksForMeasure = normalizeHeaderFooterMeasureBlocks(blocks);
|
|
325
|
+
const blocksForMeasure = normalizeHeaderFooterMeasureBlocks(blocks, metrics.section);
|
|
316
326
|
const measures = options.measureBlocks(blocksForMeasure, contentWidth);
|
|
317
327
|
let flowHeight = 0;
|
|
318
328
|
for (let i = 0; i < measures.length; i++) {
|
|
@@ -732,6 +732,7 @@ function convertParagraphAttrs(pmAttrs, theme, listCounters, listAbstractCounter
|
|
|
732
732
|
if (pmAttrs.pageBreakBefore) attrs.pageBreakBefore = true;
|
|
733
733
|
if (pmAttrs.keepNext) attrs.keepNext = true;
|
|
734
734
|
if (pmAttrs.keepLines) attrs.keepLines = true;
|
|
735
|
+
if (pmAttrs.widowControl === false) attrs.widowControl = false;
|
|
735
736
|
if (pmAttrs.contextualSpacing) attrs.contextualSpacing = true;
|
|
736
737
|
if (pmAttrs.runInWithNext) attrs.runInWithNext = true;
|
|
737
738
|
if (directionIsRtl(pmAttrs.direction)) attrs.bidi = true;
|
|
@@ -941,6 +942,7 @@ function convertTableRow(node, startPos, options, tableCellMargins) {
|
|
|
941
942
|
if (attrs.height) row.height = twipsToPixels(attrs.height);
|
|
942
943
|
if (attrs.heightRule) row.heightRule = attrs.heightRule;
|
|
943
944
|
if (attrs.isHeader) row.isHeader = attrs.isHeader;
|
|
945
|
+
if (attrs.hidden) row.hidden = attrs.hidden;
|
|
944
946
|
return row;
|
|
945
947
|
}
|
|
946
948
|
/**
|
|
@@ -72,6 +72,9 @@ function getSpacingAfter(block) {
|
|
|
72
72
|
if (isEmptyParagraph(block) && !block.attrs?.spacingExplicit?.after) return 0;
|
|
73
73
|
return value;
|
|
74
74
|
}
|
|
75
|
+
function hasWidowControl(block) {
|
|
76
|
+
return block.attrs?.widowControl !== false;
|
|
77
|
+
}
|
|
75
78
|
/**
|
|
76
79
|
* Apply contextual spacing suppression (OOXML §17.3.1.9).
|
|
77
80
|
*
|
|
@@ -309,6 +312,28 @@ function layoutParagraph(block, measure, paginator, contentWidth, footnoteHeight
|
|
|
309
312
|
fittingLines++;
|
|
310
313
|
} else break;
|
|
311
314
|
}
|
|
315
|
+
let forceBreakAfterFragment = false;
|
|
316
|
+
if (hasWidowControl(block)) {
|
|
317
|
+
const remainingAfter = lines.length - (currentLineIndex + fittingLines);
|
|
318
|
+
if (fittingLines > 1 && remainingAfter === 1) {
|
|
319
|
+
if (currentLineIndex === 0 && fittingLines === 2 && state.cursorY !== state.topMargin) {
|
|
320
|
+
paginator.forceColumnBreak();
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
fittingLines -= 1;
|
|
324
|
+
forceBreakAfterFragment = true;
|
|
325
|
+
linesHeight = 0;
|
|
326
|
+
linesFnHeight = 0;
|
|
327
|
+
linesFnIds.length = 0;
|
|
328
|
+
for (let j = currentLineIndex; j < currentLineIndex + fittingLines; j++) {
|
|
329
|
+
const line = lines[j];
|
|
330
|
+
linesHeight += measuredLineAdvance(line);
|
|
331
|
+
const lineRefs = getLineFootnoteRefs(block, line.fromRun, line.toRun, footnoteHeightById);
|
|
332
|
+
linesFnHeight += lineRefs.height;
|
|
333
|
+
for (const id of lineRefs.ids) linesFnIds.push(id);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
312
337
|
const isFirstFragment = currentLineIndex === 0;
|
|
313
338
|
const isLastFragment = currentLineIndex + fittingLines >= lines.length;
|
|
314
339
|
const effectiveSpaceBefore = isFirstFragment ? spaceBefore : 0;
|
|
@@ -336,7 +361,8 @@ function layoutParagraph(block, measure, paginator, contentWidth, footnoteHeight
|
|
|
336
361
|
fragment.y = paginator.addFragment(fragment, linesHeight, effectiveSpaceBefore, effectiveSpaceAfter).y;
|
|
337
362
|
if (linesFnHeight > 0) paginator.addFootnoteHeight(linesFnHeight, linesFnIds);
|
|
338
363
|
currentLineIndex += fittingLines;
|
|
339
|
-
if (currentLineIndex < lines.length) paginator.
|
|
364
|
+
if (currentLineIndex < lines.length) if (forceBreakAfterFragment) paginator.forceColumnBreak();
|
|
365
|
+
else paginator.ensureFits(measuredLineAdvance(lines[currentLineIndex]));
|
|
340
366
|
}
|
|
341
367
|
}
|
|
342
368
|
/**
|