From bbfacd152c0f30f012f59be7d5184c21b3d2b646 Mon Sep 17 00:00:00 2001 From: marcopiraccini Date: Sat, 31 Oct 2026 08:22:52 +0100 Subject: [PATCH] lib: add experimental node:yaml module Add a YAML 1.2 parser for the core schema as `node:yaml`, behind `--experimental-yaml`, with `yaml.parse(input)` following the shape of `Bun.YAML.parse()`. The parser is written from scratch, in one pass from text to values, with no new dependency. Anchors and aliases resolve to shared objects, core schema tags are applied, mappings become plain objects and duplicate keys are an error. The official yaml-test-suite is vendored as a JSON fixture and run by test-yaml-test-suite.js: 400 of its 402 cases pass, the two left are documents with duplicate keys. Refs: https://github.com/nodejs/node/issues/53787 Signed-off-by: marcopiraccini --- doc/api/cli.md | 12 + doc/api/errors.md | 12 + doc/api/index.md | 1 + doc/api/yaml.md | 80 + doc/node-config-schema.json | 4 + doc/node.1 | 5 + lib/internal/bootstrap/realm.js | 1 + lib/internal/errors.js | 1 + lib/internal/yaml/parser.js | 1616 ++++++ lib/yaml.js | 47 + src/node_builtins.cc | 1 + src/node_options.cc | 4 + src/node_options.h | 1 + test/fixtures/yaml-test-suite.json | 4330 +++++++++++++++++ .../test-module-builtin-experimental.js | 1 + test/parallel/test-process-get-builtin.mjs | 2 + test/parallel/test-require-resolve.js | 2 + test/parallel/test-yaml-test-suite.js | 57 + test/parallel/test-yaml.js | 413 ++ 19 files changed, 6590 insertions(+) create mode 100644 doc/api/yaml.md create mode 100644 lib/internal/yaml/parser.js create mode 100644 lib/yaml.js create mode 100644 test/fixtures/yaml-test-suite.json create mode 100644 test/parallel/test-yaml-test-suite.js create mode 100644 test/parallel/test-yaml.js diff --git a/doc/api/cli.md b/doc/api/cli.md index bddd4a5c0a5f..479d2b8b31fe 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1780,6 +1780,16 @@ added: Enable experimental support for the worker inspection with Chrome DevTools. +### `--experimental-yaml` + + + +> Stability: 1.0 - Early development + +Enable the experimental [`node:yaml`][] module. + ### `--force-context-aware` + +The input passed to [`yaml.parse()`][] is not valid YAML. The message names +the problem and its line and column. + ### `ERR_IPC_CHANNEL_CLOSED` @@ -4858,6 +4869,7 @@ An error occurred trying to allocate memory. This should never happen. [`util.inspect()`]: util.md#utilinspectobject-options [`util.parseArgs()`]: util.md#utilparseargsconfig [`v8.startupSnapshot.setDeserializeMainFunction()`]: v8.md#v8startupsnapshotsetdeserializemainfunctioncallback-data +[`yaml.parse()`]: yaml.md#yamlparseinput [`zlib`]: zlib.md [crypto digest algorithm]: crypto.md#cryptogethashes [debugger]: debugger.md diff --git a/doc/api/index.md b/doc/api/index.md index a30724c064e1..d865d43ef7ff 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -71,6 +71,7 @@ * [Web Crypto API](webcrypto.md) * [Web Streams API](webstreams.md) * [Worker threads](worker_threads.md) +* [YAML](yaml.md) * [Zlib](zlib.md)
diff --git a/doc/api/yaml.md b/doc/api/yaml.md new file mode 100644 index 000000000000..0e8b69c9c75c --- /dev/null +++ b/doc/api/yaml.md @@ -0,0 +1,80 @@ +# YAML + + + + + +> Stability: 1.0 - Early development + + + +The `node:yaml` module parses [YAML 1.2][] into JavaScript values. Enable it +with [`--experimental-yaml`][] and import it with `node:`: + +```mjs +import yaml from 'node:yaml'; +``` + +```cjs +const yaml = require('node:yaml'); +``` + +Loading the module emits an `ExperimentalWarning`. + +## `yaml.parse(input)` + + + +* `input` {string|ArrayBuffer|SharedArrayBuffer|TypedArray|DataView} The YAML + text. Binary input is decoded as UTF-8. +* Returns: {any} The value of the document, or an array with the value of each + document when the stream holds more than one. An empty stream yields `null`. + +`parse()` uses the YAML 1.2 [core schema][]. Plain scalars resolve to null, +booleans, numbers or strings. For example, `yes` and `no` are strings. Quoted +and block scalars are strings unless a tag changes their type, as in +`!!int "42"`. Mappings become plain objects and sequences become arrays, as +with [`JSON.parse()`][]. + +```mjs +import yaml from 'node:yaml'; + +const config = yaml.parse(` +name: app +ports: + - 8080 + - 8443 +tls: true +`); +console.log(config); +// { name: 'app', ports: [ 8080, 8443 ], tls: true } + +console.log(yaml.parse('--- 1\n--- 2\n')); +// [ 1, 2 ] +``` + +Aliases to a collection refer to the same object. A collection may contain an +alias to itself, so the result can contain cycles. + +The parser applies the core tags `!!str`, `!!int`, `!!float`, `!!bool`, +`!!null`, `!!map` and `!!seq`. A tag must match its value: `!!int abc` throws +an error. Other tags leave collections unchanged and make scalars strings. +For example, `!custom 123` becomes `'123'`. + +Mapping keys become property names. Scalar keys use their string form, so +`null`, `true` and `1` become `'null'`, `'true'` and `'1'`. Collection keys use +their JSON text and are limited to 1024 characters. Duplicate keys throw. + +Invalid YAML throws an [`ERR_INVALID_YAML`][] `SyntaxError` with the line and +column. The parser rejects disallowed control characters and nesting deeper +than 1000 levels. + +[YAML 1.2]: https://yaml.org/spec/1.2.2/ +[`--experimental-yaml`]: cli.md#--experimental-yaml +[`ERR_INVALID_YAML`]: errors.md#err_invalid_yaml +[`JSON.parse()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse +[core schema]: https://yaml.org/spec/1.2.2/#103-core-schema diff --git a/doc/node-config-schema.json b/doc/node-config-schema.json index 0d2cb83b517d..6f74077b3582 100644 --- a/doc/node-config-schema.json +++ b/doc/node-config-schema.json @@ -418,6 +418,10 @@ "type": "boolean", "description": "experimental Web Storage API" }, + "experimental-yaml": { + "type": "boolean", + "description": "experimental node:yaml module" + }, "extra-info-on-fatal-exception": { "type": "boolean", "description": "hide extra information on fatal exception that causes exit" diff --git a/doc/node.1 b/doc/node.1 index 7a32ece46d21..c0236f96a901 100644 --- a/doc/node.1 +++ b/doc/node.1 @@ -955,6 +955,9 @@ Enable experimental support for the Web Worker API. .It Fl -experimental-worker-inspection Enable experimental support for the worker inspection with Chrome DevTools. . +.It Fl -experimental-yaml +Enable the experimental \fBnode:yaml\fR module. +. .It Fl -force-context-aware Disable loading native addons that are not context-aware. . @@ -2304,6 +2307,8 @@ one is included in the list below. .It \fB--experimental-websocket\fR .It +\fB--experimental-yaml\fR +.It \fB--force-context-aware\fR .It \fB--force-fips\fR diff --git a/lib/internal/bootstrap/realm.js b/lib/internal/bootstrap/realm.js index 3e3473b000c5..43c649f2ce1e 100644 --- a/lib/internal/bootstrap/realm.js +++ b/lib/internal/bootstrap/realm.js @@ -208,6 +208,7 @@ const builtinModulePolicies = [ ['test', true, null], ['test/reporters', true, null], ['vfs', true, '--experimental-vfs'], + ['yaml', true, '--experimental-yaml'], ['zlib/iter', false, '--experimental-stream-iter'], ]; diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 88c4102eee4f..2fd6dcfe8fa1 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -1596,6 +1596,7 @@ E('ERR_INVALID_URL_SCHEME', `of scheme ${expected[0]}`; return `The URL must be ${res}`; }, TypeError); +E('ERR_INVALID_YAML', '%s', SyntaxError); E('ERR_IPC_CHANNEL_CLOSED', 'Channel closed', Error); E('ERR_IPC_DISCONNECTED', 'IPC channel is already disconnected', Error); E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe', Error); diff --git a/lib/internal/yaml/parser.js b/lib/internal/yaml/parser.js new file mode 100644 index 000000000000..efc28599994e --- /dev/null +++ b/lib/internal/yaml/parser.js @@ -0,0 +1,1616 @@ +'use strict'; + +// A YAML 1.2 parser for the core schema that builds JavaScript values +// directly, in one pass over the text. It follows the structure of the +// specification: documents and directives, block collections driven by +// indentation, flow collections, and the scalar styles. + +const { + ArrayIsArray, + ArrayPrototypeIncludes, + ArrayPrototypePush, + JSONStringify, + NumberParseFloat, + NumberParseInt, + NumberPrototypeToString, + ObjectDefineProperty, + ObjectKeys, + ObjectPrototypeHasOwnProperty, + SafeMap, + SafeSet, + String, + StringFromCharCode, + StringFromCodePoint, + StringPrototypeCharCodeAt, + StringPrototypeRepeat, + StringPrototypeSlice, + Symbol, +} = primordials; + +const { + codes: { + ERR_INVALID_YAML, + }, +} = require('internal/errors'); + +const kMaxDepth = 1000; +const kMaxKeyLength = 1024; + +const kEOF = -1; +const kTab = 0x09; +const kLF = 0x0A; +const kCR = 0x0D; +const kSpace = 0x20; +const kBang = 0x21; +const kDoubleQuote = 0x22; +const kHash = 0x23; +const kPercent = 0x25; +const kAmpersand = 0x26; +const kSingleQuote = 0x27; +const kAsterisk = 0x2A; +const kPlus = 0x2B; +const kComma = 0x2C; +const kMinus = 0x2D; +const kDot = 0x2E; +const kSlash = 0x2F; +const kZero = 0x30; +const kNine = 0x39; +const kColon = 0x3A; +const kLess = 0x3C; +const kGreater = 0x3E; +const kQuestion = 0x3F; +const kAt = 0x40; +const kLeftBracket = 0x5B; +const kBackslash = 0x5C; +const kRightBracket = 0x5D; +const kBacktick = 0x60; +const kLeftBrace = 0x7B; +const kPipe = 0x7C; +const kRightBrace = 0x7D; +const kBOM = 0xFEFF; + +const kClip = 0; +const kStrip = 1; +const kKeep = 2; + +// How a node was written, which decides how a tag applies to it. +const kPlain = 0; +const kQuoted = 1; +const kSequence = 2; +const kMapping = 3; + +const kCoreTagPrefix = 'tag:yaml.org,2002:'; + +// Returned by the compose functions when there is no node at the position. +const kNoNode = Symbol('kNoNode'); + +class State { + constructor(input) { + this.input = input; + this.length = input.length; + this.pos = 0; + this.line = 0; + this.lineStart = 0; + // Number of leading spaces of the current line. + this.lineIndent = 0; + // Whether the current line has content after a tab in its indentation. + this.tabIndented = false; + this.depth = 0; + this.anchors = new SafeMap(); + this.tagHandles = new SafeMap(); + this.version = null; + this.explicitEnd = false; + // Set by the scalar readers: whether the scalar spanned several lines. + this.multiline = false; + // Set by readFlowNode: whether the node was quoted or a flow collection, + // which allows ":" to follow it without a space. + this.lastJsonLike = false; + } +} + +function at(s, pos) { + return pos < s.length ? StringPrototypeCharCodeAt(s.input, pos) : kEOF; +} + +function fail(s, message, pos = s.pos) { + let line = s.line; + let lineStart = s.lineStart; + if (pos < lineStart) { + // The error is on an earlier line: recount from the start. + line = 0; + lineStart = 0; + for (let i = 0; i < pos; i++) { + const c = StringPrototypeCharCodeAt(s.input, i); + if (c === kLF || (c === kCR && at(s, i + 1) !== kLF)) { + line++; + lineStart = i + 1; + } + } + } + throw new ERR_INVALID_YAML( + `${message} (line ${line + 1}, column ${pos - lineStart + 1})`); +} + +function isWhite(c) { + return c === kSpace || c === kTab; +} + +function isBreak(c) { + return c === kLF || c === kCR; +} + +function isBlank(c) { + return c === kSpace || c === kTab || c === kLF || c === kCR || c === kEOF; +} + +function isFlowIndicator(c) { + return c === kComma || c === kLeftBracket || c === kRightBracket || + c === kLeftBrace || c === kRightBrace; +} + +function isDigit(c) { + return c >= kZero && c <= kNine; +} + +function isHexDigit(c) { + return isDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66); +} + +// Consumes one line break and the indentation of the next line. +function consumeBreak(s) { + const c = at(s, s.pos); + s.pos++; + if (c === kCR && at(s, s.pos) === kLF) s.pos++; + s.line++; + s.lineStart = s.pos; + let indent = 0; + while (at(s, s.pos) === kSpace) { + s.pos++; + indent++; + } + s.lineIndent = indent; + s.tabIndented = false; + if (at(s, s.pos) === kTab) { + let p = s.pos; + while (isWhite(at(s, p))) p++; + const n = at(s, p); + s.tabIndented = n !== kEOF && !isBreak(n) && n !== kHash; + } +} + +function skipWhite(s) { + while (isWhite(at(s, s.pos))) s.pos++; +} + +function skipToEndOfLine(s) { + while (s.pos < s.length && !isBreak(at(s, s.pos))) s.pos++; +} + +function atDocumentMarker(s, pos = s.pos) { + if (pos !== s.lineStart) return false; + const c = at(s, pos); + if (c !== kMinus && c !== kDot) return false; + return at(s, pos + 1) === c && at(s, pos + 2) === c && isBlank(at(s, pos + 3)); +} + +// Skips whitespace, comments and line breaks. Returns the number of line +// breaks crossed. A comment must be separated from content by whitespace. +function skipSeparation(s) { + let breaks = 0; + for (;;) { + const c = at(s, s.pos); + if (isWhite(c)) { + s.pos++; + } else if (c === kHash) { + if (s.pos > 0 && !isBlank(at(s, s.pos - 1))) { + fail(s, 'Comments must be separated from other tokens by whitespace'); + } + skipToEndOfLine(s); + } else if (isBreak(c)) { + consumeBreak(s); + breaks++; + } else { + return breaks; + } + } +} + +function setProperty(s, map, key, value, keyPos) { + if (ObjectPrototypeHasOwnProperty(map, key)) { + fail(s, `Duplicate mapping key "${key}"`, keyPos); + } + ObjectDefineProperty(map, key, { + __proto__: null, + value, + writable: true, + enumerable: true, + configurable: true, + }); +} + +// Mapping keys become property names: scalars as their string form, +// collections as their JSON text, bounded in size since aliases can make a +// small key expand to a huge text. +function keyToString(s, key, pos) { + if (typeof key === 'string') return key; + if (key === null || typeof key === 'number' || typeof key === 'boolean') { + return String(key); + } + const text = { __proto__: null, value: '', budget: kMaxKeyLength }; + appendKeyJSON(s, text, key, pos); + return text.value; +} + +function appendKeyText(s, text, part, pos) { + text.budget -= part.length; + if (text.budget < 0) { + fail(s, `A collection key must not exceed ${kMaxKeyLength} characters`, pos); + } + text.value += part; +} + +function appendKeyJSON(s, text, value, pos) { + if (value === null || typeof value !== 'object') { + appendKeyText(s, text, JSONStringify(value), pos); + } else if (ArrayIsArray(value)) { + appendKeyText(s, text, '[', pos); + for (let i = 0; i < value.length; i++) { + if (i > 0) appendKeyText(s, text, ',', pos); + appendKeyJSON(s, text, value[i], pos); + } + appendKeyText(s, text, ']', pos); + } else { + appendKeyText(s, text, '{', pos); + const keys = ObjectKeys(value); + for (let i = 0; i < keys.length; i++) { + if (i > 0) appendKeyText(s, text, ',', pos); + appendKeyText(s, text, `${JSONStringify(keys[i])}:`, pos); + appendKeyJSON(s, text, value[keys[i]], pos); + } + appendKeyText(s, text, '}', pos); + } +} + +function enter(s) { + if (++s.depth > kMaxDepth) { + fail(s, `Nesting deeper than ${kMaxDepth} levels`); + } +} + +// --- Scalars --------------------------------------------------------------- + +// Resolves a plain scalar with the core schema. +function resolvePlain(text) { + const length = text.length; + if (length === 0) return null; + const c = StringPrototypeCharCodeAt(text, 0); + switch (c) { + case 0x7E: // ~ + if (length === 1) return null; + break; + case 0x6E: case 0x4E: // n N + if (text === 'null' || text === 'Null' || text === 'NULL') return null; + break; + case 0x74: case 0x54: // t T + if (text === 'true' || text === 'True' || text === 'TRUE') return true; + break; + case 0x66: case 0x46: // f F + if (text === 'false' || text === 'False' || text === 'FALSE') return false; + break; + case kDot: case kMinus: case kPlus: { + const special = parseSpecialFloat(text); + if (special !== undefined) return special; + break; + } + } + if (c === kDot || c === kMinus || c === kPlus || isDigit(c)) { + const number = parseNumber(text); + if (number !== undefined) return number; + } + return text; +} + +function parseSpecialFloat(text) { + let i = 0; + const first = StringPrototypeCharCodeAt(text, 0); + if (first === kMinus || first === kPlus) i = 1; + if (StringPrototypeCharCodeAt(text, i) !== kDot) return undefined; + const word = StringPrototypeSlice(text, i + 1); + if (word === 'inf' || word === 'Inf' || word === 'INF') { + return first === kMinus ? -Infinity : Infinity; + } + if (i === 0 && (word === 'nan' || word === 'NaN' || word === 'NAN')) { + return NaN; + } + return undefined; +} + +// Core schema integers and floats, or undefined. +function parseNumber(text) { + const length = text.length; + let i = 0; + let c = StringPrototypeCharCodeAt(text, 0); + const negative = c === kMinus; + if (c === kMinus || c === kPlus) { + i++; + c = StringPrototypeCharCodeAt(text, i); + } + if (c === kZero && i === 0 && length > 2) { + const base = StringPrototypeCharCodeAt(text, 1); + if (base === 0x78) { // 0x + for (let j = 2; j < length; j++) { + if (!isHexDigit(StringPrototypeCharCodeAt(text, j))) return undefined; + } + return NumberParseInt(StringPrototypeSlice(text, 2), 16); + } + if (base === 0x6F) { // 0o + for (let j = 2; j < length; j++) { + const d = StringPrototypeCharCodeAt(text, j); + if (d < kZero || d > 0x37) return undefined; + } + return NumberParseInt(StringPrototypeSlice(text, 2), 8); + } + } + let digits = 0; + while (i < length && isDigit(StringPrototypeCharCodeAt(text, i))) { + i++; + digits++; + } + let isFloat = false; + if (i < length && StringPrototypeCharCodeAt(text, i) === kDot) { + isFloat = true; + i++; + while (i < length && isDigit(StringPrototypeCharCodeAt(text, i))) { + i++; + digits++; + } + } + if (digits === 0) return undefined; + if (i < length) { + c = StringPrototypeCharCodeAt(text, i); + if (c !== 0x65 && c !== 0x45) return undefined; // e E + isFloat = true; + i++; + c = StringPrototypeCharCodeAt(text, i); + if (c === kMinus || c === kPlus) i++; + let exponentDigits = 0; + while (i < length && isDigit(StringPrototypeCharCodeAt(text, i))) { + i++; + exponentDigits++; + } + if (exponentDigits === 0 || i < length) return undefined; + } + if (isFloat) return NumberParseFloat(text); + const value = NumberParseInt(text, 10); + // -0 is an integer in YAML; keep it a plain 0 like JSON.parse does. + return negative && value === 0 ? 0 : value; +} + +// Whether text has the form of a core schema integer: decimal with an +// optional sign, 0x hexadecimal or 0o octal. +function isIntegerText(text) { + const length = text.length; + let i = 0; + const c = StringPrototypeCharCodeAt(text, 0); + if (c === kMinus || c === kPlus) i = 1; + if (c === kZero && length > 2) { + const base = StringPrototypeCharCodeAt(text, 1); + if (base === 0x78 || base === 0x6F) { + for (let j = 2; j < length; j++) { + const d = StringPrototypeCharCodeAt(text, j); + if (base === 0x78 ? !isHexDigit(d) : (d < kZero || d > 0x37)) return false; + } + return true; + } + } + if (i === length) return false; + for (; i < length; i++) { + if (!isDigit(StringPrototypeCharCodeAt(text, i))) return false; + } + return true; +} + +// Characters that never start a plain scalar: whitespace, the indicators of +// the other node kinds, comments and the reserved indicators. +const kNoPlainStart = new SafeSet([ + kEOF, kSpace, kTab, kLF, kCR, kHash, kAmpersand, kAsterisk, kBang, kPipe, + kGreater, kSingleQuote, kDoubleQuote, kPercent, kAt, kBacktick, +]); + +function canStartPlain(s, c, inFlow) { + if (kNoPlainStart.has(c)) return false; + if (c === kMinus || c === kQuestion || c === kColon) { + const next = at(s, s.pos + 1); + return !isBlank(next) && !(inFlow && isFlowIndicator(next)); + } + if (isFlowIndicator(c)) return !inFlow; + return true; +} + +// A line break inside a scalar folds into a space, or into one newline per +// empty line that follows it. +function foldBreaks(count) { + return count === 1 ? ' ' : StringPrototypeRepeat('\n', count - 1); +} + +// Reads a plain scalar and returns its text. In block context the +// continuation lines must be indented more than the parent; in flow context +// the scalar also ends at flow indicators. +function readPlain(s, parentIndent, inFlow, singleLine) { + const startLine = s.line; + let result = ''; + let segmentStart = s.pos; + let segmentEnd = s.pos; + let pendingBreaks = 0; + + const flush = () => { + if (segmentEnd > segmentStart) { + result += (pendingBreaks > 0 ? foldBreaks(pendingBreaks) : '') + + StringPrototypeSlice(s.input, segmentStart, segmentEnd); + } + }; + + for (;;) { + const c = at(s, s.pos); + if (c === kEOF || isBreak(c)) { + flush(); + if (c === kEOF || singleLine) break; + // Look at the following lines without committing to them. + const savedLine = s.line; + const savedLineStart = s.lineStart; + const savedIndent = s.lineIndent; + const savedTab = s.tabIndented; + let breaks = 0; + let continues = false; + while (isBreak(at(s, s.pos))) { + consumeBreak(s); + breaks++; + let p = s.pos; + while (isWhite(at(s, p))) p++; + const n = at(s, p); + if (n === kEOF) break; + if (isBreak(n)) { + // A blank line. + s.pos = p; + continue; + } + if (atDocumentMarker(s) || n === kHash) break; + if (s.lineIndent <= parentIndent) break; + if (inFlow && isFlowIndicator(n)) break; + s.pos = p; + continues = true; + break; + } + if (!continues) { + s.line = savedLine; + s.lineStart = savedLineStart; + s.lineIndent = savedIndent; + s.tabIndented = savedTab; + break; + } + pendingBreaks = breaks; + segmentStart = s.pos; + segmentEnd = s.pos; + continue; + } + if (c === kColon) { + const next = at(s, s.pos + 1); + if (isBlank(next) || (inFlow && isFlowIndicator(next))) { + flush(); + break; + } + } else if (c === kHash) { + if (isWhite(at(s, s.pos - 1))) { + flush(); + break; + } + } else if (inFlow && isFlowIndicator(c)) { + flush(); + break; + } + s.pos++; + if (!isWhite(c)) segmentEnd = s.pos; + } + // The scalar ends after its last non-blank character. + s.pos = segmentEnd; + s.multiline = s.line !== startLine; + return result; +} + +function trimTrailingWhite(text) { + let end = text.length; + while (end > 0 && isWhite(StringPrototypeCharCodeAt(text, end - 1))) end--; + return StringPrototypeSlice(text, 0, end); +} + +// Called at a line break inside a quoted scalar. Consumes the breaks and +// blank lines and returns their number. Continuation lines must be indented +// more than the parent node. +function skipQuotedBreaks(s, parentIndent) { + let breaks = 0; + while (isBreak(at(s, s.pos))) { + consumeBreak(s); + breaks++; + skipWhite(s); + const c = at(s, s.pos); + if (c === kEOF) fail(s, 'Unterminated quoted scalar'); + if (isBreak(c)) continue; + if (atDocumentMarker(s)) { + fail(s, 'Unexpected document marker inside a quoted scalar'); + } + if (s.lineIndent <= parentIndent) fail(s, 'Bad indentation in a quoted scalar'); + } + return breaks; +} + +function readSingleQuoted(s, parentIndent) { + const startLine = s.line; + const startPos = s.pos; + s.pos++; // Opening quote. + let result = ''; + let segmentStart = s.pos; + for (;;) { + const c = at(s, s.pos); + if (c === kEOF) { + fail(s, 'Unterminated single-quoted scalar', startPos); + } else if (c === kSingleQuote) { + result += StringPrototypeSlice(s.input, segmentStart, s.pos); + s.pos++; + if (at(s, s.pos) === kSingleQuote) { + result += "'"; + s.pos++; + segmentStart = s.pos; + continue; + } + break; + } else if (isBreak(c)) { + result += trimTrailingWhite(StringPrototypeSlice(s.input, segmentStart, s.pos)); + result += foldBreaks(skipQuotedBreaks(s, parentIndent)); + segmentStart = s.pos; + } else { + s.pos++; + } + } + s.multiline = s.line !== startLine; + return result; +} + +function readDoubleQuoted(s, parentIndent) { + const startLine = s.line; + const startPos = s.pos; + s.pos++; // Opening quote. + let result = ''; + let segmentStart = s.pos; + for (;;) { + const c = at(s, s.pos); + if (c === kEOF) { + fail(s, 'Unterminated double-quoted scalar', startPos); + } else if (c === kDoubleQuote) { + result += StringPrototypeSlice(s.input, segmentStart, s.pos); + s.pos++; + break; + } else if (c === kBackslash) { + result += StringPrototypeSlice(s.input, segmentStart, s.pos); + s.pos++; + const e = at(s, s.pos); + if (isBreak(e)) { + // Escaped line break: join with the next line without a space. Empty + // lines in between are kept. + const breaks = skipQuotedBreaks(s, parentIndent); + if (breaks > 1) result += StringPrototypeRepeat('\n', breaks - 1); + } else { + result += readEscape(s, e); + } + segmentStart = s.pos; + } else if (isBreak(c)) { + result += trimTrailingWhite(StringPrototypeSlice(s.input, segmentStart, s.pos)); + result += foldBreaks(skipQuotedBreaks(s, parentIndent)); + segmentStart = s.pos; + } else { + s.pos++; + } + } + s.multiline = s.line !== startLine; + return result; +} + +function readEscape(s, c) { + s.pos++; + switch (c) { + case 0x30: return '\0'; + case 0x61: return '\x07'; + case 0x62: return '\b'; + case 0x74: case kTab: return '\t'; + case 0x6E: return '\n'; + case 0x76: return '\v'; + case 0x66: return '\f'; + case 0x72: return '\r'; + case 0x65: return '\x1B'; + case kSpace: return ' '; + case kDoubleQuote: return '"'; + case kSlash: return '/'; + case kBackslash: return '\\'; + case 0x4E: return '\x85'; + case 0x5F: return '\xA0'; + case 0x4C: return '\u2028'; + case 0x50: return '\u2029'; + case 0x78: return readHexEscape(s, 2); + case 0x75: return readHexEscape(s, 4); + case 0x55: return readHexEscape(s, 8); + } + fail(s, `Unknown escape sequence "\\${c === kEOF ? '' : StringFromCharCode(c)}"`, s.pos - 2); +} + +function readHexEscape(s, digits) { + let value = 0; + for (let i = 0; i < digits; i++) { + const c = at(s, s.pos); + if (!isHexDigit(c)) fail(s, 'Invalid hexadecimal escape sequence'); + value = value * 16 + NumberParseInt(StringFromCharCode(c), 16); + s.pos++; + } + if (value > 0x10FFFF || (digits === 8 && value >= 0xD800 && value <= 0xDFFF)) { + fail(s, 'Invalid code point in escape sequence', s.pos - digits); + } + return value > 0xFFFF ? StringFromCodePoint(value) : StringFromCharCode(value); +} + +// Reads a literal (|) or folded (>) block scalar. parentIndent is the +// indentation of the node that owns it. +function readBlockScalar(s, parentIndent) { + const folded = at(s, s.pos) === kGreater; + const headerPos = s.pos; + s.pos++; + let chomping = kClip; + let indicator = 0; + for (let i = 0; i < 2; i++) { + const c = at(s, s.pos); + if (c === kMinus || c === kPlus) { + if (chomping !== kClip) fail(s, 'Repeated chomping indicator'); + chomping = c === kMinus ? kStrip : kKeep; + s.pos++; + } else if (isDigit(c)) { + if (indicator !== 0) fail(s, 'Repeated indentation indicator'); + if (c === kZero) fail(s, 'Indentation indicator must be between 1 and 9'); + indicator = c - kZero; + s.pos++; + } + } + skipWhite(s); + if (at(s, s.pos) === kHash) { + if (!isBlank(at(s, s.pos - 1))) { + fail(s, 'Comments must be separated from other tokens by whitespace'); + } + skipToEndOfLine(s); + } + const afterHeader = at(s, s.pos); + if (afterHeader !== kEOF && !isBreak(afterHeader)) { + fail(s, 'Unexpected characters after block scalar header', headerPos); + } + s.multiline = true; + if (afterHeader === kEOF) return ''; + consumeBreak(s); + + let contentIndent = indicator > 0 ? parentIndent + indicator : -1; + + // Leading empty lines: lines of spaces only. With auto-detection they must + // not be indented more than the content. + let leadingEmpty = 0; + let maxLeadingSpaces = 0; + for (;;) { + const c = at(s, s.pos); + if (c === kEOF && s.pos === s.lineStart) break; + if (c !== kEOF && !isBreak(c)) break; + if (s.lineIndent > maxLeadingSpaces) maxLeadingSpaces = s.lineIndent; + leadingEmpty++; + if (c === kEOF) break; + consumeBreak(s); + } + if (at(s, s.pos) === kEOF) { + return chomping === kKeep ? StringPrototypeRepeat('\n', leadingEmpty) : ''; + } + if (contentIndent < 0) { + // Auto-detect from the first non-empty line. + if (s.lineIndent <= parentIndent || atDocumentMarker(s)) { + contentIndent = parentIndent + 1; + } else { + contentIndent = s.lineIndent; + if (maxLeadingSpaces > contentIndent) { + fail(s, 'A leading empty line is indented more than the block scalar content'); + } + } + } + + let result = ''; + let emptyLines = leadingEmpty; + let hasContent = false; + let prevMoreIndented = false; + for (;;) { + const c = at(s, s.pos); + if (c === kEOF && s.pos === s.lineStart) break; + if (s.lineIndent < contentIndent) { + // Less indented: an empty line of spaces, or the end of the scalar. + if (c === kTab) fail(s, 'Tabs cannot be used for indentation'); + if (c !== kEOF && !isBreak(c)) break; + emptyLines++; + if (c === kEOF) break; + consumeBreak(s); + continue; + } + if (atDocumentMarker(s)) break; + // A content line: everything after contentIndent spaces. + const textStart = s.lineStart + contentIndent; + let end = textStart; + while (end < s.length && !isBreak(at(s, end))) end++; + const text = StringPrototypeSlice(s.input, textStart, end); + if (text.length === 0) { + emptyLines++; + } else { + const moreIndented = isWhite(StringPrototypeCharCodeAt(text, 0)); + if (hasContent) { + if (folded && !moreIndented && !prevMoreIndented) { + result += emptyLines === 0 ? ' ' : StringPrototypeRepeat('\n', emptyLines); + } else { + result += StringPrototypeRepeat('\n', emptyLines + 1); + } + } else if (emptyLines > 0) { + result += StringPrototypeRepeat('\n', emptyLines); + } + result += text; + hasContent = true; + emptyLines = 0; + prevMoreIndented = moreIndented; + } + s.pos = end; + if (at(s, s.pos) === kEOF) break; + consumeBreak(s); + } + if (!hasContent) { + // Only empty lines: keep yields the line breaks, the others nothing. + return chomping === kKeep ? StringPrototypeRepeat('\n', emptyLines) : ''; + } + if (chomping === kClip) { + result += '\n'; + } else if (chomping === kKeep) { + result += StringPrototypeRepeat('\n', emptyLines + 1); + } + return result; +} + +// --- Properties, tags, anchors, aliases ------------------------------------- + +function readAnchorName(s) { + const start = s.pos; + for (;;) { + const c = at(s, s.pos); + if (isBlank(c) || isFlowIndicator(c)) break; + s.pos++; + } + if (s.pos === start) fail(s, 'Anchor name must not be empty'); + return StringPrototypeSlice(s.input, start, s.pos); +} + +function decodeTagEscapes(s, text) { + let result = ''; + for (let i = 0; i < text.length; i++) { + const c = StringPrototypeCharCodeAt(text, i); + if (c !== kPercent) { + result += StringFromCharCode(c); + continue; + } + const hex = StringPrototypeSlice(text, i + 1, i + 3); + if (hex.length !== 2 || !isHexDigit(StringPrototypeCharCodeAt(hex, 0)) || + !isHexDigit(StringPrototypeCharCodeAt(hex, 1))) { + fail(s, 'Invalid escape in tag'); + } + result += StringFromCharCode(NumberParseInt(hex, 16)); + i += 2; + } + return result; +} + +// Reads a tag property and returns the resolved tag. +function readTag(s, inFlow) { + const start = s.pos; + s.pos++; // ! + if (at(s, s.pos) === kLess) { + s.pos++; + const uriStart = s.pos; + while (at(s, s.pos) !== kGreater) { + if (isBlank(at(s, s.pos))) fail(s, 'Unterminated verbatim tag', start); + s.pos++; + } + const uri = StringPrototypeSlice(s.input, uriStart, s.pos); + s.pos++; + if (uri.length === 0 || uri === '!') fail(s, 'Verbatim tag must not be empty', start); + if (!isBlank(at(s, s.pos)) && !isFlowIndicator(at(s, s.pos))) { + fail(s, 'Unexpected characters after tag'); + } + return uri; + } + // The handle is "!", "!!" or "!name!". + let handleEnd = s.pos; + if (at(s, handleEnd) === kBang) { + handleEnd++; + } else { + let p = handleEnd; + while (!isBlank(at(s, p)) && !isFlowIndicator(at(s, p)) && at(s, p) !== kBang) p++; + if (at(s, p) === kBang) handleEnd = p + 1; + } + const handle = StringPrototypeSlice(s.input, start, handleEnd); + s.pos = handleEnd; + const suffixStart = s.pos; + while (!isBlank(at(s, s.pos)) && !isFlowIndicator(at(s, s.pos))) { + if (at(s, s.pos) === kBang) fail(s, 'Invalid character in tag'); + s.pos++; + } + const suffix = decodeTagEscapes(s, StringPrototypeSlice(s.input, suffixStart, s.pos)); + if (!isBlank(at(s, s.pos)) && !(inFlow && isFlowIndicator(at(s, s.pos)))) { + fail(s, 'Invalid character in tag'); + } + if (handle === '!') { + if (suffix.length === 0) return '!'; + const prefix = s.tagHandles.get('!'); + return prefix === undefined ? `!${suffix}` : prefix + suffix; + } + if (suffix.length === 0) fail(s, 'Tag suffix must not be empty', start); + if (handle === '!!') { + const prefix = s.tagHandles.get('!!'); + return (prefix === undefined ? kCoreTagPrefix : prefix) + suffix; + } + const prefix = s.tagHandles.get(handle); + if (prefix === undefined) fail(s, `Undeclared tag handle "${handle}"`, start); + return prefix + suffix; +} + +// Reads the optional anchor and tag before a node. Returns null when there +// are none. +function readProperties(s, inFlow) { + let anchor = null; + let tag = null; + for (;;) { + const c = at(s, s.pos); + if (c === kAmpersand) { + if (anchor !== null) fail(s, 'A node can have only one anchor'); + s.pos++; + anchor = readAnchorName(s); + } else if (c === kBang) { + if (tag !== null) fail(s, 'A node can have only one tag'); + tag = readTag(s, inFlow); + } else { + break; + } + skipWhite(s); + const n = at(s, s.pos); + // Properties may be on a line of their own; the caller skips on. + if (isBreak(n) || n === kHash || n === kEOF) break; + } + if (anchor === null && tag === null) return null; + return { __proto__: null, anchor, tag }; +} + +// Joins two groups of properties of the same node. +function mergeProperties(s, a, b, pos) { + if (a === null) return b; + if (b === null) return a; + if (a.anchor !== null && b.anchor !== null) fail(s, 'A node can have only one anchor', pos); + if (a.tag !== null && b.tag !== null) fail(s, 'A node can have only one tag', pos); + return { + __proto__: null, + anchor: a.anchor !== null ? a.anchor : b.anchor, + tag: a.tag !== null ? a.tag : b.tag, + }; +} + +function isCoreTag(tag) { + return tag.length > kCoreTagPrefix.length && + StringPrototypeSlice(tag, 0, kCoreTagPrefix.length) === kCoreTagPrefix; +} + +// Resolves a scalar, applying its tag, and registers its anchor. +function finishScalar(s, text, kind, props, pos) { + const tag = props === null ? null : props.tag; + let value; + if (tag === null) { + value = kind === kPlain ? resolvePlain(text) : text; + } else if (tag === '!' || tag === `${kCoreTagPrefix}str`) { + value = text; + } else if (tag === `${kCoreTagPrefix}int`) { + if (!isIntegerText(text)) fail(s, `"${text}" is not an integer`, pos); + value = parseNumber(text); + } else if (tag === `${kCoreTagPrefix}float`) { + value = parseSpecialFloat(text); + if (value === undefined) value = parseNumber(text); + if (typeof value !== 'number') fail(s, `"${text}" is not a float`, pos); + } else if (tag === `${kCoreTagPrefix}bool`) { + value = resolvePlain(text); + if (typeof value !== 'boolean') fail(s, `"${text}" is not a boolean`, pos); + } else if (tag === `${kCoreTagPrefix}null`) { + value = resolvePlain(text); + if (value !== null) fail(s, `"${text}" is not null`, pos); + } else if (tag === `${kCoreTagPrefix}map` || tag === `${kCoreTagPrefix}seq`) { + fail(s, `Tag ${tag} cannot be applied to a scalar`, pos); + } else { + // A tag outside the core schema: the scalar stays a string. + value = text; + } + if (props !== null && props.anchor !== null) s.anchors.set(props.anchor, value); + return value; +} + +const kScalarTags = ['str', 'int', 'float', 'bool', 'null']; + +function checkCollectionTag(s, tag, kind, pos) { + if (tag === null || !isCoreTag(tag)) return; + const name = StringPrototypeSlice(tag, kCoreTagPrefix.length); + const other = kind === kSequence ? 'map' : 'seq'; + if (name === other || ArrayPrototypeIncludes(kScalarTags, name)) { + fail(s, `Tag ${tag} cannot be applied to a ${kind === kSequence ? 'sequence' : 'mapping'}`, pos); + } +} + +// Applies properties that were read on a line before a flow collection. +function applyCollectionProps(s, value, props, kind, pos) { + if (props === null) return value; + checkCollectionTag(s, props.tag, kind, pos); + if (props.anchor !== null) s.anchors.set(props.anchor, value); + return value; +} + +function readAlias(s) { + const start = s.pos; + s.pos++; // * + const name = readAnchorName(s); + if (!s.anchors.has(name)) fail(s, `Unknown anchor "${name}"`, start); + return s.anchors.get(name); +} + +// --- Flow collections --------------------------------------------------------- + +// Skips whitespace, breaks and comments inside a flow collection. The lines +// of the collection must be indented more than the enclosing block node. +function skipFlowSeparation(s, parentIndent) { + const breaks = skipSeparation(s); + if (at(s, s.pos) === kEOF) fail(s, 'Unterminated flow collection'); + if (atDocumentMarker(s)) fail(s, 'Unexpected document marker inside a flow collection'); + if (breaks > 0 && s.lineIndent <= parentIndent) fail(s, 'Bad indentation in a flow collection'); +} + +function readFlowSequence(s, parentIndent, props, lineAnchor = null) { + enter(s); + const start = s.pos; + s.pos++; // [ + const result = []; + if (lineAnchor !== null) s.anchors.set(lineAnchor, result); + if (props !== null && props.anchor !== null) s.anchors.set(props.anchor, result); + for (;;) { + skipFlowSeparation(s, parentIndent); + if (at(s, s.pos) === kRightBracket) { + s.pos++; + break; + } + const entryPos = s.pos; + let explicit = false; + if (at(s, s.pos) === kQuestion && isBlank(at(s, s.pos + 1))) { + explicit = true; + s.pos++; + skipFlowSeparation(s, parentIndent); + } + const entry = readFlowPair(s, parentIndent, explicit); + if (entry === kNoNode) fail(s, 'Unexpected character in flow sequence', entryPos); + ArrayPrototypePush(result, entry); + skipFlowSeparation(s, parentIndent); + const c = at(s, s.pos); + if (c === kComma) { + s.pos++; + continue; + } + if (c === kRightBracket) { + s.pos++; + break; + } + fail(s, 'Expected "," or "]" in flow sequence'); + } + if (props !== null) checkCollectionTag(s, props.tag, kSequence, start); + s.depth--; + s.multiline = false; + return result; +} + +// Reads one flow sequence entry: a node, or a single-pair mapping "key: value". +function readFlowPair(s, parentIndent, explicit) { + const keyPos = s.pos; + let key = readFlowNode(s, parentIndent); + const jsonLike = s.lastJsonLike; + const multiline = s.multiline; + skipWhite(s); + if (at(s, s.pos) === kColon) { + const next = at(s, s.pos + 1); + if (isBlank(next) || isFlowIndicator(next) || jsonLike) { + if (multiline && !jsonLike && !explicit) { + fail(s, 'Implicit mapping keys must be on a single line', keyPos); + } + if (key === kNoNode) key = null; + s.pos++; + skipFlowSeparation(s, parentIndent); + let value = kNoNode; + if (at(s, s.pos) !== kComma && at(s, s.pos) !== kRightBracket) { + value = readFlowNode(s, parentIndent); + } + if (value === kNoNode) value = null; + const pair = {}; + setProperty(s, pair, keyToString(s, key, keyPos), value, keyPos); + return pair; + } + } + if (explicit) { + const pair = {}; + setProperty(s, pair, keyToString(s, key === kNoNode ? null : key, keyPos), null, keyPos); + return pair; + } + return key; +} + +function readFlowMapping(s, parentIndent, props, lineAnchor = null) { + enter(s); + const start = s.pos; + s.pos++; // { + const result = {}; + if (lineAnchor !== null) s.anchors.set(lineAnchor, result); + if (props !== null && props.anchor !== null) s.anchors.set(props.anchor, result); + for (;;) { + skipFlowSeparation(s, parentIndent); + if (at(s, s.pos) === kRightBrace) { + s.pos++; + break; + } + let explicit = false; + if (at(s, s.pos) === kQuestion && isBlank(at(s, s.pos + 1))) { + explicit = true; + s.pos++; + skipFlowSeparation(s, parentIndent); + } + const keyPos = s.pos; + let key = kNoNode; + const c0 = at(s, s.pos); + const c1 = at(s, s.pos + 1); + if (!(c0 === kColon && (isBlank(c1) || isFlowIndicator(c1)))) { + key = readFlowNode(s, parentIndent); + } + const jsonLike = s.lastJsonLike; + if (key === kNoNode) { + if (!explicit && at(s, s.pos) !== kColon) { + fail(s, 'Unexpected character in flow mapping', keyPos); + } + key = null; + } + skipFlowSeparation(s, parentIndent); + let value = null; + if (at(s, s.pos) === kColon) { + const next = at(s, s.pos + 1); + if (!(isBlank(next) || isFlowIndicator(next) || jsonLike)) { + fail(s, 'Expected whitespace after ":" in flow mapping'); + } + s.pos++; + skipFlowSeparation(s, parentIndent); + const c = at(s, s.pos); + if (c !== kComma && c !== kRightBrace) { + value = readFlowNode(s, parentIndent); + if (value === kNoNode) value = null; + } + } + setProperty(s, result, keyToString(s, key, keyPos), value, keyPos); + skipFlowSeparation(s, parentIndent); + const c = at(s, s.pos); + if (c === kComma) { + s.pos++; + continue; + } + if (c === kRightBrace) { + s.pos++; + break; + } + fail(s, 'Expected "," or "}" in flow mapping'); + } + if (props !== null) checkCollectionTag(s, props.tag, kMapping, start); + s.depth--; + s.multiline = false; + return result; +} + +// Reads a node inside a flow collection. Returns kNoNode when the position +// holds no node (an empty entry or value). +function readFlowNode(s, parentIndent) { + s.lastJsonLike = false; + const props = readProperties(s, true); + if (props !== null) skipFlowSeparation(s, parentIndent); + const pos = s.pos; + const c = at(s, s.pos); + switch (c) { + case kAsterisk: + if (props !== null) fail(s, 'An alias cannot have properties'); + return readAlias(s); + case kLeftBracket: { + const value = readFlowSequence(s, parentIndent, props); + s.lastJsonLike = true; + return value; + } + case kLeftBrace: { + const value = readFlowMapping(s, parentIndent, props); + s.lastJsonLike = true; + return value; + } + case kSingleQuote: + s.lastJsonLike = true; + return finishScalar(s, readSingleQuoted(s, parentIndent), kQuoted, props, pos); + case kDoubleQuote: + s.lastJsonLike = true; + return finishScalar(s, readDoubleQuoted(s, parentIndent), kQuoted, props, pos); + case kPipe: case kGreater: + fail(s, 'Block scalars are not allowed in flow collections'); + } + if (canStartPlain(s, c, true)) { + return finishScalar(s, readPlain(s, parentIndent, true, false), kPlain, props, pos); + } + if (props !== null) { + // Properties with no content: an empty scalar. + return finishScalar(s, '', kPlain, props, pos); + } + return kNoNode; +} + +// --- Block collections ----------------------------------------------------- + +// Whether the current position starts a block sequence entry. +function atSequenceEntry(s) { + return at(s, s.pos) === kMinus && isBlank(at(s, s.pos + 1)); +} + +// Whether the current line is indented enough for a node of a collection at +// parentIndent. Only spaces count as indentation. +function indentedEnough(s, parentIndent, allowSameIndentSequence) { + if (s.lineIndent > parentIndent) return true; + return allowSameIndentSequence && s.lineIndent === parentIndent && atSequenceEntry(s); +} + +// Whether the whitespace before pos on its line holds a tab. +function tabBefore(s, pos) { + for (let p = pos - 1; p >= s.lineStart && isWhite(at(s, p)); p--) { + if (at(s, p) === kTab) return true; + } + return false; +} + +// Composes a node in block context. parentIndent is the indentation of the +// enclosing collection (-1 at the document root). The node may start on the +// current line or on a following line indented more than the parent. With +// allowSameIndentSequence, a block sequence may start at the parent's own +// indentation, as a mapping value. With compact, a block collection may start +// on the current line, after "-", "?" or an explicit ":". Returns kNoNode +// when there is no node. +function composeBlock(s, parentIndent, allowSameIndentSequence, compact) { + let breaks = skipSeparation(s); + if (at(s, s.pos) === kEOF || atDocumentMarker(s)) return kNoNode; + if (breaks > 0 && !indentedEnough(s, parentIndent, allowSameIndentSequence)) { + return kNoNode; + } + // Properties followed by a line break belong to the node on the next line, + // or to the collection whose first key follows them. Properties on the + // node's own line belong to that node, or to the first key. + let lineProps = null; + let propsStart = s.pos; + let props = readProperties(s, false); + while (props !== null) { + const more = skipSeparation(s); + if (more === 0 && at(s, s.pos) !== kEOF) break; + breaks += more; + lineProps = mergeProperties(s, lineProps, props, propsStart); + props = null; + if (at(s, s.pos) === kEOF || atDocumentMarker(s) || + !indentedEnough(s, parentIndent, allowSameIndentSequence)) { + // Properties with no content: an empty scalar node. + return finishScalar(s, '', kPlain, lineProps, propsStart); + } + propsStart = s.pos; + props = readProperties(s, false); + } + const pos = s.pos; + const startLine = s.line; + const nodeStart = props === null ? pos : propsStart; + const indent = nodeStart - s.lineStart; + // A block collection cannot start on the line of the parent indicator + // unless the compact form is allowed, and never after a tab. + const collectionAllowed = breaks > 0 || compact; + const collectionAfterTab = tabBefore(s, nodeStart); + const c = at(s, s.pos); + let value; + switch (c) { + case kAsterisk: { + if (props !== null) fail(s, 'An alias cannot have properties', propsStart); + value = readAlias(s); + let p = s.pos; + while (isWhite(at(s, p))) p++; + if (at(s, p) === kColon && isBlank(at(s, p + 1))) { + if (collectionAfterTab) fail(s, 'Tabs cannot be used for indentation', pos); + if (!collectionAllowed) fail(s, 'A block mapping is not allowed here', pos); + s.pos = p; + return readBlockMapping(s, indent, lineProps, value, pos); + } + if (lineProps !== null) fail(s, 'An alias cannot have properties', pos); + return value; + } + case kLeftBracket: + value = readFlowSequence(s, parentIndent, props, lineProps === null ? null : lineProps.anchor); + break; + case kLeftBrace: + value = readFlowMapping(s, parentIndent, props, lineProps === null ? null : lineProps.anchor); + break; + case kSingleQuote: + value = readSingleQuoted(s, parentIndent); + break; + case kDoubleQuote: + value = readDoubleQuoted(s, parentIndent); + break; + case kPipe: case kGreater: + return finishScalar(s, readBlockScalar(s, parentIndent), + kQuoted, mergeProperties(s, lineProps, props, pos), pos); + case kMinus: + if (isBlank(at(s, s.pos + 1))) { + if (collectionAfterTab) fail(s, 'Tabs cannot be used for indentation', pos); + if (!collectionAllowed || props !== null) { + fail(s, 'A block sequence is not allowed here', pos); + } + return readBlockSequence(s, indent, lineProps); + } + break; + case kQuestion: + if (isBlank(at(s, s.pos + 1))) { + if (collectionAfterTab) fail(s, 'Tabs cannot be used for indentation', pos); + if (!collectionAllowed || props !== null) { + fail(s, 'A block mapping is not allowed here', pos); + } + return readBlockMapping(s, indent, lineProps, kNoNode, pos); + } + break; + case kColon: + if (isBlank(at(s, s.pos + 1))) { + // An empty key, which takes the properties on its line. + if (collectionAfterTab) fail(s, 'Tabs cannot be used for indentation', pos); + if (!collectionAllowed) fail(s, 'A block mapping is not allowed here', pos); + const key = finishScalar(s, '', kPlain, props, pos); + return readBlockMapping(s, indent, lineProps, key, pos); + } + break; + } + if (value === undefined) { + if (!canStartPlain(s, c, false)) { + fail(s, `Unexpected character "${StringFromCharCode(c)}"`); + } + value = readPlain(s, parentIndent, false, false); + } + // A scalar or flow collection on one line followed by ":" is the first + // key of a block mapping. + let p = s.pos; + while (isWhite(at(s, p))) p++; + if (at(s, p) === kColon && isBlank(at(s, p + 1))) { + if (s.line !== startLine) fail(s, 'Implicit mapping keys must be on a single line', pos); + if (collectionAfterTab) fail(s, 'Tabs cannot be used for indentation', pos); + if (!collectionAllowed) fail(s, 'A block mapping is not allowed here', pos); + s.pos = p; + const key = finishNode(s, value, c, props, pos); + return readBlockMapping(s, indent, lineProps, key, pos); + } + if (c === kLeftBracket || c === kLeftBrace) { + const kind = c === kLeftBracket ? kSequence : kMapping; + return applyCollectionProps(s, value, lineProps, kind, pos); + } + return finishNode(s, value, c, mergeProperties(s, lineProps, props, pos), pos); +} + +// Resolves a node that was read as raw scalar text or as a flow collection. +function finishNode(s, value, c, props, pos) { + if (c === kLeftBracket || c === kLeftBrace) return value; + const kind = c === kSingleQuote || c === kDoubleQuote ? kQuoted : kPlain; + return finishScalar(s, value, kind, props, pos); +} + +// Reads a block sequence whose entries are at the given indentation. +function readBlockSequence(s, indent, props) { + enter(s); + const start = s.pos; + const result = []; + if (props !== null && props.anchor !== null) s.anchors.set(props.anchor, result); + for (;;) { + // At "-". + s.pos++; + const entry = composeBlock(s, indent, false, true); + ArrayPrototypePush(result, entry === kNoNode ? null : entry); + if (!nextLineAt(s, indent) || !atSequenceEntry(s)) break; + } + if (props !== null) checkCollectionTag(s, props.tag, kSequence, start); + s.depth--; + s.multiline = true; + return result; +} + +// After a collection entry, moves to the next line at the collection's +// indentation. Returns false when the collection ends there. Lines indented +// more than the collection are an error. +function nextLineAt(s, indent) { + const pos = s.pos; + const breaks = skipSeparation(s); + if (at(s, s.pos) === kEOF || atDocumentMarker(s)) return false; + const atLineContent = s.pos === s.lineStart + s.lineIndent; + if (!atLineContent) { + if (tabBefore(s, s.pos)) fail(s, 'Tabs cannot be used for indentation'); + if (breaks === 0) fail(s, 'Unexpected content after a collection entry', pos); + } + if (s.lineIndent < indent) return false; + if (s.lineIndent > indent) fail(s, 'Bad indentation'); + return true; +} + +// Reads a block mapping at the given indentation. firstKey is the already +// read first key, with the position at its ":", or kNoNode when the mapping +// starts with "?". +function readBlockMapping(s, indent, props, firstKey, start) { + enter(s); + const result = {}; + if (props !== null && props.anchor !== null) s.anchors.set(props.anchor, result); + let key = firstKey; + let keyPos = start; + let first = true; + for (;;) { + if (!first || key === kNoNode) { + keyPos = s.pos; + if (at(s, s.pos) === kQuestion && isBlank(at(s, s.pos + 1))) { + // Explicit key, possibly a collection on the following lines. Its + // value, if any, starts with ":" at the mapping indentation. + s.pos++; + key = composeBlock(s, indent, true, true); + if (key === kNoNode) key = null; + if (!nextLineAt(s, indent)) { + setProperty(s, result, keyToString(s, key, keyPos), null, keyPos); + break; + } + if (!(at(s, s.pos) === kColon && isBlank(at(s, s.pos + 1)))) { + setProperty(s, result, keyToString(s, key, keyPos), null, keyPos); + first = false; + continue; + } + s.pos++; + let value = composeBlock(s, indent, true, true); + if (value === kNoNode) value = null; + setProperty(s, result, keyToString(s, key, keyPos), value, keyPos); + if (!nextLineAt(s, indent)) break; + first = false; + continue; + } + key = readImplicitKey(s, indent); + } + first = false; + // At ":". + s.pos++; + let value = composeBlock(s, indent, true, false); + if (value === kNoNode) value = null; + setProperty(s, result, keyToString(s, key, keyPos), value, keyPos); + if (!nextLineAt(s, indent)) break; + if (atSequenceEntry(s)) fail(s, 'Unexpected sequence entry in a mapping'); + } + if (props !== null) checkCollectionTag(s, props.tag, kMapping, start); + s.depth--; + s.multiline = true; + return result; +} + +// Reads an implicit key: a single-line node followed by ":", and leaves the +// position at the ":". +function readImplicitKey(s, indent) { + const propsStart = s.pos; + const props = readProperties(s, false); + const pos = s.pos; + const c = at(s, s.pos); + let value; + switch (c) { + case kAsterisk: + if (props !== null) fail(s, 'An alias cannot have properties', propsStart); + value = readAlias(s); + skipWhite(s); + if (at(s, s.pos) !== kColon || !isBlank(at(s, s.pos + 1))) { + fail(s, 'Expected ":" after mapping key', pos); + } + return value; + case kColon: + // An empty key. + if (isBlank(at(s, s.pos + 1))) return finishScalar(s, '', kPlain, props, pos); + break; + case kLeftBracket: + value = readFlowSequence(s, indent, props); + break; + case kLeftBrace: + value = readFlowMapping(s, indent, props); + break; + case kSingleQuote: + value = readSingleQuoted(s, indent); + break; + case kDoubleQuote: + value = readDoubleQuoted(s, indent); + break; + } + if (value === undefined) { + if (!canStartPlain(s, c, false)) fail(s, 'Expected a mapping key', pos); + value = readPlain(s, indent, false, true); + } + if (s.multiline) fail(s, 'Implicit mapping keys must be on a single line', pos); + skipWhite(s); + if (at(s, s.pos) !== kColon || !isBlank(at(s, s.pos + 1))) { + fail(s, 'Expected ":" after mapping key', pos); + } + return finishNode(s, value, c, props, pos); +} + +// --- Documents ------------------------------------------------------------- + +function readDirective(s) { + const start = s.pos; + s.pos++; // % + const nameStart = s.pos; + while (!isBlank(at(s, s.pos))) s.pos++; + const name = StringPrototypeSlice(s.input, nameStart, s.pos); + const args = []; + for (;;) { + skipWhite(s); + const c = at(s, s.pos); + if (c === kEOF || isBreak(c) || c === kHash) break; + const argStart = s.pos; + while (!isBlank(at(s, s.pos))) s.pos++; + ArrayPrototypePush(args, StringPrototypeSlice(s.input, argStart, s.pos)); + } + if (at(s, s.pos) === kHash) skipToEndOfLine(s); + if (name === 'YAML') { + if (s.version !== null) fail(s, 'Repeated %YAML directive', start); + if (args.length !== 1) fail(s, 'The %YAML directive takes one argument', start); + const version = args[0]; + let valid = version.length >= 3 && + isDigit(StringPrototypeCharCodeAt(version, 0)) && + StringPrototypeCharCodeAt(version, 1) === kDot; + for (let i = 2; valid && i < version.length; i++) { + valid = isDigit(StringPrototypeCharCodeAt(version, i)); + } + if (!valid) fail(s, `Invalid YAML version "${version}"`, start); + if (StringPrototypeCharCodeAt(version, 0) !== 0x31) { + fail(s, `Unsupported YAML version "${version}"`, start); + } + s.version = version; + } else if (name === 'TAG') { + if (args.length !== 2) fail(s, 'The %TAG directive takes two arguments', start); + const { 0: handle, 1: prefix } = args; + if (StringPrototypeCharCodeAt(handle, 0) !== kBang || + (handle.length > 1 && StringPrototypeCharCodeAt(handle, handle.length - 1) !== kBang)) { + fail(s, `Invalid tag handle "${handle}"`, start); + } + if (s.tagHandles.has(handle)) fail(s, `Repeated tag handle "${handle}"`, start); + s.tagHandles.set(handle, decodeTagEscapes(s, prefix)); + } + // Other directives are reserved and ignored. +} + +// Reads one document. Returns kNoNode at the end of the stream. +function readDocument(s) { + s.anchors.clear(); + s.tagHandles.clear(); + s.version = null; + let hasDirectives = false; + for (;;) { + skipSeparation(s); + if (at(s, s.pos) === kPercent && s.pos === s.lineStart) { + readDirective(s); + hasDirectives = true; + continue; + } + break; + } + let explicitStart = false; + if (at(s, s.pos) === kEOF) { + if (hasDirectives) fail(s, 'Directives must be followed by "---"'); + return kNoNode; + } + if (atDocumentMarker(s)) { + if (at(s, s.pos) === kDot) { + if (hasDirectives) fail(s, 'Directives must be followed by "---"'); + skipDocumentEnd(s); + return kNoNode; + } + s.pos += 3; + explicitStart = true; + } else if (hasDirectives) { + fail(s, 'Directives must be followed by "---"'); + } + let root = composeBlock(s, -1, true, !explicitStart); + if (root === kNoNode) root = null; + // After the root node: whitespace, comments, then a document marker or EOF. + skipSeparation(s); + const c = at(s, s.pos); + if (c !== kEOF && !atDocumentMarker(s)) { + fail(s, 'Unexpected content after the document root'); + } + s.explicitEnd = false; + if (c === kDot) { + skipDocumentEnd(s); + s.explicitEnd = true; + } + return root; +} + +// Consumes "..." markers. Only a comment may follow one on its line. +function skipDocumentEnd(s) { + for (;;) { + s.pos += 3; + skipWhite(s); + const c = at(s, s.pos); + if (c === kHash) skipToEndOfLine(s); + else if (c !== kEOF && !isBreak(c)) fail(s, 'Unexpected content after "..."'); + skipSeparation(s); + if (!(atDocumentMarker(s) && at(s, s.pos) === kDot)) break; + } +} + +function skipBOM(s) { + if (at(s, s.pos) === kBOM) { + s.pos++; + s.lineStart = s.pos; + while (at(s, s.pos) === kSpace) { + s.pos++; + s.lineIndent++; + } + } +} + +// YAML allows tab, line feed, carriage return, the printable characters of +// the BMP except the C1 controls other than NEL, and the supplementary planes, +// which are surrogate pairs here. +function checkPrintable(s) { + for (let i = 0; i < s.length; i++) { + const c = StringPrototypeCharCodeAt(s.input, i); + if (c >= kSpace && c < 0x7F) continue; + if (c === kTab || c === kLF || c === kCR || c === 0x85) continue; + if ((c >= 0xA0 && c < 0xD800) || (c >= 0xE000 && c < 0xFFFE)) continue; + if (c >= 0xD800 && c < 0xDC00) { + const next = at(s, i + 1); + if (next >= 0xDC00 && next < 0xE000) { + i++; + continue; + } + } + fail(s, `Character U+${NumberPrototypeToString(c, 16)} is not allowed in YAML`, i); + } +} + +// Parses a YAML stream and returns its documents as an array. +function parseDocuments(text) { + const s = new State(text); + checkPrintable(s); + const documents = []; + while (at(s, s.pos) === kSpace) { + s.pos++; + s.lineIndent++; + } + skipBOM(s); + for (;;) { + const document = readDocument(s); + if (document === kNoNode) { + if (at(s, s.pos) === kEOF) break; + // A "..." before any document. + continue; + } + ArrayPrototypePush(documents, document); + if (at(s, s.pos) === kEOF) break; + // A byte order mark may also precede each document. + if (s.explicitEnd) skipBOM(s); + } + return documents; +} + +module.exports = { + parseDocuments, +}; diff --git a/lib/yaml.js b/lib/yaml.js new file mode 100644 index 000000000000..316de289b920 --- /dev/null +++ b/lib/yaml.js @@ -0,0 +1,47 @@ +'use strict'; + +const { + codes: { + ERR_INVALID_ARG_TYPE, + }, +} = require('internal/errors'); +const { emitExperimentalWarning } = require('internal/util'); +const { + isAnyArrayBuffer, + isArrayBufferView, +} = require('internal/util/types'); +const { parseDocuments } = require('internal/yaml/parser'); + +emitExperimentalWarning('YAML'); + +let TextDecoder; +function decode(input) { + TextDecoder ??= require('internal/encoding').TextDecoder; + return new TextDecoder('utf-8').decode(input); +} + +/** + * Parses a YAML stream. A stream with one document yields its value, a + * stream with several documents yields an array of their values. + * @param {string | ArrayBuffer | SharedArrayBuffer | ArrayBufferView} input + * @returns {unknown} + */ +function parse(input) { + let text; + if (typeof input === 'string') { + text = input; + } else if (isArrayBufferView(input) || isAnyArrayBuffer(input)) { + text = decode(input); + } else { + throw new ERR_INVALID_ARG_TYPE( + 'input', ['string', 'ArrayBuffer', 'SharedArrayBuffer', 'ArrayBufferView'], input); + } + const documents = parseDocuments(text); + if (documents.length === 0) return null; + if (documents.length === 1) return documents[0]; + return documents; +} + +module.exports = { + parse, +}; diff --git a/src/node_builtins.cc b/src/node_builtins.cc index 21af988c95d8..97a716a64bda 100644 --- a/src/node_builtins.cc +++ b/src/node_builtins.cc @@ -168,6 +168,7 @@ BuiltinLoader::BuiltinCategories BuiltinLoader::GetBuiltinCategories() const { "sys", // Deprecated. "vfs", // Experimental. "wasi", // Experimental. + "yaml", // Experimental. "zlib/iter", // Experimental. #if !HAVE_SQLITE "internal/webstorage", // Experimental. diff --git a/src/node_options.cc b/src/node_options.cc index 4ba4801ad1dc..e2e7584cab5c 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -814,6 +814,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { BOOL_FIELD(experimental_vfs), kAllowedInEnvvar, true); + AddOption("--experimental-yaml", + "experimental node:yaml module", + BOOL_FIELD(experimental_yaml), + kAllowedInEnvvar); // Choosing the entry point is the command line's alone: an environment // variable must not be able to redirect what a `node ` invocation runs, // so this is rejected in NODE_OPTIONS. The source and whether to run from it diff --git a/src/node_options.h b/src/node_options.h index f8aa58253862..b04d6a097ce6 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -216,6 +216,7 @@ class EnvironmentOptions : public Options { DEFINE_BOOL_FIELD(experimental_sqlite) = HAVE_SQLITE; DEFINE_BOOL_FIELD(experimental_stream_iter) = EXPERIMENTALS_DEFAULT_VALUE; DEFINE_BOOL_FIELD(experimental_vfs) = true; + DEFINE_BOOL_FIELD(experimental_yaml) = EXPERIMENTALS_DEFAULT_VALUE; DEFINE_BOOL_FIELD(vfs_load) = false; DEFINE_BOOL_FIELD(sea_vfs_archive) = false; DEFINE_BOOL_FIELD(webstorage) = HAVE_SQLITE; diff --git a/test/fixtures/yaml-test-suite.json b/test/fixtures/yaml-test-suite.json new file mode 100644 index 000000000000..564d5515e20b --- /dev/null +++ b/test/fixtures/yaml-test-suite.json @@ -0,0 +1,4330 @@ +[ + { + "id": "229Q", + "name": "Spec Example 2.4. Sequence of Mappings", + "yaml": "-\n name: Mark McGwire\n hr: 65\n avg: 0.278\n-\n name: Sammy Sosa\n hr: 63\n avg: 0.288\n", + "error": false, + "json": [ + [ + { + "name": "Mark McGwire", + "hr": 65, + "avg": 0.278 + }, + { + "name": "Sammy Sosa", + "hr": 63, + "avg": 0.288 + } + ] + ] + }, + { + "id": "236B", + "name": "Invalid value after mapping", + "yaml": "foo:\n bar\ninvalid\n", + "error": true + }, + { + "id": "26DV", + "name": "Whitespace around colon in mappings", + "yaml": "\"top1\" : \n \"key1\" : &alias1 scalar1\n'top2' : \n 'key2' : &alias2 scalar2\ntop3: &node3 \n *alias1 : scalar3\ntop4: \n *alias2 : scalar4\ntop5 : \n scalar5\ntop6: \n &anchor6 'key6' : scalar6\n", + "error": false, + "json": [ + { + "top1": { + "key1": "scalar1" + }, + "top2": { + "key2": "scalar2" + }, + "top3": { + "scalar1": "scalar3" + }, + "top4": { + "scalar2": "scalar4" + }, + "top5": "scalar5", + "top6": { + "key6": "scalar6" + } + } + ] + }, + { + "id": "27NA", + "name": "Spec Example 5.9. Directive Indicator", + "yaml": "%YAML 1.2\n--- text\n", + "error": false, + "json": [ + "text" + ] + }, + { + "id": "2AUY", + "name": "Tags in Block Sequence", + "yaml": " - !!str a\n - b\n - !!int 42\n - d\n", + "error": false, + "json": [ + [ + "a", + "b", + 42, + "d" + ] + ] + }, + { + "id": "2CMS", + "name": "Invalid mapping in plain multiline", + "yaml": "this\n is\n invalid: x\n", + "error": true + }, + { + "id": "2EBW", + "name": "Allowed characters in keys", + "yaml": "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~: safe\n?foo: safe question mark\n:foo: safe colon\n-foo: safe dash\nthis is#not: a comment\n", + "error": false, + "json": [ + { + "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~": "safe", + "?foo": "safe question mark", + ":foo": "safe colon", + "-foo": "safe dash", + "this is#not": "a comment" + } + ] + }, + { + "id": "2G84/00", + "name": "Literal modifers", + "yaml": "--- |0\n", + "error": true + }, + { + "id": "2G84/01", + "name": "Literal modifers", + "yaml": "--- |10\n", + "error": true + }, + { + "id": "2G84/02", + "name": "Literal modifers", + "yaml": "--- |1-", + "error": false, + "json": [ + "" + ] + }, + { + "id": "2G84/03", + "name": "Literal modifers", + "yaml": "--- |1+", + "error": false, + "json": [ + "" + ] + }, + { + "id": "2JQS", + "name": "Block Mapping with Missing Keys", + "yaml": ": a\n: b\n", + "error": false + }, + { + "id": "2LFX", + "name": "Spec Example 6.13. Reserved Directives [1.3]", + "yaml": "%FOO bar baz # Should be ignored\n # with a warning.\n---\n\"foo\"\n", + "error": false, + "json": [ + "foo" + ] + }, + { + "id": "2SXE", + "name": "Anchors With Colon in Name", + "yaml": "&a: key: &a value\nfoo:\n *a:\n", + "error": false, + "json": [ + { + "key": "value", + "foo": "key" + } + ] + }, + { + "id": "2XXW", + "name": "Spec Example 2.25. Unordered Sets", + "yaml": "# Sets are represented as a\n# Mapping where each key is\n# associated with a null value\n--- !!set\n? Mark McGwire\n? Sammy Sosa\n? Ken Griff\n", + "error": false, + "json": [ + { + "Mark McGwire": null, + "Sammy Sosa": null, + "Ken Griff": null + } + ] + }, + { + "id": "33X3", + "name": "Three explicit integers in a block sequence", + "yaml": "---\n- !!int 1\n- !!int -2\n- !!int 33\n", + "error": false, + "json": [ + [ + 1, + -2, + 33 + ] + ] + }, + { + "id": "35KP", + "name": "Tags for Root Objects", + "yaml": "--- !!map\n? a\n: b\n--- !!seq\n- !!str c\n--- !!str\nd\ne\n", + "error": false, + "json": [ + { + "a": "b" + }, + [ + "c" + ], + "d e" + ] + }, + { + "id": "36F6", + "name": "Multiline plain scalar with empty line", + "yaml": "---\nplain: a\n b\n\n c\n", + "error": false, + "json": [ + { + "plain": "a b\nc" + } + ] + }, + { + "id": "3ALJ", + "name": "Block Sequence in Block Sequence", + "yaml": "- - s1_i1\n - s1_i2\n- s2\n", + "error": false, + "json": [ + [ + [ + "s1_i1", + "s1_i2" + ], + "s2" + ] + ] + }, + { + "id": "3GZX", + "name": "Spec Example 7.1. Alias Nodes", + "yaml": "First occurrence: &anchor Foo\nSecond occurrence: *anchor\nOverride anchor: &anchor Bar\nReuse anchor: *anchor\n", + "error": false, + "json": [ + { + "First occurrence": "Foo", + "Second occurrence": "Foo", + "Override anchor": "Bar", + "Reuse anchor": "Bar" + } + ] + }, + { + "id": "3HFZ", + "name": "Invalid content after document end marker", + "yaml": "---\nkey: value\n... invalid\n", + "error": true + }, + { + "id": "3MYT", + "name": "Plain Scalar looking like key, comment, anchor and tag", + "yaml": "---\nk:#foo\n &a !t s\n", + "error": false, + "json": [ + "k:#foo &a !t s" + ] + }, + { + "id": "3R3P", + "name": "Single block sequence with anchor", + "yaml": "&sequence\n- a\n", + "error": false, + "json": [ + [ + "a" + ] + ] + }, + { + "id": "3RLN/00", + "name": "Leading tabs in double quoted", + "yaml": "\"1 leading\n \\ttab\"\n", + "error": false, + "json": [ + "1 leading \ttab" + ] + }, + { + "id": "3RLN/01", + "name": "Leading tabs in double quoted", + "yaml": "\"2 leading\n \\\ttab\"\n", + "error": false, + "json": [ + "2 leading \ttab" + ] + }, + { + "id": "3RLN/02", + "name": "Leading tabs in double quoted", + "yaml": "\"3 leading\n \ttab\"\n", + "error": false, + "json": [ + "3 leading tab" + ] + }, + { + "id": "3RLN/03", + "name": "Leading tabs in double quoted", + "yaml": "\"4 leading\n \\t tab\"\n", + "error": false, + "json": [ + "4 leading \t tab" + ] + }, + { + "id": "3RLN/04", + "name": "Leading tabs in double quoted", + "yaml": "\"5 leading\n \\\t tab\"\n", + "error": false, + "json": [ + "5 leading \t tab" + ] + }, + { + "id": "3RLN/05", + "name": "Leading tabs in double quoted", + "yaml": "\"6 leading\n \t tab\"\n", + "error": false, + "json": [ + "6 leading tab" + ] + }, + { + "id": "3UYS", + "name": "Escaped slash in double quotes", + "yaml": "escaped slash: \"a\\/b\"\n", + "error": false, + "json": [ + { + "escaped slash": "a/b" + } + ] + }, + { + "id": "4ABK", + "name": "Flow Mapping Separate Values", + "yaml": "{\nunquoted : \"separate\",\nhttp://foo.com,\nomitted value:,\n}\n", + "error": false + }, + { + "id": "4CQQ", + "name": "Spec Example 2.18. Multi-line Flow Scalars", + "yaml": "plain:\n This unquoted scalar\n spans many lines.\n\nquoted: \"So does this\n quoted scalar.\\n\"\n", + "error": false, + "json": [ + { + "plain": "This unquoted scalar spans many lines.", + "quoted": "So does this quoted scalar.\n" + } + ] + }, + { + "id": "4EJS", + "name": "Invalid tabs as indendation in a mapping", + "yaml": "---\na:\n\tb:\n\t\tc: value\n", + "error": true + }, + { + "id": "4FJ6", + "name": "Nested implicit complex keys", + "yaml": "---\n[\n [ a, [ [[b,c]]: d, e]]: 23\n]\n", + "error": false + }, + { + "id": "4GC6", + "name": "Spec Example 7.7. Single Quoted Characters", + "yaml": "'here''s to \"quotes\"'\n", + "error": false, + "json": [ + "here's to \"quotes\"" + ] + }, + { + "id": "4H7K", + "name": "Flow sequence with invalid extra closing bracket", + "yaml": "---\n[ a, b, c ] ]\n", + "error": true + }, + { + "id": "4HVU", + "name": "Wrong indendation in Sequence", + "yaml": "key:\n - ok\n - also ok\n - wrong\n", + "error": true + }, + { + "id": "4JVG", + "name": "Scalar value with two anchors", + "yaml": "top1: &node1\n &k1 key1: val1\ntop2: &node2\n &v2 val2\n", + "error": true + }, + { + "id": "4MUZ/00", + "name": "Flow mapping colon on line after key", + "yaml": "{\"foo\"\n: \"bar\"}\n", + "error": false, + "json": [ + { + "foo": "bar" + } + ] + }, + { + "id": "4MUZ/01", + "name": "Flow mapping colon on line after key", + "yaml": "{\"foo\"\n: bar}\n", + "error": false, + "json": [ + { + "foo": "bar" + } + ] + }, + { + "id": "4MUZ/02", + "name": "Flow mapping colon on line after key", + "yaml": "{foo\n: bar}\n", + "error": false, + "json": [ + { + "foo": "bar" + } + ] + }, + { + "id": "4Q9F", + "name": "Folded Block Scalar [1.3]", + "yaml": "--- >\n ab\n cd\n \n ef\n\n\n gh\n", + "error": false, + "json": [ + "ab cd\nef\n\ngh\n" + ] + }, + { + "id": "4QFQ", + "name": "Spec Example 8.2. Block Indentation Indicator [1.3]", + "yaml": "- |\n detected\n- >\n \n \n # detected\n- |1\n explicit\n- >\n detected\n", + "error": false, + "json": [ + [ + "detected\n", + "\n\n# detected\n", + " explicit\n", + "detected\n" + ] + ] + }, + { + "id": "4RWC", + "name": "Trailing spaces after flow collection", + "yaml": " [1, 2, 3] \n ", + "error": false, + "json": [ + [ + 1, + 2, + 3 + ] + ] + }, + { + "id": "4UYU", + "name": "Colon in Double Quoted String", + "yaml": "\"foo: bar\\\": baz\"\n", + "error": false, + "json": [ + "foo: bar\": baz" + ] + }, + { + "id": "4V8U", + "name": "Plain scalar with backslashes", + "yaml": "---\nplain\\value\\with\\backslashes\n", + "error": false, + "json": [ + "plain\\value\\with\\backslashes" + ] + }, + { + "id": "4WA9", + "name": "Literal scalars", + "yaml": "- aaa: |2\n xxx\n bbb: |\n xxx\n", + "error": false, + "json": [ + [ + { + "aaa": "xxx\n", + "bbb": "xxx\n" + } + ] + ] + }, + { + "id": "4ZYM", + "name": "Spec Example 6.4. Line Prefixes", + "yaml": "plain: text\n lines\nquoted: \"text\n \tlines\"\nblock: |\n text\n \tlines\n", + "error": false, + "json": [ + { + "plain": "text lines", + "quoted": "text lines", + "block": "text\n \tlines\n" + } + ] + }, + { + "id": "52DL", + "name": "Explicit Non-Specific Tag [1.3]", + "yaml": "---\n! a\n", + "error": false, + "json": [ + "a" + ] + }, + { + "id": "54T7", + "name": "Flow Mapping", + "yaml": "{foo: you, bar: far}\n", + "error": false, + "json": [ + { + "foo": "you", + "bar": "far" + } + ] + }, + { + "id": "55WF", + "name": "Invalid escape in double quoted string", + "yaml": "---\n\"\\.\"\n", + "error": true + }, + { + "id": "565N", + "name": "Construct Binary", + "yaml": "canonical: !!binary \"\\\n R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\\\n OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\\\n +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\\\n AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=\"\ngeneric: !!binary |\n R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\n OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\n +f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\n AgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=\ndescription:\n The binary value above is a tiny arrow encoded as a gif image.\n", + "error": false, + "json": [ + { + "canonical": "R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5OTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLCAgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=", + "generic": "R0lGODlhDAAMAIQAAP//9/X17unp5WZmZgAAAOfn515eXvPz7Y6OjuDg4J+fn5\nOTk6enp56enmlpaWNjY6Ojo4SEhP/++f/++f/++f/++f/++f/++f/++f/++f/+\n+f/++f/++f/++f/++f/++SH+Dk1hZGUgd2l0aCBHSU1QACwAAAAADAAMAAAFLC\nAgjoEwnuNAFOhpEMTRiggcz4BNJHrv/zCFcLiwMWYNG84BwwEeECcgggoBADs=\n", + "description": "The binary value above is a tiny arrow encoded as a gif image." + } + ] + }, + { + "id": "57H4", + "name": "Spec Example 8.22. Block Collection Nodes", + "yaml": "sequence: !!seq\n- entry\n- !!seq\n - nested\nmapping: !!map\n foo: bar\n", + "error": false, + "json": [ + { + "sequence": [ + "entry", + [ + "nested" + ] + ], + "mapping": { + "foo": "bar" + } + } + ] + }, + { + "id": "58MP", + "name": "Flow mapping edge cases", + "yaml": "{x: :x}\n", + "error": false, + "json": [ + { + "x": ":x" + } + ] + }, + { + "id": "5BVJ", + "name": "Spec Example 5.7. Block Scalar Indicators", + "yaml": "literal: |\n some\n text\nfolded: >\n some\n text\n", + "error": false, + "json": [ + { + "literal": "some\ntext\n", + "folded": "some text\n" + } + ] + }, + { + "id": "5C5M", + "name": "Spec Example 7.15. Flow Mappings", + "yaml": "- { one : two , three: four , }\n- {five: six,seven : eight}\n", + "error": false, + "json": [ + [ + { + "one": "two", + "three": "four" + }, + { + "five": "six", + "seven": "eight" + } + ] + ] + }, + { + "id": "5GBF", + "name": "Spec Example 6.5. Empty Lines", + "yaml": "Folding:\n \"Empty line\n \t\n as a line feed\"\nChomping: |\n Clipped empty lines\n \n\n", + "error": false, + "json": [ + { + "Folding": "Empty line\nas a line feed", + "Chomping": "Clipped empty lines\n" + } + ] + }, + { + "id": "5KJE", + "name": "Spec Example 7.13. Flow Sequence", + "yaml": "- [ one, two, ]\n- [three ,four]\n", + "error": false, + "json": [ + [ + [ + "one", + "two" + ], + [ + "three", + "four" + ] + ] + ] + }, + { + "id": "5LLU", + "name": "Block scalar with wrong indented line after spaces only", + "yaml": "block scalar: >\n \n \n \n invalid\n", + "error": true + }, + { + "id": "5MUD", + "name": "Colon and adjacent value on next line", + "yaml": "---\n{ \"foo\"\n :bar }\n", + "error": false, + "json": [ + { + "foo": "bar" + } + ] + }, + { + "id": "5NYZ", + "name": "Spec Example 6.9. Separated Comment", + "yaml": "key: # Comment\n value\n", + "error": false, + "json": [ + { + "key": "value" + } + ] + }, + { + "id": "5T43", + "name": "Colon at the beginning of adjacent flow scalar", + "yaml": "- { \"key\":value }\n- { \"key\"::value }\n", + "error": false, + "json": [ + [ + { + "key": "value" + }, + { + "key": ":value" + } + ] + ] + }, + { + "id": "5TRB", + "name": "Invalid document-start marker in doublequoted tring", + "yaml": "---\n\"\n---\n\"\n", + "error": true + }, + { + "id": "5TYM", + "name": "Spec Example 6.21. Local Tag Prefix", + "yaml": "%TAG !m! !my-\n--- # Bulb here\n!m!light fluorescent\n...\n%TAG !m! !my-\n--- # Color here\n!m!light green\n", + "error": false, + "json": [ + "fluorescent", + "green" + ] + }, + { + "id": "5U3A", + "name": "Sequence on same Line as Mapping Key", + "yaml": "key: - a\n - b\n", + "error": true + }, + { + "id": "5WE3", + "name": "Spec Example 8.17. Explicit Block Mapping Entries", + "yaml": "? explicit key # Empty value\n? |\n block key\n: - one # Explicit compact\n - two # block value\n", + "error": false, + "json": [ + { + "explicit key": null, + "block key\n": [ + "one", + "two" + ] + } + ] + }, + { + "id": "62EZ", + "name": "Invalid block mapping key on same line as previous key", + "yaml": "---\nx: { y: z }in: valid\n", + "error": true + }, + { + "id": "652Z", + "name": "Question mark at start of flow key", + "yaml": "{ ?foo: bar,\nbar: 42\n}\n", + "error": false, + "json": [ + { + "?foo": "bar", + "bar": 42 + } + ] + }, + { + "id": "65WH", + "name": "Single Entry Block Sequence", + "yaml": "- foo\n", + "error": false, + "json": [ + [ + "foo" + ] + ] + }, + { + "id": "6BCT", + "name": "Spec Example 6.3. Separation Spaces", + "yaml": "- foo:\t bar\n- - baz\n -\tbaz\n", + "error": false, + "json": [ + [ + { + "foo": "bar" + }, + [ + "baz", + "baz" + ] + ] + ] + }, + { + "id": "6BFJ", + "name": "Mapping, key and flow sequence item anchors", + "yaml": "---\n&mapping\n&key [ &item a, b, c ]: value\n", + "error": false + }, + { + "id": "6CA3", + "name": "Tab indented top flow", + "yaml": "\t[\n\t]\n", + "error": false, + "json": [ + [] + ] + }, + { + "id": "6CK3", + "name": "Spec Example 6.26. Tag Shorthands", + "yaml": "%TAG !e! tag:example.com,2000:app/\n---\n- !local foo\n- !!str bar\n- !e!tag%21 baz\n", + "error": false, + "json": [ + [ + "foo", + "bar", + "baz" + ] + ] + }, + { + "id": "6FWR", + "name": "Block Scalar Keep", + "yaml": "--- |+\n ab\n \n \n...\n", + "error": false, + "json": [ + "ab\n\n \n" + ] + }, + { + "id": "6H3V", + "name": "Backslashes in singlequotes", + "yaml": "'foo: bar\\': baz'\n", + "error": false, + "json": [ + { + "foo: bar\\": "baz'" + } + ] + }, + { + "id": "6HB6", + "name": "Spec Example 6.1. Indentation Spaces", + "yaml": " # Leading comment line spaces are\n # neither content nor indentation.\n \nNot indented:\n By one space: |\n By four\n spaces\n Flow style: [ # Leading spaces\n By two, # in flow style\n Also by two, # are neither\n \tStill by two # content nor\n ] # indentation.\n", + "error": false, + "json": [ + { + "Not indented": { + "By one space": "By four\n spaces\n", + "Flow style": [ + "By two", + "Also by two", + "Still by two" + ] + } + } + ] + }, + { + "id": "6JQW", + "name": "Spec Example 2.13. In literals, newlines are preserved", + "yaml": "# ASCII Art\n--- |\n \\//||\\/||\n // || ||__\n", + "error": false, + "json": [ + "\\//||\\/||\n// || ||__\n" + ] + }, + { + "id": "6JTT", + "name": "Flow sequence without closing bracket", + "yaml": "---\n[ [ a, b, c ]\n", + "error": true + }, + { + "id": "6JWB", + "name": "Tags for Block Objects", + "yaml": "foo: !!seq\n - !!str a\n - !!map\n key: !!str value\n", + "error": false, + "json": [ + { + "foo": [ + "a", + { + "key": "value" + } + ] + } + ] + }, + { + "id": "6KGN", + "name": "Anchor for empty node", + "yaml": "---\na: &anchor\nb: *anchor\n", + "error": false, + "json": [ + { + "a": null, + "b": null + } + ] + }, + { + "id": "6LVF", + "name": "Spec Example 6.13. Reserved Directives", + "yaml": "%FOO bar baz # Should be ignored\n # with a warning.\n--- \"foo\"\n", + "error": false, + "json": [ + "foo" + ] + }, + { + "id": "6M2F", + "name": "Aliases in Explicit Block Mapping", + "yaml": "? &a a\n: &b b\n: *a\n", + "error": false + }, + { + "id": "6PBE", + "name": "Zero-indented sequences in explicit mapping keys", + "yaml": "---\n?\n- a\n- b\n:\n- c\n- d\n", + "error": false + }, + { + "id": "6S55", + "name": "Invalid scalar at the end of sequence", + "yaml": "key:\n - bar\n - baz\n invalid\n", + "error": true + }, + { + "id": "6SLA", + "name": "Allowed characters in quoted mapping key", + "yaml": "\"foo\\nbar:baz\\tx \\\\$%^&*()x\": 23\n'x\\ny:z\\tx $%^&*()x': 24\n", + "error": false, + "json": [ + { + "foo\nbar:baz\tx \\$%^&*()x": 23, + "x\\ny:z\\tx $%^&*()x": 24 + } + ] + }, + { + "id": "6VJK", + "name": "Spec Example 2.15. Folded newlines are preserved for \"more indented\" and blank lines", + "yaml": ">\n Sammy Sosa completed another\n fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\n What a year!\n", + "error": false, + "json": [ + "Sammy Sosa completed another fine season with great stats.\n\n 63 Home Runs\n 0.288 Batting Average\n\nWhat a year!\n" + ] + }, + { + "id": "6WLZ", + "name": "Spec Example 6.18. Primary Tag Handle [1.3]", + "yaml": "# Private\n---\n!foo \"bar\"\n...\n# Global\n%TAG ! tag:example.com,2000:app/\n---\n!foo \"bar\"\n", + "error": false, + "json": [ + "bar", + "bar" + ] + }, + { + "id": "6WPF", + "name": "Spec Example 6.8. Flow Folding [1.3]", + "yaml": "---\n\"\n foo \n \n bar\n\n baz\n\"\n", + "error": false, + "json": [ + " foo\nbar\nbaz " + ] + }, + { + "id": "6XDY", + "name": "Two document start markers", + "yaml": "---\n---\n", + "error": false, + "json": [ + null, + null + ] + }, + { + "id": "6ZKB", + "name": "Spec Example 9.6. Stream", + "yaml": "Document\n---\n# Empty\n...\n%YAML 1.2\n---\nmatches %: 20\n", + "error": false, + "json": [ + "Document", + null, + { + "matches %": 20 + } + ] + }, + { + "id": "735Y", + "name": "Spec Example 8.20. Block Node Types", + "yaml": "-\n \"flow in block\"\n- >\n Block scalar\n- !!map # Block collection\n foo : bar\n", + "error": false, + "json": [ + [ + "flow in block", + "Block scalar\n", + { + "foo": "bar" + } + ] + ] + }, + { + "id": "74H7", + "name": "Tags in Implicit Mapping", + "yaml": "!!str a: b\nc: !!int 42\ne: !!str f\ng: h\n!!str 23: !!bool false\n", + "error": false, + "json": [ + { + "a": "b", + "c": 42, + "e": "f", + "g": "h", + "23": false + } + ] + }, + { + "id": "753E", + "name": "Block Scalar Strip [1.3]", + "yaml": "--- |-\n ab\n \n \n...\n", + "error": false, + "json": [ + "ab" + ] + }, + { + "id": "7A4E", + "name": "Spec Example 7.6. Double Quoted Lines", + "yaml": "\" 1st non-empty\n\n 2nd non-empty \n\t3rd non-empty \"\n", + "error": false, + "json": [ + " 1st non-empty\n2nd non-empty 3rd non-empty " + ] + }, + { + "id": "7BMT", + "name": "Node and Mapping Key Anchors [1.3]", + "yaml": "---\ntop1: &node1\n &k1 key1: one\ntop2: &node2 # comment\n key2: two\ntop3:\n &k3 key3: three\ntop4: &node4\n &k4 key4: four\ntop5: &node5\n key5: five\ntop6: &val6\n six\ntop7:\n &val7 seven\n", + "error": false, + "json": [ + { + "top1": { + "key1": "one" + }, + "top2": { + "key2": "two" + }, + "top3": { + "key3": "three" + }, + "top4": { + "key4": "four" + }, + "top5": { + "key5": "five" + }, + "top6": "six", + "top7": "seven" + } + ] + }, + { + "id": "7BUB", + "name": "Spec Example 2.10. Node for \u201cSammy Sosa\u201d appears twice in this document", + "yaml": "---\nhr:\n - Mark McGwire\n # Following node labeled SS\n - &SS Sammy Sosa\nrbi:\n - *SS # Subsequent occurrence\n - Ken Griffey\n", + "error": false, + "json": [ + { + "hr": [ + "Mark McGwire", + "Sammy Sosa" + ], + "rbi": [ + "Sammy Sosa", + "Ken Griffey" + ] + } + ] + }, + { + "id": "7FWL", + "name": "Spec Example 6.24. Verbatim Tags", + "yaml": "! foo :\n ! baz\n", + "error": false, + "json": [ + { + "foo": "baz" + } + ] + }, + { + "id": "7LBH", + "name": "Multiline double quoted implicit keys", + "yaml": "\"a\\nb\": 1\n\"c\n d\": 1\n", + "error": true + }, + { + "id": "7MNF", + "name": "Missing colon", + "yaml": "top1:\n key1: val1\ntop2\n", + "error": true + }, + { + "id": "7T8X", + "name": "Spec Example 8.10. Folded Lines - 8.13. Final Empty Lines", + "yaml": ">\n\n folded\n line\n\n next\n line\n * bullet\n\n * list\n * lines\n\n last\n line\n\n# Comment\n", + "error": false, + "json": [ + "\nfolded line\nnext line\n * bullet\n\n * list\n * lines\n\nlast line\n" + ] + }, + { + "id": "7TMG", + "name": "Comment in flow sequence before comma", + "yaml": "---\n[ word1\n# comment\n, word2]\n", + "error": false, + "json": [ + [ + "word1", + "word2" + ] + ] + }, + { + "id": "7W2P", + "name": "Block Mapping with Missing Values", + "yaml": "? a\n? b\nc:\n", + "error": false, + "json": [ + { + "a": null, + "b": null, + "c": null + } + ] + }, + { + "id": "7Z25", + "name": "Bare document after document end marker", + "yaml": "---\nscalar1\n...\nkey: value\n", + "error": false, + "json": [ + "scalar1", + { + "key": "value" + } + ] + }, + { + "id": "7ZZ5", + "name": "Empty flow collections", + "yaml": "---\nnested sequences:\n- - - []\n- - - {}\nkey1: []\nkey2: {}\n", + "error": false, + "json": [ + { + "nested sequences": [ + [ + [ + [] + ] + ], + [ + [ + {} + ] + ] + ], + "key1": [], + "key2": {} + } + ] + }, + { + "id": "82AN", + "name": "Three dashes and content without space", + "yaml": "---word1\nword2\n", + "error": false, + "json": [ + "---word1 word2" + ] + }, + { + "id": "87E4", + "name": "Spec Example 7.8. Single Quoted Implicit Keys", + "yaml": "'implicit block key' : [\n 'implicit flow key' : value,\n ]\n", + "error": false, + "json": [ + { + "implicit block key": [ + { + "implicit flow key": "value" + } + ] + } + ] + }, + { + "id": "8CWC", + "name": "Plain mapping key ending with colon", + "yaml": "---\nkey ends with two colons::: value\n", + "error": false, + "json": [ + { + "key ends with two colons::": "value" + } + ] + }, + { + "id": "8G76", + "name": "Spec Example 6.10. Comment Lines", + "yaml": " # Comment\n \n\n\n", + "error": false, + "json": [] + }, + { + "id": "8KB6", + "name": "Multiline plain flow mapping key without value", + "yaml": "---\n- { single line, a: b}\n- { multi\n line, a: b}\n", + "error": false, + "json": [ + [ + { + "single line": null, + "a": "b" + }, + { + "multi line": null, + "a": "b" + } + ] + ] + }, + { + "id": "8MK2", + "name": "Explicit Non-Specific Tag", + "yaml": "! a\n", + "error": false, + "json": [ + "a" + ] + }, + { + "id": "8QBE", + "name": "Block Sequence in Block Mapping", + "yaml": "key:\n - item1\n - item2\n", + "error": false, + "json": [ + { + "key": [ + "item1", + "item2" + ] + } + ] + }, + { + "id": "8UDB", + "name": "Spec Example 7.14. Flow Sequence Entries", + "yaml": "[\n\"double\n quoted\", 'single\n quoted',\nplain\n text, [ nested ],\nsingle: pair,\n]\n", + "error": false, + "json": [ + [ + "double quoted", + "single quoted", + "plain text", + [ + "nested" + ], + { + "single": "pair" + } + ] + ] + }, + { + "id": "8XDJ", + "name": "Comment in plain multiline value", + "yaml": "key: word1\n# xxx\n word2\n", + "error": true + }, + { + "id": "8XYN", + "name": "Anchor with unicode character", + "yaml": "---\n- &\ud83d\ude01 unicode anchor\n", + "error": false, + "json": [ + [ + "unicode anchor" + ] + ] + }, + { + "id": "93JH", + "name": "Block Mappings in Block Sequence", + "yaml": " - key: value\n key2: value2\n -\n key3: value3\n", + "error": false, + "json": [ + [ + { + "key": "value", + "key2": "value2" + }, + { + "key3": "value3" + } + ] + ] + }, + { + "id": "93WF", + "name": "Spec Example 6.6. Line Folding [1.3]", + "yaml": "--- >-\n trimmed\n \n \n\n as\n space\n", + "error": false, + "json": [ + "trimmed\n\n\nas space" + ] + }, + { + "id": "96L6", + "name": "Spec Example 2.14. In the folded scalars, newlines become spaces", + "yaml": "--- >\n Mark McGwire's\n year was crippled\n by a knee injury.\n", + "error": false, + "json": [ + "Mark McGwire's year was crippled by a knee injury.\n" + ] + }, + { + "id": "96NN/00", + "name": "Leading tab content in literals", + "yaml": "foo: |-\n \tbar\n", + "error": false, + "json": [ + { + "foo": "\tbar" + } + ] + }, + { + "id": "96NN/01", + "name": "Leading tab content in literals", + "yaml": "foo: |-\n \tbar", + "error": false, + "json": [ + { + "foo": "\tbar" + } + ] + }, + { + "id": "98YD", + "name": "Spec Example 5.5. Comment Indicator", + "yaml": "# Comment only.\n", + "error": false, + "json": [] + }, + { + "id": "9BXH", + "name": "Multiline doublequoted flow mapping key without value", + "yaml": "---\n- { \"single line\", a: b}\n- { \"multi\n line\", a: b}\n", + "error": false, + "json": [ + [ + { + "single line": null, + "a": "b" + }, + { + "multi line": null, + "a": "b" + } + ] + ] + }, + { + "id": "9C9N", + "name": "Wrong indented flow sequence", + "yaml": "---\nflow: [a,\nb,\nc]\n", + "error": true + }, + { + "id": "9CWY", + "name": "Invalid scalar at the end of mapping", + "yaml": "key:\n - item1\n - item2\ninvalid\n", + "error": true + }, + { + "id": "9DXL", + "name": "Spec Example 9.6. Stream [1.3]", + "yaml": "Mapping: Document\n---\n# Empty\n...\n%YAML 1.2\n---\nmatches %: 20\n", + "error": false, + "json": [ + { + "Mapping": "Document" + }, + null, + { + "matches %": 20 + } + ] + }, + { + "id": "9FMG", + "name": "Multi-level Mapping Indent", + "yaml": "a:\n b:\n c: d\n e:\n f: g\nh: i\n", + "error": false, + "json": [ + { + "a": { + "b": { + "c": "d" + }, + "e": { + "f": "g" + } + }, + "h": "i" + } + ] + }, + { + "id": "9HCY", + "name": "Need document footer before directives", + "yaml": "!foo \"bar\"\n%TAG ! tag:example.com,2000:app/\n---\n!foo \"bar\"\n", + "error": true + }, + { + "id": "9J7A", + "name": "Simple Mapping Indent", + "yaml": "foo:\n bar: baz\n", + "error": false, + "json": [ + { + "foo": { + "bar": "baz" + } + } + ] + }, + { + "id": "9JBA", + "name": "Invalid comment after end of flow sequence", + "yaml": "---\n[ a, b, c, ]#invalid\n", + "error": true + }, + { + "id": "9KAX", + "name": "Various combinations of tags and anchors", + "yaml": "---\n&a1\n!!str\nscalar1\n---\n!!str\n&a2\nscalar2\n---\n&a3\n!!str scalar3\n---\n&a4 !!map\n&a5 !!str key5: value4\n---\na6: 1\n&anchor6 b6: 2\n---\n!!map\n&a8 !!str key8: value7\n---\n!!map\n!!str &a10 key10: value9\n---\n!!str &a11\nvalue11\n", + "error": false, + "json": [ + "scalar1", + "scalar2", + "scalar3", + { + "key5": "value4" + }, + { + "a6": 1, + "b6": 2 + }, + { + "key8": "value7" + }, + { + "key10": "value9" + }, + "value11" + ] + }, + { + "id": "9KBC", + "name": "Mapping starting at --- line", + "yaml": "--- key1: value1\n key2: value2\n", + "error": true + }, + { + "id": "9MAG", + "name": "Flow sequence with invalid comma at the beginning", + "yaml": "---\n[ , a, b, c ]\n", + "error": true + }, + { + "id": "9MMA", + "name": "Directive by itself with no document", + "yaml": "%YAML 1.2\n", + "error": true + }, + { + "id": "9MMW", + "name": "Single Pair Implicit Entries", + "yaml": "- [ YAML : separate ]\n- [ \"JSON like\":adjacent ]\n- [ {JSON: like}:adjacent ]\n", + "error": false + }, + { + "id": "9MQT/00", + "name": "Scalar doc with '...' in content", + "yaml": "--- \"a\n...x\nb\"\n", + "error": false, + "json": [ + "a ...x b" + ] + }, + { + "id": "9MQT/01", + "name": "Scalar doc with '...' in content", + "yaml": "--- \"a\n... x\nb\"\n", + "error": true, + "json": [ + "a ...x b" + ] + }, + { + "id": "9SA2", + "name": "Multiline double quoted flow mapping key", + "yaml": "---\n- { \"single line\": value}\n- { \"multi\n line\": value}\n", + "error": false, + "json": [ + [ + { + "single line": "value" + }, + { + "multi line": "value" + } + ] + ] + }, + { + "id": "9SHH", + "name": "Spec Example 5.8. Quoted Scalar Indicators", + "yaml": "single: 'text'\ndouble: \"text\"\n", + "error": false, + "json": [ + { + "single": "text", + "double": "text" + } + ] + }, + { + "id": "9TFX", + "name": "Spec Example 7.6. Double Quoted Lines [1.3]", + "yaml": "---\n\" 1st non-empty\n\n 2nd non-empty \n 3rd non-empty \"\n", + "error": false, + "json": [ + " 1st non-empty\n2nd non-empty 3rd non-empty " + ] + }, + { + "id": "9U5K", + "name": "Spec Example 2.12. Compact Nested Mapping", + "yaml": "---\n# Products purchased\n- item : Super Hoop\n quantity: 1\n- item : Basketball\n quantity: 4\n- item : Big Shoes\n quantity: 1\n", + "error": false, + "json": [ + [ + { + "item": "Super Hoop", + "quantity": 1 + }, + { + "item": "Basketball", + "quantity": 4 + }, + { + "item": "Big Shoes", + "quantity": 1 + } + ] + ] + }, + { + "id": "9WXW", + "name": "Spec Example 6.18. Primary Tag Handle", + "yaml": "# Private\n!foo \"bar\"\n...\n# Global\n%TAG ! tag:example.com,2000:app/\n---\n!foo \"bar\"\n", + "error": false, + "json": [ + "bar", + "bar" + ] + }, + { + "id": "9YRD", + "name": "Multiline Scalar at Top Level", + "yaml": "a\nb \n c\nd\n\ne\n", + "error": false, + "json": [ + "a b c d\ne" + ] + }, + { + "id": "A2M4", + "name": "Spec Example 6.2. Indentation Indicators", + "yaml": "? a\n: -\tb\n - -\tc\n - d\n", + "error": false, + "json": [ + { + "a": [ + "b", + [ + "c", + "d" + ] + ] + } + ] + }, + { + "id": "A6F9", + "name": "Spec Example 8.4. Chomping Final Line Break", + "yaml": "strip: |-\n text\nclip: |\n text\nkeep: |+\n text\n", + "error": false, + "json": [ + { + "strip": "text", + "clip": "text\n", + "keep": "text\n" + } + ] + }, + { + "id": "A984", + "name": "Multiline Scalar in Mapping", + "yaml": "a: b\n c\nd:\n e\n f\n", + "error": false, + "json": [ + { + "a": "b c", + "d": "e f" + } + ] + }, + { + "id": "AB8U", + "name": "Sequence entry that looks like two with wrong indentation", + "yaml": "- single multiline\n - sequence entry\n", + "error": false, + "json": [ + [ + "single multiline - sequence entry" + ] + ] + }, + { + "id": "AVM7", + "name": "Empty Stream", + "yaml": "", + "error": false, + "json": [] + }, + { + "id": "AZ63", + "name": "Sequence With Same Indentation as Parent Mapping", + "yaml": "one:\n- 2\n- 3\nfour: 5\n", + "error": false, + "json": [ + { + "one": [ + 2, + 3 + ], + "four": 5 + } + ] + }, + { + "id": "AZW3", + "name": "Lookahead test cases", + "yaml": "- bla\"keks: foo\n- bla]keks: foo\n", + "error": false, + "json": [ + [ + { + "bla\"keks": "foo" + }, + { + "bla]keks": "foo" + } + ] + ] + }, + { + "id": "B3HG", + "name": "Spec Example 8.9. Folded Scalar [1.3]", + "yaml": "--- >\n folded\n text\n\n\n", + "error": false, + "json": [ + "folded text\n" + ] + }, + { + "id": "B63P", + "name": "Directive without document", + "yaml": "%YAML 1.2\n...\n", + "error": true + }, + { + "id": "BD7L", + "name": "Invalid mapping after sequence", + "yaml": "- item1\n- item2\ninvalid: x\n", + "error": true + }, + { + "id": "BEC7", + "name": "Spec Example 6.14. \u201cYAML\u201d directive", + "yaml": "%YAML 1.3 # Attempt parsing\n # with a warning\n---\n\"foo\"\n", + "error": false, + "json": [ + "foo" + ] + }, + { + "id": "BF9H", + "name": "Trailing comment in multiline plain scalar", + "yaml": "---\nplain: a\n b # end of scalar\n c\n", + "error": true + }, + { + "id": "BS4K", + "name": "Comment between plain scalar lines", + "yaml": "word1 # comment\nword2\n", + "error": true + }, + { + "id": "BU8L", + "name": "Node Anchor and Tag on Seperate Lines", + "yaml": "key: &anchor\n !!map\n a: b\n", + "error": false, + "json": [ + { + "key": { + "a": "b" + } + } + ] + }, + { + "id": "C2DT", + "name": "Spec Example 7.18. Flow Mapping Adjacent Values", + "yaml": "{\n\"adjacent\":value,\n\"readable\": value,\n\"empty\":\n}\n", + "error": false, + "json": [ + { + "adjacent": "value", + "readable": "value", + "empty": null + } + ] + }, + { + "id": "C2SP", + "name": "Flow Mapping Key on two lines", + "yaml": "[23\n]: 42\n", + "error": true + }, + { + "id": "C4HZ", + "name": "Spec Example 2.24. Global Tags", + "yaml": "%TAG ! tag:clarkevans.com,2002:\n--- !shape\n # Use the ! handle for presenting\n # tag:clarkevans.com,2002:circle\n- !circle\n center: &ORIGIN {x: 73, y: 129}\n radius: 7\n- !line\n start: *ORIGIN\n finish: { x: 89, y: 102 }\n- !label\n start: *ORIGIN\n color: 0xFFEEBB\n text: Pretty vector drawing.\n", + "error": false, + "json": [ + [ + { + "center": { + "x": 73, + "y": 129 + }, + "radius": 7 + }, + { + "start": { + "x": 73, + "y": 129 + }, + "finish": { + "x": 89, + "y": 102 + } + }, + { + "start": { + "x": 73, + "y": 129 + }, + "color": 16772795, + "text": "Pretty vector drawing." + } + ] + ] + }, + { + "id": "CC74", + "name": "Spec Example 6.20. Tag Handles", + "yaml": "%TAG !e! tag:example.com,2000:app/\n---\n!e!foo \"bar\"\n", + "error": false, + "json": [ + "bar" + ] + }, + { + "id": "CFD4", + "name": "Empty implicit key in single pair flow sequences", + "yaml": "- [ : empty key ]\n- [: another empty key]\n", + "error": false + }, + { + "id": "CML9", + "name": "Missing comma in flow", + "yaml": "key: [ word1\n# xxx\n word2 ]\n", + "error": true + }, + { + "id": "CN3R", + "name": "Various location of anchors in flow sequence", + "yaml": "&flowseq [\n a: b,\n &c c: d,\n { &e e: f },\n &g { g: h }\n]\n", + "error": false, + "json": [ + [ + { + "a": "b" + }, + { + "c": "d" + }, + { + "e": "f" + }, + { + "g": "h" + } + ] + ] + }, + { + "id": "CPZ3", + "name": "Doublequoted scalar starting with a tab", + "yaml": "---\ntab: \"\\tstring\"\n", + "error": false, + "json": [ + { + "tab": "\tstring" + } + ] + }, + { + "id": "CQ3W", + "name": "Double quoted string without closing quote", + "yaml": "---\nkey: \"missing closing quote\n", + "error": true + }, + { + "id": "CT4Q", + "name": "Spec Example 7.20. Single Pair Explicit Entry", + "yaml": "[\n? foo\n bar : baz\n]\n", + "error": false, + "json": [ + [ + { + "foo bar": "baz" + } + ] + ] + }, + { + "id": "CTN5", + "name": "Flow sequence with invalid extra comma", + "yaml": "---\n[ a, b, c, , ]\n", + "error": true + }, + { + "id": "CUP7", + "name": "Spec Example 5.6. Node Property Indicators", + "yaml": "anchored: !local &anchor value\nalias: *anchor\n", + "error": false, + "json": [ + { + "anchored": "value", + "alias": "value" + } + ] + }, + { + "id": "CVW2", + "name": "Invalid comment after comma", + "yaml": "---\n[ a, b, c,#invalid\n]\n", + "error": true + }, + { + "id": "CXX2", + "name": "Mapping with anchor on document start line", + "yaml": "--- &anchor a: b\n", + "error": true + }, + { + "id": "D49Q", + "name": "Multiline single quoted implicit keys", + "yaml": "'a\\nb': 1\n'c\n d': 1\n", + "error": true + }, + { + "id": "D83L", + "name": "Block scalar indicator order", + "yaml": "- |2-\n explicit indent and chomp\n- |-2\n chomp and explicit indent\n", + "error": false, + "json": [ + [ + "explicit indent and chomp", + "chomp and explicit indent" + ] + ] + }, + { + "id": "D88J", + "name": "Flow Sequence in Block Mapping", + "yaml": "a: [b, c]\n", + "error": false, + "json": [ + { + "a": [ + "b", + "c" + ] + } + ] + }, + { + "id": "D9TU", + "name": "Single Pair Block Mapping", + "yaml": "foo: bar\n", + "error": false, + "json": [ + { + "foo": "bar" + } + ] + }, + { + "id": "DBG4", + "name": "Spec Example 7.10. Plain Characters", + "yaml": "# Outside flow collection:\n- ::vector\n- \": - ()\"\n- Up, up, and away!\n- -123\n- http://example.com/foo#bar\n# Inside flow collection:\n- [ ::vector,\n \": - ()\",\n \"Up, up and away!\",\n -123,\n http://example.com/foo#bar ]\n", + "error": false, + "json": [ + [ + "::vector", + ": - ()", + "Up, up, and away!", + -123, + "http://example.com/foo#bar", + [ + "::vector", + ": - ()", + "Up, up and away!", + -123, + "http://example.com/foo#bar" + ] + ] + ] + }, + { + "id": "DC7X", + "name": "Various trailing tabs", + "yaml": "a: b\t\nseq:\t\n - a\t\nc: d\t#X\n", + "error": false, + "json": [ + { + "a": "b", + "seq": [ + "a" + ], + "c": "d" + } + ] + }, + { + "id": "DE56/00", + "name": "Trailing tabs in double quoted", + "yaml": "\"1 trailing\\t\n tab\"\n", + "error": false, + "json": [ + "1 trailing\t tab" + ] + }, + { + "id": "DE56/01", + "name": "Trailing tabs in double quoted", + "yaml": "\"2 trailing\\t \n tab\"\n", + "error": false, + "json": [ + "2 trailing\t tab" + ] + }, + { + "id": "DE56/02", + "name": "Trailing tabs in double quoted", + "yaml": "\"3 trailing\\\t\n tab\"\n", + "error": false, + "json": [ + "3 trailing\t tab" + ] + }, + { + "id": "DE56/03", + "name": "Trailing tabs in double quoted", + "yaml": "\"4 trailing\\\t \n tab\"\n", + "error": false, + "json": [ + "4 trailing\t tab" + ] + }, + { + "id": "DE56/04", + "name": "Trailing tabs in double quoted", + "yaml": "\"5 trailing\t\n tab\"\n", + "error": false, + "json": [ + "5 trailing tab" + ] + }, + { + "id": "DE56/05", + "name": "Trailing tabs in double quoted", + "yaml": "\"6 trailing\t \n tab\"\n", + "error": false, + "json": [ + "6 trailing tab" + ] + }, + { + "id": "DFF7", + "name": "Spec Example 7.16. Flow Mapping Entries", + "yaml": "{\n? explicit: entry,\nimplicit: entry,\n?\n}\n", + "error": false + }, + { + "id": "DHP8", + "name": "Flow Sequence", + "yaml": "[foo, bar, 42]\n", + "error": false, + "json": [ + [ + "foo", + "bar", + 42 + ] + ] + }, + { + "id": "DK3J", + "name": "Zero indented block scalar with line that looks like a comment", + "yaml": "--- >\nline1\n# no comment\nline3\n", + "error": false, + "json": [ + "line1 # no comment line3\n" + ] + }, + { + "id": "DK4H", + "name": "Implicit key followed by newline", + "yaml": "---\n[ key\n : value ]\n", + "error": true + }, + { + "id": "DK95/00", + "name": "Tabs that look like indentation", + "yaml": "foo:\n \tbar\n", + "error": false, + "json": [ + { + "foo": "bar" + } + ] + }, + { + "id": "DK95/01", + "name": "Tabs that look like indentation", + "yaml": "foo: \"bar\n\tbaz\"\n", + "error": true, + "json": [ + { + "foo": "bar" + } + ] + }, + { + "id": "DK95/02", + "name": "Tabs that look like indentation", + "yaml": "foo: \"bar\n \tbaz\"\n", + "error": false, + "json": [ + { + "foo": "bar baz" + } + ] + }, + { + "id": "DK95/03", + "name": "Tabs that look like indentation", + "yaml": " \t\nfoo: 1\n", + "error": false, + "json": [ + { + "foo": 1 + } + ] + }, + { + "id": "DK95/04", + "name": "Tabs that look like indentation", + "yaml": "foo: 1\n\t\nbar: 2\n", + "error": false, + "json": [ + { + "foo": 1, + "bar": 2 + } + ] + }, + { + "id": "DK95/05", + "name": "Tabs that look like indentation", + "yaml": "foo: 1\n \t\nbar: 2\n", + "error": false, + "json": [ + { + "foo": 1, + "bar": 2 + } + ] + }, + { + "id": "DK95/06", + "name": "Tabs that look like indentation", + "yaml": "foo:\n a: 1\n \tb: 2\n", + "error": true, + "json": [ + { + "foo": 1, + "bar": 2 + } + ] + }, + { + "id": "DK95/07", + "name": "Tabs that look like indentation", + "yaml": "%YAML 1.2\n\t\n---\n", + "error": false, + "json": [ + null + ] + }, + { + "id": "DK95/08", + "name": "Tabs that look like indentation", + "yaml": "foo: \"bar\n \t \t baz \t \t \"\n", + "error": false, + "json": [ + { + "foo": "bar baz \t \t " + } + ] + }, + { + "id": "DMG6", + "name": "Wrong indendation in Map", + "yaml": "key:\n ok: 1\n wrong: 2\n", + "error": true + }, + { + "id": "DWX9", + "name": "Spec Example 8.8. Literal Content", + "yaml": "|\n \n \n literal\n \n \n text\n\n # Comment\n", + "error": false, + "json": [ + "\n\nliteral\n \n\ntext\n" + ] + }, + { + "id": "E76Z", + "name": "Aliases in Implicit Block Mapping", + "yaml": "&a a: &b b\n*b : *a\n", + "error": false, + "json": [ + { + "a": "b", + "b": "a" + } + ] + }, + { + "id": "EB22", + "name": "Missing document-end marker before directive", + "yaml": "---\nscalar1 # comment\n%YAML 1.2\n---\nscalar2\n", + "error": true + }, + { + "id": "EHF6", + "name": "Tags for Flow Objects", + "yaml": "!!map {\n k: !!seq\n [ a, !!str b]\n}\n", + "error": false, + "json": [ + { + "k": [ + "a", + "b" + ] + } + ] + }, + { + "id": "EW3V", + "name": "Wrong indendation in mapping", + "yaml": "k1: v1\n k2: v2\n", + "error": true + }, + { + "id": "EX5H", + "name": "Multiline Scalar at Top Level [1.3]", + "yaml": "---\na\nb \n c\nd\n\ne\n", + "error": false, + "json": [ + "a b c d\ne" + ] + }, + { + "id": "EXG3", + "name": "Three dashes and content without space [1.3]", + "yaml": "---\n---word1\nword2\n", + "error": false, + "json": [ + "---word1 word2" + ] + }, + { + "id": "F2C7", + "name": "Anchors and Tags", + "yaml": " - &a !!str a\n - !!int 2\n - !!int &c 4\n - &d d\n", + "error": false, + "json": [ + [ + "a", + 2, + 4, + "d" + ] + ] + }, + { + "id": "F3CP", + "name": "Nested flow collections on one line", + "yaml": "---\n{ a: [b, c, { d: [e, f] } ] }\n", + "error": false, + "json": [ + { + "a": [ + "b", + "c", + { + "d": [ + "e", + "f" + ] + } + ] + } + ] + }, + { + "id": "F6MC", + "name": "More indented lines at the beginning of folded block scalars", + "yaml": "---\na: >2\n more indented\n regular\nb: >2\n\n\n more indented\n regular\n", + "error": false, + "json": [ + { + "a": " more indented\nregular\n", + "b": "\n\n more indented\nregular\n" + } + ] + }, + { + "id": "F8F9", + "name": "Spec Example 8.5. Chomping Trailing Lines", + "yaml": " # Strip\n # Comments:\nstrip: |-\n # text\n \n # Clip\n # comments:\n\nclip: |\n # text\n \n # Keep\n # comments:\n\nkeep: |+\n # text\n\n # Trail\n # comments.\n", + "error": false, + "json": [ + { + "strip": "# text", + "clip": "# text\n", + "keep": "# text\n\n" + } + ] + }, + { + "id": "FBC9", + "name": "Allowed characters in plain scalars", + "yaml": "safe: a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\n !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~\nsafe question mark: ?foo\nsafe colon: :foo\nsafe dash: -foo\n", + "error": false, + "json": [ + { + "safe": "a!\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~ !\"#$%&'()*+,-./09:;<=>?@AZ[\\]^_`az{|}~", + "safe question mark": "?foo", + "safe colon": ":foo", + "safe dash": "-foo" + } + ] + }, + { + "id": "FH7J", + "name": "Tags on Empty Scalars", + "yaml": "- !!str\n-\n !!null : a\n b: !!str\n- !!str : !!null\n", + "error": false + }, + { + "id": "FP8R", + "name": "Zero indented block scalar", + "yaml": "--- >\nline1\nline2\nline3\n", + "error": false, + "json": [ + "line1 line2 line3\n" + ] + }, + { + "id": "FQ7F", + "name": "Spec Example 2.1. Sequence of Scalars", + "yaml": "- Mark McGwire\n- Sammy Sosa\n- Ken Griffey\n", + "error": false, + "json": [ + [ + "Mark McGwire", + "Sammy Sosa", + "Ken Griffey" + ] + ] + }, + { + "id": "FRK4", + "name": "Spec Example 7.3. Completely Empty Flow Nodes", + "yaml": "{\n ? foo :,\n : bar,\n}\n", + "error": false + }, + { + "id": "FTA2", + "name": "Single block sequence with anchor and explicit document start", + "yaml": "--- &sequence\n- a\n", + "error": false, + "json": [ + [ + "a" + ] + ] + }, + { + "id": "FUP4", + "name": "Flow Sequence in Flow Sequence", + "yaml": "[a, [b, c]]\n", + "error": false, + "json": [ + [ + "a", + [ + "b", + "c" + ] + ] + ] + }, + { + "id": "G4RS", + "name": "Spec Example 2.17. Quoted Scalars", + "yaml": "unicode: \"Sosa did fine.\\u263A\"\ncontrol: \"\\b1998\\t1999\\t2000\\n\"\nhex esc: \"\\x0d\\x0a is \\r\\n\"\n\nsingle: '\"Howdy!\" he cried.'\nquoted: ' # Not a ''comment''.'\ntie-fighter: '|\\-*-/|'\n", + "error": false, + "json": [ + { + "unicode": "Sosa did fine.\u263a", + "control": "\b1998\t1999\t2000\n", + "hex esc": "\r\n is \r\n", + "single": "\"Howdy!\" he cried.", + "quoted": " # Not a 'comment'.", + "tie-fighter": "|\\-*-/|" + } + ] + }, + { + "id": "G5U8", + "name": "Plain dashes in flow sequence", + "yaml": "---\n- [-, -]\n", + "error": true + }, + { + "id": "G7JE", + "name": "Multiline implicit keys", + "yaml": "a\\nb: 1\nc\n d: 1\n", + "error": true + }, + { + "id": "G992", + "name": "Spec Example 8.9. Folded Scalar", + "yaml": ">\n folded\n text\n\n\n", + "error": false, + "json": [ + "folded text\n" + ] + }, + { + "id": "G9HC", + "name": "Invalid anchor in zero indented sequence", + "yaml": "---\nseq:\n&anchor\n- a\n- b\n", + "error": true + }, + { + "id": "GDY7", + "name": "Comment that looks like a mapping key", + "yaml": "key: value\nthis is #not a: key\n", + "error": true + }, + { + "id": "GH63", + "name": "Mixed Block Mapping (explicit to implicit)", + "yaml": "? a\n: 1.3\nfifteen: d\n", + "error": false, + "json": [ + { + "a": 1.3, + "fifteen": "d" + } + ] + }, + { + "id": "GT5M", + "name": "Node anchor in sequence", + "yaml": "- item1\n&node\n- item2\n", + "error": true + }, + { + "id": "H2RW", + "name": "Blank lines", + "yaml": "foo: 1\n\nbar: 2\n \ntext: |\n a\n \n b\n\n c\n \n d\n", + "error": false, + "json": [ + { + "foo": 1, + "bar": 2, + "text": "a\n \nb\n\nc\n\nd\n" + } + ] + }, + { + "id": "H3Z8", + "name": "Literal unicode", + "yaml": "---\nwanted: love \u2665 and peace \u262e\n", + "error": false, + "json": [ + { + "wanted": "love \u2665 and peace \u262e" + } + ] + }, + { + "id": "H7J7", + "name": "Node anchor not indented", + "yaml": "key: &x\n!!map\n a: b\n", + "error": true + }, + { + "id": "H7TQ", + "name": "Extra words on %YAML directive", + "yaml": "%YAML 1.2 foo\n---\n", + "error": true + }, + { + "id": "HM87/00", + "name": "Scalars in flow start with syntax char", + "yaml": "[:x]\n", + "error": false, + "json": [ + [ + ":x" + ] + ] + }, + { + "id": "HM87/01", + "name": "Scalars in flow start with syntax char", + "yaml": "[?x]\n", + "error": false, + "json": [ + [ + "?x" + ] + ] + }, + { + "id": "HMK4", + "name": "Spec Example 2.16. Indentation determines scope", + "yaml": "name: Mark McGwire\naccomplishment: >\n Mark set a major league\n home run record in 1998.\nstats: |\n 65 Home Runs\n 0.278 Batting Average\n", + "error": false, + "json": [ + { + "name": "Mark McGwire", + "accomplishment": "Mark set a major league home run record in 1998.\n", + "stats": "65 Home Runs\n0.278 Batting Average\n" + } + ] + }, + { + "id": "HMQ5", + "name": "Spec Example 6.23. Node Properties", + "yaml": "!!str &a1 \"foo\":\n !!str bar\n&a2 baz : *a1\n", + "error": false, + "json": [ + { + "foo": "bar", + "baz": "foo" + } + ] + }, + { + "id": "HRE5", + "name": "Double quoted scalar with escaped single quote", + "yaml": "---\ndouble: \"quoted \\' scalar\"\n", + "error": true + }, + { + "id": "HS5T", + "name": "Spec Example 7.12. Plain Lines", + "yaml": "1st non-empty\n\n 2nd non-empty \n\t3rd non-empty\n", + "error": false, + "json": [ + "1st non-empty\n2nd non-empty 3rd non-empty" + ] + }, + { + "id": "HU3P", + "name": "Invalid Mapping in plain scalar", + "yaml": "key:\n word1 word2\n no: key\n", + "error": true + }, + { + "id": "HWV9", + "name": "Document-end marker", + "yaml": "...\n", + "error": false, + "json": [] + }, + { + "id": "J3BT", + "name": "Spec Example 5.12. Tabs and Spaces", + "yaml": "# Tabs and spaces\nquoted: \"Quoted \t\"\nblock:\t|\n void main() {\n \tprintf(\"Hello, world!\\n\");\n }\n", + "error": false, + "json": [ + { + "quoted": "Quoted \t", + "block": "void main() {\n\tprintf(\"Hello, world!\\n\");\n}\n" + } + ] + }, + { + "id": "J5UC", + "name": "Multiple Pair Block Mapping", + "yaml": "foo: blue\nbar: arrr\nbaz: jazz\n", + "error": false, + "json": [ + { + "foo": "blue", + "bar": "arrr", + "baz": "jazz" + } + ] + }, + { + "id": "J7PZ", + "name": "Spec Example 2.26. Ordered Mappings", + "yaml": "# The !!omap tag is one of the optional types\n# introduced for YAML 1.1. In 1.2, it is not\n# part of the standard tags and should not be\n# enabled by default.\n# Ordered maps are represented as\n# A sequence of mappings, with\n# each mapping having one key\n--- !!omap\n- Mark McGwire: 65\n- Sammy Sosa: 63\n- Ken Griffy: 58\n", + "error": false, + "json": [ + [ + { + "Mark McGwire": 65 + }, + { + "Sammy Sosa": 63 + }, + { + "Ken Griffy": 58 + } + ] + ] + }, + { + "id": "J7VC", + "name": "Empty Lines Between Mapping Elements", + "yaml": "one: 2\n\n\nthree: 4\n", + "error": false, + "json": [ + { + "one": 2, + "three": 4 + } + ] + }, + { + "id": "J9HZ", + "name": "Spec Example 2.9. Single Document with Two Comments", + "yaml": "---\nhr: # 1998 hr ranking\n - Mark McGwire\n - Sammy Sosa\nrbi:\n # 1998 rbi ranking\n - Sammy Sosa\n - Ken Griffey\n", + "error": false, + "json": [ + { + "hr": [ + "Mark McGwire", + "Sammy Sosa" + ], + "rbi": [ + "Sammy Sosa", + "Ken Griffey" + ] + } + ] + }, + { + "id": "JEF9/00", + "name": "Trailing whitespace in streams", + "yaml": "- |+\n\n\n", + "error": false, + "json": [ + [ + "\n\n" + ] + ] + }, + { + "id": "JEF9/01", + "name": "Trailing whitespace in streams", + "yaml": "- |+\n \n", + "error": false, + "json": [ + [ + "\n" + ] + ] + }, + { + "id": "JEF9/02", + "name": "Trailing whitespace in streams", + "yaml": "- |+\n ", + "error": false, + "json": [ + [ + "\n" + ] + ] + }, + { + "id": "JHB9", + "name": "Spec Example 2.7. Two Documents in a Stream", + "yaml": "# Ranking of 1998 home runs\n---\n- Mark McGwire\n- Sammy Sosa\n- Ken Griffey\n\n# Team ranking\n---\n- Chicago Cubs\n- St Louis Cardinals\n", + "error": false, + "json": [ + [ + "Mark McGwire", + "Sammy Sosa", + "Ken Griffey" + ], + [ + "Chicago Cubs", + "St Louis Cardinals" + ] + ] + }, + { + "id": "JKF3", + "name": "Multiline unidented double quoted block key", + "yaml": "- - \"bar\nbar\": x\n", + "error": true + }, + { + "id": "JQ4R", + "name": "Spec Example 8.14. Block Sequence", + "yaml": "block sequence:\n - one\n - two : three\n", + "error": false, + "json": [ + { + "block sequence": [ + "one", + { + "two": "three" + } + ] + } + ] + }, + { + "id": "JR7V", + "name": "Question marks in scalars", + "yaml": "- a?string\n- another ? string\n- key: value?\n- [a?string]\n- [another ? string]\n- {key: value? }\n- {key: value?}\n- {key?: value }\n", + "error": false, + "json": [ + [ + "a?string", + "another ? string", + { + "key": "value?" + }, + [ + "a?string" + ], + [ + "another ? string" + ], + { + "key": "value?" + }, + { + "key": "value?" + }, + { + "key?": "value" + } + ] + ] + }, + { + "id": "JS2J", + "name": "Spec Example 6.29. Node Anchors", + "yaml": "First occurrence: &anchor Value\nSecond occurrence: *anchor\n", + "error": false, + "json": [ + { + "First occurrence": "Value", + "Second occurrence": "Value" + } + ] + }, + { + "id": "JTV5", + "name": "Block Mapping with Multiline Scalars", + "yaml": "? a\n true\n: null\n d\n? e\n 42\n", + "error": false, + "json": [ + { + "a true": "null d", + "e 42": null + } + ] + }, + { + "id": "JY7Z", + "name": "Trailing content that looks like a mapping", + "yaml": "key1: \"quoted1\"\nkey2: \"quoted2\" no key: nor value\nkey3: \"quoted3\"\n", + "error": true + }, + { + "id": "K3WX", + "name": "Colon and adjacent value after comment on next line", + "yaml": "---\n{ \"foo\" # comment\n :bar }\n", + "error": false, + "json": [ + { + "foo": "bar" + } + ] + }, + { + "id": "K4SU", + "name": "Multiple Entry Block Sequence", + "yaml": "- foo\n- bar\n- 42\n", + "error": false, + "json": [ + [ + "foo", + "bar", + 42 + ] + ] + }, + { + "id": "K527", + "name": "Spec Example 6.6. Line Folding", + "yaml": ">-\n trimmed\n \n \n\n as\n space\n", + "error": false, + "json": [ + "trimmed\n\n\nas space" + ] + }, + { + "id": "K54U", + "name": "Tab after document header", + "yaml": "---\tscalar\n", + "error": false, + "json": [ + "scalar" + ] + }, + { + "id": "K858", + "name": "Spec Example 8.6. Empty Scalar Chomping", + "yaml": "strip: >-\n\nclip: >\n\nkeep: |+\n\n", + "error": false, + "json": [ + { + "strip": "", + "clip": "", + "keep": "\n" + } + ] + }, + { + "id": "KH5V/00", + "name": "Inline tabs in double quoted", + "yaml": "\"1 inline\\ttab\"\n", + "error": false, + "json": [ + "1 inline\ttab" + ] + }, + { + "id": "KH5V/01", + "name": "Inline tabs in double quoted", + "yaml": "\"2 inline\\\ttab\"\n", + "error": false, + "json": [ + "2 inline\ttab" + ] + }, + { + "id": "KH5V/02", + "name": "Inline tabs in double quoted", + "yaml": "\"3 inline\ttab\"\n", + "error": false, + "json": [ + "3 inline\ttab" + ] + }, + { + "id": "KK5P", + "name": "Various combinations of explicit block mappings", + "yaml": "complex1:\n ? - a\ncomplex2:\n ? - a\n : b\ncomplex3:\n ? - a\n : >\n b\ncomplex4:\n ? >\n a\n :\ncomplex5:\n ? - a\n : - b\n", + "error": false + }, + { + "id": "KMK3", + "name": "Block Submapping", + "yaml": "foo:\n bar: 1\nbaz: 2\n", + "error": false, + "json": [ + { + "foo": { + "bar": 1 + }, + "baz": 2 + } + ] + }, + { + "id": "KS4U", + "name": "Invalid item after end of flow sequence", + "yaml": "---\n[\nsequence item\n]\ninvalid item\n", + "error": true + }, + { + "id": "KSS4", + "name": "Scalars on --- line", + "yaml": "--- \"quoted\nstring\"\n--- &node foo\n", + "error": false, + "json": [ + "quoted string", + "foo" + ] + }, + { + "id": "L24T/00", + "name": "Trailing line of spaces", + "yaml": "foo: |\n x\n \n", + "error": false, + "json": [ + { + "foo": "x\n \n" + } + ] + }, + { + "id": "L24T/01", + "name": "Trailing line of spaces", + "yaml": "foo: |\n x\n ", + "error": false, + "json": [ + { + "foo": "x\n \n" + } + ] + }, + { + "id": "L383", + "name": "Two scalar docs with trailing comments", + "yaml": "--- foo # comment\n--- foo # comment\n", + "error": false, + "json": [ + "foo", + "foo" + ] + }, + { + "id": "L94M", + "name": "Tags in Explicit Mapping", + "yaml": "? !!str a\n: !!int 47\n? c\n: !!str d\n", + "error": false, + "json": [ + { + "a": 47, + "c": "d" + } + ] + }, + { + "id": "L9U5", + "name": "Spec Example 7.11. Plain Implicit Keys", + "yaml": "implicit block key : [\n implicit flow key : value,\n ]\n", + "error": false, + "json": [ + { + "implicit block key": [ + { + "implicit flow key": "value" + } + ] + } + ] + }, + { + "id": "LE5A", + "name": "Spec Example 7.24. Flow Nodes", + "yaml": "- !!str \"a\"\n- 'b'\n- &anchor \"c\"\n- *anchor\n- !!str\n", + "error": false, + "json": [ + [ + "a", + "b", + "c", + "c", + "" + ] + ] + }, + { + "id": "LHL4", + "name": "Invalid tag", + "yaml": "---\n!invalid{}tag scalar\n", + "error": true + }, + { + "id": "LP6E", + "name": "Whitespace After Scalars in Flow", + "yaml": "- [a, b , c ]\n- { \"a\" : b\n , c : 'd' ,\n e : \"f\"\n }\n- [ ]\n", + "error": false, + "json": [ + [ + [ + "a", + "b", + "c" + ], + { + "a": "b", + "c": "d", + "e": "f" + }, + [] + ] + ] + }, + { + "id": "LQZ7", + "name": "Spec Example 7.4. Double Quoted Implicit Keys", + "yaml": "\"implicit block key\" : [\n \"implicit flow key\" : value,\n ]\n", + "error": false, + "json": [ + { + "implicit block key": [ + { + "implicit flow key": "value" + } + ] + } + ] + }, + { + "id": "LX3P", + "name": "Implicit Flow Mapping Key on one line", + "yaml": "[flow]: block\n", + "error": false + }, + { + "id": "M29M", + "name": "Literal Block Scalar", + "yaml": "a: |\n ab\n \n cd\n ef\n \n\n...\n", + "error": false, + "json": [ + { + "a": "ab\n\ncd\nef\n" + } + ] + }, + { + "id": "M2N8/00", + "name": "Question mark edge cases", + "yaml": "- ? : x\n", + "error": false + }, + { + "id": "M2N8/01", + "name": "Question mark edge cases", + "yaml": "? []: x\n", + "error": false + }, + { + "id": "M5C3", + "name": "Spec Example 8.21. Block Scalar Nodes", + "yaml": "literal: |2\n value\nfolded:\n !foo\n >1\n value\n", + "error": false, + "json": [ + { + "literal": "value\n", + "folded": "value\n" + } + ] + }, + { + "id": "M5DY", + "name": "Spec Example 2.11. Mapping between Sequences", + "yaml": "? - Detroit Tigers\n - Chicago cubs\n:\n - 2001-07-23\n\n? [ New York Yankees,\n Atlanta Braves ]\n: [ 2001-07-02, 2001-08-12,\n 2001-08-14 ]\n", + "error": false + }, + { + "id": "M6YH", + "name": "Block sequence indentation", + "yaml": "- |\n x\n-\n foo: bar\n-\n - 42\n", + "error": false, + "json": [ + [ + "x\n", + { + "foo": "bar" + }, + [ + 42 + ] + ] + ] + }, + { + "id": "M7A3", + "name": "Spec Example 9.3. Bare Documents", + "yaml": "Bare\ndocument\n...\n# No document\n...\n|\n%!PS-Adobe-2.0 # Not the first line\n", + "error": false, + "json": [ + "Bare document", + "%!PS-Adobe-2.0 # Not the first line\n" + ] + }, + { + "id": "M7NX", + "name": "Nested flow collections", + "yaml": "---\n{\n a: [\n b, c, {\n d: [e, f]\n }\n ]\n}\n", + "error": false, + "json": [ + { + "a": [ + "b", + "c", + { + "d": [ + "e", + "f" + ] + } + ] + } + ] + }, + { + "id": "M9B4", + "name": "Spec Example 8.7. Literal Scalar", + "yaml": "|\n literal\n \ttext\n\n\n", + "error": false, + "json": [ + "literal\n\ttext\n" + ] + }, + { + "id": "MJS9", + "name": "Spec Example 6.7. Block Folding", + "yaml": ">\n foo \n \n \t bar\n\n baz\n", + "error": false, + "json": [ + "foo \n\n\t bar\n\nbaz\n" + ] + }, + { + "id": "MUS6/00", + "name": "Directive variants", + "yaml": "%YAML 1.1#...\n---\n", + "error": true + }, + { + "id": "MUS6/01", + "name": "Directive variants", + "yaml": "%YAML 1.2\n---\n%YAML 1.2\n---\n", + "error": true + }, + { + "id": "MUS6/02", + "name": "Directive variants", + "yaml": "%YAML 1.1\n---\n", + "error": false, + "json": [ + null + ] + }, + { + "id": "MUS6/03", + "name": "Directive variants", + "yaml": "%YAML \t 1.1\n---\n", + "error": false, + "json": [ + null + ] + }, + { + "id": "MUS6/04", + "name": "Directive variants", + "yaml": "%YAML 1.1 # comment\n---\n", + "error": false, + "json": [ + null + ] + }, + { + "id": "MUS6/05", + "name": "Directive variants", + "yaml": "%YAM 1.1\n---\n", + "error": false, + "json": [ + null + ] + }, + { + "id": "MUS6/06", + "name": "Directive variants", + "yaml": "%YAMLL 1.1\n---\n", + "error": false, + "json": [ + null + ] + }, + { + "id": "MXS3", + "name": "Flow Mapping in Block Sequence", + "yaml": "- {a: b}\n", + "error": false, + "json": [ + [ + { + "a": "b" + } + ] + ] + }, + { + "id": "MYW6", + "name": "Block Scalar Strip", + "yaml": "|-\n ab\n \n \n...\n", + "error": false, + "json": [ + "ab" + ] + }, + { + "id": "MZX3", + "name": "Non-Specific Tags on Scalars", + "yaml": "- plain\n- \"double quoted\"\n- 'single quoted'\n- >\n block\n- plain again\n", + "error": false, + "json": [ + [ + "plain", + "double quoted", + "single quoted", + "block\n", + "plain again" + ] + ] + }, + { + "id": "N4JP", + "name": "Bad indentation in mapping", + "yaml": "map:\n key1: \"quoted1\"\n key2: \"bad indentation\"\n", + "error": true + }, + { + "id": "N782", + "name": "Invalid document markers in flow style", + "yaml": "[\n--- ,\n...\n]\n", + "error": true + }, + { + "id": "NAT4", + "name": "Various empty or newline only quoted strings", + "yaml": "---\na: '\n '\nb: ' \n '\nc: \"\n \"\nd: \" \n \"\ne: '\n\n '\nf: \"\n\n \"\ng: '\n\n\n '\nh: \"\n\n\n \"\n", + "error": false, + "json": [ + { + "a": " ", + "b": " ", + "c": " ", + "d": " ", + "e": "\n", + "f": "\n", + "g": "\n\n", + "h": "\n\n" + } + ] + }, + { + "id": "NB6Z", + "name": "Multiline plain value with tabs on empty lines", + "yaml": "key:\n value\n with\n \t\n tabs\n", + "error": false, + "json": [ + { + "key": "value with\ntabs" + } + ] + }, + { + "id": "NHX8", + "name": "Empty Lines at End of Document", + "yaml": ":\n\n\n", + "error": false + }, + { + "id": "NJ66", + "name": "Multiline plain flow mapping key", + "yaml": "---\n- { single line: value}\n- { multi\n line: value}\n", + "error": false, + "json": [ + [ + { + "single line": "value" + }, + { + "multi line": "value" + } + ] + ] + }, + { + "id": "NKF9", + "name": "Empty keys in block and flow mapping", + "yaml": "---\nkey: value\n: empty key\n---\n{\n key: value, : empty key\n}\n---\n# empty key and value\n:\n---\n# empty key and value\n{ : }\n", + "error": false + }, + { + "id": "NP9H", + "name": "Spec Example 7.5. Double Quoted Line Breaks", + "yaml": "\"folded \nto a space,\t\n \nto a line feed, or \t\\\n \\ \tnon-content\"\n", + "error": false, + "json": [ + "folded to a space,\nto a line feed, or \t \tnon-content" + ] + }, + { + "id": "P2AD", + "name": "Spec Example 8.1. Block Scalar Header", + "yaml": "- | # Empty header\u2193\n literal\n- >1 # Indentation indicator\u2193\n folded\n- |+ # Chomping indicator\u2193\n keep\n\n- >1- # Both indicators\u2193\n strip\n", + "error": false, + "json": [ + [ + "literal\n", + " folded\n", + "keep\n\n", + " strip" + ] + ] + }, + { + "id": "P2EQ", + "name": "Invalid sequene item on same line as previous item", + "yaml": "---\n- { y: z }- invalid\n", + "error": true + }, + { + "id": "P76L", + "name": "Spec Example 6.19. Secondary Tag Handle", + "yaml": "%TAG !! tag:example.com,2000:app/\n---\n!!int 1 - 3 # Interval, not integer\n", + "error": false, + "json": [ + "1 - 3" + ] + }, + { + "id": "P94K", + "name": "Spec Example 6.11. Multi-Line Comments", + "yaml": "key: # Comment\n # lines\n value\n\n\n", + "error": false, + "json": [ + { + "key": "value" + } + ] + }, + { + "id": "PBJ2", + "name": "Spec Example 2.3. Mapping Scalars to Sequences", + "yaml": "american:\n - Boston Red Sox\n - Detroit Tigers\n - New York Yankees\nnational:\n - New York Mets\n - Chicago Cubs\n - Atlanta Braves\n", + "error": false, + "json": [ + { + "american": [ + "Boston Red Sox", + "Detroit Tigers", + "New York Yankees" + ], + "national": [ + "New York Mets", + "Chicago Cubs", + "Atlanta Braves" + ] + } + ] + }, + { + "id": "PRH3", + "name": "Spec Example 7.9. Single Quoted Lines", + "yaml": "' 1st non-empty\n\n 2nd non-empty \n\t3rd non-empty '\n", + "error": false, + "json": [ + " 1st non-empty\n2nd non-empty 3rd non-empty " + ] + }, + { + "id": "PUW8", + "name": "Document start on last line", + "yaml": "---\na: b\n---\n", + "error": false, + "json": [ + { + "a": "b" + }, + null + ] + }, + { + "id": "PW8X", + "name": "Anchors on Empty Scalars", + "yaml": "- &a\n- a\n-\n &a : a\n b: &b\n-\n &c : &a\n-\n ? &d\n-\n ? &e\n : &a\n", + "error": false + }, + { + "id": "Q4CL", + "name": "Trailing content after quoted value", + "yaml": "key1: \"quoted1\"\nkey2: \"quoted2\" trailing content\nkey3: \"quoted3\"\n", + "error": true + }, + { + "id": "Q5MG", + "name": "Tab at beginning of line followed by a flow mapping", + "yaml": "\t{}\n", + "error": false, + "json": [ + {} + ] + }, + { + "id": "Q88A", + "name": "Spec Example 7.23. Flow Content", + "yaml": "- [ a, b ]\n- { a: b }\n- \"a\"\n- 'b'\n- c\n", + "error": false, + "json": [ + [ + [ + "a", + "b" + ], + { + "a": "b" + }, + "a", + "b", + "c" + ] + ] + }, + { + "id": "Q8AD", + "name": "Spec Example 7.5. Double Quoted Line Breaks [1.3]", + "yaml": "---\n\"folded \nto a space,\n \nto a line feed, or \t\\\n \\ \tnon-content\"\n", + "error": false, + "json": [ + "folded to a space,\nto a line feed, or \t \tnon-content" + ] + }, + { + "id": "Q9WF", + "name": "Spec Example 6.12. Separation Spaces", + "yaml": "{ first: Sammy, last: Sosa }:\n# Statistics:\n hr: # Home runs\n 65\n avg: # Average\n 0.278\n", + "error": false + }, + { + "id": "QB6E", + "name": "Wrong indented multiline quoted scalar", + "yaml": "---\nquoted: \"a\nb\nc\"\n", + "error": true + }, + { + "id": "QF4Y", + "name": "Spec Example 7.19. Single Pair Flow Mappings", + "yaml": "[\nfoo: bar\n]\n", + "error": false, + "json": [ + [ + { + "foo": "bar" + } + ] + ] + }, + { + "id": "QLJ7", + "name": "Tag shorthand used in documents but only defined in the first", + "yaml": "%TAG !prefix! tag:example.com,2011:\n--- !prefix!A\na: b\n--- !prefix!B\nc: d\n--- !prefix!C\ne: f\n", + "error": true + }, + { + "id": "QT73", + "name": "Comment and document-end marker", + "yaml": "# comment\n...\n", + "error": false, + "json": [] + }, + { + "id": "R4YG", + "name": "Spec Example 8.2. Block Indentation Indicator", + "yaml": "- |\n detected\n- >\n \n \n # detected\n- |1\n explicit\n- >\n \t\n detected\n", + "error": false, + "json": [ + [ + "detected\n", + "\n\n# detected\n", + " explicit\n", + "\t\ndetected\n" + ] + ] + }, + { + "id": "R52L", + "name": "Nested flow mapping sequence and mappings", + "yaml": "---\n{ top1: [item1, {key2: value2}, item3], top2: value2 }\n", + "error": false, + "json": [ + { + "top1": [ + "item1", + { + "key2": "value2" + }, + "item3" + ], + "top2": "value2" + } + ] + }, + { + "id": "RHX7", + "name": "YAML directive without document end marker", + "yaml": "---\nkey: value\n%YAML 1.2\n---\n", + "error": true + }, + { + "id": "RLU9", + "name": "Sequence Indent", + "yaml": "foo:\n- 42\nbar:\n - 44\n", + "error": false, + "json": [ + { + "foo": [ + 42 + ], + "bar": [ + 44 + ] + } + ] + }, + { + "id": "RR7F", + "name": "Mixed Block Mapping (implicit to explicit)", + "yaml": "a: 4.2\n? d\n: 23\n", + "error": false, + "json": [ + { + "d": 23, + "a": 4.2 + } + ] + }, + { + "id": "RTP8", + "name": "Spec Example 9.2. Document Markers", + "yaml": "%YAML 1.2\n---\nDocument\n... # Suffix\n", + "error": false, + "json": [ + "Document" + ] + }, + { + "id": "RXY3", + "name": "Invalid document-end marker in single quoted string", + "yaml": "---\n'\n...\n'\n", + "error": true + }, + { + "id": "RZP5", + "name": "Various Trailing Comments [1.3]", + "yaml": "a: \"double\n quotes\" # lala\nb: plain\n value # lala\nc : #lala\n d\n? # lala\n - seq1\n: # lala\n - #lala\n seq2\ne: &node # lala\n - x: y\nblock: > # lala\n abcde\n", + "error": false + }, + { + "id": "RZT7", + "name": "Spec Example 2.28. Log File", + "yaml": "---\nTime: 2001-11-23 15:01:42 -5\nUser: ed\nWarning:\n This is an error message\n for the log file\n---\nTime: 2001-11-23 15:02:31 -5\nUser: ed\nWarning:\n A slightly different error\n message.\n---\nDate: 2001-11-23 15:03:17 -5\nUser: ed\nFatal:\n Unknown variable \"bar\"\nStack:\n - file: TopClass.py\n line: 23\n code: |\n x = MoreObject(\"345\\n\")\n - file: MoreClass.py\n line: 58\n code: |-\n foo = bar\n", + "error": false, + "json": [ + { + "Time": "2001-11-23 15:01:42 -5", + "User": "ed", + "Warning": "This is an error message for the log file" + }, + { + "Time": "2001-11-23 15:02:31 -5", + "User": "ed", + "Warning": "A slightly different error message." + }, + { + "Date": "2001-11-23 15:03:17 -5", + "User": "ed", + "Fatal": "Unknown variable \"bar\"", + "Stack": [ + { + "file": "TopClass.py", + "line": 23, + "code": "x = MoreObject(\"345\\n\")\n" + }, + { + "file": "MoreClass.py", + "line": 58, + "code": "foo = bar" + } + ] + } + ] + }, + { + "id": "S3PD", + "name": "Spec Example 8.18. Implicit Block Mapping Entries", + "yaml": "plain key: in-line value\n: # Both empty\n\"quoted key\":\n- entry\n", + "error": false + }, + { + "id": "S4GJ", + "name": "Invalid text after block scalar indicator", + "yaml": "---\nfolded: > first line\n second line\n", + "error": true + }, + { + "id": "S4JQ", + "name": "Spec Example 6.28. Non-Specific Tags", + "yaml": "# Assuming conventional resolution:\n- \"12\"\n- 12\n- ! 12\n", + "error": false, + "json": [ + [ + "12", + 12, + "12" + ] + ] + }, + { + "id": "S4T7", + "name": "Document with footer", + "yaml": "aaa: bbb\n...\n", + "error": false, + "json": [ + { + "aaa": "bbb" + } + ] + }, + { + "id": "S7BG", + "name": "Colon followed by comma", + "yaml": "---\n- :,\n", + "error": false, + "json": [ + [ + ":," + ] + ] + }, + { + "id": "S98Z", + "name": "Block scalar with more spaces than first content line", + "yaml": "empty block scalar: >\n \n \n \n # comment\n", + "error": true + }, + { + "id": "S9E8", + "name": "Spec Example 5.3. Block Structure Indicators", + "yaml": "sequence:\n- one\n- two\nmapping:\n ? sky\n : blue\n sea : green\n", + "error": false, + "json": [ + { + "sequence": [ + "one", + "two" + ], + "mapping": { + "sky": "blue", + "sea": "green" + } + } + ] + }, + { + "id": "SBG9", + "name": "Flow Sequence in Flow Mapping", + "yaml": "{a: [b, c], [d, e]: f}\n", + "error": false + }, + { + "id": "SF5V", + "name": "Duplicate YAML directive", + "yaml": "%YAML 1.2\n%YAML 1.2\n---\n", + "error": true + }, + { + "id": "SKE5", + "name": "Anchor before zero indented sequence", + "yaml": "---\nseq:\n &anchor\n- a\n- b\n", + "error": false, + "json": [ + { + "seq": [ + "a", + "b" + ] + } + ] + }, + { + "id": "SM9W/00", + "name": "Single character streams", + "yaml": "-", + "error": false, + "json": [ + [ + null + ] + ] + }, + { + "id": "SM9W/01", + "name": "Single character streams", + "yaml": ":", + "error": false + }, + { + "id": "SR86", + "name": "Anchor plus Alias", + "yaml": "key1: &a value\nkey2: &b *a\n", + "error": true + }, + { + "id": "SSW6", + "name": "Spec Example 7.7. Single Quoted Characters [1.3]", + "yaml": "---\n'here''s to \"quotes\"'\n", + "error": false, + "json": [ + "here's to \"quotes\"" + ] + }, + { + "id": "SU5Z", + "name": "Comment without whitespace after doublequoted scalar", + "yaml": "key: \"value\"# invalid comment\n", + "error": true + }, + { + "id": "SU74", + "name": "Anchor and alias as mapping key", + "yaml": "key1: &alias value1\n&b *alias : value2\n", + "error": true + }, + { + "id": "SY6V", + "name": "Anchor before sequence entry on same line", + "yaml": "&anchor - sequence entry\n", + "error": true + }, + { + "id": "SYW4", + "name": "Spec Example 2.2. Mapping Scalars to Scalars", + "yaml": "hr: 65 # Home runs\navg: 0.278 # Batting average\nrbi: 147 # Runs Batted In\n", + "error": false, + "json": [ + { + "hr": 65, + "avg": 0.278, + "rbi": 147 + } + ] + }, + { + "id": "T26H", + "name": "Spec Example 8.8. Literal Content [1.3]", + "yaml": "--- |\n \n \n literal\n \n \n text\n\n # Comment\n", + "error": false, + "json": [ + "\n\nliteral\n \n\ntext\n" + ] + }, + { + "id": "T4YY", + "name": "Spec Example 7.9. Single Quoted Lines [1.3]", + "yaml": "---\n' 1st non-empty\n\n 2nd non-empty \n 3rd non-empty '\n", + "error": false, + "json": [ + " 1st non-empty\n2nd non-empty 3rd non-empty " + ] + }, + { + "id": "T5N4", + "name": "Spec Example 8.7. Literal Scalar [1.3]", + "yaml": "--- |\n literal\n \ttext\n\n\n", + "error": false, + "json": [ + "literal\n\ttext\n" + ] + }, + { + "id": "T833", + "name": "Flow mapping missing a separating comma", + "yaml": "---\n{\n foo: 1\n bar: 2 }\n", + "error": true + }, + { + "id": "TD5N", + "name": "Invalid scalar after sequence", + "yaml": "- item1\n- item2\ninvalid\n", + "error": true + }, + { + "id": "TE2A", + "name": "Spec Example 8.16. Block Mappings", + "yaml": "block mapping:\n key: value\n", + "error": false, + "json": [ + { + "block mapping": { + "key": "value" + } + } + ] + }, + { + "id": "TL85", + "name": "Spec Example 6.8. Flow Folding", + "yaml": "\"\n foo \n \n \t bar\n\n baz\n\"\n", + "error": false, + "json": [ + " foo\nbar\nbaz " + ] + }, + { + "id": "TS54", + "name": "Folded Block Scalar", + "yaml": ">\n ab\n cd\n \n ef\n\n\n gh\n", + "error": false, + "json": [ + "ab cd\nef\n\ngh\n" + ] + }, + { + "id": "U3C3", + "name": "Spec Example 6.16. \u201cTAG\u201d directive", + "yaml": "%TAG !yaml! tag:yaml.org,2002:\n---\n!yaml!str \"foo\"\n", + "error": false, + "json": [ + "foo" + ] + }, + { + "id": "U3XV", + "name": "Node and Mapping Key Anchors", + "yaml": "---\ntop1: &node1\n &k1 key1: one\ntop2: &node2 # comment\n key2: two\ntop3:\n &k3 key3: three\ntop4:\n &node4\n &k4 key4: four\ntop5:\n &node5\n key5: five\ntop6: &val6\n six\ntop7:\n &val7 seven\n", + "error": false, + "json": [ + { + "top1": { + "key1": "one" + }, + "top2": { + "key2": "two" + }, + "top3": { + "key3": "three" + }, + "top4": { + "key4": "four" + }, + "top5": { + "key5": "five" + }, + "top6": "six", + "top7": "seven" + } + ] + }, + { + "id": "U44R", + "name": "Bad indentation in mapping (2)", + "yaml": "map:\n key1: \"quoted1\"\n key2: \"bad indentation\"\n", + "error": true + }, + { + "id": "U99R", + "name": "Invalid comma in tag", + "yaml": "- !!str, xxx\n", + "error": true + }, + { + "id": "U9NS", + "name": "Spec Example 2.8. Play by Play Feed from a Game", + "yaml": "---\ntime: 20:03:20\nplayer: Sammy Sosa\naction: strike (miss)\n...\n---\ntime: 20:03:47\nplayer: Sammy Sosa\naction: grand slam\n...\n", + "error": false, + "json": [ + { + "time": "20:03:20", + "player": "Sammy Sosa", + "action": "strike (miss)" + }, + { + "time": "20:03:47", + "player": "Sammy Sosa", + "action": "grand slam" + } + ] + }, + { + "id": "UDM2", + "name": "Plain URL in flow mapping", + "yaml": "- { url: http://example.org }\n", + "error": false, + "json": [ + [ + { + "url": "http://example.org" + } + ] + ] + }, + { + "id": "UDR7", + "name": "Spec Example 5.4. Flow Collection Indicators", + "yaml": "sequence: [ one, two, ]\nmapping: { sky: blue, sea: green }\n", + "error": false, + "json": [ + { + "sequence": [ + "one", + "two" + ], + "mapping": { + "sky": "blue", + "sea": "green" + } + } + ] + }, + { + "id": "UGM3", + "name": "Spec Example 2.27. Invoice", + "yaml": "--- !\ninvoice: 34843\ndate : 2001-01-23\nbill-to: &id001\n given : Chris\n family : Dumars\n address:\n lines: |\n 458 Walkman Dr.\n Suite #292\n city : Royal Oak\n state : MI\n postal : 48046\nship-to: *id001\nproduct:\n - sku : BL394D\n quantity : 4\n description : Basketball\n price : 450.00\n - sku : BL4438H\n quantity : 1\n description : Super Hoop\n price : 2392.00\ntax : 251.42\ntotal: 4443.52\ncomments:\n Late afternoon is best.\n Backup contact is Nancy\n Billsmer @ 338-4338.\n", + "error": false, + "json": [ + { + "invoice": 34843, + "date": "2001-01-23", + "bill-to": { + "given": "Chris", + "family": "Dumars", + "address": { + "lines": "458 Walkman Dr.\nSuite #292\n", + "city": "Royal Oak", + "state": "MI", + "postal": 48046 + } + }, + "ship-to": { + "given": "Chris", + "family": "Dumars", + "address": { + "lines": "458 Walkman Dr.\nSuite #292\n", + "city": "Royal Oak", + "state": "MI", + "postal": 48046 + } + }, + "product": [ + { + "sku": "BL394D", + "quantity": 4, + "description": "Basketball", + "price": 450 + }, + { + "sku": "BL4438H", + "quantity": 1, + "description": "Super Hoop", + "price": 2392 + } + ], + "tax": 251.42, + "total": 4443.52, + "comments": "Late afternoon is best. Backup contact is Nancy Billsmer @ 338-4338." + } + ] + }, + { + "id": "UKK6/00", + "name": "Syntax character edge cases", + "yaml": "- :\n", + "error": false + }, + { + "id": "UKK6/01", + "name": "Syntax character edge cases", + "yaml": "::\n", + "error": false, + "json": [ + { + ":": null + } + ] + }, + { + "id": "UKK6/02", + "name": "Syntax character edge cases", + "yaml": "!\n", + "error": false + }, + { + "id": "UT92", + "name": "Spec Example 9.4. Explicit Documents", + "yaml": "---\n{ matches\n% : 20 }\n...\n---\n# Empty\n...\n", + "error": false, + "json": [ + { + "matches %": 20 + }, + null + ] + }, + { + "id": "UV7Q", + "name": "Legal tab after indentation", + "yaml": "x:\n - x\n \tx\n", + "error": false, + "json": [ + { + "x": [ + "x x" + ] + } + ] + }, + { + "id": "V55R", + "name": "Aliases in Block Sequence", + "yaml": "- &a a\n- &b b\n- *a\n- *b\n", + "error": false, + "json": [ + [ + "a", + "b", + "a", + "b" + ] + ] + }, + { + "id": "V9D5", + "name": "Spec Example 8.19. Compact Block Mappings", + "yaml": "- sun: yellow\n- ? earth: blue\n : moon: white\n", + "error": false + }, + { + "id": "VJP3/00", + "name": "Flow collections over many lines", + "yaml": "k: {\nk\n:\nv\n}\n", + "error": true + }, + { + "id": "VJP3/01", + "name": "Flow collections over many lines", + "yaml": "k: {\n k\n :\n v\n }\n", + "error": false, + "json": [ + { + "k": { + "k": "v" + } + } + ] + }, + { + "id": "W42U", + "name": "Spec Example 8.15. Block Sequence Entry Types", + "yaml": "- # Empty\n- |\n block node\n- - one # Compact\n - two # sequence\n- one: two # Compact mapping\n", + "error": false, + "json": [ + [ + null, + "block node\n", + [ + "one", + "two" + ], + { + "one": "two" + } + ] + ] + }, + { + "id": "W4TN", + "name": "Spec Example 9.5. Directives Documents", + "yaml": "%YAML 1.2\n--- |\n%!PS-Adobe-2.0\n...\n%YAML 1.2\n---\n# Empty\n...\n", + "error": false, + "json": [ + "%!PS-Adobe-2.0\n", + null + ] + }, + { + "id": "W5VH", + "name": "Allowed characters in alias", + "yaml": "a: &:@*!$\": scalar a\nb: *:@*!$\":\n", + "error": false, + "json": [ + { + "a": "scalar a", + "b": "scalar a" + } + ] + }, + { + "id": "W9L4", + "name": "Literal block scalar with more spaces in first line", + "yaml": "---\nblock scalar: |\n \n more spaces at the beginning\n are invalid\n", + "error": true + }, + { + "id": "WZ62", + "name": "Spec Example 7.2. Empty Content", + "yaml": "{\n foo : !!str,\n !!str : bar,\n}\n", + "error": false, + "json": [ + { + "foo": "", + "": "bar" + } + ] + }, + { + "id": "X38W", + "name": "Aliases in Flow Objects", + "yaml": "{ &a [a, &b b]: *b, *a : [c, *b, d]}\n", + "error": false + }, + { + "id": "X4QW", + "name": "Comment without whitespace after block scalar indicator", + "yaml": "block: ># comment\n scalar\n", + "error": true + }, + { + "id": "X8DW", + "name": "Explicit key and value seperated by comment", + "yaml": "---\n? key\n# comment\n: value\n", + "error": false, + "json": [ + { + "key": "value" + } + ] + }, + { + "id": "XLQ9", + "name": "Multiline scalar that looks like a YAML directive", + "yaml": "---\nscalar\n%YAML 1.2\n", + "error": false, + "json": [ + "scalar %YAML 1.2" + ] + }, + { + "id": "XV9V", + "name": "Spec Example 6.5. Empty Lines [1.3]", + "yaml": "Folding:\n \"Empty line\n\n as a line feed\"\nChomping: |\n Clipped empty lines\n \n\n", + "error": false, + "json": [ + { + "Folding": "Empty line\nas a line feed", + "Chomping": "Clipped empty lines\n" + } + ] + }, + { + "id": "XW4D", + "name": "Various Trailing Comments", + "yaml": "a: \"double\n quotes\" # lala\nb: plain\n value # lala\nc : #lala\n d\n? # lala\n - seq1\n: # lala\n - #lala\n seq2\ne:\n &node # lala\n - x: y\nblock: > # lala\n abcde\n", + "error": false + }, + { + "id": "Y2GN", + "name": "Anchor with colon in the middle", + "yaml": "---\nkey: &an:chor value\n", + "error": false, + "json": [ + { + "key": "value" + } + ] + }, + { + "id": "Y79Y/000", + "name": "Tabs in various contexts", + "yaml": "foo: |\n\t\nbar: 1\n", + "error": true + }, + { + "id": "Y79Y/001", + "name": "Tabs in various contexts", + "yaml": "foo: |\n \t\nbar: 1\n", + "error": false, + "json": [ + { + "foo": "\t\n", + "bar": 1 + } + ] + }, + { + "id": "Y79Y/002", + "name": "Tabs in various contexts", + "yaml": "- [\n\t\n foo\n ]\n", + "error": false, + "json": [ + [ + [ + "foo" + ] + ] + ] + }, + { + "id": "Y79Y/003", + "name": "Tabs in various contexts", + "yaml": "- [\n\tfoo,\n foo\n ]\n", + "error": true + }, + { + "id": "Y79Y/004", + "name": "Tabs in various contexts", + "yaml": "-\t-\n", + "error": true + }, + { + "id": "Y79Y/005", + "name": "Tabs in various contexts", + "yaml": "- \t-\n", + "error": true + }, + { + "id": "Y79Y/006", + "name": "Tabs in various contexts", + "yaml": "?\t-\n", + "error": true + }, + { + "id": "Y79Y/007", + "name": "Tabs in various contexts", + "yaml": "? -\n:\t-\n", + "error": true + }, + { + "id": "Y79Y/008", + "name": "Tabs in various contexts", + "yaml": "?\tkey:\n", + "error": true + }, + { + "id": "Y79Y/009", + "name": "Tabs in various contexts", + "yaml": "? key:\n:\tkey:\n", + "error": true + }, + { + "id": "Y79Y/010", + "name": "Tabs in various contexts", + "yaml": "-\t-1\n", + "error": false, + "json": [ + [ + -1 + ] + ] + }, + { + "id": "YD5X", + "name": "Spec Example 2.5. Sequence of Sequences", + "yaml": "- [name , hr, avg ]\n- [Mark McGwire, 65, 0.278]\n- [Sammy Sosa , 63, 0.288]\n", + "error": false, + "json": [ + [ + [ + "name", + "hr", + "avg" + ], + [ + "Mark McGwire", + 65, + 0.278 + ], + [ + "Sammy Sosa", + 63, + 0.288 + ] + ] + ] + }, + { + "id": "YJV2", + "name": "Dash in flow sequence", + "yaml": "[-]\n", + "error": true + }, + { + "id": "Z67P", + "name": "Spec Example 8.21. Block Scalar Nodes [1.3]", + "yaml": "literal: |2\n value\nfolded: !foo >1\n value\n", + "error": false, + "json": [ + { + "literal": "value\n", + "folded": "value\n" + } + ] + }, + { + "id": "Z9M4", + "name": "Spec Example 6.22. Global Tag Prefix", + "yaml": "%TAG !e! tag:example.com,2000:app/\n---\n- !e!foo \"bar\"\n", + "error": false, + "json": [ + [ + "bar" + ] + ] + }, + { + "id": "ZCZ6", + "name": "Invalid mapping in plain single line value", + "yaml": "a: b: c: d\n", + "error": true + }, + { + "id": "ZF4X", + "name": "Spec Example 2.6. Mapping of Mappings", + "yaml": "Mark McGwire: {hr: 65, avg: 0.278}\nSammy Sosa: {\n hr: 63,\n avg: 0.288\n }\n", + "error": false, + "json": [ + { + "Mark McGwire": { + "hr": 65, + "avg": 0.278 + }, + "Sammy Sosa": { + "hr": 63, + "avg": 0.288 + } + } + ] + }, + { + "id": "ZH7C", + "name": "Anchors in Mapping", + "yaml": "&a a: b\nc: &d d\n", + "error": false, + "json": [ + { + "a": "b", + "c": "d" + } + ] + }, + { + "id": "ZK9H", + "name": "Nested top level flow mapping", + "yaml": "{ key: [[[\n value\n ]]]\n}\n", + "error": false, + "json": [ + { + "key": [ + [ + [ + "value" + ] + ] + ] + } + ] + }, + { + "id": "ZL4Z", + "name": "Invalid nested mapping", + "yaml": "---\na: 'b': c\n", + "error": true + }, + { + "id": "ZVH3", + "name": "Wrong indented sequence item", + "yaml": "- key: value\n - item1\n", + "error": true + }, + { + "id": "ZWK4", + "name": "Key with anchor after missing explicit mapping value", + "yaml": "---\na: 1\n? b\n&anchor c: 3\n", + "error": false, + "json": [ + { + "a": 1, + "b": null, + "c": 3 + } + ] + }, + { + "id": "ZXT5", + "name": "Implicit key followed by newline and adjacent value", + "yaml": "[ \"key\"\n :value ]\n", + "error": true + } +] \ No newline at end of file diff --git a/test/parallel/test-module-builtin-experimental.js b/test/parallel/test-module-builtin-experimental.js index 444c254b4ffd..46f17bcf1f39 100644 --- a/test/parallel/test-module-builtin-experimental.js +++ b/test/parallel/test-module-builtin-experimental.js @@ -9,6 +9,7 @@ const experimentalBuiltins = [ ['bench/reporters', '--experimental-bench', true], ['dtls', '--experimental-dtls', common.hasDtls], ['quic', '--experimental-quic', common.hasQuic], + ['yaml', '--experimental-yaml', true], ].filter(([, , available]) => available); for (const [id, flag] of experimentalBuiltins) { diff --git a/test/parallel/test-process-get-builtin.mjs b/test/parallel/test-process-get-builtin.mjs index a4feabf5b93c..a180d79f0e82 100644 --- a/test/parallel/test-process-get-builtin.mjs +++ b/test/parallel/test-process-get-builtin.mjs @@ -40,6 +40,8 @@ publicBuiltins.delete('node:dtls'); publicBuiltins.delete('node:quic'); // Remove this once node:vfs graduates from unflagged. publicBuiltins.delete('node:vfs'); +// Remove this once node:yaml graduates from unflagged. +publicBuiltins.delete('node:yaml'); if (!hasInspector) { publicBuiltins.delete('inspector'); diff --git a/test/parallel/test-require-resolve.js b/test/parallel/test-require-resolve.js index 984afa78a1a9..794f062a7692 100644 --- a/test/parallel/test-require-resolve.js +++ b/test/parallel/test-require-resolve.js @@ -62,6 +62,8 @@ require(fixtures.path('resolve-paths', 'default', 'verify-paths.js')); builtinModules.forEach((mod) => { // TODO(@jasnell): Remove once node:quic is no longer flagged if (mod === 'node:quic') return; + // Remove once node:yaml is no longer flagged + if (mod === 'node:yaml') return; if (mod === 'node:sqlite' && !common.hasSQLite) return; assert.strictEqual(require.resolve.paths(mod), null); if (!mod.startsWith('node:')) { diff --git a/test/parallel/test-yaml-test-suite.js b/test/parallel/test-yaml-test-suite.js new file mode 100644 index 000000000000..cccc0cdb6d6a --- /dev/null +++ b/test/parallel/test-yaml-test-suite.js @@ -0,0 +1,57 @@ +// Flags: --expose-internals +'use strict'; + +// Runs the official YAML test suite (https://github.com/yaml/yaml-test-suite, +// data branch, commit 6ad3d2c62885d82fc349026c136ef560838fdf3d) against the +// parser. test/fixtures/yaml-test-suite.json holds, for every test, the input +// and either the expected documents as JSON or a flag that the input is an +// error. + +require('../common'); +const assert = require('node:assert'); +const fixtures = require('../common/fixtures'); +const { parseDocuments } = require('internal/yaml/parser'); + +// Tests the parser does not pass yet. A test that starts to pass must be +// removed from this list. +const knownFailures = new Set([ + // Duplicate keys, two empty keys and an alias of the first key: the parser + // rejects them, which the suite does not check. + '2JQS', + 'X38W', +]); + +const tests = JSON.parse(fixtures.readSync('yaml-test-suite.json', 'utf8')); +assert.ok(tests.length > 300); + +const failures = []; +let passed = 0; +for (const test of tests) { + let failure; + try { + const documents = parseDocuments(test.yaml); + if (test.error) { + failure = `expected an error, parsed ${JSON.stringify(documents)}`; + } else if (test.json !== undefined) { + assert.deepStrictEqual(documents, test.json); + } + } catch (error) { + if (!test.error) { + failure = error.message; + } else if (error.code !== 'ERR_INVALID_YAML') { + failure = `unexpected ${error.name}: ${error.message}`; + } + } + if (failure === undefined) { + if (knownFailures.has(test.id)) { + failures.push(`${test.id} (${test.name}) passes and must leave knownFailures`); + } else { + passed++; + } + } else if (!knownFailures.has(test.id)) { + failures.push(`${test.id} (${test.name}): ${failure}`); + } +} + +assert.deepStrictEqual(failures, [], `${failures.length} failures:\n${failures.join('\n')}`); +assert.ok(passed > 0); diff --git a/test/parallel/test-yaml.js b/test/parallel/test-yaml.js new file mode 100644 index 000000000000..fbbea0f7d005 --- /dev/null +++ b/test/parallel/test-yaml.js @@ -0,0 +1,413 @@ +// Flags: --experimental-yaml +'use strict'; + +const common = require('../common'); +const assert = require('node:assert'); +const { parse } = require('node:yaml'); + +common.expectWarning('ExperimentalWarning', 'YAML is an experimental feature and might change at any time'); + +// Scalars of the core schema. +assert.deepStrictEqual(parse(` +nothing: ~ +also: null +empty: +yes: true +no: FALSE +int: -42 +hex: 0x1F +octal: 0o17 +float: 1.5e3 +inf: -.inf +str: yes +version: 1.2.3 +quoted: "0x10" +`), { + nothing: null, + also: null, + empty: null, + yes: true, + no: false, + int: -42, + hex: 31, + octal: 15, + float: 1500, + inf: -Infinity, + str: 'yes', + version: '1.2.3', + quoted: '0x10', +}); +assert.ok(Number.isNaN(parse('.NaN'))); + +// Collections. +assert.deepStrictEqual(parse(` +- a +- - nested + - seq +- key: value + other: 2 +- + late: true +- [1, 2, {x: y}] +`), [ + 'a', + ['nested', 'seq'], + { key: 'value', other: 2 }, + { late: true }, + [1, 2, { x: 'y' }], +]); + +assert.deepStrictEqual(parse(` +map: + seq: + - compact + - entries + flow: { a: 1, "b":2, c, d: } +? explicit key +: explicit value +? only key +`), { + 'map': { seq: ['compact', 'entries'], flow: { a: 1, b: 2, c: null, d: null } }, + 'explicit key': 'explicit value', + 'only key': null, +}); + +assert.deepStrictEqual(parse('[a: 1, b, "c": d]'), + [{ a: 1 }, 'b', { c: 'd' }]); +assert.deepStrictEqual(parse('{ 1: one, true: yes, ~: nothing }'), + { 1: 'one', true: 'yes', null: 'nothing' }); +// Collection keys become their JSON text. +assert.deepStrictEqual(parse('? [a, b]\n: c\n{x: 1}: d'), + { '["a","b"]': 'c', '{"x":1}': 'd' }); + +// Scalar styles. +assert.deepStrictEqual(parse(` +plain: a multi + line plain + + scalar +single: 'it''s + folded' +double: "tab\\there\\ + joined \\u00e9 \\x41" +literal: | + line one + indented + + line three +folded: >- + folded + text + + kept +keep: |+ + text + +strip: |- + text +indicator: |2 + two extra +`), { + plain: 'a multi line plain\nscalar', + single: "it's folded", + double: 'tab\therejoined \u00e9 A', + literal: 'line one\n indented\n\nline three\n', + folded: 'folded text\nkept', + keep: 'text\n\n', + strip: 'text', + indicator: ' two extra\n', +}); + +// Anchors, aliases and tags. +{ + const value = parse(` +base: &base + name: base +copy: *base +list: &list + - *base + - *list +str: !!str 123 +int: !!int "42" +float: !!float 1 +tagged: !custom 123 +`); + assert.strictEqual(value.copy, value.base); + assert.strictEqual(value.list[0], value.base); + assert.strictEqual(value.list[1], value.list); + assert.strictEqual(value.str, '123'); + assert.strictEqual(value.int, 42); + assert.deepStrictEqual(parse('[!!int -7, !!int 0x1f, !!int 0o17, !!float 2, !!float .inf]'), + [-7, 31, 15, 2, Infinity]); + assert.strictEqual(value.float, 1); + assert.strictEqual(value.tagged, '123'); +} + +// Documents. +assert.strictEqual(parse(''), null); +assert.strictEqual(parse('# only a comment\n'), null); +assert.strictEqual(parse('---\n'), null); +assert.strictEqual(parse('--- text\n...\n'), 'text'); +assert.deepStrictEqual(parse('%YAML 1.2\n---\na: 1\n...\n---\n- 2\n'), + [{ a: 1 }, [2]]); +assert.strictEqual(parse('%TAG !e! tag:example.com,2000:\n--- !e!thing\nx\n'), 'x'); +assert.deepStrictEqual(parse('...\n---\n...\nvalue\n...\n...\n- 1'), [null, 'value', [1]]); +assert.strictEqual(parse('!!binary abc'), 'abc'); +assert.strictEqual(parse('\uFEFFbom'), 'bom'); +assert.deepStrictEqual(parse('\uFEFF a: 1\n b: 2'), { a: 1, b: 2 }); + +// Binary input is decoded as UTF-8. +assert.deepStrictEqual(parse(Buffer.from('caf\u00e9: 1')), { 'caf\u00e9': 1 }); +assert.deepStrictEqual(parse(new TextEncoder().encode('[1]').buffer), [1]); +{ + const bytes = new TextEncoder().encode('xx[2]yy'); + assert.deepStrictEqual(parse(new Uint8Array(bytes.buffer, 2, 3)), [2]); + assert.deepStrictEqual(parse(new DataView(bytes.buffer, 2, 3)), [2]); + const shared = new SharedArrayBuffer(3); + new Uint8Array(shared).set(bytes.subarray(2, 5)); + assert.deepStrictEqual(parse(shared), [2]); + assert.deepStrictEqual(parse(new Uint8Array(shared)), [2]); +} + +// Prototype keys are own properties, as with JSON.parse(). +{ + const value = parse('__proto__: 1\nconstructor: 2\n'); + assert.strictEqual(Object.getPrototypeOf(value), Object.prototype); + assert.deepStrictEqual(Object.keys(value), ['__proto__', 'constructor']); + assert.strictEqual(Object.getOwnPropertyDescriptor(value, '__proto__').value, 1); +} + +// Errors. +for (const input of [1, null, undefined, {}, [], Symbol('x')]) { + assert.throws(() => parse(input), { code: 'ERR_INVALID_ARG_TYPE' }); +} +const invalid = [ + ['a: 1\n b: 2', /Implicit mapping keys must be on a single line \(line 1, column 4\)/], + ['a: [1]\n b: 2', /Bad indentation \(line 2, column 3\)/], + ['a: 1\na: 2', /Duplicate mapping key "a" \(line 2, column 1\)/], + ['[1, 2', /Unterminated flow collection/], + ['"open', /Unterminated double-quoted scalar \(line 1, column 1\)/], + ['"\\q"', /Unknown escape sequence "\\q"/], + ['*missing', /Unknown anchor "missing"/], + ['a\n b: 1', /Implicit mapping keys must be on a single line/], + ['!!int abc', /"abc" is not an integer/], + ['!!int 1.0', /"1\.0" is not an integer/], + ['!!int 1e2', /"1e2" is not an integer/], + ['!!int 0x', /"0x" is not an integer/], + ['!!float abc', /"abc" is not a float/], + ['!!seq {}', /cannot be applied to a mapping/], + ['%YAML 1.2\na: 1', /Directives must be followed by "---"/], + ['%YAML 2.0\n---\na', /Unsupported YAML version "2\.0"/], + ['a: |\n x\n b', /Bad indentation/], + ['a:\n\tb: 1', /Tabs cannot be used for indentation/], + ['key: "a"#c', /Comments must be separated/], + ['... trailing', /Unexpected content after "\.\.\."/], + ['--- a: 1', /A block mapping is not allowed here/], + ['key: - a', /A block sequence is not allowed here/], + ['a: b: c', /A block mapping is not allowed here/], + ['-\t- a', /Tabs cannot be used for indentation/], + ['key: "a\nb"', /Bad indentation in a quoted scalar/], + ['key: [a,\nb]', /Bad indentation in a flow collection/], +]; +for (const [input, message] of invalid) { + assert.throws(() => parse(input), { + code: 'ERR_INVALID_YAML', + name: 'SyntaxError', + message, + }, input); +} + +// Line endings, leading spaces and the special floats. +assert.deepStrictEqual(parse('a: 1\r\nb: |\r\n x\r\n\r\n y\rc: 3\r'), + { a: 1, b: 'x\n\ny\n', c: 3 }); +assert.deepStrictEqual(parse(' a: 1\n b: 2'), { a: 1, b: 2 }); +assert.ok(Number.isNaN(parse('.nan'))); +assert.deepStrictEqual(parse('[.inf, +.INF, -.Inf, -0, +3, 1e3, 1E+3, 1.5e-1]'), + [Infinity, Infinity, -Infinity, 0, 3, 1000, 1000, 0.15]); +// Things that look like numbers but are strings in the core schema. +assert.deepStrictEqual(parse('[0xZZ, 0o9, 1e, 1e+, 1.2.3, +, ., ~x, 0x, 1_000, .e1]'), + ['0xZZ', '0o9', '1e', '1e+', '1.2.3', '+', '.', '~x', '0x', '1_000', '.e1']); + +// Every escape sequence of double-quoted scalars. +assert.strictEqual( + parse('"\\0\\a\\b\\t\\\t\\n\\v\\f\\r\\e\\ \\"\\/\\\\\\N\\_\\L\\P\\x41\\u00e9\\U0001F600"'), + '\0\x07\b\t\t\n\v\f\r\x1B "/\\\x85\xA0\u2028\u2029A\u00e9\u{1F600}'); + +// Tags. +assert.deepStrictEqual(parse('%TAG !e! tag:example.com,2000:\n--- [!e!a%41 x, ! y, !!null ~, !!bool True]'), + ['x', 'y', null, true]); +{ + const value = parse('&a\n[1, 2]\n'); + assert.deepStrictEqual(value, [1, 2]); + assert.deepStrictEqual(parse('- &a\n [1]\n- *a')[1], [1]); +} +assert.deepStrictEqual(parse('x: 1\n[a, b]: 2\n{c: 3}: 4'), + { 'x': 1, '["a","b"]': 2, '{"c":3}': 4 }); +assert.deepStrictEqual(parse('[ ? a, ? b : c, ? : d ]'), [{ a: null }, { b: 'c' }, { null: 'd' }]); +assert.deepStrictEqual(parse('? a\n? b\n:\n? \n'), { a: null, b: null, null: null }); +assert.strictEqual(parse('|+\n\n\n'), '\n\n'); +assert.strictEqual(parse('- >+\n\n\n')[0], '\n\n'); +assert.strictEqual(parse('|-\n\n'), ''); +assert.strictEqual(parse('a: |\n text\n ').a, 'text\n'); +assert.strictEqual(parse('a: |+\n text\n ').a, 'text\n\n'); +assert.strictEqual(parse('a: |+\n\n').a, '\n'); +assert.deepStrictEqual(parse('a: |+\n\nb: 1'), { a: '\n', b: 1 }); +assert.deepStrictEqual(parse('{:}'), { null: null }); +assert.deepStrictEqual(parse('[ ? ]'), [{ null: null }]); +// Properties at the end of the input are an empty scalar. +assert.strictEqual(parse('!!str'), ''); +assert.strictEqual(parse('&a #c'), null); +assert.deepStrictEqual(parse('a: !!str'), { a: '' }); +{ + // An alias may refer to the collection that holds it, with the anchor on + // the same line or on the line before. + const value = parse('...\n--- &a [*a, {b: *a}]'); + assert.strictEqual(value[0], value); + assert.strictEqual(value[1].b, value); + const seq = parse('&a\n[*a]'); + assert.strictEqual(seq[0], seq); + const map = parse('&a\n{k: *a}'); + assert.strictEqual(map.k, map); + const block = parse('&a\n- *a\n- &b\n k: *b'); + assert.strictEqual(block[0], block); + assert.strictEqual(block[1].k, block[1]); + const nested = parse('key: &a\n [*a]\nother: !!seq &b\n - *b'); + assert.strictEqual(nested.key[0], nested.key); + assert.strictEqual(nested.other[0], nested.other); +} + +// Raw control characters are rejected, their escaped forms are not. +assert.strictEqual(parse('"\\0\\a\\x7f\\x85\\x9f"'), '\0\x07\x7f\x85\x9f'); +for (const c of ['\0', '\x07', '\x0b', '\x1f', '\x7f', '\x80', '\x9f', '\ufffe']) { + const expected = { code: 'ERR_INVALID_YAML', message: /is not allowed in YAML/ }; + assert.throws(() => parse(`"a${c}b"`), expected); + assert.throws(() => parse(`'a${c}b'`), expected); + assert.throws(() => parse(`a${c}b`), expected); + assert.throws(() => parse(`|\n a${c}b`), expected); + assert.throws(() => parse(`# ${c}`), expected); +} +assert.strictEqual(parse('"\u0085\u00a0\u2028\ud83d\ude00"'), '\u0085\u00a0\u2028\ud83d\ude00'); +assert.strictEqual(parse('"😀"'), '😀'); +for (const lone of ['\ud83d', '\ude00', '\ude00\ud83d', '\ud83d\ud83d']) { + assert.throws(() => parse(`"${lone}"`), { code: 'ERR_INVALID_YAML', message: /U\+d(83d|e00) is not allowed/ }); +} + +// An escaped line break joins the lines; empty lines in between are kept. +assert.strictEqual(parse('"a\\\n b"'), 'ab'); +assert.strictEqual(parse('"a\\\n\n\n b"'), 'a\n\nb'); +assert.strictEqual(parse('key: "a \\\n \\ b"').key, 'a b'); +assert.throws(() => parse('key: "a\\\nb"'), { code: 'ERR_INVALID_YAML', message: /Bad indentation in a quoted scalar/ }); +assert.throws(() => parse('"a\\\n---\nb"'), { code: 'ERR_INVALID_YAML', message: /Unexpected document marker inside a quoted scalar/ }); +assert.throws(() => parse('"a\\\n'), { code: 'ERR_INVALID_YAML', message: /Unterminated quoted scalar/ }); + +// A collection key is bounded, so aliases cannot blow it up. +{ + let yaml = 'a0: &a0 [x, x, x, x, x, x, x, x, x, x]\n'; + for (let i = 1; i < 12; i++) { + const prev = `*a${i - 1}`; + yaml += `a${i}: &a${i} [${Array(10).fill(prev).join(', ')}]\n`; + } + assert.throws(() => parse(`${yaml}? *a11\n: 1\n`), { + code: 'ERR_INVALID_YAML', + message: /A collection key must not exceed 1024 characters \(line 13, column 1\)/, + }); + assert.strictEqual(parse(yaml).a11.length, 10); +} +assert.deepStrictEqual(parse('- ? a'), [{ a: null }]); + +const moreInvalid = [ + ['key: "a\n b', /Unterminated double-quoted scalar \(line 1, column 6\)/], + ["'open", /Unterminated single-quoted scalar/], + ['"\\x4"', /Invalid hexadecimal escape sequence/], + ['"\\U00110000"', /Invalid code point in escape sequence/], + ['"\\UD800DC00"', /Invalid code point in escape sequence/], + ['"\\', /Unknown escape sequence/], + ['"a\\\n', /Unterminated quoted scalar/], + ['|--\n a', /Repeated chomping indicator/], + ['|22\n a', /Repeated indentation indicator/], + ['|0\n a', /Indentation indicator must be between 1 and 9/], + ['| x\n a', /Unexpected characters after block scalar header/], + ['|#c\n a', /Comments must be separated/], + ['& a', /Anchor name must not be empty/], + ['&a &b c', /A node can have only one anchor/], + ['!!str !!int 1', /A node can have only one tag/], + ['&a\n&b c', /A node can have only one anchor/], + ['!!str\n!!int 1', /A node can have only one tag/], + ['&a *b', /An alias cannot have properties/], + ['- &b b\n- &a\n *b', /An alias cannot have properties/], + ['[&a *b]', /An alias cannot have properties/], + ['&a *b: 1', /An alias cannot have properties/], + ['!e!x a', /Undeclared tag handle "!e!"/], + ['!!str%4 a', /Invalid escape in tag/], + ['! a', /Verbatim tag must not be empty/], + ['!a', /Unexpected characters after tag/], + ['!!str!x a', /Invalid character in tag/], + ['!! a', /Tag suffix must not be empty/], + ['!!bool yes', /"yes" is not a boolean/], + ['!!null x', /"x" is not null/], + ['!!map 1', /cannot be applied to a scalar/], + ['!!str [a]', /cannot be applied to a sequence/], + ['!!int {a: 1}', /cannot be applied to a mapping/], + ['[ | ]', /Block scalars are not allowed in flow collections/], + ['[ a, ]b', /Unexpected content after the document root/], + ['[ "a" b ]', /Expected "," or "\]" in flow sequence/], + ['[ , ]', /Unexpected character in flow sequence/], + ['{ , }', /Unexpected character in flow mapping/], + ['{ *a:1 }', /Unknown anchor/], + ['- &a x\n- { *a :b }', /Expected whitespace after ":" in flow mapping/], + ['&a ? b', /A block mapping is not allowed here/], + ['{ a: "1" b }', /Expected "," or "}" in flow mapping/], + ['{ a: 1\n', /Unterminated flow collection/], + ['[ a,\n--- ]', /Unexpected document marker inside a flow collection/], + ['"a\n---\n"', /Unexpected document marker inside a quoted scalar/], + ['? a\n- b', /Expected a mapping key/], + ['a: 1\n- b', /Unexpected sequence entry in a mapping/], + ['*a: 1', /Unknown anchor "a:"/], + ['- &a a\n- *a : 1\n *a x: 2', /Expected ":" after mapping key/], + ['a: 1\n"b" c: 2', /Expected ":" after mapping key/], + ['a: 1\n@b: 2', /Expected a mapping key/], + ['a: 1\n&x\nb: 2', /Expected a mapping key/], + ['%YAML 1.2#c\n---\na', /Invalid YAML version "1\.2#c"/], + ['%YAML 1.2\n%YAML 1.2\n---\na', /Repeated %YAML directive/], + ['%YAML\n---\na', /The %YAML directive takes one argument/], + ['%YAML x.2\n---\na', /Invalid YAML version "x\.2"/], + ['%YAML 1.x\n---\na', /Invalid YAML version "1\.x"/], + ['%TAG !e!\n---\na', /The %TAG directive takes two arguments/], + ['%TAG e! tag:x\n---\na', /Invalid tag handle "e!"/], + ['%TAG !e tag:x\n---\na', /Invalid tag handle "!e"/], + ['%TAG !e! tag:x\n%TAG !e! tag:y\n---\na', /Repeated tag handle "!e!"/], + ['%YAML 1.2', /Directives must be followed by "---"/], + ['%YAML 1.2\n...', /Directives must be followed by "---"/], + ['-\t- a', /Tabs cannot be used for indentation/], + ['- \t? a', /Tabs cannot be used for indentation/], + ['- \ta: 1', /Tabs cannot be used for indentation/], + ['- &a x\n-\t*a : v', /Tabs cannot be used for indentation/], + ['- &a x\n- k: *a : v', /A block mapping is not allowed here/], + ['a: : b', /A block mapping is not allowed here/], + ['-\t: b', /Tabs cannot be used for indentation/], + ['x: 1\ry: "a\r b', /Unterminated double-quoted scalar \(line 2, column 4\)/], + ['a: 1\n| x', /Expected a mapping key/], + ['!!int 0x1g', /"0x1g" is not an integer/], + ['!!int 0o8', /"0o8" is not an integer/], + ['!!int +', /"\+" is not an integer/], + ['[ a: }', /Expected "," or "\]" in flow sequence/], + ['{ a: ] }', /Expected "," or "}" in flow mapping/], + ['"a":b', /Unexpected content after the document root/], + ['[a]:b', /Unexpected content after the document root/], + ['a: 1\n"b":2', /Expected ":" after mapping key/], + ['a: 1\n&b', /Expected a mapping key/], + ['a:\n b\n\tc: 1', /Tabs cannot be used for indentation|Bad indentation/], +]; +for (const [input, message] of moreInvalid) { + assert.throws(() => parse(input), { + code: 'ERR_INVALID_YAML', + name: 'SyntaxError', + message, + }, input); +} + +// Deep nesting is bounded. +assert.throws(() => parse('['.repeat(1001) + ']'.repeat(1001)), + { code: 'ERR_INVALID_YAML', message: /Nesting deeper than 1000 levels/ }); +assert.ok(Array.isArray(parse('['.repeat(1000) + ']'.repeat(1000))));