@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
@@ -12,9 +12,15 @@
12
12
  * .ttc (TrueType Collection) files are supported — parseTtf() extracts
13
13
  * the first font from the collection automatically.
14
14
  *
15
- * Results are cached: the filesystem search runs only once per process.
15
+ * Discovery is exposed both as a generator
16
+ * ({@link iterateSystemFontCandidates}) for early-exit callers and as
17
+ * an array snapshot ({@link discoverSystemFontCandidates}) for tests
18
+ * and full enumeration. Once the full snapshot has been produced it is
19
+ * cached and replayed on subsequent calls; partial iterations rely on
20
+ * the OS page cache to make repeat reads of the same font files cheap.
16
21
  */
17
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.iterateSystemFontCandidates = iterateSystemFontCandidates;
18
24
  exports.discoverSystemFontCandidates = discoverSystemFontCandidates;
19
25
  exports.discoverSystemFont = discoverSystemFont;
20
26
  exports.resetFontDiscoveryCache = resetFontDiscoveryCache;
@@ -94,75 +100,114 @@ const PREFERRED_FONTS = [
94
100
  // =============================================================================
95
101
  // Font Discovery
96
102
  // =============================================================================
103
+ // Cached, fully-populated candidate list. Set when the generator runs
104
+ // to completion via `discoverSystemFontCandidates()`, or when a test
105
+ // injects candidates via `_setCandidatesForTest`. Partial iterations
106
+ // (where the caller `break`s after a match) intentionally do not
107
+ // populate this cache — they rely on the OS page cache to keep repeat
108
+ // reads cheap.
97
109
  let _cachedCandidates;
98
110
  /**
99
- * Return all discoverable system font candidates, ordered by preference.
111
+ * Lazily yield discoverable system font candidates, in preference order.
100
112
  *
101
113
  * Each entry is the raw font file bytes of a `.ttf` or `.ttc` file.
102
114
  * The caller decides which candidate to use (e.g. by checking cmap coverage).
103
115
  *
104
- * Results are cached the filesystem scan runs only once per process.
116
+ * Iterating one candidate at a time lets callers `break` as soon as
117
+ * they find a match, avoiding the cost of recursively reading every
118
+ * font in every system font directory just to discard them.
105
119
  */
106
- function discoverSystemFontCandidates() {
120
+ function* iterateSystemFontCandidates() {
121
+ // Fast path: a previous call already produced the full snapshot.
107
122
  if (_cachedCandidates !== undefined) {
108
- return _cachedCandidates;
123
+ for (const c of _cachedCandidates) {
124
+ yield c;
125
+ }
126
+ return;
109
127
  }
110
128
  if (typeof process === "undefined" || !process.platform) {
111
- _cachedCandidates = [];
112
- return _cachedCandidates;
129
+ return;
113
130
  }
114
- const candidates = [];
115
- const seen = new Set(); // dedupe by path
131
+ const seen = new Set(); // dedupe by path within this iteration
116
132
  const dirs = getSystemFontDirs();
117
- // Strategy 1: Check preferred font filenames (in order)
133
+ // Strategy 1: Check preferred font filenames (in order). These stat
134
+ // calls are cheap and run regardless — they are the only path most
135
+ // callers need to traverse.
118
136
  for (const fontName of PREFERRED_FONTS) {
119
137
  for (const dir of dirs) {
120
138
  const fontPath = `${dir}/${fontName}`;
121
139
  if (seen.has(fontPath)) {
122
140
  continue;
123
141
  }
142
+ seen.add(fontPath);
124
143
  if ((0, fs_1.fileExistsSync)(fontPath)) {
125
144
  const data = tryReadFont(fontPath);
126
145
  if (data) {
127
- candidates.push(data);
128
- seen.add(fontPath);
146
+ yield data;
129
147
  }
130
148
  }
131
149
  }
132
150
  }
133
- // Strategy 2: Scan directories for any .ttf/.ttc not already found
151
+ // Strategy 2: Recursively scan each directory for any other .ttf/.ttc.
152
+ // This is the expensive step (hundreds of MB on macOS / Windows), so
153
+ // it walks one directory at a time and yields candidates as it goes —
154
+ // a caller that found a match in Strategy 1 (or in an earlier
155
+ // directory here) never reaches further directories.
134
156
  const broadRe = /noto|unicode|cjk|yahei|heiti|gothic|sans|serif|ming|song|dejavu|liberation|droid|wqy/i;
135
157
  for (const dir of dirs) {
158
+ let entries;
136
159
  try {
137
- const entries = (0, fs_1.traverseDirectorySync)(dir, { recursive: true, filter: e => !e.isDirectory });
138
- const fonts = entries.filter(e => /\.tt[cf]$/i.test(e.absolutePath) && !seen.has(e.absolutePath));
139
- // Broad-coverage names first, then large files
140
- const broad = fonts.filter(e => broadRe.test(e.absolutePath));
141
- const rest = fonts.filter(e => !broadRe.test(e.absolutePath) && e.size > 50000);
142
- for (const entry of [...broad, ...rest]) {
143
- if (seen.has(entry.absolutePath)) {
144
- continue;
145
- }
146
- const data = tryReadFont(entry.absolutePath);
147
- if (data) {
148
- candidates.push(data);
149
- seen.add(entry.absolutePath);
150
- }
151
- }
160
+ entries = (0, fs_1.traverseDirectorySync)(dir, { recursive: true, filter: e => !e.isDirectory });
152
161
  }
153
162
  catch {
154
163
  // Directory doesn't exist or not readable
164
+ continue;
165
+ }
166
+ const fonts = entries.filter(e => /\.tt[cf]$/i.test(e.absolutePath) && !seen.has(e.absolutePath));
167
+ // Broad-coverage names first, then large files
168
+ const broad = fonts.filter(e => broadRe.test(e.absolutePath));
169
+ const rest = fonts.filter(e => !broadRe.test(e.absolutePath) && e.size > 50000);
170
+ for (const entry of [...broad, ...rest]) {
171
+ if (seen.has(entry.absolutePath)) {
172
+ continue;
173
+ }
174
+ seen.add(entry.absolutePath);
175
+ const data = tryReadFont(entry.absolutePath);
176
+ if (data) {
177
+ yield data;
178
+ }
155
179
  }
156
180
  }
157
- _cachedCandidates = candidates;
158
- return candidates;
181
+ }
182
+ /**
183
+ * Return all discoverable system font candidates, ordered by preference.
184
+ *
185
+ * Each entry is the raw font file bytes of a `.ttf` or `.ttc` file.
186
+ * The caller decides which candidate to use (e.g. by checking cmap coverage).
187
+ *
188
+ * The full snapshot is cached: once produced, repeated calls return the
189
+ * same array without touching the filesystem.
190
+ *
191
+ * Prefer {@link iterateSystemFontCandidates} when you only need the
192
+ * first candidate that satisfies a predicate: it avoids reading every
193
+ * font in every system directory just to discard them.
194
+ */
195
+ function discoverSystemFontCandidates() {
196
+ if (_cachedCandidates !== undefined) {
197
+ return _cachedCandidates;
198
+ }
199
+ const all = [];
200
+ for (const candidate of iterateSystemFontCandidates()) {
201
+ all.push(candidate);
202
+ }
203
+ _cachedCandidates = all;
204
+ return all;
159
205
  }
160
206
  /**
161
207
  * Search for a system font suitable for Unicode rendering.
162
208
  *
163
209
  * Returns the raw font file bytes of the highest-priority candidate,
164
- * or `null` if no font was found. This is a convenience wrapper around
165
- * {@link discoverSystemFontCandidates}.
210
+ * or `null` if no font was found.
166
211
  */
167
212
  function discoverSystemFont() {
168
213
  const candidates = discoverSystemFontCandidates();
@@ -61,8 +61,11 @@ function prepareExport(workbook, options) {
61
61
  // Collect non-WinAnsi code points from the document (single pass)
62
62
  const nonWinAnsi = collectNonWinAnsiCodePoints(sheets);
63
63
  if (nonWinAnsi.size > 0) {
64
- // Try system font candidates in preference order until one covers all chars
65
- for (const candidate of (0, system_fonts_1.discoverSystemFontCandidates)()) {
64
+ // Try system font candidates lazily in preference order until one
65
+ // covers all chars. Iterating instead of materializing the full
66
+ // candidate list lets us stop the moment a match is found, which
67
+ // avoids the cost of recursively scanning every system font dir.
68
+ for (const candidate of (0, system_fonts_1.iterateSystemFontCandidates)()) {
66
69
  try {
67
70
  const testTtf = (0, ttf_parser_1.parseTtf)(candidate);
68
71
  const allCovered = [...nonWinAnsi].every(cp => testTtf.cmap.has(cp));
@@ -184,32 +184,160 @@ function parseSeqSwitches(args) {
184
184
  }
185
185
  /** Parse IF field: IF expr1 op expr2 "trueText" "falseText" */
186
186
  function parseIfField(args) {
187
- // Pattern: expr1 = expr2 "trueText" "falseText"
188
- // Supports both quoted and unquoted operands.
189
- // Two-character operators must come first or `<=` would match `<` only.
190
- const match = /^"?([^"=<>!]*?)"?\s*(<=|>=|<>|=|<|>)\s*"?([^"]*?)"?\s+"([^"]*)"\s+"([^"]*)"/.exec(args);
191
- if (match) {
192
- return {
193
- left: match[1].trim(),
194
- operator: match[2],
195
- right: match[3].trim(),
196
- trueText: match[4],
197
- falseText: match[5]
198
- };
199
- }
200
- // Simpler pattern without quotes on operands
201
- const simpleMatch = /^(\S+)\s*(<=|>=|<>|=|<|>)\s*(\S+)\s+"([^"]*)"\s+"([^"]*)"/.exec(args);
202
- if (simpleMatch) {
203
- return {
204
- left: simpleMatch[1],
205
- operator: simpleMatch[2],
206
- right: simpleMatch[3],
207
- trueText: simpleMatch[4],
208
- falseText: simpleMatch[5]
209
- };
187
+ // Hand-rolled parser used in place of the previous chained regex
188
+ // (`/^"?([^"=<>!]*?)"?\s*(<=|>=|<>|=|<|>)\s*"?([^"]*?)"?\s+"([^"]*)"\s+"([^"]*)"/`).
189
+ // CodeQL flagged that regex as polynomial-redos. The grammar is also
190
+ // permissive in ways the regex captured implicitly: real Word IF
191
+ // fields contain operands such as `MERGEFIELD foo` (with internal
192
+ // whitespace) and operands wrapped in quotes. The scanner below
193
+ // mirrors the regex's accepted shape:
194
+ //
195
+ // args := SP* leftOperand SP* op SP* rightOperand SP+ "trueText" SP+ "falseText" …
196
+ //
197
+ // where `leftOperand` runs up to the first comparison operator that
198
+ // is not inside a quoted span, and `rightOperand` runs up to the
199
+ // first `"` that begins the trueText literal.
200
+ // 1. Find the comparison operator outside any quoted span.
201
+ const opPos = findIfOperator(args, 0);
202
+ if (!opPos) {
203
+ return null;
204
+ }
205
+ // 2. Left operand: everything before the operator, with surrounding
206
+ // whitespace and outer quotes stripped.
207
+ const left = stripOuterQuotes(args.slice(0, opPos.start).trim());
208
+ // 3. Right operand: text between the operator and the first quoted
209
+ // literal, with surrounding whitespace and outer quotes stripped.
210
+ let cursor = opPos.next;
211
+ // Scan to the next `"` that is not the immediate value-quoted operand.
212
+ // We have to be careful: the right operand itself may be quoted, e.g.
213
+ // `1 = "bar" "y" "n"`. To match the previous regex we adopt: skip
214
+ // whitespace, optionally consume one quoted span as the right operand,
215
+ // otherwise consume up to the next whitespace+`"` boundary.
216
+ cursor = skipSpaces(args, cursor);
217
+ let right;
218
+ if (args.charCodeAt(cursor) === 0x22 /* '"' */) {
219
+ const close = args.indexOf('"', cursor + 1);
220
+ if (close < 0) {
221
+ return null;
222
+ }
223
+ right = args.slice(cursor + 1, close);
224
+ cursor = close + 1;
225
+ }
226
+ else {
227
+ // Read until the next `"` (which begins the trueText literal).
228
+ const nextQuote = args.indexOf('"', cursor);
229
+ if (nextQuote < 0) {
230
+ return null;
231
+ }
232
+ right = args.slice(cursor, nextQuote).trim();
233
+ cursor = nextQuote;
234
+ }
235
+ // 4. trueText (required quoted string).
236
+ cursor = skipSpaces(args, cursor);
237
+ const trueRead = readQuotedString(args, cursor);
238
+ if (!trueRead) {
239
+ return null;
240
+ }
241
+ cursor = skipSpaces(args, trueRead.next);
242
+ // 5. falseText (required quoted string).
243
+ const falseRead = readQuotedString(args, cursor);
244
+ if (!falseRead) {
245
+ return null;
246
+ }
247
+ return {
248
+ left,
249
+ operator: opPos.value,
250
+ right,
251
+ trueText: trueRead.value,
252
+ falseText: falseRead.value
253
+ };
254
+ }
255
+ /**
256
+ * Find the first IF-field comparison operator (`<=`, `>=`, `<>`, `=`,
257
+ * `<`, `>`) starting at `from`, skipping over any quoted (`"…"`) spans
258
+ * so that operators inside operand strings are not mistaken for the
259
+ * top-level comparator.
260
+ *
261
+ * Bare `!` is reported as "no operator" rather than absorbed into the
262
+ * preceding operand: the previous regex excluded `!` from the left
263
+ * operand character class, so `1 != 1 …` was rejected outright. We
264
+ * preserve that rejection here to avoid silently parsing `!=` (not a
265
+ * Word IF-field operator) as `=` with `!` glued to the left operand.
266
+ */
267
+ function findIfOperator(s, from) {
268
+ const n = s.length;
269
+ let i = from;
270
+ while (i < n) {
271
+ const c = s.charCodeAt(i);
272
+ if (c === 0x22 /* '"' */) {
273
+ // Skip quoted span.
274
+ const close = s.indexOf('"', i + 1);
275
+ if (close < 0) {
276
+ return null;
277
+ }
278
+ i = close + 1;
279
+ continue;
280
+ }
281
+ if (c === 0x21 /* '!' */) {
282
+ // Reject `!` outside quotes — matches the previous regex behaviour.
283
+ return null;
284
+ }
285
+ if (c === 0x3c /* '<' */) {
286
+ const next = s.charCodeAt(i + 1);
287
+ if (next === 0x3d) {
288
+ return { start: i, next: i + 2, value: "<=" };
289
+ }
290
+ if (next === 0x3e) {
291
+ return { start: i, next: i + 2, value: "<>" };
292
+ }
293
+ return { start: i, next: i + 1, value: "<" };
294
+ }
295
+ if (c === 0x3e /* '>' */) {
296
+ if (s.charCodeAt(i + 1) === 0x3d) {
297
+ return { start: i, next: i + 2, value: ">=" };
298
+ }
299
+ return { start: i, next: i + 1, value: ">" };
300
+ }
301
+ if (c === 0x3d /* '=' */) {
302
+ return { start: i, next: i + 1, value: "=" };
303
+ }
304
+ i++;
210
305
  }
211
306
  return null;
212
307
  }
308
+ /**
309
+ * Strip a single matched pair of outer quotes (`"…"`) from a trimmed
310
+ * operand. Mirrors the implicit `"?…"?` shape of the original regex.
311
+ */
312
+ function stripOuterQuotes(s) {
313
+ if (s.length >= 2 && s.charCodeAt(0) === 0x22 && s.charCodeAt(s.length - 1) === 0x22) {
314
+ return s.slice(1, -1);
315
+ }
316
+ return s;
317
+ }
318
+ function skipSpaces(s, from) {
319
+ const n = s.length;
320
+ let i = from;
321
+ while (i < n) {
322
+ const c = s.charCodeAt(i);
323
+ if (c !== 0x20 && c !== 0x09 && c !== 0x0a && c !== 0x0d) {
324
+ break;
325
+ }
326
+ i++;
327
+ }
328
+ return i;
329
+ }
330
+ /** Read a quoted (`"…"`) string starting at `from`, or return null. */
331
+ function readQuotedString(s, from) {
332
+ if (s.charCodeAt(from) !== 0x22 /* '"' */) {
333
+ return null;
334
+ }
335
+ const close = s.indexOf('"', from + 1);
336
+ if (close < 0) {
337
+ return null;
338
+ }
339
+ return { value: s.slice(from + 1, close), next: close + 1 };
340
+ }
213
341
  /** Parse STYLEREF field to get the style name. */
214
342
  function parseStyleRef(args) {
215
343
  // STYLEREF "StyleName" or STYLEREF StyleName
@@ -409,7 +409,8 @@ function parseMMLTree(xml) {
409
409
  // forever (indexOf returning -1 used to set pos = 0, hanging the CPU).
410
410
  const end = xml.indexOf(">", pos);
411
411
  if (end === -1) {
412
- pos = xml.length;
412
+ // Malformed input — stop parsing. (`pos` is unused after the
413
+ // outer loop terminates, so no further assignment is needed.)
413
414
  break;
414
415
  }
415
416
  pos = end + 1;
@@ -300,14 +300,52 @@ function parseTarget(targetStr) {
300
300
  }
301
301
  const tagName = tagMatch[1];
302
302
  const className = tagMatch[2] ? tagMatch[2].substring(1).replace(/\./g, " ") : undefined;
303
- // Parse attributes [key=value]
303
+ // Parse attributes [key=value]. Implemented as a linear scan rather
304
+ // than a global regex so that adversarial mapping strings cannot
305
+ // trigger CodeQL's `js/polynomial-redos`. The regex form
306
+ // `/\[([^=]+)=([^\]]+)\]/g` exhibits backtracking on inputs like
307
+ // `[xxxxxxxxxxxxx`.
304
308
  let attributes;
305
- if (tagMatch[3]) {
309
+ const attrSection = tagMatch[3];
310
+ if (attrSection) {
306
311
  attributes = {};
307
- const attrRegex = /\[([^=]+)=([^\]]+)\]/g;
308
- let attrMatch;
309
- while ((attrMatch = attrRegex.exec(tagMatch[3])) !== null) {
310
- attributes[attrMatch[1]] = attrMatch[2].replace(/^['"]|['"]$/g, "");
312
+ const len = attrSection.length;
313
+ let i = 0;
314
+ while (i < len) {
315
+ if (attrSection.charCodeAt(i) !== 0x5b /* '[' */) {
316
+ i++;
317
+ continue;
318
+ }
319
+ const eq = attrSection.indexOf("=", i + 1);
320
+ const close = attrSection.indexOf("]", i + 1);
321
+ if (close < 0) {
322
+ break;
323
+ }
324
+ if (eq < 0 || eq > close) {
325
+ // No `=` inside this `[...]` — skip past it.
326
+ i = close + 1;
327
+ continue;
328
+ }
329
+ const key = attrSection.slice(i + 1, eq);
330
+ const rawValue = attrSection.slice(eq + 1, close);
331
+ // Strip a single leading and trailing quote (' or "). Two
332
+ // independent linear strips replace the previous
333
+ // `/^['"]|['"]$/g` regex.
334
+ let v = rawValue;
335
+ if (v.length >= 2) {
336
+ const first = v.charCodeAt(0);
337
+ if (first === 0x27 || first === 0x22) {
338
+ v = v.slice(1);
339
+ }
340
+ }
341
+ if (v.length >= 1) {
342
+ const last = v.charCodeAt(v.length - 1);
343
+ if (last === 0x27 || last === 0x22) {
344
+ v = v.slice(0, -1);
345
+ }
346
+ }
347
+ attributes[key] = v;
348
+ i = close + 1;
311
349
  }
312
350
  }
313
351
  return { tagName, className: className || undefined, attributes };