// Ported from js-yaml v3.13.1: // https://github.com/nodeca/js-yaml/commit/665aadda42349dcae869f12040d9b10ef18d12da // Copyright 2011-2015 by Vitaly Puzrin. All rights reserved. MIT license. // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. /* eslint-disable max-len */ import { YAMLError } from "../error.ts"; import { Mark } from "../mark.ts"; import { Type } from "../type.ts"; import * as common from "../utils.ts"; import { LoaderState, LoaderStateOptions, ResultType } from "./loader_state.ts"; type Any = common.Any; type ArrayObject = common.ArrayObject; const _hasOwnProperty = Object.prototype.hasOwnProperty; const CONTEXT_FLOW_IN = 1; const CONTEXT_FLOW_OUT = 2; const CONTEXT_BLOCK_IN = 3; const CONTEXT_BLOCK_OUT = 4; const CHOMPING_CLIP = 1; const CHOMPING_STRIP = 2; const CHOMPING_KEEP = 3; const PATTERN_NON_PRINTABLE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/; const PATTERN_NON_ASCII_LINE_BREAKS = /[\x85\u2028\u2029]/; const PATTERN_FLOW_INDICATORS = /[,\[\]\{\}]/; const PATTERN_TAG_HANDLE = /^(?:!|!!|![a-z\-]+!)$/i; /* eslint-disable-next-line max-len */ const PATTERN_TAG_URI = /^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i; function _class(obj: unknown): string { return Object.prototype.toString.call(obj); } function isEOL(c: number): boolean { return c === 0x0a || /* LF */ c === 0x0d /* CR */; } function isWhiteSpace(c: number): boolean { return c === 0x09 || /* Tab */ c === 0x20 /* Space */; } function isWsOrEol(c: number): boolean { return ( c === 0x09 /* Tab */ || c === 0x20 /* Space */ || c === 0x0a /* LF */ || c === 0x0d /* CR */ ); } function isFlowIndicator(c: number): boolean { return ( c === 0x2c /* , */ || c === 0x5b /* [ */ || c === 0x5d /* ] */ || c === 0x7b /* { */ || c === 0x7d /* } */ ); } function fromHexCode(c: number): number { if (0x30 <= /* 0 */ c && c <= 0x39 /* 9 */) { return c - 0x30; } const lc = c | 0x20; if (0x61 <= /* a */ lc && lc <= 0x66 /* f */) { return lc - 0x61 + 10; } return -1; } function escapedHexLen(c: number): number { if (c === 0x78 /* x */) { return 2; } if (c === 0x75 /* u */) { return 4; } if (c === 0x55 /* U */) { return 8; } return 0; } function fromDecimalCode(c: number): number { if (0x30 <= /* 0 */ c && c <= 0x39 /* 9 */) { return c - 0x30; } return -1; } function simpleEscapeSequence(c: number): string { /* eslint:disable:prettier */ return c === 0x30 /* 0 */ ? "\x00" : c === 0x61 /* a */ ? "\x07" : c === 0x62 /* b */ ? "\x08" : c === 0x74 /* t */ ? "\x09" : c === 0x09 /* Tab */ ? "\x09" : c === 0x6e /* n */ ? "\x0A" : c === 0x76 /* v */ ? "\x0B" : c === 0x66 /* f */ ? "\x0C" : c === 0x72 /* r */ ? "\x0D" : c === 0x65 /* e */ ? "\x1B" : c === 0x20 /* Space */ ? " " : c === 0x22 /* " */ ? "\x22" : c === 0x2f /* / */ ? "/" : c === 0x5c /* \ */ ? "\x5C" : c === 0x4e /* N */ ? "\x85" : c === 0x5f /* _ */ ? "\xA0" : c === 0x4c /* L */ ? "\u2028" : c === 0x50 /* P */ ? "\u2029" : ""; /* eslint:enable:prettier */ } function charFromCodepoint(c: number): string { if (c <= 0xffff) { return String.fromCharCode(c); } // Encode UTF-16 surrogate pair // https://en.wikipedia.org/wiki/UTF-16#Code_points_U.2B010000_to_U.2B10FFFF return String.fromCharCode( ((c - 0x010000) >> 10) + 0xd800, ((c - 0x010000) & 0x03ff) + 0xdc00 ); } const simpleEscapeCheck = new Array(256); // integer, for fast access const simpleEscapeMap = new Array(256); for (let i = 0; i < 256; i++) { simpleEscapeCheck[i] = simpleEscapeSequence(i) ? 1 : 0; simpleEscapeMap[i] = simpleEscapeSequence(i); } function generateError(state: LoaderState, message: string): YAMLError { return new YAMLError( message, new Mark( state.filename as string, state.input, state.position, state.line, state.position - state.lineStart ) ); } function throwError(state: LoaderState, message: string): never { throw generateError(state, message); } function throwWarning(state: LoaderState, message: string): void { if (state.onWarning) { state.onWarning.call(null, generateError(state, message)); } } interface DirectiveHandlers { [directive: string]: ( state: LoaderState, name: string, ...args: string[] ) => void; } const directiveHandlers: DirectiveHandlers = { YAML(state, _name, ...args: string[]) { if (state.version !== null) { return throwError(state, "duplication of %YAML directive"); } if (args.length !== 1) { return throwError(state, "YAML directive accepts exactly one argument"); } const match = /^([0-9]+)\.([0-9]+)$/.exec(args[0]); if (match === null) { return throwError(state, "ill-formed argument of the YAML directive"); } const major = parseInt(match[1], 10); const minor = parseInt(match[2], 10); if (major !== 1) { return throwError(state, "unacceptable YAML version of the document"); } state.version = args[0]; state.checkLineBreaks = minor < 2; if (minor !== 1 && minor !== 2) { return throwWarning(state, "unsupported YAML version of the document"); } }, TAG(state, _name, ...args: string[]): void { if (args.length !== 2) { return throwError(state, "TAG directive accepts exactly two arguments"); } const handle = args[0]; const prefix = args[1]; if (!PATTERN_TAG_HANDLE.test(handle)) { return throwError( state, "ill-formed tag handle (first argument) of the TAG directive" ); } if (_hasOwnProperty.call(state.tagMap, handle)) { return throwError( state, `there is a previously declared suffix for "${handle}" tag handle` ); } if (!PATTERN_TAG_URI.test(prefix)) { return throwError( state, "ill-formed tag prefix (second argument) of the TAG directive" ); } if (typeof state.tagMap === "undefined") { state.tagMap = {}; } state.tagMap[handle] = prefix; }, }; function captureSegment( state: LoaderState, start: number, end: number, checkJson: boolean ): void { let result: string; if (start < end) { result = state.input.slice(start, end); if (checkJson) { for ( let position = 0, length = result.length; position < length; position++ ) { const character = result.charCodeAt(position); if ( !(character === 0x09 || (0x20 <= character && character <= 0x10ffff)) ) { return throwError(state, "expected valid JSON character"); } } } else if (PATTERN_NON_PRINTABLE.test(result)) { return throwError(state, "the stream contains non-printable characters"); } state.result += result; } } function mergeMappings( state: LoaderState, destination: ArrayObject, source: ArrayObject, overridableKeys: ArrayObject ): void { if (!common.isObject(source)) { return throwError( state, "cannot merge mappings; the provided source object is unacceptable" ); } const keys = Object.keys(source); for (let i = 0, len = keys.length; i < len; i++) { const key = keys[i]; if (!_hasOwnProperty.call(destination, key)) { destination[key] = (source as ArrayObject)[key]; overridableKeys[key] = true; } } } function storeMappingPair( state: LoaderState, result: ArrayObject | null, overridableKeys: ArrayObject, keyTag: string | null, keyNode: Any, valueNode: unknown, startLine?: number, startPos?: number ): ArrayObject { // The output is a plain object here, so keys can only be strings. // We need to convert keyNode to a string, but doing so can hang the process // (deeply nested arrays that explode exponentially using aliases). if (Array.isArray(keyNode)) { keyNode = Array.prototype.slice.call(keyNode); for (let index = 0, quantity = keyNode.length; index < quantity; index++) { if (Array.isArray(keyNode[index])) { return throwError(state, "nested arrays are not supported inside keys"); } if ( typeof keyNode === "object" && _class(keyNode[index]) === "[object Object]" ) { keyNode[index] = "[object Object]"; } } } // Avoid code execution in load() via toString property // (still use its own toString for arrays, timestamps, // and whatever user schema extensions happen to have @@toStringTag) if (typeof keyNode === "object" && _class(keyNode) === "[object Object]") { keyNode = "[object Object]"; } keyNode = String(keyNode); if (result === null) { result = {}; } if (keyTag === "tag:yaml.org,2002:merge") { if (Array.isArray(valueNode)) { for ( let index = 0, quantity = valueNode.length; index < quantity; index++ ) { mergeMappings(state, result, valueNode[index], overridableKeys); } } else { mergeMappings(state, result, valueNode as ArrayObject, overridableKeys); } } else { if ( !state.json && !_hasOwnProperty.call(overridableKeys, keyNode) && _hasOwnProperty.call(result, keyNode) ) { state.line = startLine || state.line; state.position = startPos || state.position; return throwError(state, "duplicated mapping key"); } result[keyNode] = valueNode; delete overridableKeys[keyNode]; } return result; } function readLineBreak(state: LoaderState): void { const ch = state.input.charCodeAt(state.position); if (ch === 0x0a /* LF */) { state.position++; } else if (ch === 0x0d /* CR */) { state.position++; if (state.input.charCodeAt(state.position) === 0x0a /* LF */) { state.position++; } } else { return throwError(state, "a line break is expected"); } state.line += 1; state.lineStart = state.position; } function skipSeparationSpace( state: LoaderState, allowComments: boolean, checkIndent: number ): number { let lineBreaks = 0, ch = state.input.charCodeAt(state.position); while (ch !== 0) { while (isWhiteSpace(ch)) { ch = state.input.charCodeAt(++state.position); } if (allowComments && ch === 0x23 /* # */) { do { ch = state.input.charCodeAt(++state.position); } while (ch !== 0x0a && /* LF */ ch !== 0x0d && /* CR */ ch !== 0); } if (isEOL(ch)) { readLineBreak(state); ch = state.input.charCodeAt(state.position); lineBreaks++; state.lineIndent = 0; while (ch === 0x20 /* Space */) { state.lineIndent++; ch = state.input.charCodeAt(++state.position); } } else { break; } } if ( checkIndent !== -1 && lineBreaks !== 0 && state.lineIndent < checkIndent ) { throwWarning(state, "deficient indentation"); } return lineBreaks; } function testDocumentSeparator(state: LoaderState): boolean { let _position = state.position; let ch = state.input.charCodeAt(_position); // Condition state.position === state.lineStart is tested // in parent on each call, for efficiency. No needs to test here again. if ( (ch === 0x2d || /* - */ ch === 0x2e) /* . */ && ch === state.input.charCodeAt(_position + 1) && ch === state.input.charCodeAt(_position + 2) ) { _position += 3; ch = state.input.charCodeAt(_position); if (ch === 0 || isWsOrEol(ch)) { return true; } } return false; } function writeFoldedLines(state: LoaderState, count: number): void { if (count === 1) { state.result += " "; } else if (count > 1) { state.result += common.repeat("\n", count - 1); } } function readPlainScalar( state: LoaderState, nodeIndent: number, withinFlowCollection: boolean ): boolean { const kind = state.kind; const result = state.result; let ch = state.input.charCodeAt(state.position); if ( isWsOrEol(ch) || isFlowIndicator(ch) || ch === 0x23 /* # */ || ch === 0x26 /* & */ || ch === 0x2a /* * */ || ch === 0x21 /* ! */ || ch === 0x7c /* | */ || ch === 0x3e /* > */ || ch === 0x27 /* ' */ || ch === 0x22 /* " */ || ch === 0x25 /* % */ || ch === 0x40 /* @ */ || ch === 0x60 /* ` */ ) { return false; } let following: number; if (ch === 0x3f || /* ? */ ch === 0x2d /* - */) { following = state.input.charCodeAt(state.position + 1); if ( isWsOrEol(following) || (withinFlowCollection && isFlowIndicator(following)) ) { return false; } } state.kind = "scalar"; state.result = ""; let captureEnd: number, captureStart = (captureEnd = state.position); let hasPendingContent = false; let line = 0; while (ch !== 0) { if (ch === 0x3a /* : */) { following = state.input.charCodeAt(state.position + 1); if ( isWsOrEol(following) || (withinFlowCollection && isFlowIndicator(following)) ) { break; } } else if (ch === 0x23 /* # */) { const preceding = state.input.charCodeAt(state.position - 1); if (isWsOrEol(preceding)) { break; } } else if ( (state.position === state.lineStart && testDocumentSeparator(state)) || (withinFlowCollection && isFlowIndicator(ch)) ) { break; } else if (isEOL(ch)) { line = state.line; const lineStart = state.lineStart; const lineIndent = state.lineIndent; skipSeparationSpace(state, false, -1); if (state.lineIndent >= nodeIndent) { hasPendingContent = true; ch = state.input.charCodeAt(state.position); continue; } else { state.position = captureEnd; state.line = line; state.lineStart = lineStart; state.lineIndent = lineIndent; break; } } if (hasPendingContent) { captureSegment(state, captureStart, captureEnd, false); writeFoldedLines(state, state.line - line); captureStart = captureEnd = state.position; hasPendingContent = false; } if (!isWhiteSpace(ch)) { captureEnd = state.position + 1; } ch = state.input.charCodeAt(++state.position); } captureSegment(state, captureStart, captureEnd, false); if (state.result) { return true; } state.kind = kind; state.result = result; return false; } function readSingleQuotedScalar( state: LoaderState, nodeIndent: number ): boolean { let ch, captureStart, captureEnd; ch = state.input.charCodeAt(state.position); if (ch !== 0x27 /* ' */) { return false; } state.kind = "scalar"; state.result = ""; state.position++; captureStart = captureEnd = state.position; while ((ch = state.input.charCodeAt(state.position)) !== 0) { if (ch === 0x27 /* ' */) { captureSegment(state, captureStart, state.position, true); ch = state.input.charCodeAt(++state.position); if (ch === 0x27 /* ' */) { captureStart = state.position; state.position++; captureEnd = state.position; } else { return true; } } else if (isEOL(ch)) { captureSegment(state, captureStart, captureEnd, true); writeFoldedLines(state, skipSeparationSpace(state, false, nodeIndent)); captureStart = captureEnd = state.position; } else if ( state.position === state.lineStart && testDocumentSeparator(state) ) { return throwError( state, "unexpected end of the document within a single quoted scalar" ); } else { state.position++; captureEnd = state.position; } } return throwError( state, "unexpected end of the stream within a single quoted scalar" ); } function readDoubleQuotedScalar( state: LoaderState, nodeIndent: number ): boolean { let ch = state.input.charCodeAt(state.position); if (ch !== 0x22 /* " */) { return false; } state.kind = "scalar"; state.result = ""; state.position++; let captureEnd: number, captureStart = (captureEnd = state.position); let tmp: number; while ((ch = state.input.charCodeAt(state.position)) !== 0) { if (ch === 0x22 /* " */) { captureSegment(state, captureStart, state.position, true); state.position++; return true; } if (ch === 0x5c /* \ */) { captureSegment(state, captureStart, state.position, true); ch = state.input.charCodeAt(++state.position); if (isEOL(ch)) { skipSeparationSpace(state, false, nodeIndent); // TODO: rework to inline fn with no type cast? } else if (ch < 256 && simpleEscapeCheck[ch]) { state.result += simpleEscapeMap[ch]; state.position++; } else if ((tmp = escapedHexLen(ch)) > 0) { let hexLength = tmp; let hexResult = 0; for (; hexLength > 0; hexLength--) { ch = state.input.charCodeAt(++state.position); if ((tmp = fromHexCode(ch)) >= 0) { hexResult = (hexResult << 4) + tmp; } else { return throwError(state, "expected hexadecimal character"); } } state.result += charFromCodepoint(hexResult); state.position++; } else { return throwError(state, "unknown escape sequence"); } captureStart = captureEnd = state.position; } else if (isEOL(ch)) { captureSegment(state, captureStart, captureEnd, true); writeFoldedLines(state, skipSeparationSpace(state, false, nodeIndent)); captureStart = captureEnd = state.position; } else if ( state.position === state.lineStart && testDocumentSeparator(state) ) { return throwError( state, "unexpected end of the document within a double quoted scalar" ); } else { state.position++; captureEnd = state.position; } } return throwError( state, "unexpected end of the stream within a double quoted scalar" ); } function readFlowCollection(state: LoaderState, nodeIndent: number): boolean { let ch = state.input.charCodeAt(state.position); let terminator: number; let isMapping = true; let result: ResultType = {}; if (ch === 0x5b /* [ */) { terminator = 0x5d; /* ] */ isMapping = false; result = []; } else if (ch === 0x7b /* { */) { terminator = 0x7d; /* } */ } else { return false; } if ( state.anchor !== null && typeof state.anchor != "undefined" && typeof state.anchorMap != "undefined" ) { state.anchorMap[state.anchor] = result; } ch = state.input.charCodeAt(++state.position); const tag = state.tag, anchor = state.anchor; let readNext = true; let valueNode, keyNode, keyTag: string | null = (keyNode = valueNode = null), isExplicitPair: boolean, isPair = (isExplicitPair = false); let following = 0, line = 0; const overridableKeys: ArrayObject = {}; while (ch !== 0) { skipSeparationSpace(state, true, nodeIndent); ch = state.input.charCodeAt(state.position); if (ch === terminator) { state.position++; state.tag = tag; state.anchor = anchor; state.kind = isMapping ? "mapping" : "sequence"; state.result = result; return true; } if (!readNext) { return throwError(state, "missed comma between flow collection entries"); } keyTag = keyNode = valueNode = null; isPair = isExplicitPair = false; if (ch === 0x3f /* ? */) { following = state.input.charCodeAt(state.position + 1); if (isWsOrEol(following)) { isPair = isExplicitPair = true; state.position++; skipSeparationSpace(state, true, nodeIndent); } } line = state.line; // eslint-disable-next-line @typescript-eslint/no-use-before-define composeNode(state, nodeIndent, CONTEXT_FLOW_IN, false, true); keyTag = state.tag || null; keyNode = state.result; skipSeparationSpace(state, true, nodeIndent); ch = state.input.charCodeAt(state.position); if ((isExplicitPair || state.line === line) && ch === 0x3a /* : */) { isPair = true; ch = state.input.charCodeAt(++state.position); skipSeparationSpace(state, true, nodeIndent); // eslint-disable-next-line @typescript-eslint/no-use-before-define composeNode(state, nodeIndent, CONTEXT_FLOW_IN, false, true); valueNode = state.result; } if (isMapping) { storeMappingPair( state, result, overridableKeys, keyTag, keyNode, valueNode ); } else if (isPair) { (result as Array<{}>).push( storeMappingPair( state, null, overridableKeys, keyTag, keyNode, valueNode ) ); } else { (result as ResultType[]).push(keyNode as ResultType); } skipSeparationSpace(state, true, nodeIndent); ch = state.input.charCodeAt(state.position); if (ch === 0x2c /* , */) { readNext = true; ch = state.input.charCodeAt(++state.position); } else { readNext = false; } } return throwError( state, "unexpected end of the stream within a flow collection" ); } function readBlockScalar(state: LoaderState, nodeIndent: number): boolean { let chomping = CHOMPING_CLIP, didReadContent = false, detectedIndent = false, textIndent = nodeIndent, emptyLines = 0, atMoreIndented = false; let ch = state.input.charCodeAt(state.position); let folding = false; if (ch === 0x7c /* | */) { folding = false; } else if (ch === 0x3e /* > */) { folding = true; } else { return false; } state.kind = "scalar"; state.result = ""; let tmp = 0; while (ch !== 0) { ch = state.input.charCodeAt(++state.position); if (ch === 0x2b || /* + */ ch === 0x2d /* - */) { if (CHOMPING_CLIP === chomping) { chomping = ch === 0x2b /* + */ ? CHOMPING_KEEP : CHOMPING_STRIP; } else { return throwError(state, "repeat of a chomping mode identifier"); } } else if ((tmp = fromDecimalCode(ch)) >= 0) { if (tmp === 0) { return throwError( state, "bad explicit indentation width of a block scalar; it cannot be less than one" ); } else if (!detectedIndent) { textIndent = nodeIndent + tmp - 1; detectedIndent = true; } else { return throwError(state, "repeat of an indentation width identifier"); } } else { break; } } if (isWhiteSpace(ch)) { do { ch = state.input.charCodeAt(++state.position); } while (isWhiteSpace(ch)); if (ch === 0x23 /* # */) { do { ch = state.input.charCodeAt(++state.position); } while (!isEOL(ch) && ch !== 0); } } while (ch !== 0) { readLineBreak(state); state.lineIndent = 0; ch = state.input.charCodeAt(state.position); while ( (!detectedIndent || state.lineIndent < textIndent) && ch === 0x20 /* Space */ ) { state.lineIndent++; ch = state.input.charCodeAt(++state.position); } if (!detectedIndent && state.lineIndent > textIndent) { textIndent = state.lineIndent; } if (isEOL(ch)) { emptyLines++; continue; } // End of the scalar. if (state.lineIndent < textIndent) { // Perform the chomping. if (chomping === CHOMPING_KEEP) { state.result += common.repeat( "\n", didReadContent ? 1 + emptyLines : emptyLines ); } else if (chomping === CHOMPING_CLIP) { if (didReadContent) { // i.e. only if the scalar is not empty. state.result += "\n"; } } // Break this `while` cycle and go to the funciton's epilogue. break; } // Folded style: use fancy rules to handle line breaks. if (folding) { // Lines starting with white space characters (more-indented lines) are not folded. if (isWhiteSpace(ch)) { atMoreIndented = true; // except for the first content line (cf. Example 8.1) state.result += common.repeat( "\n", didReadContent ? 1 + emptyLines : emptyLines ); // End of more-indented block. } else if (atMoreIndented) { atMoreIndented = false; state.result += common.repeat("\n", emptyLines + 1); // Just one line break - perceive as the same line. } else if (emptyLines === 0) { if (didReadContent) { // i.e. only if we have already read some scalar content. state.result += " "; } // Several line breaks - perceive as different lines. } else { state.result += common.repeat("\n", emptyLines); } // Literal style: just add exact number of line breaks between content lines. } else { // Keep all line breaks except the header line break. state.result += common.repeat( "\n", didReadContent ? 1 + emptyLines : emptyLines ); } didReadContent = true; detectedIndent = true; emptyLines = 0; const captureStart = state.position; while (!isEOL(ch) && ch !== 0) { ch = state.input.charCodeAt(++state.position); } captureSegment(state, captureStart, state.position, false); } return true; } function readBlockSequence(state: LoaderState, nodeIndent: number): boolean { let line: number, following: number, detected = false, ch: number; const tag = state.tag, anchor = state.anchor, result: unknown[] = []; if ( state.anchor !== null && typeof state.anchor !== "undefined" && typeof state.anchorMap !== "undefined" ) { state.anchorMap[state.anchor] = result; } ch = state.input.charCodeAt(state.position); while (ch !== 0) { if (ch !== 0x2d /* - */) { break; } following = state.input.charCodeAt(state.position + 1); if (!isWsOrEol(following)) { break; } detected = true; state.position++; if (skipSeparationSpace(state, true, -1)) { if (state.lineIndent <= nodeIndent) { result.push(null); ch = state.input.charCodeAt(state.position); continue; } } line = state.line; // eslint-disable-next-line @typescript-eslint/no-use-before-define composeNode(state, nodeIndent, CONTEXT_BLOCK_IN, false, true); result.push(state.result); skipSeparationSpace(state, true, -1); ch = state.input.charCodeAt(state.position); if ((state.line === line || state.lineIndent > nodeIndent) && ch !== 0) { return throwError(state, "bad indentation of a sequence entry"); } else if (state.lineIndent < nodeIndent) { break; } } if (detected) { state.tag = tag; state.anchor = anchor; state.kind = "sequence"; state.result = result; return true; } return false; } function readBlockMapping( state: LoaderState, nodeIndent: number, flowIndent: number ): boolean { const tag = state.tag, anchor = state.anchor, result = {}, overridableKeys = {}; let following: number, allowCompact = false, line: number, pos: number, keyTag = null, keyNode = null, valueNode = null, atExplicitKey = false, detected = false, ch: number; if ( state.anchor !== null && typeof state.anchor !== "undefined" && typeof state.anchorMap !== "undefined" ) { state.anchorMap[state.anchor] = result; } ch = state.input.charCodeAt(state.position); while (ch !== 0) { following = state.input.charCodeAt(state.position + 1); line = state.line; // Save the current line. pos = state.position; // // Explicit notation case. There are two separate blocks: // first for the key (denoted by "?") and second for the value (denoted by ":") // if ((ch === 0x3f || /* ? */ ch === 0x3a) && /* : */ isWsOrEol(following)) { if (ch === 0x3f /* ? */) { if (atExplicitKey) { storeMappingPair( state, result, overridableKeys, keyTag as string, keyNode, null ); keyTag = keyNode = valueNode = null; } detected = true; atExplicitKey = true; allowCompact = true; } else if (atExplicitKey) { // i.e. 0x3A/* : */ === character after the explicit key. atExplicitKey = false; allowCompact = true; } else { return throwError( state, "incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line" ); } state.position += 1; ch = following; // // Implicit notation case. Flow-style node as the key first, then ":", and the value. // // eslint-disable-next-line @typescript-eslint/no-use-before-define } else if (composeNode(state, flowIndent, CONTEXT_FLOW_OUT, false, true)) { if (state.line === line) { ch = state.input.charCodeAt(state.position); while (isWhiteSpace(ch)) { ch = state.input.charCodeAt(++state.position); } if (ch === 0x3a /* : */) { ch = state.input.charCodeAt(++state.position); if (!isWsOrEol(ch)) { return throwError( state, "a whitespace character is expected after the key-value separator within a block mapping" ); } if (atExplicitKey) { storeMappingPair( state, result, overridableKeys, keyTag as string, keyNode, null ); keyTag = keyNode = valueNode = null; } detected = true; atExplicitKey = false; allowCompact = false; keyTag = state.tag; keyNode = state.result; } else if (detected) { return throwError( state, "can not read an implicit mapping pair; a colon is missed" ); } else { state.tag = tag; state.anchor = anchor; return true; // Keep the result of `composeNode`. } } else if (detected) { return throwError( state, "can not read a block mapping entry; a multiline key may not be an implicit key" ); } else { state.tag = tag; state.anchor = anchor; return true; // Keep the result of `composeNode`. } } else { break; // Reading is done. Go to the epilogue. } // // Common reading code for both explicit and implicit notations. // if (state.line === line || state.lineIndent > nodeIndent) { if ( // eslint-disable-next-line @typescript-eslint/no-use-before-define composeNode(state, nodeIndent, CONTEXT_BLOCK_OUT, true, allowCompact) ) { if (atExplicitKey) { keyNode = state.result; } else { valueNode = state.result; } } if (!atExplicitKey) { storeMappingPair( state, result, overridableKeys, keyTag as string, keyNode, valueNode, line, pos ); keyTag = keyNode = valueNode = null; } skipSeparationSpace(state, true, -1); ch = state.input.charCodeAt(state.position); } if (state.lineIndent > nodeIndent && ch !== 0) { return throwError(state, "bad indentation of a mapping entry"); } else if (state.lineIndent < nodeIndent) { break; } } // // Epilogue. // // Special case: last mapping's node contains only the key in explicit notation. if (atExplicitKey) { storeMappingPair( state, result, overridableKeys, keyTag as string, keyNode, null ); } // Expose the resulting mapping. if (detected) { state.tag = tag; state.anchor = anchor; state.kind = "mapping"; state.result = result; } return detected; } function readTagProperty(state: LoaderState): boolean { let position: number, isVerbatim = false, isNamed = false, tagHandle = "", tagName: string, ch: number; ch = state.input.charCodeAt(state.position); if (ch !== 0x21 /* ! */) return false; if (state.tag !== null) { return throwError(state, "duplication of a tag property"); } ch = state.input.charCodeAt(++state.position); if (ch === 0x3c /* < */) { isVerbatim = true; ch = state.input.charCodeAt(++state.position); } else if (ch === 0x21 /* ! */) { isNamed = true; tagHandle = "!!"; ch = state.input.charCodeAt(++state.position); } else { tagHandle = "!"; } position = state.position; if (isVerbatim) { do { ch = state.input.charCodeAt(++state.position); } while (ch !== 0 && ch !== 0x3e /* > */); if (state.position < state.length) { tagName = state.input.slice(position, state.position); ch = state.input.charCodeAt(++state.position); } else { return throwError( state, "unexpected end of the stream within a verbatim tag" ); } } else { while (ch !== 0 && !isWsOrEol(ch)) { if (ch === 0x21 /* ! */) { if (!isNamed) { tagHandle = state.input.slice(position - 1, state.position + 1); if (!PATTERN_TAG_HANDLE.test(tagHandle)) { return throwError( state, "named tag handle cannot contain such characters" ); } isNamed = true; position = state.position + 1; } else { return throwError( state, "tag suffix cannot contain exclamation marks" ); } } ch = state.input.charCodeAt(++state.position); } tagName = state.input.slice(position, state.position); if (PATTERN_FLOW_INDICATORS.test(tagName)) { return throwError( state, "tag suffix cannot contain flow indicator characters" ); } } if (tagName && !PATTERN_TAG_URI.test(tagName)) { return throwError( state, `tag name cannot contain such characters: ${tagName}` ); } if (isVerbatim) { state.tag = tagName; } else if ( typeof state.tagMap !== "undefined" && _hasOwnProperty.call(state.tagMap, tagHandle) ) { state.tag = state.tagMap[tagHandle] + tagName; } else if (tagHandle === "!") { state.tag = `!${tagName}`; } else if (tagHandle === "!!") { state.tag = `tag:yaml.org,2002:${tagName}`; } else { return throwError(state, `undeclared tag handle "${tagHandle}"`); } return true; } function readAnchorProperty(state: LoaderState): boolean { let ch = state.input.charCodeAt(state.position); if (ch !== 0x26 /* & */) return false; if (state.anchor !== null) { return throwError(state, "duplication of an anchor property"); } ch = state.input.charCodeAt(++state.position); const position = state.position; while (ch !== 0 && !isWsOrEol(ch) && !isFlowIndicator(ch)) { ch = state.input.charCodeAt(++state.position); } if (state.position === position) { return throwError( state, "name of an anchor node must contain at least one character" ); } state.anchor = state.input.slice(position, state.position); return true; } function readAlias(state: LoaderState): boolean { let ch = state.input.charCodeAt(state.position); if (ch !== 0x2a /* * */) return false; ch = state.input.charCodeAt(++state.position); const _position = state.position; while (ch !== 0 && !isWsOrEol(ch) && !isFlowIndicator(ch)) { ch = state.input.charCodeAt(++state.position); } if (state.position === _position) { return throwError( state, "name of an alias node must contain at least one character" ); } const alias = state.input.slice(_position, state.position); if ( typeof state.anchorMap !== "undefined" && !state.anchorMap.hasOwnProperty(alias) ) { return throwError(state, `unidentified alias "${alias}"`); } if (typeof state.anchorMap !== "undefined") { state.result = state.anchorMap[alias]; } skipSeparationSpace(state, true, -1); return true; } function composeNode( state: LoaderState, parentIndent: number, nodeContext: number, allowToSeek: boolean, allowCompact: boolean ): boolean { let allowBlockScalars: boolean, allowBlockCollections: boolean, indentStatus = 1, // 1: this>parent, 0: this=parent, -1: this parentIndent) { indentStatus = 1; } else if (state.lineIndent === parentIndent) { indentStatus = 0; } else if (state.lineIndent < parentIndent) { indentStatus = -1; } } } if (indentStatus === 1) { while (readTagProperty(state) || readAnchorProperty(state)) { if (skipSeparationSpace(state, true, -1)) { atNewLine = true; allowBlockCollections = allowBlockStyles; if (state.lineIndent > parentIndent) { indentStatus = 1; } else if (state.lineIndent === parentIndent) { indentStatus = 0; } else if (state.lineIndent < parentIndent) { indentStatus = -1; } } else { allowBlockCollections = false; } } } if (allowBlockCollections) { allowBlockCollections = atNewLine || allowCompact; } if (indentStatus === 1 || CONTEXT_BLOCK_OUT === nodeContext) { const cond = CONTEXT_FLOW_IN === nodeContext || CONTEXT_FLOW_OUT === nodeContext; flowIndent = cond ? parentIndent : parentIndent + 1; blockIndent = state.position - state.lineStart; if (indentStatus === 1) { if ( (allowBlockCollections && (readBlockSequence(state, blockIndent) || readBlockMapping(state, blockIndent, flowIndent))) || readFlowCollection(state, flowIndent) ) { hasContent = true; } else { if ( (allowBlockScalars && readBlockScalar(state, flowIndent)) || readSingleQuotedScalar(state, flowIndent) || readDoubleQuotedScalar(state, flowIndent) ) { hasContent = true; } else if (readAlias(state)) { hasContent = true; if (state.tag !== null || state.anchor !== null) { return throwError( state, "alias node should not have Any properties" ); } } else if ( readPlainScalar(state, flowIndent, CONTEXT_FLOW_IN === nodeContext) ) { hasContent = true; if (state.tag === null) { state.tag = "?"; } } if (state.anchor !== null && typeof state.anchorMap !== "undefined") { state.anchorMap[state.anchor] = state.result; } } } else if (indentStatus === 0) { // Special case: block sequences are allowed to have same indentation level as the parent. // http://www.yaml.org/spec/1.2/spec.html#id2799784 hasContent = allowBlockCollections && readBlockSequence(state, blockIndent); } } if (state.tag !== null && state.tag !== "!") { if (state.tag === "?") { for ( let typeIndex = 0, typeQuantity = state.implicitTypes.length; typeIndex < typeQuantity; typeIndex++ ) { type = state.implicitTypes[typeIndex]; // Implicit resolving is not allowed for non-scalar types, and '?' // non-specific tag is only assigned to plain scalars. So, it isn't // needed to check for 'kind' conformity. if (type.resolve(state.result)) { // `state.result` updated in resolver if matched state.result = type.construct(state.result); state.tag = type.tag; if (state.anchor !== null && typeof state.anchorMap !== "undefined") { state.anchorMap[state.anchor] = state.result; } break; } } } else if ( _hasOwnProperty.call(state.typeMap[state.kind || "fallback"], state.tag) ) { type = state.typeMap[state.kind || "fallback"][state.tag]; if (state.result !== null && type.kind !== state.kind) { return throwError( state, `unacceptable node kind for !<${state.tag}> tag; it should be "${type.kind}", not "${state.kind}"` ); } if (!type.resolve(state.result)) { // `state.result` updated in resolver if matched return throwError( state, `cannot resolve a node with !<${state.tag}> explicit tag` ); } else { state.result = type.construct(state.result); if (state.anchor !== null && typeof state.anchorMap !== "undefined") { state.anchorMap[state.anchor] = state.result; } } } else { return throwError(state, `unknown tag !<${state.tag}>`); } } if (state.listener && state.listener !== null) { state.listener("close", state); } return state.tag !== null || state.anchor !== null || hasContent; } function readDocument(state: LoaderState): void { const documentStart = state.position; let position: number, directiveName: string, directiveArgs: string[], hasDirectives = false, ch: number; state.version = null; state.checkLineBreaks = state.legacy; state.tagMap = {}; state.anchorMap = {}; while ((ch = state.input.charCodeAt(state.position)) !== 0) { skipSeparationSpace(state, true, -1); ch = state.input.charCodeAt(state.position); if (state.lineIndent > 0 || ch !== 0x25 /* % */) { break; } hasDirectives = true; ch = state.input.charCodeAt(++state.position); position = state.position; while (ch !== 0 && !isWsOrEol(ch)) { ch = state.input.charCodeAt(++state.position); } directiveName = state.input.slice(position, state.position); directiveArgs = []; if (directiveName.length < 1) { return throwError( state, "directive name must not be less than one character in length" ); } while (ch !== 0) { while (isWhiteSpace(ch)) { ch = state.input.charCodeAt(++state.position); } if (ch === 0x23 /* # */) { do { ch = state.input.charCodeAt(++state.position); } while (ch !== 0 && !isEOL(ch)); break; } if (isEOL(ch)) break; position = state.position; while (ch !== 0 && !isWsOrEol(ch)) { ch = state.input.charCodeAt(++state.position); } directiveArgs.push(state.input.slice(position, state.position)); } if (ch !== 0) readLineBreak(state); if (_hasOwnProperty.call(directiveHandlers, directiveName)) { directiveHandlers[directiveName](state, directiveName, ...directiveArgs); } else { throwWarning(state, `unknown document directive "${directiveName}"`); } } skipSeparationSpace(state, true, -1); if ( state.lineIndent === 0 && state.input.charCodeAt(state.position) === 0x2d /* - */ && state.input.charCodeAt(state.position + 1) === 0x2d /* - */ && state.input.charCodeAt(state.position + 2) === 0x2d /* - */ ) { state.position += 3; skipSeparationSpace(state, true, -1); } else if (hasDirectives) { return throwError(state, "directives end mark is expected"); } composeNode(state, state.lineIndent - 1, CONTEXT_BLOCK_OUT, false, true); skipSeparationSpace(state, true, -1); if ( state.checkLineBreaks && PATTERN_NON_ASCII_LINE_BREAKS.test( state.input.slice(documentStart, state.position) ) ) { throwWarning(state, "non-ASCII line breaks are interpreted as content"); } state.documents.push(state.result); if (state.position === state.lineStart && testDocumentSeparator(state)) { if (state.input.charCodeAt(state.position) === 0x2e /* . */) { state.position += 3; skipSeparationSpace(state, true, -1); } return; } if (state.position < state.length - 1) { return throwError( state, "end of the stream or a document separator is expected" ); } else { return; } } function loadDocuments(input: string, options?: LoaderStateOptions): unknown[] { input = String(input); options = options || {}; if (input.length !== 0) { // Add tailing `\n` if not exists if ( input.charCodeAt(input.length - 1) !== 0x0a /* LF */ && input.charCodeAt(input.length - 1) !== 0x0d /* CR */ ) { input += "\n"; } // Strip BOM if (input.charCodeAt(0) === 0xfeff) { input = input.slice(1); } } const state = new LoaderState(input, options); // Use 0 as string terminator. That significantly simplifies bounds check. state.input += "\0"; while (state.input.charCodeAt(state.position) === 0x20 /* Space */) { state.lineIndent += 1; state.position += 1; } while (state.position < state.length - 1) { readDocument(state); } return state.documents; } export type CbFunction = (doc: unknown) => void; function isCbFunction(fn: unknown): fn is CbFunction { return typeof fn === "function"; } export function loadAll( input: string, iteratorOrOption?: T, options?: LoaderStateOptions ): T extends CbFunction ? void : unknown[] { if (!isCbFunction(iteratorOrOption)) { return loadDocuments(input, iteratorOrOption as LoaderStateOptions) as Any; } const documents = loadDocuments(input, options); const iterator = iteratorOrOption; for (let index = 0, length = documents.length; index < length; index++) { iterator(documents[index]); } return void 0 as Any; } export function load(input: string, options?: LoaderStateOptions): unknown { const documents = loadDocuments(input, options); if (documents.length === 0) { return; } if (documents.length === 1) { return documents[0]; } throw new YAMLError( "expected a single document in the stream, but found more" ); }