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))));