@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
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
120
|
+
function* iterateSystemFontCandidates() {
|
|
121
|
+
// Fast path: a previous call already produced the full snapshot.
|
|
107
122
|
if (_cachedCandidates !== undefined) {
|
|
108
|
-
|
|
123
|
+
for (const c of _cachedCandidates) {
|
|
124
|
+
yield c;
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
109
127
|
}
|
|
110
128
|
if (typeof process === "undefined" || !process.platform) {
|
|
111
|
-
|
|
112
|
-
return _cachedCandidates;
|
|
129
|
+
return;
|
|
113
130
|
}
|
|
114
|
-
const
|
|
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
|
-
|
|
128
|
-
seen.add(fontPath);
|
|
146
|
+
yield data;
|
|
129
147
|
}
|
|
130
148
|
}
|
|
131
149
|
}
|
|
132
150
|
}
|
|
133
|
-
// Strategy 2:
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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.
|
|
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
|
|
65
|
-
|
|
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
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
//
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
return
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
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
|
-
|
|
309
|
+
const attrSection = tagMatch[3];
|
|
310
|
+
if (attrSection) {
|
|
306
311
|
attributes = {};
|
|
307
|
-
const
|
|
308
|
-
let
|
|
309
|
-
while (
|
|
310
|
-
|
|
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 };
|