@blamejs/core 0.18.35 → 0.18.37
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/CHANGELOG.md +67 -0
- package/README.md +1 -1
- package/lib/auth/password.js +14 -5
- package/lib/crypto.js +74 -8
- package/lib/eat.js +7 -3
- package/lib/http-message-signature.js +9 -5
- package/lib/middleware/dpop.js +10 -6
- package/lib/parsers/safe-env.js +126 -17
- package/lib/parsers/safe-ini.js +115 -9
- package/lib/parsers/safe-toml.js +71 -16
- package/lib/parsers/safe-xml.js +7 -1
- package/lib/parsers/safe-yaml.js +213 -40
- package/lib/standard-webhooks.js +7 -5
- package/lib/totp.js +11 -2
- package/lib/webhook-hmac.js +11 -9
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/parsers/safe-ini.js
CHANGED
|
@@ -41,6 +41,7 @@ var C = require("../constants");
|
|
|
41
41
|
var pick = require("../pick");
|
|
42
42
|
var numericBounds = require("../numeric-bounds");
|
|
43
43
|
var safeBuffer = require("../safe-buffer");
|
|
44
|
+
var codepointClass = require("../codepoint-class");
|
|
44
45
|
var { defineClass } = require("../framework-error");
|
|
45
46
|
|
|
46
47
|
var IniSafeError = defineClass("IniSafeError", { alwaysPermanent: true });
|
|
@@ -69,7 +70,7 @@ function _stripComment(line) {
|
|
|
69
70
|
if (c === "\"" && !inSingle) { inDouble = !inDouble; continue; }
|
|
70
71
|
if (c === "'" && !inDouble) { inSingle = !inSingle; continue; }
|
|
71
72
|
if (!inSingle && !inDouble && (c === ";" || c === "#")) {
|
|
72
|
-
if (i === 0 ||
|
|
73
|
+
if (i === 0 || codepointClass.inRanges(line.charCodeAt(i - 1), codepointClass.WHITESPACE_RANGES)) {
|
|
73
74
|
return line.slice(0, i);
|
|
74
75
|
}
|
|
75
76
|
}
|
|
@@ -110,6 +111,113 @@ function _unquote(raw) {
|
|
|
110
111
|
return s;
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
// Lexical shape tests for the value forms INI coerces. Each replaces an
|
|
115
|
+
// anchored pattern and walks the string once, so the cost is its length —
|
|
116
|
+
// values arrive from a file an operator may not control, and a screen whose
|
|
117
|
+
// cost depends on the arrangement of the characters is what this family avoids.
|
|
118
|
+
//
|
|
119
|
+
// `\d` in the patterns these replace is ASCII-only, so isAsciiDigit matches it
|
|
120
|
+
// exactly; a walk using a Unicode digit test would accept more than the pattern
|
|
121
|
+
// did and silently widen what parses as a number.
|
|
122
|
+
|
|
123
|
+
// `/^0x[0-9a-f]+$/i` — the prefix, then at least one hex digit, nothing else.
|
|
124
|
+
function _isHexInteger(s) {
|
|
125
|
+
if (s.length < 3) return false;
|
|
126
|
+
if (s.charAt(0) !== "0") return false;
|
|
127
|
+
var x = s.charAt(1);
|
|
128
|
+
if (x !== "x" && x !== "X") return false;
|
|
129
|
+
return codepointClass.isRunOf(s.slice(2), codepointClass.ASCII_HEX);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Digits from `from` to the end, at least one. The shared tail of every shape
|
|
133
|
+
// below, and the reason none of them needs a quantifier.
|
|
134
|
+
function _digitsToEnd(s, from) {
|
|
135
|
+
if (from >= s.length) return false;
|
|
136
|
+
return codepointClass.isRunOf(s.slice(from), codepointClass.ASCII_DIGITS);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _digitRunEnd(s, from) {
|
|
140
|
+
var i = from;
|
|
141
|
+
while (i < s.length && codepointClass.isAsciiDigit(s.charCodeAt(i))) i += 1;
|
|
142
|
+
return i; // === from when no digits
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// `/^-?\d+$/`
|
|
146
|
+
function _isDecimalInteger(s) {
|
|
147
|
+
return _digitsToEnd(s, s.charAt(0) === "-" ? 1 : 0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// `/^-?\d+\.\d+([eE][+-]?\d+)?$/` or `/^-?\d+[eE][+-]?\d+$/` — a float needs
|
|
151
|
+
// either a fractional part, an exponent, or both, which is what keeps a bare
|
|
152
|
+
// integer out of this branch and in the one above it.
|
|
153
|
+
function _isDecimalFloat(s) {
|
|
154
|
+
var i = s.charAt(0) === "-" ? 1 : 0;
|
|
155
|
+
var afterInt = _digitRunEnd(s, i);
|
|
156
|
+
if (afterInt === i) return false; // no integer part
|
|
157
|
+
i = afterInt;
|
|
158
|
+
var sawFraction = false;
|
|
159
|
+
if (s.charAt(i) === ".") {
|
|
160
|
+
var afterFrac = _digitRunEnd(s, i + 1);
|
|
161
|
+
if (afterFrac === i + 1) return false; // `1.` is not a float here
|
|
162
|
+
i = afterFrac;
|
|
163
|
+
sawFraction = true;
|
|
164
|
+
}
|
|
165
|
+
var e = s.charAt(i);
|
|
166
|
+
if (e === "e" || e === "E") {
|
|
167
|
+
var j = i + 1;
|
|
168
|
+
var sign = s.charAt(j);
|
|
169
|
+
if (sign === "+" || sign === "-") j += 1;
|
|
170
|
+
if (!_digitsToEnd(s, j)) return false;
|
|
171
|
+
return true; // exponent consumed the rest
|
|
172
|
+
}
|
|
173
|
+
return sawFraction && i === s.length;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// `[name "subsection"]` — the git-config form. Replaces
|
|
177
|
+
// `/^([A-Za-z0-9._-]+)\s+"([^"\\]*(?:\\.[^"\\]*)*)"$/`, whose inner group is the
|
|
178
|
+
// unrolled-loop idiom for a quoted string; walking it is both clearer and free
|
|
179
|
+
// of the backtracking that idiom exists to avoid.
|
|
180
|
+
//
|
|
181
|
+
// Two details of the pattern are load-bearing and preserved here. The value is
|
|
182
|
+
// captured VERBATIM, escapes included — `_unquote` does the unescaping later, so
|
|
183
|
+
// consuming the backslash here would double-unescape. And `\\.` cannot match a
|
|
184
|
+
// line terminator, so a backslash before one is not an escape and the whole
|
|
185
|
+
// header fails to match rather than swallowing the break.
|
|
186
|
+
var _SECTION_NAME_CHARS = codepointClass.ASCII_ALNUM + "._-";
|
|
187
|
+
|
|
188
|
+
function _isDotAtom(ch) {
|
|
189
|
+
// The characters `.` does NOT match without the `s` flag.
|
|
190
|
+
return ch !== "\n" && ch !== "\r" && ch !== "\u2028" && ch !== "\u2029";
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function _parseQuotedSectionHeader(inner) {
|
|
194
|
+
var i = 0;
|
|
195
|
+
while (i < inner.length && _SECTION_NAME_CHARS.indexOf(inner.charAt(i)) !== -1) i += 1;
|
|
196
|
+
if (i === 0) return null; // name needs one char
|
|
197
|
+
var name = inner.slice(0, i);
|
|
198
|
+
var afterName = i;
|
|
199
|
+
while (i < inner.length &&
|
|
200
|
+
codepointClass.inRanges(inner.charCodeAt(i), codepointClass.WHITESPACE_RANGES)) i += 1;
|
|
201
|
+
if (i === afterName) return null; // `\s+` needs one
|
|
202
|
+
if (inner.charAt(i) !== "\"") return null;
|
|
203
|
+
i += 1;
|
|
204
|
+
var value = "";
|
|
205
|
+
while (i < inner.length) {
|
|
206
|
+
var ch = inner.charAt(i);
|
|
207
|
+
if (ch === "\\") {
|
|
208
|
+
var next = inner.charAt(i + 1);
|
|
209
|
+
if (i + 1 >= inner.length || !_isDotAtom(next)) return null;
|
|
210
|
+
value += ch + next;
|
|
211
|
+
i += 2;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (ch === "\"") return i === inner.length - 1 ? [name, value] : null;
|
|
215
|
+
value += ch;
|
|
216
|
+
i += 1;
|
|
217
|
+
}
|
|
218
|
+
return null; // unterminated quote
|
|
219
|
+
}
|
|
220
|
+
|
|
113
221
|
function _coerceValue(raw) {
|
|
114
222
|
if (raw.length === 0) return raw;
|
|
115
223
|
var first = raw.charAt(0);
|
|
@@ -117,21 +225,21 @@ function _coerceValue(raw) {
|
|
|
117
225
|
var lower = raw.toLowerCase();
|
|
118
226
|
if (TRUE_VALUES.has(lower)) return true;
|
|
119
227
|
if (FALSE_VALUES.has(lower)) return false;
|
|
120
|
-
if (
|
|
228
|
+
if (_isHexInteger(raw)) {
|
|
121
229
|
var hex = parseInt(raw, RADIX_HEX);
|
|
122
230
|
if (!Number.isSafeInteger(hex)) {
|
|
123
231
|
throw _err("ini/value-out-of-range", "hex integer exceeds safe-integer range: " + raw);
|
|
124
232
|
}
|
|
125
233
|
return hex;
|
|
126
234
|
}
|
|
127
|
-
if (
|
|
235
|
+
if (_isDecimalInteger(raw)) {
|
|
128
236
|
var n = Number(raw);
|
|
129
237
|
if (!Number.isSafeInteger(n)) {
|
|
130
238
|
throw _err("ini/value-out-of-range", "integer exceeds safe-integer range: " + raw);
|
|
131
239
|
}
|
|
132
240
|
return n;
|
|
133
241
|
}
|
|
134
|
-
if (
|
|
242
|
+
if (_isDecimalFloat(raw)) {
|
|
135
243
|
var f = Number(raw);
|
|
136
244
|
// Float overflow (e.g. 1e999) coerces to ±Infinity — the same
|
|
137
245
|
// never-silently-coerce refusal the integer/hex branches enforce with
|
|
@@ -178,10 +286,8 @@ function _parseSectionHeader(line) {
|
|
|
178
286
|
if (inner.length === 0) {
|
|
179
287
|
throw _err("ini/empty-section", "section header [] has no name");
|
|
180
288
|
}
|
|
181
|
-
var
|
|
182
|
-
if (
|
|
183
|
-
return [quotedMatch[1], quotedMatch[2]];
|
|
184
|
-
}
|
|
289
|
+
var quoted = _parseQuotedSectionHeader(inner);
|
|
290
|
+
if (quoted) return quoted;
|
|
185
291
|
var parts = inner.split(".");
|
|
186
292
|
for (var i = 0; i < parts.length; i++) {
|
|
187
293
|
if (parts[i].length === 0) {
|
|
@@ -240,7 +346,7 @@ function parse(input, opts) {
|
|
|
240
346
|
var sectionCount = 0;
|
|
241
347
|
var keysInCurrentSection = 0;
|
|
242
348
|
|
|
243
|
-
var lines =
|
|
349
|
+
var lines = codepointClass.splitLines(input);
|
|
244
350
|
for (var li = 0; li < lines.length; li++) {
|
|
245
351
|
var raw = lines[li];
|
|
246
352
|
var stripped = _stripComment(raw).trim();
|
package/lib/parsers/safe-toml.js
CHANGED
|
@@ -71,6 +71,61 @@ var RADIX_BIN = 0x2;
|
|
|
71
71
|
var RADIX_OCTAL = 0x8;
|
|
72
72
|
var RADIX_HEX = 0x10;
|
|
73
73
|
|
|
74
|
+
// Character sets and fixed-width shapes, as walks. TOML's grammar is all
|
|
75
|
+
// fixed-width date and time fields and single-character classes, so every one of
|
|
76
|
+
// these replaces a pattern with an index comparison — nothing here needs a
|
|
77
|
+
// search, and the parser's cost stays the length of the document.
|
|
78
|
+
var _OCTAL_DIGITS = "01234567";
|
|
79
|
+
var _BINARY_DIGITS = "01";
|
|
80
|
+
var _BARE_KEY_TAIL = codepointClass.ASCII_ALNUM + "_";
|
|
81
|
+
var _NUMBER_LEADS = codepointClass.ASCII_DIGITS + "+-i n"; // `inf`, `nan`, sign, space
|
|
82
|
+
|
|
83
|
+
function _isDigit(ch) { return ch.length === 1 && codepointClass.ASCII_DIGITS.indexOf(ch) !== -1; }
|
|
84
|
+
function _isHexDigit(ch) { return ch.length === 1 && codepointClass.ASCII_HEX.indexOf(ch) !== -1; }
|
|
85
|
+
function _isOctalDigit(ch) { return ch.length === 1 && _OCTAL_DIGITS.indexOf(ch) !== -1; }
|
|
86
|
+
function _isBinaryDigit(ch){ return ch.length === 1 && _BINARY_DIGITS.indexOf(ch) !== -1; }
|
|
87
|
+
|
|
88
|
+
// `charAt` past the end returns "", and `"".indexOf` is 0 on every string, so
|
|
89
|
+
// each of these guards its length before asking. Without it an out-of-range read
|
|
90
|
+
// answers "yes" and the shape checks accept a truncated document.
|
|
91
|
+
function _isBareKeyChar(ch) { return ch.length === 1 && _BARE_KEY_TAIL.indexOf(ch) !== -1; }
|
|
92
|
+
function _isNumberLead(ch) { return ch.length === 1 && _NUMBER_LEADS.indexOf(ch) !== -1; }
|
|
93
|
+
|
|
94
|
+
function _digitsAt(text, from, count) {
|
|
95
|
+
if (from + count > text.length) return false;
|
|
96
|
+
for (var i = 0; i < count; i += 1) {
|
|
97
|
+
if (!_isDigit(text.charAt(from + i))) return false;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// `\d{4}-\d{2}-\d{2}` anchored at the start of `text`.
|
|
103
|
+
function _looksLikeDate(text) {
|
|
104
|
+
return _digitsAt(text, 0, 4) && text.charAt(4) === "-" &&
|
|
105
|
+
_digitsAt(text, 5, 2) && text.charAt(7) === "-" &&
|
|
106
|
+
_digitsAt(text, 8, 2);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// `\d{2}:\d{2}:\d{2}` anchored at the start of `text`.
|
|
110
|
+
function _looksLikeTime(text) {
|
|
111
|
+
return _digitsAt(text, 0, 2) && text.charAt(2) === ":" &&
|
|
112
|
+
_digitsAt(text, 3, 2) && text.charAt(5) === ":" &&
|
|
113
|
+
_digitsAt(text, 6, 2);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// `^[+-]\d{2}:\d{2}$` — a whole numeric UTC offset, nothing after it.
|
|
117
|
+
function _isNumericOffset(text) {
|
|
118
|
+
if (text.length !== 6) return false;
|
|
119
|
+
var sign = text.charAt(0);
|
|
120
|
+
if (sign !== "+" && sign !== "-") return false;
|
|
121
|
+
return _digitsAt(text, 1, 2) && text.charAt(3) === ":" && _digitsAt(text, 4, 2);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Strip the `_` digit separators TOML allows inside numbers.
|
|
125
|
+
function _stripUnderscores(text) {
|
|
126
|
+
return text.indexOf("_") === -1 ? text : codepointClass.stripRanges(text, [0x5F]);
|
|
127
|
+
}
|
|
128
|
+
|
|
74
129
|
// Date-time literal character widths (per TOML / RFC 3339).
|
|
75
130
|
var TIME_CHARS = 0x8; // "HH:MM:SS"
|
|
76
131
|
var OFFSET_CHARS = 0x6; // "+HH:MM"
|
|
@@ -285,7 +340,7 @@ function parse(input, opts) {
|
|
|
285
340
|
case "U": {
|
|
286
341
|
var hexLen = 0x8;
|
|
287
342
|
var hex8 = input.substring(pos, pos + hexLen);
|
|
288
|
-
if (hex8.length < hexLen ||
|
|
343
|
+
if (hex8.length < hexLen || hex8.length !== 8 || !codepointClass.isRunOf(hex8, codepointClass.ASCII_HEX)) {
|
|
289
344
|
throw _err("bad \\U escape", "toml/bad-escape");
|
|
290
345
|
}
|
|
291
346
|
_advance(hexLen);
|
|
@@ -404,19 +459,19 @@ function parse(input, opts) {
|
|
|
404
459
|
// current position doesn't start a date-time literal.
|
|
405
460
|
function _tryParseDateTime() {
|
|
406
461
|
// Date-time form: YYYY-MM-DD followed by 'T'/' ' followed by time
|
|
407
|
-
if (pos + 10 <= len &&
|
|
462
|
+
if (pos + 10 <= len && _looksLikeDate(input.substr(pos, 10))) {
|
|
408
463
|
var sep = _peek(10);
|
|
409
464
|
if (sep === "T" || sep === "t" || sep === " ") {
|
|
410
465
|
// Look ahead for time portion (HH:MM:SS = 8 chars)
|
|
411
466
|
var timeStart = pos + 11;
|
|
412
467
|
var timeChars = TIME_CHARS;
|
|
413
|
-
if (
|
|
468
|
+
if (_looksLikeTime(input.substr(timeStart, timeChars))) {
|
|
414
469
|
var datePart = input.substr(pos, 10);
|
|
415
470
|
var timeEnd = timeStart + timeChars;
|
|
416
471
|
// Optional fractional
|
|
417
472
|
if (_peek(timeEnd - pos) === ".") {
|
|
418
473
|
timeEnd += 1;
|
|
419
|
-
while (timeEnd < len &&
|
|
474
|
+
while (timeEnd < len && _isDigit(input.charAt(timeEnd))) timeEnd += 1;
|
|
420
475
|
}
|
|
421
476
|
var timePart = input.substring(timeStart, timeEnd);
|
|
422
477
|
// Optional offset
|
|
@@ -427,7 +482,7 @@ function parse(input, opts) {
|
|
|
427
482
|
// Expect ±HH:MM (6 chars)
|
|
428
483
|
var offsetChars = OFFSET_CHARS;
|
|
429
484
|
var off = input.substr(timeEnd, offsetChars);
|
|
430
|
-
if (
|
|
485
|
+
if (_isNumericOffset(off)) {
|
|
431
486
|
offsetStr = off;
|
|
432
487
|
timeEnd += offsetChars;
|
|
433
488
|
}
|
|
@@ -445,7 +500,7 @@ function parse(input, opts) {
|
|
|
445
500
|
}
|
|
446
501
|
// Date-only — but only if followed by something OTHER than digit/colon
|
|
447
502
|
var after = _peek(10);
|
|
448
|
-
if (!after ||
|
|
503
|
+
if (!after || !(_isDigit(after) || after === ":" || after === ".")) {
|
|
449
504
|
var ds = input.substr(pos, 10);
|
|
450
505
|
_advance(10);
|
|
451
506
|
return { kind: "local-date", value: ds };
|
|
@@ -453,11 +508,11 @@ function parse(input, opts) {
|
|
|
453
508
|
}
|
|
454
509
|
// Time-only: HH:MM:SS (8 chars)
|
|
455
510
|
var timeOnlyChars = TIME_CHARS;
|
|
456
|
-
if (pos + timeOnlyChars <= len &&
|
|
511
|
+
if (pos + timeOnlyChars <= len && _looksLikeTime(input.substr(pos, timeOnlyChars))) {
|
|
457
512
|
var teEnd = pos + timeOnlyChars;
|
|
458
513
|
if (input.charAt(teEnd) === ".") {
|
|
459
514
|
teEnd += 1;
|
|
460
|
-
while (teEnd < len &&
|
|
515
|
+
while (teEnd < len && _isDigit(input.charAt(teEnd))) teEnd += 1;
|
|
461
516
|
}
|
|
462
517
|
var ts = input.substring(pos, teEnd);
|
|
463
518
|
_advance(teEnd - pos);
|
|
@@ -496,12 +551,12 @@ function parse(input, opts) {
|
|
|
496
551
|
while (!_eof()) {
|
|
497
552
|
var ch = _peek();
|
|
498
553
|
if (ch === "_") { _advance(); continue; }
|
|
499
|
-
if (radix === RADIX_HEX &&
|
|
500
|
-
if (radix === RADIX_OCTAL &&
|
|
501
|
-
if (radix === RADIX_BIN &&
|
|
554
|
+
if (radix === RADIX_HEX && _isHexDigit(ch)) { _advance(); continue; }
|
|
555
|
+
if (radix === RADIX_OCTAL && _isOctalDigit(ch)) { _advance(); continue; }
|
|
556
|
+
if (radix === RADIX_BIN && _isBinaryDigit(ch)) { _advance(); continue; }
|
|
502
557
|
break;
|
|
503
558
|
}
|
|
504
|
-
var digits = input.substring(digitsStart, pos)
|
|
559
|
+
var digits = _stripUnderscores(input.substring(digitsStart, pos));
|
|
505
560
|
if (digits.length === 0) throw _err("expected digits after radix prefix", "toml/bad-number");
|
|
506
561
|
var n = parseInt(digits, radix);
|
|
507
562
|
if (!Number.isSafeInteger(n)) {
|
|
@@ -527,7 +582,7 @@ function parse(input, opts) {
|
|
|
527
582
|
}
|
|
528
583
|
break;
|
|
529
584
|
}
|
|
530
|
-
var raw = input.substring(startPos, pos)
|
|
585
|
+
var raw = _stripUnderscores(input.substring(startPos, pos));
|
|
531
586
|
if (raw === "" || raw === "-" || raw === "+") {
|
|
532
587
|
throw new SafeTomlError("invalid number", "toml/bad-number", startLine, startCol);
|
|
533
588
|
}
|
|
@@ -560,17 +615,17 @@ function parse(input, opts) {
|
|
|
560
615
|
if (c === "[") return _parseArray(depth + 1);
|
|
561
616
|
if (c === "{") return _parseInlineTable(depth + 1);
|
|
562
617
|
|
|
563
|
-
if (input.substr(pos, 4) === "true" &&
|
|
618
|
+
if (input.substr(pos, 4) === "true" && !_isBareKeyChar(input.charAt(pos + 4))) {
|
|
564
619
|
_advance(4); return true;
|
|
565
620
|
}
|
|
566
|
-
if (input.substr(pos, 5) === "false" &&
|
|
621
|
+
if (input.substr(pos, 5) === "false" && !_isBareKeyChar(input.charAt(pos + 5))) {
|
|
567
622
|
_advance(5); return false;
|
|
568
623
|
}
|
|
569
624
|
|
|
570
625
|
var dt = _tryParseDateTime();
|
|
571
626
|
if (dt !== null) return dt.value;
|
|
572
627
|
|
|
573
|
-
if (
|
|
628
|
+
if (_isNumberLead(c)) return _parseNumber(c);
|
|
574
629
|
|
|
575
630
|
throw _err("unexpected character '" + c + "'", "toml/expected-value");
|
|
576
631
|
}
|
package/lib/parsers/safe-xml.js
CHANGED
|
@@ -53,6 +53,7 @@ var C = require("../constants");
|
|
|
53
53
|
var pick = require("../pick");
|
|
54
54
|
var numericBounds = require("../numeric-bounds");
|
|
55
55
|
var safeBuffer = require("../safe-buffer");
|
|
56
|
+
var codepointClass = require("../codepoint-class");
|
|
56
57
|
var { FrameworkError } = require("../framework-error");
|
|
57
58
|
|
|
58
59
|
class SafeXmlError extends FrameworkError {
|
|
@@ -405,7 +406,12 @@ function parse(input, opts) {
|
|
|
405
406
|
}
|
|
406
407
|
}
|
|
407
408
|
Object.assign(obj, grouped);
|
|
408
|
-
|
|
409
|
+
// Collapse whitespace runs to a single space and trim, by walking rather
|
|
410
|
+
// than matching: the text came off the wire and a screen whose cost depends
|
|
411
|
+
// on the shape of the input is the thing this family does not do.
|
|
412
|
+
// splitOnWhitespace drops empty segments, so joining restores exactly the
|
|
413
|
+
// collapsed-and-trimmed form.
|
|
414
|
+
var combinedText = codepointClass.splitOnWhitespace(textParts.join("")).join(" ");
|
|
409
415
|
if (combinedText.length > 0) obj["#text"] = combinedText;
|
|
410
416
|
return _make(name, obj);
|
|
411
417
|
}
|