@blamejs/core 0.18.50 → 0.18.53

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.
@@ -0,0 +1,533 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // Where in a YAML document a character actually SITS.
6
+ //
7
+ // Two modules were answering that question separately and getting different
8
+ // wrong answers. `guard-yaml` decided a `!` opened a tag if it followed
9
+ // whitespace, so it reported one inside a quoted scalar, inside a block-scalar
10
+ // body, and inside a comment. `parsers/safe-yaml` masked quoted scalars but
11
+ // passed comment text through verbatim and had no block-scalar handling at all,
12
+ // despite its own note promising both. Between them every ordinary document
13
+ // carrying an exclamation mark in prose was refused, by one module or the other.
14
+ //
15
+ // The question is not "what precedes this character" — it is "what region is
16
+ // this character in", and no amount of looking at the previous byte answers it.
17
+ // Region is a property of everything before, so it takes a scan. This module is
18
+ // that scan, written once, so the two callers cannot drift apart again.
19
+ //
20
+ // `maskNonStructural` returns a string the SAME LENGTH as its input, with every
21
+ // non-structural region replaced by spaces and newlines preserved. Callers keep
22
+ // their existing sigil searches and run them against the mask instead of the
23
+ // source: an index into one is an index into the other, so reported locations
24
+ // and line numbers stay true.
25
+ //
26
+ // What survives the mask, because it is what the callers are looking for:
27
+ // structural punctuation, node properties (`!tag`, `&anchor`, `*alias`),
28
+ // directive lines, and document markers. What is masked is scalar content in
29
+ // every form it takes: quoted, plain, and block.
30
+ //
31
+ // The mask is deliberately readier to KEEP than to hide. Masking something
32
+ // structural is the dangerous direction: it does not refuse a good document, it
33
+ // hides a real tag inside a bad one and hands the callers something they then
34
+ // call clean. Every case below that looked like a tidy simplification and was
35
+ // not — blanking the rest of a line, measuring a block body from the wrong
36
+ // column — failed in exactly that direction.
37
+
38
+ // No line-splitting helper is used here on purpose: `codepointClass.splitLines`
39
+ // strips the carriage return of a CRLF pair, and this function's whole contract
40
+ // is that its output is the same length as its input. See the split below.
41
+
42
+ // Deliberately not a regex, in either sense. The guard and safe families forbid
43
+ // them, and a scanner is what this file exists to be.
44
+ function _isSpace(code) { return code === 0x20 || code === 0x09; }
45
+
46
+ function _isPlainSpace(ch) { return ch === " " || ch === "\t"; }
47
+
48
+ // A node property is a tag, an anchor, or an alias. They chain, so `!tag &a`
49
+ // and `&a !tag` are both a single node's properties and the scalar begins after
50
+ // the last of them.
51
+ function _isPropertySigil(ch) { return ch === "!" || ch === "&" || ch === "*"; }
52
+
53
+ // A block-scalar header is `|` or `>`, optionally followed by the chomping and
54
+ // indentation indicators in EITHER order (`|2-` and `|-2` are equally valid),
55
+ // and then nothing but whitespace or a comment. Misreading one scans a shell
56
+ // script as if it were YAML.
57
+ // Returns `{ end, declared }` or null. `declared` is the indentation indicator's
58
+ // digit when the header carries one, or 0. That digit is not decoration: it
59
+ // DECLARES the body's indentation relative to the parent node, so a body whose
60
+ // first line happens to be indented further does not move it. Detecting from
61
+ // the first line in that case ends the block early and hands the rest of a
62
+ // scalar to the structural scan.
63
+ function _blockHeaderEnd(line, at) {
64
+ var ch = line.charAt(at);
65
+ if (ch !== "|" && ch !== ">") return null;
66
+ var i = at + 1;
67
+ var declared = 0, sawChomp = false;
68
+ while (i < line.length) {
69
+ var c = line.charAt(i);
70
+ if (c >= "1" && c <= "9" && !declared) { declared = Number(c); i += 1; continue; }
71
+ if ((c === "-" || c === "+") && !sawChomp) { sawChomp = true; i += 1; continue; }
72
+ break;
73
+ }
74
+ // Only a header if the rest of the line is blank or a comment; `x: |foo` is
75
+ // an ordinary plain scalar that happens to start with a bar.
76
+ var j = i;
77
+ while (j < line.length && _isPlainSpace(line.charAt(j))) j += 1;
78
+ if (j < line.length && line.charAt(j) !== "#") return null;
79
+ return { end: i, declared: declared };
80
+ }
81
+
82
+ // The document markers and directive lines a caller still needs to see. A
83
+ // directive is only a directive at column zero, and `---` / `...` only at the
84
+ // start of a line.
85
+ function _isVerbatimLine(line, indent) {
86
+ if (line.charAt(0) === "%") return true;
87
+ // Column ZERO only. YAML puts document markers there and nowhere else, so an
88
+ // indented `---` is scalar content — `- hello` / ` ---` / ` !world` is one
89
+ // plain scalar. Matching it wherever it appeared ended the document, threw
90
+ // away the scalar state, and left the continuation to be read as structure.
91
+ if (indent !== 0) return false;
92
+ if (line.indexOf("---") === 0 &&
93
+ (line.length === 3 || _isPlainSpace(line.charAt(3)))) return true;
94
+ if (line.indexOf("...") === 0 &&
95
+ (line.length === 3 || _isPlainSpace(line.charAt(3)))) return true;
96
+ return false;
97
+ }
98
+
99
+ function maskNonStructural(text) {
100
+ var src = String(text == null ? "" : text);
101
+ // Split on the newline ALONE, keeping any `\r` with the line it terminates.
102
+ // `codepointClass.splitLines` strips it, and rejoining those with "\n" drops
103
+ // one character per CRLF line — which would shift every index after the first
104
+ // such line and silently break the alignment this whole function exists to
105
+ // provide. A mask that is not the same length as its source is worse than no
106
+ // mask, because the locations it reports are confidently wrong.
107
+ var lines = src.split("\n");
108
+ var out = [];
109
+ // A block scalar's body indentation is DETECTED from its first content line,
110
+ // which is what YAML specifies and the only thing that works. Two fixed
111
+ // columns were tried and each failed the other's case: measuring from the
112
+ // key's column masks nothing when extra spacing after a dash puts the body in
113
+ // that same column, and measuring from a canonical column swallows a sibling
114
+ // key written between the two. With extra spacing the body can sit anywhere
115
+ // past the dash, so only the body itself says where it starts.
116
+ //
117
+ // `blockOwner` is the column the body must beat for the block to have one at
118
+ // all; `blockBody` is the detected indentation, -1 until the first content
119
+ // line sets it. Both -1 when no block is open.
120
+ var blockOwner = -1;
121
+ var blockBody = -1;
122
+ // A quoted scalar may span lines, so the quote that opened one carries across
123
+ // until it closes. Scanning each line independently would read the
124
+ // CONTINUATION as structure, which puts back the exact false positive this
125
+ // module exists to remove: `x: "hello` / ` !world"` would name a tag on the
126
+ // second line.
127
+ var openQuote = null;
128
+ // A flow collection may also span lines, so its depth carries across too. It
129
+ // decides where a plain scalar ends, and resetting it per line would end one
130
+ // at the wrong place inside a multi-line `[ ... ]`.
131
+ var flowDepth = 0;
132
+ // A PLAIN scalar spans lines as well — that is how a long description gets
133
+ // written without quotes — and its continuation lines are indented further
134
+ // than the node that opened it. Reading one as a fresh line puts the false
135
+ // positive back a fourth way: in
136
+ //
137
+ // x: hello
138
+ // !world
139
+ //
140
+ // the value is `hello !world` and the bang introduces nothing.
141
+ //
142
+ // -1 when no plain scalar is open. Otherwise the indent of the node that
143
+ // opened it, which a continuation must beat. This is set ONLY when a line
144
+ // actually read plain-scalar content, so `x:` with its value underneath does
145
+ // not arm it and a nested mapping is still read as structure.
146
+ var plainOpen = -1;
147
+ // ...and whether that scalar is a sequence item's own, which decides whether
148
+ // a continuation at the SAME column counts.
149
+ var plainFromDash = false;
150
+
151
+ for (var li = 0; li < lines.length; li += 1) {
152
+ var raw = lines[li];
153
+ // The carriage return of a CRLF pair is a line TERMINATOR, not content, so
154
+ // it is held aside and put back verbatim. Letting it into the scan would
155
+ // have it read as the first character of a plain scalar and masked to a
156
+ // space, which changes the bytes of a document the mask is meant to mirror.
157
+ var cr = raw.length && raw.charAt(raw.length - 1) === "\r";
158
+ var line = cr ? raw.slice(0, raw.length - 1) : raw;
159
+ var indent = 0;
160
+ while (indent < line.length && _isSpace(line.charCodeAt(indent))) indent += 1;
161
+ var blank = indent === line.length;
162
+
163
+ // A quoted scalar carried over from an earlier line owns this one until it
164
+ // closes, and nothing before that point is structure — not a `---` that
165
+ // looks like a document marker, and not a `%` in column zero that looks
166
+ // like a directive. So this is answered before either of those.
167
+ //
168
+ // What follows the closing quote on that line IS structure again, and the
169
+ // scan resumes there rather than blanking it. Blanking hid a real tag: in
170
+ // `x: ["first` / ` second", !tag value]` the comma and the tag after the
171
+ // scalar belong to the collection, and masking them takes a tagged document
172
+ // and hands both screens something they call clean.
173
+ var resumeAt = -1;
174
+ var resumePrefix = "";
175
+ if (openQuote !== null) {
176
+ var cont = _maskQuotedBody(line, 0, openQuote);
177
+ if (!cont.closed) {
178
+ out.push(cont.masked + (cr ? "\r" : ""));
179
+ continue; // the whole line is body
180
+ }
181
+ openQuote = null;
182
+ resumeAt = cont.end;
183
+ resumePrefix = cont.masked;
184
+ }
185
+
186
+ if (resumeAt === -1) {
187
+ if (blockOwner >= 0) {
188
+ if (blank) { out.push(_blanked(line) + (cr ? "\r" : "")); continue; }
189
+ if (blockBody === -1) {
190
+ // The first content line sets the body's indentation — provided it is
191
+ // past the owner at all. If it is not, the block has an empty body and
192
+ // this line is structure.
193
+ if (indent > blockOwner) {
194
+ blockBody = indent;
195
+ out.push(_blanked(line) + (cr ? "\r" : ""));
196
+ continue;
197
+ }
198
+ blockOwner = -1;
199
+ } else if (indent >= blockBody) {
200
+ out.push(_blanked(line) + (cr ? "\r" : ""));
201
+ continue;
202
+ } else {
203
+ blockOwner = -1;
204
+ blockBody = -1; // the body ended here
205
+ }
206
+ }
207
+ if (blank) { out.push(raw); continue; }
208
+ // A directive or a document marker is structure the callers still need to
209
+ // see, but only the marker itself is. A comment may follow one on the same
210
+ // line, and its text is no more YAML there than anywhere else — passing
211
+ // the whole line through left `--- # note !bang` naming a tag, which is
212
+ // the very class this module removes, surviving in the one branch that
213
+ // skipped the scan.
214
+ if (_isVerbatimLine(line, indent)) {
215
+ out.push(_maskTrailingComment(line) + (cr ? "\r" : ""));
216
+ // A document marker ends the document, so EVERY piece of carried state
217
+ // ends with it. Resetting only the flow depth left a scalar open across
218
+ // the boundary, and with the sequence-item rule that meant
219
+ // `- hello` / `---` / ` !tag v` read the new document's first line as
220
+ // a continuation of the old document's last item — masking a real tag.
221
+ // A marker is the one place where "carry this across lines" is always
222
+ // wrong, so the reset is total rather than itemised.
223
+ flowDepth = 0;
224
+ plainOpen = -1;
225
+ plainFromDash = false;
226
+ blockOwner = -1;
227
+ blockBody = -1;
228
+ continue;
229
+ }
230
+ }
231
+
232
+ // Resuming after a closed continuation starts where the quote ended, with
233
+ // that scalar already behind us; otherwise the line's own content begins.
234
+ var masked = resumeAt === -1 ? line.slice(0, indent) : resumePrefix;
235
+ var i = resumeAt === -1 ? indent : resumeAt;
236
+ // A node may begin here: at the start of the line's content, and again
237
+ // after every structural token that introduces one.
238
+ //
239
+ // Two things mean it does NOT begin here. A resumed line has just finished
240
+ // reading a quoted scalar. And a line continuing a PLAIN scalar opened
241
+ // earlier is more of that scalar, so its first character is content:
242
+ //
243
+ // x: hello
244
+ // !world
245
+ //
246
+ // The continuation is scanned rather than blanked, because inside a flow
247
+ // collection it may still end at a `,` or a `]` that belongs to the
248
+ // collection — `x: [hello` / ` !world]` closes a sequence on its second
249
+ // line. Closing the node position is enough to make the leading sigil fall
250
+ // into the plain-scalar branch, and it costs no special case.
251
+ // A scalar that IS a sequence item has its continuation aligned with the
252
+ // item's content column rather than deeper than it — `- hello` then
253
+ // ` !world` is the scalar `hello !world`, and both sit at column 2. So
254
+ // equality continues that one, while a mapping entry's value still needs a
255
+ // strictly deeper line, where equality would be a sibling key.
256
+ var continuesPlain = resumeAt === -1 && plainOpen >= 0 && !blank &&
257
+ (plainFromDash ? indent >= plainOpen : indent > plainOpen);
258
+ var atNodeStart = resumeAt === -1 && !continuesPlain;
259
+ // Where the NODE on this line begins, which is the line's indent until a
260
+ // sequence dash moves it along. A block scalar's body is measured from
261
+ // here, not from the leading whitespace.
262
+ var nodeIndent = indent;
263
+ // The column of the innermost sequence dash read on this line, or -1. A
264
+ // block scalar standing where that dash's own node goes belongs to the
265
+ // ITEM, so this is what bounds its body.
266
+ var dashIndent = -1;
267
+ // Was the token just read a JSON-LIKE key? YAML's JSON compatibility lets
268
+ // one take its value colon with no space after it, and there are two kinds:
269
+ // a quoted scalar, and a flow collection. Both end this flag set.
270
+ var prevWasJsonKey = false;
271
+ // Did the line finish inside a plain scalar? Set by the plain-scalar branch
272
+ // when it runs to the end of the line, and false the moment anything else
273
+ // is read after it.
274
+ var sawPlainToEol = false;
275
+ // The column the last plain scalar on this line started in.
276
+ var plainStartCol = -1;
277
+
278
+ while (i < line.length) {
279
+ var ch = line.charAt(i);
280
+ // Cleared here and re-set only by the quoted-scalar branch, so no branch
281
+ // can leave it true by forgetting to. The colon rule reads the captured
282
+ // copy rather than the live flag.
283
+ var jsonKeyBefore = prevWasJsonKey;
284
+ prevWasJsonKey = false;
285
+
286
+ // A comment opens on `#` at the start of the content or after whitespace,
287
+ // and runs to the end of the line. Its text is not YAML, whatever it
288
+ // says: `x: 1 # note !bang` names no tag.
289
+ if (ch === "#" && (i === indent || _isPlainSpace(line.charAt(i - 1)))) {
290
+ masked += _blanked(line.slice(i));
291
+ break;
292
+ }
293
+
294
+ // Whitespace carries the quoted-key flag rather than clearing it. YAML
295
+ // allows separation between a JSON-style key and its adjacent value, so
296
+ // `{"a" :!!python/object x}` is as valid as `{"a":!!python/object x}` —
297
+ // and clearing here left the second form closed and the first one open,
298
+ // which is a deserialization tag hidden behind one space.
299
+ if (_isPlainSpace(ch)) {
300
+ masked += ch; i += 1; prevWasJsonKey = jsonKeyBefore; continue;
301
+ }
302
+
303
+ // Structural punctuation stays visible, and each of these opens a node
304
+ // position after it.
305
+ if (ch === "{" || ch === "[") {
306
+ masked += ch; i += 1; flowDepth += 1; atNodeStart = true; continue;
307
+ }
308
+ if (ch === "}" || ch === "]") {
309
+ masked += ch; i += 1; if (flowDepth > 0) flowDepth -= 1;
310
+ atNodeStart = false;
311
+ // A closing delimiter ends a JSON-LIKE key just as a closing quote
312
+ // does, and YAML lets that kind of key take its colon with no space
313
+ // after it. `{{a: b}:!!python/object x}` is valid, and reading the `:`
314
+ // as ordinary text left the tag masked — the same bypass as the quoted
315
+ // key, through the other production for the same rule.
316
+ prevWasJsonKey = true;
317
+ continue;
318
+ }
319
+ if (ch === ",") { masked += ch; i += 1; atNodeStart = true; continue; }
320
+ if (ch === "-" && (i + 1 >= line.length || _isPlainSpace(line.charAt(i + 1)))) {
321
+ masked += ch; i += 1; atNodeStart = true;
322
+ // A sequence entry written inline starts a node PAST the dash, and any
323
+ // block scalar it opens is measured from there rather than from the
324
+ // line's leading whitespace. Getting this wrong is not a false refusal
325
+ // but a false ACCEPT: in
326
+ //
327
+ // - key: |
328
+ // body
329
+ // evil: !tag x
330
+ //
331
+ // `evil` is a sibling of `key`, and measuring the body against the
332
+ // dash's indent of zero swallows it — masking a real tag and handing
333
+ // both screens a document they then call clean.
334
+ var afterDash = i;
335
+ while (afterDash < line.length && _isPlainSpace(line.charAt(afterDash))) afterDash += 1;
336
+ if (afterDash < line.length) nodeIndent = afterDash;
337
+ // ...unless the item's node IS a block scalar, with no mapping in
338
+ // between: `- |` then ` !hello`. There the body starts at the same
339
+ // column the `|` sits in, so measuring from that column masks nothing
340
+ // and the scalar's first line reads as a tag. The owner of the block is
341
+ // the sequence ITEM, so the dash's column is what bounds it.
342
+ dashIndent = i - 1;
343
+ continue;
344
+ }
345
+ if (ch === "?" && (i + 1 >= line.length || _isPlainSpace(line.charAt(i + 1)))) {
346
+ masked += ch; i += 1; atNodeStart = true; continue;
347
+ }
348
+ // A colon separates a key from its value when whitespace follows, when a
349
+ // flow delimiter does — and, inside a flow collection, when the key was
350
+ // QUOTED. That last form is YAML's JSON compatibility: `{"a":value}` is
351
+ // valid and needs no space, so requiring one left the colon unread, the
352
+ // node position closed, and the value masked as though it were more of
353
+ // the key's scalar. `{"a":!!python/object x}` therefore reached both
354
+ // screens with its tag hidden — a deserialization tag, which is the
355
+ // single most dangerous thing this scan exists to surface.
356
+ if (ch === ":" &&
357
+ (i + 1 >= line.length || _isPlainSpace(line.charAt(i + 1)) ||
358
+ (flowDepth > 0 && ",}]".indexOf(line.charAt(i + 1)) !== -1) ||
359
+ (flowDepth > 0 && jsonKeyBefore))) {
360
+ masked += ch; i += 1; atNodeStart = true; continue;
361
+ }
362
+
363
+ // A node's properties survive: they are exactly what the callers scan for,
364
+ // and they are the ONLY place a `!`, `&` or `*` means what it looks like.
365
+ if (atNodeStart && _isPropertySigil(ch)) {
366
+ var pEnd = i + 1;
367
+ if (ch === "!" && line.charAt(pEnd) === "!") pEnd += 1;
368
+ while (pEnd < line.length && !_isPlainSpace(line.charAt(pEnd)) &&
369
+ (flowDepth === 0 || ",}]".indexOf(line.charAt(pEnd)) === -1)) pEnd += 1;
370
+ masked += line.slice(i, pEnd);
371
+ i = pEnd;
372
+ continue; // properties chain
373
+ }
374
+
375
+ // A quoted scalar: the quotes stay, the body goes. The body is content by
376
+ // construction, so nothing in it is ever structure.
377
+ if (ch === '"' || ch === "'") {
378
+ var qr = _maskQuotedBody(line, i + 1, ch);
379
+ masked += ch + qr.masked;
380
+ i = qr.end;
381
+ // Not closed on this line means the scalar CONTINUES, which YAML allows
382
+ // and which the line-at-a-time reading would otherwise lose. The quote
383
+ // is remembered so the next line is read as its body rather than as
384
+ // structure.
385
+ if (!qr.closed) { openQuote = ch; break; }
386
+ atNodeStart = false;
387
+ // Remembered for the colon rule above: a quoted scalar is one of the
388
+ // two JSON-like key forms that may take its colon with no space between.
389
+ prevWasJsonKey = true;
390
+ continue;
391
+ }
392
+
393
+ // A block-scalar header ends the line's structure; the body is masked by
394
+ // the outer loop.
395
+ var bHead = _blockHeaderEnd(line, i);
396
+ if (bHead) {
397
+ masked += line.slice(i, bHead.end);
398
+ // The body must beat the DASH when there is one, and the line's node
399
+ // otherwise. That is only a floor: where the body actually starts is
400
+ // detected from its first content line, because with extra spacing
401
+ // after a dash it can sit anywhere past that floor.
402
+ // The owner is the node the block hangs off: the ITEM when the header
403
+ // stands where the item's own node goes (`- |`), and the MAPPING ENTRY
404
+ // when it follows a key (`- key: |`). Using the dash for the second
405
+ // case made a sibling written straight after an empty block look like
406
+ // its first body line — `- key: |` / ` evil: !tag x` masked the tag.
407
+ //
408
+ // It also settles the padded case the honest way. `- key: |` puts the
409
+ // mapping at column 4, so a body at column 4 is not content by YAML's
410
+ // own indentation rule; that document is malformed, and reading its
411
+ // next line as structure surfaces whatever it says rather than hiding
412
+ // it. Where a shape is ambiguous, the reading that keeps a sibling
413
+ // VISIBLE is the one to take — the other hides real tags.
414
+ blockOwner = (dashIndent >= 0 && i === nodeIndent) ? dashIndent : nodeIndent;
415
+ // ...unless the header DECLARED it. `|2` fixes the body's indentation
416
+ // relative to the parent, so a first line indented further does not
417
+ // move it, and a later line back at the declared column is still body.
418
+ // Detecting in that case ends the block early and hands the rest of the
419
+ // scalar to the structural scan.
420
+ //
421
+ // Counted from that same owner: for `- key: |2` the indicator is
422
+ // relative to the mapping entry, not the dash, and counting from the
423
+ // dash puts the body two columns too far left and swallows the entry's
424
+ // siblings.
425
+ blockBody = bHead.declared ? blockOwner + bHead.declared : -1;
426
+ i = bHead.end;
427
+ atNodeStart = false;
428
+ continue;
429
+ }
430
+
431
+ // Anything else begins a PLAIN scalar, and everything to the end of it is
432
+ // content. This is the case the previous implementations had no notion of:
433
+ // in `x: hello !world` the `!world` sits inside a scalar that started at
434
+ // `hello`, so it names no tag, and only knowing a scalar had already begun
435
+ // can tell you that.
436
+ var s = i;
437
+ while (s < line.length) {
438
+ var c3 = line.charAt(s);
439
+ if (c3 === "#" && _isPlainSpace(line.charAt(s - 1))) break;
440
+ if (c3 === ":" &&
441
+ (s + 1 >= line.length || _isPlainSpace(line.charAt(s + 1)) ||
442
+ (flowDepth > 0 && ",}]".indexOf(line.charAt(s + 1)) !== -1))) break;
443
+ if (flowDepth > 0 && ",}][{".indexOf(c3) !== -1) break;
444
+ s += 1;
445
+ }
446
+ masked += _blanked(line.slice(i, s));
447
+ // Where this scalar BEGAN. A dash earlier on the line is not enough to
448
+ // make the scalar the item's own: in `- key: hello` the scalar belongs to
449
+ // `key`, and treating the next line at the mapping's column as its
450
+ // continuation masked the tag on a sibling key.
451
+ plainStartCol = i;
452
+ i = s;
453
+ atNodeStart = false;
454
+ // A plain scalar reaching the end of the line may continue on the next
455
+ // one. Only the LAST thing read on a line can, so this is recorded here
456
+ // and cleared by anything that follows it.
457
+ sawPlainToEol = s >= line.length;
458
+ }
459
+ // Armed only when the line ended inside a plain scalar, and measured
460
+ // against the node that opened it rather than the line's own indent. `x:`
461
+ // with nothing after it never arms this, so the mapping written underneath
462
+ // it is still read as structure rather than swallowed as text.
463
+ // Armed inside a flow collection too: a plain scalar spans lines there just
464
+ // as it does outside one, and requiring depth zero left `x: [hello` /
465
+ // ` !world]` reading its second line at a fresh node start.
466
+ if (sawPlainToEol && openQuote === null && blockOwner < 0) {
467
+ if (!continuesPlain) {
468
+ plainOpen = nodeIndent;
469
+ // Whether the scalar hangs directly off a sequence dash decides how its
470
+ // continuation is measured, so it is remembered with the column.
471
+ // Only when the scalar IS the item's node: a dash on the line, and the
472
+ // scalar starting exactly where that item's node begins.
473
+ plainFromDash = dashIndent >= 0 && plainStartCol === nodeIndent;
474
+ }
475
+ } else {
476
+ plainOpen = -1;
477
+ plainFromDash = false;
478
+ }
479
+ out.push(masked + (cr ? "\r" : ""));
480
+ }
481
+ return out.join("\n");
482
+ }
483
+
484
+ // A line kept for its structure, with any comment on it masked. Used for the
485
+ // directive and document-marker lines, which are passed through whole because
486
+ // what makes them structural is their shape rather than a scan of their parts.
487
+ // The comment is still content and still has to go.
488
+ function _maskTrailingComment(line) {
489
+ for (var i = 0; i < line.length; i += 1) {
490
+ if (line.charAt(i) !== "#") continue;
491
+ if (i !== 0 && !_isPlainSpace(line.charAt(i - 1))) continue;
492
+ return line.slice(0, i) + _blanked(line.slice(i));
493
+ }
494
+ return line;
495
+ }
496
+
497
+ // The body of a quoted scalar from `at`, masked, stopping at the closing quote.
498
+ // Returns where the scan ended (past the quote when it closed) and whether it
499
+ // did close, which is what tells the caller the scalar runs onto the next line.
500
+ //
501
+ // Both the escape forms are honoured because both hide a quote that would
502
+ // otherwise look like the end: `\"` inside a double-quoted scalar and `''`
503
+ // inside a single-quoted one. Reading either as a terminator ends the mask
504
+ // early and hands the rest of the scalar back to the structural scan as though
505
+ // it were YAML.
506
+ function _maskQuotedBody(line, at, quote) {
507
+ var body = "";
508
+ var k = at;
509
+ while (k < line.length) {
510
+ var c = line.charAt(k);
511
+ if (quote === '"' && c === "\\" && k + 1 < line.length) { body += " "; k += 2; continue; }
512
+ if (c === quote) {
513
+ if (quote === "'" && line.charAt(k + 1) === "'") { body += " "; k += 2; continue; }
514
+ return { masked: body + quote, end: k + 1, closed: true };
515
+ }
516
+ body += c === "\t" ? "\t" : " ";
517
+ k += 1;
518
+ }
519
+ return { masked: body, end: k, closed: false };
520
+ }
521
+
522
+ // Same length, spaces throughout. Newlines cannot appear here — the caller
523
+ // splits on them first — but a tab is preserved so column arithmetic that
524
+ // counts it as one character still agrees with the source.
525
+ function _blanked(s) {
526
+ var o = "";
527
+ for (var i = 0; i < s.length; i += 1) o += s.charAt(i) === "\t" ? "\t" : " ";
528
+ return o;
529
+ }
530
+
531
+ module.exports = {
532
+ maskNonStructural: maskNonStructural,
533
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.18.50",
3
+ "version": "0.18.53",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:0535e946-47d0-4e95-879e-65d801bc533b",
5
+ "serialNumber": "urn:uuid:f52f2ee6-253b-4bf4-9915-22d453a4407a",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-08-22T21:30:23.773Z",
8
+ "timestamp": "2026-08-24T12:08:00.505Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.18.50",
22
+ "bom-ref": "@blamejs/core@0.18.53",
23
23
  "type": "application",
24
24
  "name": "blamejs",
25
- "version": "0.18.50",
25
+ "version": "0.18.53",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.18.50",
29
+ "purl": "pkg:npm/%40blamejs/core@0.18.53",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.18.50",
57
+ "ref": "@blamejs/core@0.18.53",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]