@tabnas/yaml 0.5.10 → 0.5.12

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/src/yaml.ts CHANGED
@@ -77,8 +77,11 @@ const grammarText = `
77
77
  rule: indent: open: [
78
78
  # Key pair => map.
79
79
  { s: ['#KEY' '#CL'] p: map b: 2 g: yaml }
80
- # Element marker => list.
81
- { s: '#EL' p: list g: yaml }
80
+ # Element marker => block sequence. yamlBlockList, not jsonic's list:
81
+ # list reads a '[' as its own opening bracket, so a flow sequence as the
82
+ # first item (k:\\n - [1, 2]) became the block sequence itself, or
83
+ # failed at the next item (tabnas/yaml#88).
84
+ { s: '#EL' p: yamlBlockList g: yaml }
82
85
  # Flow collection as a block-mapping value on the FOLLOWING line:
83
86
  # required:
84
87
  # [a, b, c]
@@ -249,8 +252,18 @@ const grammarText = `
249
252
  // --- END EMBEDDED yaml-grammar.jsonic ---
250
253
 
251
254
 
255
+ // The characters updateFlowState acts on, by char code: quotes, the escape
256
+ // backslash, the comment mark, flow brackets, and block scalar indicators.
257
+ // Every other character leaves its state unchanged, so it is skipped with
258
+ // one table read.
259
+ const FLOW_SCAN_CHARS = new Uint8Array(126)
260
+ for (const c of '"\'\\#{}[]|>') FLOW_SCAN_CHARS[c.charCodeAt(0)] = 1
261
+
262
+
252
263
  const Yaml: Plugin = (tabnas: Tabnas, options: YamlOptions) => {
253
- // Guard against re-entry during options() re-application.
264
+ // Install once per instance: a second use(Yaml) on the same instance
265
+ // returns here. options() does not re-run plugins, and make() builds a
266
+ // fresh child that installs its own.
254
267
  if ((tabnas as any).__yamlInstalled) return
255
268
  ;(tabnas as any).__yamlInstalled = true
256
269
 
@@ -306,18 +319,36 @@ const Yaml: Plugin = (tabnas: Tabnas, options: YamlOptions) => {
306
319
  // Incremental flow-depth cache for text.check (avoids O(n²) rescan).
307
320
  let _flowDepth = 0
308
321
  let _flowScanPos = 0
322
+ let _flowScanUpTo = 0
309
323
  // Persistent quote state so multi-call scans handle quotes spanning slices.
310
324
  let _inSingleQuote = false
311
325
  let _inDoubleQuote = false
312
326
 
313
327
  // Bring _flowDepth up to date with `upTo` by scanning lex.src incrementally.
314
- // Skips quoted regions so embedded brackets don't mis-count the flow depth.
328
+ //
329
+ // The scan has to read the source the way the lexer does, or text is
330
+ // counted as syntax. A quoted region is skipped, and so are the regions
331
+ // where a bracket or a quote is ordinary text (tabnas/yaml#89):
332
+ // - a comment (`#` at the start of a line or after whitespace);
333
+ // - a block scalar's content lines (`|` / `>`);
334
+ // - in block context, any `{` `[` `"` `'` that does not begin a node:
335
+ // the `{` in `f({` or the `"` in `say "hi` is inside a plain scalar.
336
+ // Counting those left the depth above zero, so the rest of the document
337
+ // was read as flow (a `{` failed the parse), or left a quote open, so a
338
+ // later `[1, 2]` was read as the one string "1, 2]".
339
+ // The scan may run past `upTo` (to the end of a comment or a block
340
+ // scalar), since the lexer never stops inside either.
315
341
  function updateFlowState(src: string, upTo: number) {
316
- if (upTo < _flowScanPos) {
342
+ if (upTo < _flowScanUpTo) {
317
343
  _flowDepth = 0; _flowScanPos = 0
318
344
  _inSingleQuote = false; _inDoubleQuote = false
319
345
  }
320
- for (let fi = _flowScanPos; fi < upTo; fi++) {
346
+ _flowScanUpTo = upTo
347
+ let fi = _flowScanPos
348
+ for (; fi < upTo; fi++) {
349
+ // Fast path: only these ten characters matter in any state.
350
+ let cc = src.charCodeAt(fi)
351
+ if (cc > 125 || 0 === FLOW_SCAN_CHARS[cc]) continue
321
352
  let fc = src[fi]
322
353
  if (_inDoubleQuote) {
323
354
  if (fc === '\\') fi++
@@ -331,17 +362,213 @@ const Yaml: Plugin = (tabnas: Tabnas, options: YamlOptions) => {
331
362
  }
332
363
  continue
333
364
  }
334
- if (fc === '{' || fc === '[') _flowDepth++
335
- else if (fc === '}' || fc === ']') { if (_flowDepth > 0) _flowDepth-- }
336
- else if (fc === '"') { _inDoubleQuote = true }
337
- else if (fc === "'") {
338
- let pc = fi > 0 ? src.charCodeAt(fi - 1) : 0
339
- if (!((pc >= 65 && pc <= 90) || (pc >= 97 && pc <= 122) || (pc >= 48 && pc <= 57))) {
340
- _inSingleQuote = true
365
+ if (fc === '#' && (fi === 0 || isYamlSpace(src[fi - 1]))) {
366
+ while (fi + 1 < src.length && src[fi + 1] !== '\n' && src[fi + 1] !== '\r') fi++
367
+ continue
368
+ }
369
+ if (_flowDepth > 0) {
370
+ if (fc === '{' || fc === '[') _flowDepth++
371
+ else if (fc === '}' || fc === ']') _flowDepth--
372
+ else if (fc === '"') _inDoubleQuote = true
373
+ else if (fc === "'") {
374
+ let pc = fi > 0 ? src.charCodeAt(fi - 1) : 0
375
+ if (!((pc >= 65 && pc <= 90) || (pc >= 97 && pc <= 122) || (pc >= 48 && pc <= 57))) {
376
+ _inSingleQuote = true
377
+ }
378
+ }
379
+ continue
380
+ }
381
+ // Block context: only a node's first character opens anything.
382
+ if (fc === '{' || fc === '[' || fc === '"' || fc === "'" ||
383
+ fc === '|' || fc === '>') {
384
+ if (!startsYamlNode(src, fi)) {
385
+ // Inside a plain scalar, which nothing can open until it ends at
386
+ // `: `, ` #` or the end of the line: skip to there.
387
+ fi = plainScalarRest(src, fi) - 1
388
+ }
389
+ else if (fc === '{' || fc === '[') _flowDepth++
390
+ else if (fc === '"') _inDoubleQuote = true
391
+ else if (fc === "'") _inSingleQuote = true
392
+ else {
393
+ let end = blockScalarEnd(src, fi)
394
+ if (end > fi) fi = end - 1
341
395
  }
342
396
  }
343
397
  }
344
- _flowScanPos = upTo
398
+ _flowScanPos = fi
399
+ }
400
+
401
+ // Whether a quoted scalar opens at `i` and is a block mapping key: its
402
+ // closing quote is followed, on the same line, by `:` and then whitespace
403
+ // or the end of the line.
404
+ function quotedKeyAt(src: string, i: number): boolean {
405
+ let q = src[i]
406
+ let j = i + 1
407
+ while (j < src.length && src[j] !== '\n' && src[j] !== '\r') {
408
+ if (q === '"' && src[j] === '\\') { j += 2; continue }
409
+ if (src[j] === q) {
410
+ if (q === "'" && src[j + 1] === "'") { j += 2; continue }
411
+ break
412
+ }
413
+ j++
414
+ }
415
+ if (src[j] !== q) return false
416
+ j++
417
+ while (src[j] === ' ' || src[j] === '\t') j++
418
+ return src[j] === ':' && (j + 1 >= src.length || isYamlSpace(src[j + 1]))
419
+ }
420
+
421
+ // `text` with its quotes removed when the whole of it is one quoted
422
+ // scalar on one line; otherwise `text` unchanged. Double quotes take the
423
+ // common escapes; single quotes take '' for a quote.
424
+ function unquoteWholeScalar(text: string): string {
425
+ let q = text[0]
426
+ if ((q !== '"' && q !== "'") || text.length < 2 || text[text.length - 1] !== q) {
427
+ return text
428
+ }
429
+ let out = ''
430
+ for (let j = 1; j < text.length - 1; j++) {
431
+ let c = text[j]
432
+ if (q === "'" && c === "'") {
433
+ if (text[j + 1] !== "'" || j + 1 === text.length - 1) return text
434
+ out += "'"; j++
435
+ } else if (q === '"' && c === '"') {
436
+ return text
437
+ } else if (q === '"' && c === '\\') {
438
+ let e = text[++j]
439
+ let m: Record<string, string> =
440
+ { n: '\n', t: '\t', r: '\r', '0': '\0', '"': '"', '\\': '\\', '/': '/', ' ': ' ' }
441
+ if (j >= text.length - 1 || !(e in m)) return text
442
+ out += m[e]
443
+ } else {
444
+ out += c
445
+ }
446
+ }
447
+ return out
448
+ }
449
+
450
+ function isYamlSpace(c: string | undefined): boolean {
451
+ return c === ' ' || c === '\t' || c === '\n' || c === '\r'
452
+ }
453
+
454
+ // Whether the character at `i` (block context) is the first character of
455
+ // a node: first on its line, or after a `: ` / `- ` / `? ` indicator, a
456
+ // `---` marker, or a node property (`&anchor`, `!tag`) that itself starts
457
+ // a node. Anything else is inside a plain scalar.
458
+ function startsYamlNode(src: string, i: number): boolean {
459
+ // Char codes throughout: this runs for every bracket and quote in block
460
+ // text, and indexing a string allocates a one-character string.
461
+ let p = i - 1
462
+ let c = p >= 0 ? src.charCodeAt(p) : -1
463
+ if (c === 10 || c === 13 || c < 0) return true
464
+ if (c !== 32 && c !== 9) return false
465
+ while (c === 32 || c === 9) c = --p >= 0 ? src.charCodeAt(p) : -1
466
+ if (c === 10 || c === 13 || c < 0) return true
467
+ if (c === 58) return true // `:`
468
+ if (c === 45 || c === 63) { // `-` / `?`
469
+ if (c === 45 && src.charCodeAt(p - 1) === 45 && src.charCodeAt(p - 2) === 45 &&
470
+ (p - 3 < 0 || src.charCodeAt(p - 3) === 10 || src.charCodeAt(p - 3) === 13)) return true
471
+ return startsYamlNode(src, p)
472
+ }
473
+ let w = p
474
+ while (w > 0 && !isYamlSpaceCode(src.charCodeAt(w - 1))) w--
475
+ c = src.charCodeAt(w)
476
+ if (c === 38 || c === 33) return startsYamlNode(src, w) // `&` / `!`
477
+ return false
478
+ }
479
+
480
+ // From `i` inside a block-context plain scalar, the offset where it can
481
+ // end: the next line break, a `:` followed by whitespace or the end, or a
482
+ // `#` after whitespace.
483
+ function plainScalarRest(src: string, i: number): number {
484
+ let j = i + 1
485
+ for (; j < src.length; j++) {
486
+ let c = src.charCodeAt(j)
487
+ if (c === 10 || c === 13) break
488
+ if (c === 58) { // `:`
489
+ let n = j + 1 < src.length ? src.charCodeAt(j + 1) : -1
490
+ if (n < 0 || isYamlSpaceCode(n)) break
491
+ }
492
+ else if (c === 35) { // `#`
493
+ let b = src.charCodeAt(j - 1)
494
+ if (b === 32 || b === 9) break
495
+ }
496
+ }
497
+ return j
498
+ }
499
+
500
+ function isYamlSpaceCode(c: number): boolean {
501
+ return c === 32 || c === 9 || c === 10 || c === 13
502
+ }
503
+
504
+ // A block scalar indicator (`|` or `>`, optional chomping/indentation
505
+ // indicators, then only a comment) at `i`: the offset just past its content
506
+ // lines, or `i` when it is not one. Content runs while lines are blank or
507
+ // indented at least as far as the first content line, which must be deeper
508
+ // than the indicator's line (column 0 is allowed for a document's root).
509
+ function blockScalarEnd(src: string, i: number): number {
510
+ let j = i + 1
511
+ let explicit = 0
512
+ for (let k = 0; k < 2; k++) {
513
+ if (src[j] === '+' || src[j] === '-') j++
514
+ else if (src[j] >= '1' && src[j] <= '9') { explicit = +src[j]; j++ }
515
+ }
516
+ while (src[j] === ' ' || src[j] === '\t') j++
517
+ if (src[j] === '#') {
518
+ while (j < src.length && src[j] !== '\n' && src[j] !== '\r') j++
519
+ }
520
+ if (j < src.length && src[j] !== '\n' && src[j] !== '\r') return i
521
+ let ls = i
522
+ while (ls > 0 && src[ls - 1] !== '\n' && src[ls - 1] !== '\r') ls--
523
+ let lineIndent = 0
524
+ while (src[ls + lineIndent] === ' ') lineIndent++
525
+ let isRoot = ls + lineIndent === i ||
526
+ (src[ls] === '-' && src[ls + 1] === '-' && src[ls + 2] === '-')
527
+ let parentIndent = isRoot && lineIndent === 0 ? -1 : lineIndent
528
+ let pos = j
529
+ if (src[pos] === '\r') pos++
530
+ if (src[pos] === '\n') pos++
531
+ let blockIndent = -1
532
+ if (explicit > 0) {
533
+ // As the block scalar handler does: after a key on the same line
534
+ // (`- a: |2`), the indent the indicator counts from includes each
535
+ // `- ` before the key, not only the line's leading spaces.
536
+ let base = Math.max(parentIndent, 0)
537
+ let hasColon = false
538
+ for (let ci = ls + lineIndent; ci < i; ci++) {
539
+ if (src[ci] === ':' && (src[ci + 1] === ' ' || src[ci + 1] === '\t')) { hasColon = true; break }
540
+ }
541
+ if (hasColon) {
542
+ let si = ls + lineIndent
543
+ while (si < i && src[si] === '-' && (src[si + 1] === ' ' || src[si + 1] === '\t')) {
544
+ base += 2
545
+ si += 2
546
+ while (si < i && src[si] === ' ') { base++; si++ }
547
+ }
548
+ }
549
+ blockIndent = base + explicit
550
+ }
551
+ let end = pos
552
+ while (pos < src.length) {
553
+ let n = 0
554
+ while (src[pos + n] === ' ') n++
555
+ let c = src[pos + n]
556
+ let lineEnd = pos + n
557
+ while (lineEnd < src.length && src[lineEnd] !== '\n' && src[lineEnd] !== '\r') lineEnd++
558
+ let next = lineEnd
559
+ if (src[next] === '\r') next++
560
+ if (src[next] === '\n') next++
561
+ if (c === undefined || c === '\n' || c === '\r') { pos = next; continue }
562
+ if (blockIndent < 0) {
563
+ if (n <= parentIndent) break
564
+ blockIndent = n
565
+ }
566
+ if (n < blockIndent) break
567
+ if (n === 0 && (src.startsWith('---', pos) || src.startsWith('...', pos))) break
568
+ end = lineEnd
569
+ pos = next
570
+ }
571
+ return end
345
572
  }
346
573
 
347
574
 
@@ -1038,6 +1265,7 @@ const Yaml: Plugin = (tabnas: Tabnas, options: YamlOptions) => {
1038
1265
  yamlStreamCurMeta = null
1039
1266
  _flowDepth = 0
1040
1267
  _flowScanPos = 0
1268
+ _flowScanUpTo = 0
1041
1269
  _inSingleQuote = false
1042
1270
  _inDoubleQuote = false
1043
1271
  // Empty / whitespace-only / comments-only source: emit one
@@ -1542,6 +1770,9 @@ const Yaml: Plugin = (tabnas: Tabnas, options: YamlOptions) => {
1542
1770
  explicitKeyTag = tagMatch[1]
1543
1771
  key = tagMatch[2]
1544
1772
  }
1773
+ // A quoted explicit key is the scalar inside the quotes, not
1774
+ // the quoted text (`? "k"` is the key k).
1775
+ key = unquoteWholeScalar(key)
1545
1776
  let consumed = keyEnd
1546
1777
  // Track position before consuming newline (for !hasValue case).
1547
1778
  let beforeNewline = consumed
@@ -1931,7 +2162,10 @@ const Yaml: Plugin = (tabnas: Tabnas, options: YamlOptions) => {
1931
2162
  // trailing commas (e.g. 12,), or non-numeric text after a space
1932
2163
  // (e.g. "64 characters, hexadecimal.") must be captured before
1933
2164
  // jsonic's number matcher grabs just the digits.
1934
- if (fwd[0] >= '0' && fwd[0] <= '9') {
2165
+ // A sign before the digit counts too: `+190:20:30` is one plain
2166
+ // scalar, where the number matcher would take `+190`.
2167
+ if ((fwd[0] >= '0' && fwd[0] <= '9') ||
2168
+ ((fwd[0] === '+' || fwd[0] === '-') && fwd[1] >= '0' && fwd[1] <= '9')) {
1935
2169
  updateFlowState(lex.src as string, pnt.sI)
1936
2170
  let inFlow = _flowDepth > 0
1937
2171
  let hasEmbeddedColon = false
@@ -1939,10 +2173,16 @@ const Yaml: Plugin = (tabnas: Tabnas, options: YamlOptions) => {
1939
2173
  let hasBlockComma = false
1940
2174
  let pi = 1
1941
2175
  while (pi < fwd.length && fwd[pi] !== '\n' && fwd[pi] !== '\r') {
2176
+ // An embedded colon (20:03:20) does not end the scan: the
2177
+ // scalar can go on past a space ("09:00 AM"), and only the
2178
+ // trailing-text branch below reads that as one scalar.
2179
+ // Stopping here took "09:00" and left "AM" to fail the
2180
+ // parse (tabnas/yaml#90).
1942
2181
  if (fwd[pi] === ':' && fwd[pi + 1] !== ' ' && fwd[pi + 1] !== '\t' &&
1943
2182
  fwd[pi + 1] !== '\n' && fwd[pi + 1] !== '\r' && fwd[pi + 1] !== undefined) {
1944
2183
  hasEmbeddedColon = true
1945
- break
2184
+ pi++
2185
+ continue
1946
2186
  }
1947
2187
  // A COMMA IS NOT A SEPARATOR IN BLOCK CONTEXT.
1948
2188
  //
@@ -2001,10 +2241,13 @@ const Yaml: Plugin = (tabnas: Tabnas, options: YamlOptions) => {
2001
2241
  }
2002
2242
 
2003
2243
  if (hasEmbeddedColon || hasBlockComma) {
2004
- // Scan to end of plain scalar token (space, tab, newline, eof).
2244
+ // Scan to end of plain scalar token (space, tab, newline,
2245
+ // eof, or in a flow collection its `,` `]` `}`: `[1, 12:30]`
2246
+ // holds "12:30", not "12:30,").
2005
2247
  let end = 0
2006
2248
  while (end < fwd.length && fwd[end] !== ' ' && fwd[end] !== '\t' &&
2007
- fwd[end] !== '\n' && fwd[end] !== '\r') end++
2249
+ fwd[end] !== '\n' && fwd[end] !== '\r' &&
2250
+ !(inFlow && (fwd[end] === ',' || fwd[end] === ']' || fwd[end] === '}'))) end++
2008
2251
  let text = fwd.substring(0, end)
2009
2252
  let tkn = lex.token('#TX', text, text, lex.pnt)
2010
2253
  pnt.sI += end
@@ -2246,9 +2489,14 @@ const Yaml: Plugin = (tabnas: Tabnas, options: YamlOptions) => {
2246
2489
  // Skip #IN when the next content is a flow indicator or quoted
2247
2490
  // string at column 0 — there's no block to indent into. Match
2248
2491
  // the previous behavior of the inline `--- foo` handler.
2492
+ // A quoted KEY at column 0 is different: it continues (or
2493
+ // closes back to) the root block mapping, so it needs its #IN
2494
+ // like a plain key does. Without it a block sequence ending
2495
+ // just before it swallowed the pair (tabnas/yaml#86).
2249
2496
  if (spaces === 0 &&
2250
2497
  (fwd[pos] === '{' || fwd[pos] === '[' ||
2251
- fwd[pos] === '"' || fwd[pos] === "'")) {
2498
+ ((fwd[pos] === '"' || fwd[pos] === "'") &&
2499
+ !quotedKeyAt(fwd, pos)))) {
2252
2500
  pnt.sI += pos
2253
2501
  pnt.rI += rows
2254
2502
  pnt.cI = 0
@@ -2627,7 +2875,7 @@ Yaml.defaults = ({
2627
2875
  // VERSION is this package's version. It MUST equal package.json "version":
2628
2876
  // the release orchestrator rewrites both, and test/version.test.ts fails the
2629
2877
  // build if they drift. Mirrors `const VERSION` in go/yaml.go.
2630
- const VERSION = '0.5.10'
2878
+ const VERSION = '0.5.12'
2631
2879
 
2632
2880
 
2633
2881
  export {