@cj-tech-master/excelts 9.5.5 → 9.5.6
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/browser/modules/excel/worksheet.d.ts +11 -0
- package/dist/browser/modules/excel/worksheet.js +13 -0
- package/dist/browser/modules/formula/integration/apply-writeback-plan.js +17 -3
- package/dist/browser/modules/formula/integration/workbook-adapter.js +20 -1
- package/dist/browser/modules/formula/integration/workbook-snapshot.d.ts +12 -0
- package/dist/browser/modules/formula/materialize/build-writeback-plan.js +47 -0
- package/dist/browser/modules/formula/materialize/types.d.ts +19 -3
- package/dist/browser/modules/formula/materialize/types.js +13 -3
- package/dist/browser/modules/pdf/builder/document-builder.js +2 -2
- package/dist/browser/modules/pdf/font/system-fonts.d.ts +24 -4
- package/dist/browser/modules/pdf/font/system-fonts.js +76 -32
- package/dist/browser/modules/pdf/render/pdf-exporter.js +6 -3
- package/dist/browser/modules/word/advanced/field-engine.js +151 -23
- package/dist/browser/modules/word/advanced/math-convert.js +2 -1
- package/dist/browser/modules/word/advanced/style-map.js +44 -6
- package/dist/browser/modules/word/convert/html/html-import.js +434 -71
- package/dist/browser/modules/word/convert/markdown/markdown-renderer.js +11 -3
- package/dist/browser/modules/word/layout/layout-full.js +4 -1
- package/dist/browser/modules/word/security/digital-signatures.js +160 -33
- package/dist/browser/modules/word/security/encryption.js +109 -9
- package/dist/cjs/modules/excel/worksheet.js +13 -0
- package/dist/cjs/modules/formula/integration/apply-writeback-plan.js +17 -3
- package/dist/cjs/modules/formula/integration/workbook-adapter.js +20 -1
- package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +47 -0
- package/dist/cjs/modules/formula/materialize/types.js +13 -3
- package/dist/cjs/modules/pdf/builder/document-builder.js +1 -1
- package/dist/cjs/modules/pdf/font/system-fonts.js +77 -32
- package/dist/cjs/modules/pdf/render/pdf-exporter.js +5 -2
- package/dist/cjs/modules/word/advanced/field-engine.js +151 -23
- package/dist/cjs/modules/word/advanced/math-convert.js +2 -1
- package/dist/cjs/modules/word/advanced/style-map.js +44 -6
- package/dist/cjs/modules/word/convert/html/html-import.js +434 -71
- package/dist/cjs/modules/word/convert/markdown/markdown-renderer.js +11 -3
- package/dist/cjs/modules/word/layout/layout-full.js +4 -1
- package/dist/cjs/modules/word/security/digital-signatures.js +160 -33
- package/dist/cjs/modules/word/security/encryption.js +109 -9
- package/dist/esm/modules/excel/worksheet.js +13 -0
- package/dist/esm/modules/formula/integration/apply-writeback-plan.js +17 -3
- package/dist/esm/modules/formula/integration/workbook-adapter.js +20 -1
- package/dist/esm/modules/formula/materialize/build-writeback-plan.js +47 -0
- package/dist/esm/modules/formula/materialize/types.js +13 -3
- package/dist/esm/modules/pdf/builder/document-builder.js +2 -2
- package/dist/esm/modules/pdf/font/system-fonts.js +76 -32
- package/dist/esm/modules/pdf/render/pdf-exporter.js +6 -3
- package/dist/esm/modules/word/advanced/field-engine.js +151 -23
- package/dist/esm/modules/word/advanced/math-convert.js +2 -1
- package/dist/esm/modules/word/advanced/style-map.js +44 -6
- package/dist/esm/modules/word/convert/html/html-import.js +434 -71
- package/dist/esm/modules/word/convert/markdown/markdown-renderer.js +11 -3
- package/dist/esm/modules/word/layout/layout-full.js +4 -1
- package/dist/esm/modules/word/security/digital-signatures.js +160 -33
- package/dist/esm/modules/word/security/encryption.js +109 -9
- package/dist/iife/excelts.iife.js +40 -26
- package/dist/iife/excelts.iife.js.map +1 -1
- package/dist/iife/excelts.iife.min.js +3 -3
- package/dist/types/modules/excel/worksheet.d.ts +11 -0
- package/dist/types/modules/formula/integration/workbook-snapshot.d.ts +12 -0
- package/dist/types/modules/formula/materialize/types.d.ts +19 -3
- package/dist/types/modules/pdf/font/system-fonts.d.ts +24 -4
- package/package.json +1 -1
|
@@ -46,52 +46,179 @@ export function parseSignatureXml(xmlStr, fileName) {
|
|
|
46
46
|
fileName,
|
|
47
47
|
cryptographicStatus: "not-verified"
|
|
48
48
|
};
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
// Each `<TagName ...>...</TagName>` lookup used to be a regex of the
|
|
50
|
+
// form `/<Tag[^>]*>([^<]*)<\/Tag>/.exec(xmlStr)`. Although `[^>]*` and
|
|
51
|
+
// `[^<]*` are linear in isolation, running ten such regexes against
|
|
52
|
+
// attacker-controlled signature XML triggers CodeQL's
|
|
53
|
+
// `js/polynomial-redos` rule. `extractTextElement` performs the same
|
|
54
|
+
// job in a single linear scan and cannot exhibit super-linear runtime.
|
|
55
|
+
const signer = extractTextElement(xmlStr, "SignatureText");
|
|
56
|
+
if (signer !== undefined) {
|
|
57
|
+
info.signer = xmlDecode(signer);
|
|
53
58
|
}
|
|
54
|
-
const
|
|
55
|
-
if (
|
|
56
|
-
info.signatureComments = xmlDecode(
|
|
59
|
+
const sigComments = extractTextElement(xmlStr, "SignatureComments");
|
|
60
|
+
if (sigComments !== undefined) {
|
|
61
|
+
info.signatureComments = xmlDecode(sigComments);
|
|
57
62
|
}
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
info.purpose = xmlDecode(
|
|
63
|
+
const purpose = extractTextElement(xmlStr, "SignaturePurpose");
|
|
64
|
+
if (purpose !== undefined) {
|
|
65
|
+
info.purpose = xmlDecode(purpose);
|
|
61
66
|
}
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
info.signDate = xmlDecode(
|
|
67
|
+
const signDate = extractTextElement(xmlStr, "SignatureDate");
|
|
68
|
+
if (signDate !== undefined) {
|
|
69
|
+
info.signDate = xmlDecode(signDate);
|
|
65
70
|
}
|
|
66
|
-
const
|
|
67
|
-
if (
|
|
68
|
-
info.providerUrl = xmlDecode(
|
|
71
|
+
const providerUrl = extractTextElement(xmlStr, "SignatureProviderUrl");
|
|
72
|
+
if (providerUrl !== undefined) {
|
|
73
|
+
info.providerUrl = xmlDecode(providerUrl);
|
|
69
74
|
}
|
|
70
|
-
// Commitment type
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
// Commitment type — nested element. Read the full `<CommitmentType>`
|
|
76
|
+
// body (which contains nested elements, hence `allowAngleBrackets`)
|
|
77
|
+
// then look for `<CommitmentTypeId>` inside.
|
|
78
|
+
const commitmentBlock = extractTextElement(xmlStr, "CommitmentType", {
|
|
79
|
+
allowAngleBrackets: true
|
|
80
|
+
});
|
|
81
|
+
if (commitmentBlock !== undefined) {
|
|
82
|
+
const commitmentId = extractTextElement(commitmentBlock, "CommitmentTypeId");
|
|
83
|
+
if (commitmentId !== undefined) {
|
|
84
|
+
info.commitmentType = xmlDecode(commitmentId);
|
|
85
|
+
}
|
|
74
86
|
}
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
if (
|
|
78
|
-
info.signatureValue =
|
|
87
|
+
// Signature value (base64 — may legitimately span newlines, so don't strip).
|
|
88
|
+
const signatureValue = extractTextElement(xmlStr, "SignatureValue", { allowAngleBrackets: true });
|
|
89
|
+
if (signatureValue !== undefined) {
|
|
90
|
+
info.signatureValue = signatureValue.trim();
|
|
79
91
|
}
|
|
80
92
|
// Certificate details from <X509Data>
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
info.certificateSubject = xmlDecode(
|
|
93
|
+
const certSubject = extractTextElement(xmlStr, "X509SubjectName");
|
|
94
|
+
if (certSubject !== undefined) {
|
|
95
|
+
info.certificateSubject = xmlDecode(certSubject);
|
|
84
96
|
}
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
info.certificateIssuer = xmlDecode(
|
|
97
|
+
const certIssuer = extractTextElement(xmlStr, "X509IssuerName");
|
|
98
|
+
if (certIssuer !== undefined) {
|
|
99
|
+
info.certificateIssuer = xmlDecode(certIssuer);
|
|
88
100
|
}
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
91
|
-
info.certificateSerialNumber = xmlDecode(
|
|
101
|
+
const certSerial = extractTextElement(xmlStr, "X509SerialNumber");
|
|
102
|
+
if (certSerial !== undefined) {
|
|
103
|
+
info.certificateSerialNumber = xmlDecode(certSerial);
|
|
92
104
|
}
|
|
93
105
|
return info;
|
|
94
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Find the first occurrence of `<tagName ...>...</tagName>` in `xml` and
|
|
109
|
+
* return the inner text (verbatim — the caller is responsible for entity
|
|
110
|
+
* decoding via `xmlDecode`).
|
|
111
|
+
*
|
|
112
|
+
* Implemented as a linear index scan rather than a regex match. The previous
|
|
113
|
+
* regex-based implementation tripped CodeQL's polynomial-regex detector
|
|
114
|
+
* because the input is attacker-controlled signature XML.
|
|
115
|
+
*
|
|
116
|
+
* @param xml - The XML text to search.
|
|
117
|
+
* @param tagName - Local element name (no namespace prefix). The match
|
|
118
|
+
* ignores any namespace prefix actually present in the document.
|
|
119
|
+
* @param options.allowAngleBrackets - When true, the inner text is read up
|
|
120
|
+
* to the literal `</tagName>` close tag rather than the next `<`. This
|
|
121
|
+
* is appropriate for elements like `SignatureValue` where the body is
|
|
122
|
+
* base64 and cannot legitimately contain `<` anyway, but lets the function
|
|
123
|
+
* tolerate accidental whitespace/newlines that some signers insert.
|
|
124
|
+
*/
|
|
125
|
+
function extractTextElement(xml, tagName, options = {}) {
|
|
126
|
+
const n = xml.length;
|
|
127
|
+
let from = 0;
|
|
128
|
+
while (from < n) {
|
|
129
|
+
const lt = xml.indexOf("<", from);
|
|
130
|
+
if (lt < 0) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
// Skip an optional namespace prefix: <ns:Tag ...>.
|
|
134
|
+
let nameStart = lt + 1;
|
|
135
|
+
// Look ahead for either the bare tag name or `prefix:tagName`. We do
|
|
136
|
+
// a forward scan rather than an unbounded regex match.
|
|
137
|
+
const colon = xml.indexOf(":", nameStart);
|
|
138
|
+
const ws = findTagNameEnd(xml, nameStart);
|
|
139
|
+
if (colon > 0 && colon < ws) {
|
|
140
|
+
nameStart = colon + 1;
|
|
141
|
+
}
|
|
142
|
+
if (xml.slice(nameStart, nameStart + tagName.length) !== tagName ||
|
|
143
|
+
!isTagNameBoundary(xml.charCodeAt(nameStart + tagName.length))) {
|
|
144
|
+
from = lt + 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
// Found `<…tagName`. Find the closing `>` of the open tag.
|
|
148
|
+
const openEnd = xml.indexOf(">", nameStart + tagName.length);
|
|
149
|
+
if (openEnd < 0) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
// Self-closing? Then the element has no text content.
|
|
153
|
+
if (xml.charCodeAt(openEnd - 1) === 0x2f /* '/' */) {
|
|
154
|
+
return "";
|
|
155
|
+
}
|
|
156
|
+
const bodyStart = openEnd + 1;
|
|
157
|
+
if (options.allowAngleBrackets) {
|
|
158
|
+
// Search for the matching close tag (allowing namespace prefix).
|
|
159
|
+
const closeIdx = findCloseTag(xml, bodyStart, tagName);
|
|
160
|
+
if (closeIdx < 0) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
return xml.slice(bodyStart, closeIdx);
|
|
164
|
+
}
|
|
165
|
+
// Default behaviour: text content has no `<`. Stop at the next `<`.
|
|
166
|
+
const lt2 = xml.indexOf("<", bodyStart);
|
|
167
|
+
if (lt2 < 0) {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
return xml.slice(bodyStart, lt2);
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
function findTagNameEnd(xml, start) {
|
|
175
|
+
const n = xml.length;
|
|
176
|
+
let i = start;
|
|
177
|
+
while (i < n) {
|
|
178
|
+
const c = xml.charCodeAt(i);
|
|
179
|
+
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d || c === 0x2f || c === 0x3e) {
|
|
180
|
+
return i;
|
|
181
|
+
}
|
|
182
|
+
i++;
|
|
183
|
+
}
|
|
184
|
+
return n;
|
|
185
|
+
}
|
|
186
|
+
function isTagNameBoundary(c) {
|
|
187
|
+
return (c === 0x20 || // space
|
|
188
|
+
c === 0x09 || // tab
|
|
189
|
+
c === 0x0a || // LF
|
|
190
|
+
c === 0x0d || // CR
|
|
191
|
+
c === 0x2f || // '/'
|
|
192
|
+
c === 0x3e // '>'
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
function findCloseTag(xml, from, tagName) {
|
|
196
|
+
const n = xml.length;
|
|
197
|
+
let i = from;
|
|
198
|
+
while (i < n) {
|
|
199
|
+
const lt = xml.indexOf("</", i);
|
|
200
|
+
if (lt < 0) {
|
|
201
|
+
return -1;
|
|
202
|
+
}
|
|
203
|
+
let p = lt + 2;
|
|
204
|
+
// Optional namespace prefix
|
|
205
|
+
const colon = xml.indexOf(":", p);
|
|
206
|
+
const gt = xml.indexOf(">", p);
|
|
207
|
+
if (gt < 0) {
|
|
208
|
+
return -1;
|
|
209
|
+
}
|
|
210
|
+
if (colon > 0 && colon < gt) {
|
|
211
|
+
p = colon + 1;
|
|
212
|
+
}
|
|
213
|
+
if (xml.slice(p, p + tagName.length) === tagName &&
|
|
214
|
+
// Allow trailing whitespace before '>' but require a boundary char.
|
|
215
|
+
isTagNameBoundary(xml.charCodeAt(p + tagName.length))) {
|
|
216
|
+
return lt;
|
|
217
|
+
}
|
|
218
|
+
i = lt + 2;
|
|
219
|
+
}
|
|
220
|
+
return -1;
|
|
221
|
+
}
|
|
95
222
|
/**
|
|
96
223
|
* Extract all digital signatures from opaque parts of a document.
|
|
97
224
|
*
|
|
@@ -324,13 +324,17 @@ function bytesEqual(a, b) {
|
|
|
324
324
|
* @returns Parsed agile encryption info.
|
|
325
325
|
*/
|
|
326
326
|
export function parseEncryptionInfoXml(xmlStr) {
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
327
|
+
// Locate the `<keyData ... />` and `<p:encryptedKey ... />` elements
|
|
328
|
+
// with a linear scan. Using regular expressions with lazy `[\s\S]*?`
|
|
329
|
+
// quantifiers triggers CodeQL's polynomial-regex warning because the
|
|
330
|
+
// input is attacker-controlled (a hostile EncryptionInfo XML stream
|
|
331
|
+
// with very long unterminated tags caused catastrophic backtracking).
|
|
332
|
+
const keyDataAttrs = extractSelfClosingTagAttrs(xmlStr, "keyData");
|
|
333
|
+
const pwdAttrs = extractSelfClosingTagAttrs(xmlStr, "p:encryptedKey");
|
|
334
|
+
if (!keyDataAttrs || !pwdAttrs) {
|
|
331
335
|
throw new DocxDecryptionError("Invalid EncryptionInfo XML - missing keyData or encryptedKey");
|
|
332
336
|
}
|
|
333
|
-
const pwdData =
|
|
337
|
+
const pwdData = pwdAttrs;
|
|
334
338
|
// Required cryptographic fields — empty/missing values cannot decrypt and
|
|
335
339
|
// produce confusing CryptoOperation errors downstream. Fail fast with a
|
|
336
340
|
// clear message instead.
|
|
@@ -393,12 +397,108 @@ export function parseEncryptionInfoXml(xmlStr) {
|
|
|
393
397
|
blockSize
|
|
394
398
|
};
|
|
395
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Find a `<tagName ... />` element and return its parsed attributes, or
|
|
402
|
+
* `null` if no such self-closing element exists.
|
|
403
|
+
*
|
|
404
|
+
* Uses a linear scan instead of a regex with `[\s\S]*?` to avoid
|
|
405
|
+
* catastrophic backtracking on adversarial EncryptionInfo XML.
|
|
406
|
+
*/
|
|
407
|
+
function extractSelfClosingTagAttrs(xml, tagName) {
|
|
408
|
+
const needle = `<${tagName}`;
|
|
409
|
+
let from = 0;
|
|
410
|
+
while (from <= xml.length) {
|
|
411
|
+
const start = xml.indexOf(needle, from);
|
|
412
|
+
if (start < 0) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
const after = start + needle.length;
|
|
416
|
+
const ch = xml.charCodeAt(after);
|
|
417
|
+
// Require a whitespace, '/' or '>' after the tag name so `<keyDataExtra`
|
|
418
|
+
// does not match `<keyData`.
|
|
419
|
+
if (ch !== 0x20 && ch !== 0x09 && ch !== 0x0a && ch !== 0x0d && ch !== 0x2f && ch !== 0x3e) {
|
|
420
|
+
from = after;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
// Find the closing '>' from `start`. Bail out if there isn't one.
|
|
424
|
+
const close = xml.indexOf(">", after);
|
|
425
|
+
if (close < 0) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
// The element must be self-closing: the char before '>' is '/'.
|
|
429
|
+
if (xml.charCodeAt(close - 1) !== 0x2f) {
|
|
430
|
+
from = close + 1;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const inner = xml.slice(after, close - 1);
|
|
434
|
+
return parseAttrs(inner);
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Parse XML-style attributes (`name="value"`) from a fragment. Implemented
|
|
440
|
+
* as a single linear scan rather than a global regex so attacker-controlled
|
|
441
|
+
* input cannot trigger polynomial-time backtracking (CodeQL js/polynomial-redos).
|
|
442
|
+
*/
|
|
396
443
|
function parseAttrs(str) {
|
|
397
444
|
const attrs = {};
|
|
398
|
-
const
|
|
399
|
-
let
|
|
400
|
-
while (
|
|
401
|
-
|
|
445
|
+
const n = str.length;
|
|
446
|
+
let i = 0;
|
|
447
|
+
while (i < n) {
|
|
448
|
+
// Skip whitespace.
|
|
449
|
+
while (i < n) {
|
|
450
|
+
const c = str.charCodeAt(i);
|
|
451
|
+
if (c !== 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d) {
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
i++;
|
|
455
|
+
}
|
|
456
|
+
if (i >= n) {
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
// Read attribute name (\w+ equivalent: [A-Za-z0-9_]+).
|
|
460
|
+
const nameStart = i;
|
|
461
|
+
while (i < n) {
|
|
462
|
+
const c = str.charCodeAt(i);
|
|
463
|
+
const isWord = (c >= 0x30 && c <= 0x39) || // 0-9
|
|
464
|
+
(c >= 0x41 && c <= 0x5a) || // A-Z
|
|
465
|
+
(c >= 0x61 && c <= 0x7a) || // a-z
|
|
466
|
+
c === 0x5f; // _
|
|
467
|
+
if (!isWord) {
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
i++;
|
|
471
|
+
}
|
|
472
|
+
if (i === nameStart) {
|
|
473
|
+
// Not at an attribute — advance one char so we make progress.
|
|
474
|
+
i++;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const name = str.slice(nameStart, i);
|
|
478
|
+
// Expect `="` exactly. Anything else means we resync to the next
|
|
479
|
+
// whitespace and try again — robust to malformed input.
|
|
480
|
+
if (i + 1 >= n || str.charCodeAt(i) !== 0x3d || str.charCodeAt(i + 1) !== 0x22) {
|
|
481
|
+
// Skip to next whitespace.
|
|
482
|
+
while (i < n) {
|
|
483
|
+
const c = str.charCodeAt(i);
|
|
484
|
+
if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) {
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
i++;
|
|
488
|
+
}
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
i += 2; // past `="`
|
|
492
|
+
// Read until next `"`.
|
|
493
|
+
const valStart = i;
|
|
494
|
+
const valEnd = str.indexOf('"', i);
|
|
495
|
+
if (valEnd < 0) {
|
|
496
|
+
// Unterminated value — store what we have and stop.
|
|
497
|
+
attrs[name] = str.slice(valStart);
|
|
498
|
+
break;
|
|
499
|
+
}
|
|
500
|
+
attrs[name] = str.slice(valStart, valEnd);
|
|
501
|
+
i = valEnd + 1;
|
|
402
502
|
}
|
|
403
503
|
return attrs;
|
|
404
504
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* @cj-tech-master/excelts v9.5.
|
|
2
|
+
* @cj-tech-master/excelts v9.5.6
|
|
3
3
|
* Zero-dependency TypeScript toolkit — Excel (XLSX), PDF, CSV, Markdown, XML, ZIP/TAR, and streaming.
|
|
4
4
|
* (c) 2026 cjnoname
|
|
5
5
|
* Released under the MIT License
|
|
@@ -41659,6 +41659,19 @@ self.onmessage = async function(event) {
|
|
|
41659
41659
|
return Object.values(this._merges).some(Boolean);
|
|
41660
41660
|
}
|
|
41661
41661
|
/**
|
|
41662
|
+
* Read-only enumeration of every merged region on this sheet
|
|
41663
|
+
* (1-based, inclusive). Consumed by the formula engine's snapshot
|
|
41664
|
+
* builder to detect `#SPILL!` conflicts. See issue #162 follow-up.
|
|
41665
|
+
*/
|
|
41666
|
+
get mergedRegions() {
|
|
41667
|
+
return Object.values(this._merges).map((merge) => ({
|
|
41668
|
+
top: merge.top,
|
|
41669
|
+
left: merge.left,
|
|
41670
|
+
bottom: merge.bottom,
|
|
41671
|
+
right: merge.right
|
|
41672
|
+
}));
|
|
41673
|
+
}
|
|
41674
|
+
/**
|
|
41662
41675
|
* Scan the range and if any cell is part of a merge, un-merge the group.
|
|
41663
41676
|
* Note this function can affect multiple merges and merge-blocks are
|
|
41664
41677
|
* atomic - either they're all merged or all un-merged.
|
|
@@ -87052,52 +87065,53 @@ self.onmessage = async function(event) {
|
|
|
87052
87065
|
return dirs;
|
|
87053
87066
|
}
|
|
87054
87067
|
/**
|
|
87055
|
-
*
|
|
87068
|
+
* Lazily yield discoverable system font candidates, in preference order.
|
|
87056
87069
|
*
|
|
87057
87070
|
* Each entry is the raw font file bytes of a `.ttf` or `.ttc` file.
|
|
87058
87071
|
* The caller decides which candidate to use (e.g. by checking cmap coverage).
|
|
87059
87072
|
*
|
|
87060
|
-
*
|
|
87073
|
+
* Iterating one candidate at a time lets callers `break` as soon as
|
|
87074
|
+
* they find a match, avoiding the cost of recursively reading every
|
|
87075
|
+
* font in every system font directory just to discard them.
|
|
87061
87076
|
*/
|
|
87062
|
-
function
|
|
87063
|
-
if (_cachedCandidates !== void 0)
|
|
87064
|
-
|
|
87065
|
-
|
|
87066
|
-
return _cachedCandidates;
|
|
87077
|
+
function* iterateSystemFontCandidates() {
|
|
87078
|
+
if (_cachedCandidates !== void 0) {
|
|
87079
|
+
for (const c of _cachedCandidates) yield c;
|
|
87080
|
+
return;
|
|
87067
87081
|
}
|
|
87068
|
-
|
|
87082
|
+
if (typeof process === "undefined" || !process.platform) return;
|
|
87069
87083
|
const seen = /* @__PURE__ */ new Set();
|
|
87070
87084
|
const dirs = getSystemFontDirs();
|
|
87071
87085
|
for (const fontName of PREFERRED_FONTS) for (const dir of dirs) {
|
|
87072
87086
|
const fontPath = `${dir}/${fontName}`;
|
|
87073
87087
|
if (seen.has(fontPath)) continue;
|
|
87088
|
+
seen.add(fontPath);
|
|
87074
87089
|
if (fileExistsSync(fontPath)) {
|
|
87075
87090
|
const data = tryReadFont(fontPath);
|
|
87076
|
-
if (data)
|
|
87077
|
-
candidates.push(data);
|
|
87078
|
-
seen.add(fontPath);
|
|
87079
|
-
}
|
|
87091
|
+
if (data) yield data;
|
|
87080
87092
|
}
|
|
87081
87093
|
}
|
|
87082
87094
|
const broadRe = /noto|unicode|cjk|yahei|heiti|gothic|sans|serif|ming|song|dejavu|liberation|droid|wqy/i;
|
|
87083
|
-
for (const dir of dirs)
|
|
87084
|
-
|
|
87085
|
-
|
|
87086
|
-
|
|
87087
|
-
|
|
87095
|
+
for (const dir of dirs) {
|
|
87096
|
+
let entries;
|
|
87097
|
+
try {
|
|
87098
|
+
entries = traverseDirectorySync(dir, {
|
|
87099
|
+
recursive: true,
|
|
87100
|
+
filter: (e) => !e.isDirectory
|
|
87101
|
+
});
|
|
87102
|
+
} catch {
|
|
87103
|
+
continue;
|
|
87104
|
+
}
|
|
87105
|
+
const fonts = entries.filter((e) => /\.tt[cf]$/i.test(e.absolutePath) && !seen.has(e.absolutePath));
|
|
87088
87106
|
const broad = fonts.filter((e) => broadRe.test(e.absolutePath));
|
|
87089
87107
|
const rest = fonts.filter((e) => !broadRe.test(e.absolutePath) && e.size > 5e4);
|
|
87090
87108
|
for (const entry of [...broad, ...rest]) {
|
|
87091
87109
|
if (seen.has(entry.absolutePath)) continue;
|
|
87110
|
+
seen.add(entry.absolutePath);
|
|
87092
87111
|
const data = tryReadFont(entry.absolutePath);
|
|
87093
|
-
if (data)
|
|
87094
|
-
candidates.push(data);
|
|
87095
|
-
seen.add(entry.absolutePath);
|
|
87096
|
-
}
|
|
87112
|
+
if (data) yield data;
|
|
87097
87113
|
}
|
|
87098
|
-
}
|
|
87099
|
-
_cachedCandidates = candidates;
|
|
87100
|
-
return candidates;
|
|
87114
|
+
}
|
|
87101
87115
|
}
|
|
87102
87116
|
function tryReadFont(fontPath) {
|
|
87103
87117
|
try {
|
|
@@ -90198,7 +90212,7 @@ self.onmessage = async function(event) {
|
|
|
90198
90212
|
let fontData = options?.font ?? null;
|
|
90199
90213
|
if (!fontData) {
|
|
90200
90214
|
const nonWinAnsi = collectNonWinAnsiCodePoints(sheets);
|
|
90201
|
-
if (nonWinAnsi.size > 0) for (const candidate of
|
|
90215
|
+
if (nonWinAnsi.size > 0) for (const candidate of iterateSystemFontCandidates()) try {
|
|
90202
90216
|
const testTtf = parseTtf(candidate);
|
|
90203
90217
|
if ([...nonWinAnsi].every((cp) => testTtf.cmap.has(cp))) {
|
|
90204
90218
|
fontData = candidate;
|