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