@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.
Files changed (60) hide show
  1. package/dist/browser/modules/excel/worksheet.d.ts +11 -0
  2. package/dist/browser/modules/excel/worksheet.js +13 -0
  3. package/dist/browser/modules/formula/integration/apply-writeback-plan.js +17 -3
  4. package/dist/browser/modules/formula/integration/workbook-adapter.js +20 -1
  5. package/dist/browser/modules/formula/integration/workbook-snapshot.d.ts +12 -0
  6. package/dist/browser/modules/formula/materialize/build-writeback-plan.js +47 -0
  7. package/dist/browser/modules/formula/materialize/types.d.ts +19 -3
  8. package/dist/browser/modules/formula/materialize/types.js +13 -3
  9. package/dist/browser/modules/pdf/builder/document-builder.js +2 -2
  10. package/dist/browser/modules/pdf/font/system-fonts.d.ts +24 -4
  11. package/dist/browser/modules/pdf/font/system-fonts.js +76 -32
  12. package/dist/browser/modules/pdf/render/pdf-exporter.js +6 -3
  13. package/dist/browser/modules/word/advanced/field-engine.js +151 -23
  14. package/dist/browser/modules/word/advanced/math-convert.js +2 -1
  15. package/dist/browser/modules/word/advanced/style-map.js +44 -6
  16. package/dist/browser/modules/word/convert/html/html-import.js +434 -71
  17. package/dist/browser/modules/word/convert/markdown/markdown-renderer.js +11 -3
  18. package/dist/browser/modules/word/layout/layout-full.js +4 -1
  19. package/dist/browser/modules/word/security/digital-signatures.js +160 -33
  20. package/dist/browser/modules/word/security/encryption.js +109 -9
  21. package/dist/cjs/modules/excel/worksheet.js +13 -0
  22. package/dist/cjs/modules/formula/integration/apply-writeback-plan.js +17 -3
  23. package/dist/cjs/modules/formula/integration/workbook-adapter.js +20 -1
  24. package/dist/cjs/modules/formula/materialize/build-writeback-plan.js +47 -0
  25. package/dist/cjs/modules/formula/materialize/types.js +13 -3
  26. package/dist/cjs/modules/pdf/builder/document-builder.js +1 -1
  27. package/dist/cjs/modules/pdf/font/system-fonts.js +77 -32
  28. package/dist/cjs/modules/pdf/render/pdf-exporter.js +5 -2
  29. package/dist/cjs/modules/word/advanced/field-engine.js +151 -23
  30. package/dist/cjs/modules/word/advanced/math-convert.js +2 -1
  31. package/dist/cjs/modules/word/advanced/style-map.js +44 -6
  32. package/dist/cjs/modules/word/convert/html/html-import.js +434 -71
  33. package/dist/cjs/modules/word/convert/markdown/markdown-renderer.js +11 -3
  34. package/dist/cjs/modules/word/layout/layout-full.js +4 -1
  35. package/dist/cjs/modules/word/security/digital-signatures.js +160 -33
  36. package/dist/cjs/modules/word/security/encryption.js +109 -9
  37. package/dist/esm/modules/excel/worksheet.js +13 -0
  38. package/dist/esm/modules/formula/integration/apply-writeback-plan.js +17 -3
  39. package/dist/esm/modules/formula/integration/workbook-adapter.js +20 -1
  40. package/dist/esm/modules/formula/materialize/build-writeback-plan.js +47 -0
  41. package/dist/esm/modules/formula/materialize/types.js +13 -3
  42. package/dist/esm/modules/pdf/builder/document-builder.js +2 -2
  43. package/dist/esm/modules/pdf/font/system-fonts.js +76 -32
  44. package/dist/esm/modules/pdf/render/pdf-exporter.js +6 -3
  45. package/dist/esm/modules/word/advanced/field-engine.js +151 -23
  46. package/dist/esm/modules/word/advanced/math-convert.js +2 -1
  47. package/dist/esm/modules/word/advanced/style-map.js +44 -6
  48. package/dist/esm/modules/word/convert/html/html-import.js +434 -71
  49. package/dist/esm/modules/word/convert/markdown/markdown-renderer.js +11 -3
  50. package/dist/esm/modules/word/layout/layout-full.js +4 -1
  51. package/dist/esm/modules/word/security/digital-signatures.js +160 -33
  52. package/dist/esm/modules/word/security/encryption.js +109 -9
  53. package/dist/iife/excelts.iife.js +40 -26
  54. package/dist/iife/excelts.iife.js.map +1 -1
  55. package/dist/iife/excelts.iife.min.js +3 -3
  56. package/dist/types/modules/excel/worksheet.d.ts +11 -0
  57. package/dist/types/modules/formula/integration/workbook-snapshot.d.ts +12 -0
  58. package/dist/types/modules/formula/materialize/types.d.ts +19 -3
  59. package/dist/types/modules/pdf/font/system-fonts.d.ts +24 -4
  60. 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
- // Extract Office-specific metadata from <SignatureInfoV1>
50
- const sigTextMatch = /<SignatureText[^>]*>([^<]*)<\/SignatureText>/.exec(xmlStr);
51
- if (sigTextMatch) {
52
- info.signer = xmlDecode(sigTextMatch[1]);
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 sigCommentsMatch = /<SignatureComments[^>]*>([^<]*)<\/SignatureComments>/.exec(xmlStr);
55
- if (sigCommentsMatch) {
56
- info.signatureComments = xmlDecode(sigCommentsMatch[1]);
59
+ const sigComments = extractTextElement(xmlStr, "SignatureComments");
60
+ if (sigComments !== undefined) {
61
+ info.signatureComments = xmlDecode(sigComments);
57
62
  }
58
- const purposeMatch = /<SignaturePurpose[^>]*>([^<]*)<\/SignaturePurpose>/.exec(xmlStr);
59
- if (purposeMatch) {
60
- info.purpose = xmlDecode(purposeMatch[1]);
63
+ const purpose = extractTextElement(xmlStr, "SignaturePurpose");
64
+ if (purpose !== undefined) {
65
+ info.purpose = xmlDecode(purpose);
61
66
  }
62
- const dateMatch = /<SignatureDate[^>]*>([^<]*)<\/SignatureDate>/.exec(xmlStr);
63
- if (dateMatch) {
64
- info.signDate = xmlDecode(dateMatch[1]);
67
+ const signDate = extractTextElement(xmlStr, "SignatureDate");
68
+ if (signDate !== undefined) {
69
+ info.signDate = xmlDecode(signDate);
65
70
  }
66
- const providerMatch = /<SignatureProviderUrl[^>]*>([^<]*)<\/SignatureProviderUrl>/.exec(xmlStr);
67
- if (providerMatch) {
68
- info.providerUrl = xmlDecode(providerMatch[1]);
71
+ const providerUrl = extractTextElement(xmlStr, "SignatureProviderUrl");
72
+ if (providerUrl !== undefined) {
73
+ info.providerUrl = xmlDecode(providerUrl);
69
74
  }
70
- // Commitment type
71
- const commitMatch = /<CommitmentType[^>]*>\s*<CommitmentTypeIndication[^>]*>\s*<CommitmentTypeId>([^<]*)<\/CommitmentTypeId>/.exec(xmlStr);
72
- if (commitMatch) {
73
- info.commitmentType = xmlDecode(commitMatch[1]);
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
- // Extract signature value (base64)
76
- const sigValMatch = /<SignatureValue[^>]*>([^]*?)<\/SignatureValue>/.exec(xmlStr);
77
- if (sigValMatch) {
78
- info.signatureValue = sigValMatch[1].trim();
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 certSubjectMatch = /<X509SubjectName[^>]*>([^<]*)<\/X509SubjectName>/.exec(xmlStr);
82
- if (certSubjectMatch) {
83
- info.certificateSubject = xmlDecode(certSubjectMatch[1]);
93
+ const certSubject = extractTextElement(xmlStr, "X509SubjectName");
94
+ if (certSubject !== undefined) {
95
+ info.certificateSubject = xmlDecode(certSubject);
84
96
  }
85
- const certIssuerMatch = /<X509IssuerName[^>]*>([^<]*)<\/X509IssuerName>/.exec(xmlStr);
86
- if (certIssuerMatch) {
87
- info.certificateIssuer = xmlDecode(certIssuerMatch[1]);
97
+ const certIssuer = extractTextElement(xmlStr, "X509IssuerName");
98
+ if (certIssuer !== undefined) {
99
+ info.certificateIssuer = xmlDecode(certIssuer);
88
100
  }
89
- const certSerialMatch = /<X509SerialNumber[^>]*>([^<]*)<\/X509SerialNumber>/.exec(xmlStr);
90
- if (certSerialMatch) {
91
- info.certificateSerialNumber = xmlDecode(certSerialMatch[1]);
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
- // Simple regex-based extraction for the key <keyEncryptors><keyEncryptor>... element
328
- const keyDataMatch = /<keyData\s([\s\S]*?)\/>/.exec(xmlStr);
329
- const pwdEncryptorMatch = /<p:encryptedKey\s([\s\S]*?)\/>/.exec(xmlStr);
330
- if (!keyDataMatch || !pwdEncryptorMatch) {
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 = parseAttrs(pwdEncryptorMatch[1]);
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 re = /(\w+)="([^"]*)"/g;
399
- let match;
400
- while ((match = re.exec(str)) !== null) {
401
- attrs[match[1]] = match[2];
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.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
- * Return all discoverable system font candidates, ordered by preference.
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
- * Results are cached the filesystem scan runs only once per process.
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 discoverSystemFontCandidates() {
87063
- if (_cachedCandidates !== void 0) return _cachedCandidates;
87064
- if (typeof process === "undefined" || !process.platform) {
87065
- _cachedCandidates = [];
87066
- return _cachedCandidates;
87077
+ function* iterateSystemFontCandidates() {
87078
+ if (_cachedCandidates !== void 0) {
87079
+ for (const c of _cachedCandidates) yield c;
87080
+ return;
87067
87081
  }
87068
- const candidates = [];
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) try {
87084
- const fonts = traverseDirectorySync(dir, {
87085
- recursive: true,
87086
- filter: (e) => !e.isDirectory
87087
- }).filter((e) => /\.tt[cf]$/i.test(e.absolutePath) && !seen.has(e.absolutePath));
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
- } catch {}
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 discoverSystemFontCandidates()) try {
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;