// 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-2026 the Deno authors. MIT license. import { AMPERSAND, ASTERISK, BACKSLASH, CARRIAGE_RETURN, COLON, COMMA, COMMERCIAL_AT, DOT, DOUBLE_QUOTE, EXCLAMATION, GRAVE_ACCENT, GREATER_THAN, isEOL, isFlowIndicator, isWhiteSpace, isWhiteSpaceOrEOL, LEFT_CURLY_BRACKET, LEFT_SQUARE_BRACKET, LINE_FEED, MINUS, PERCENT, PLUS, QUESTION, RIGHT_CURLY_BRACKET, RIGHT_SQUARE_BRACKET, SHARP, SINGLE_QUOTE, SMALLER_THAN, SPACE, VERTICAL_LINE, } from "./_chars.ts"; import { DEFAULT_SCHEMA, type Schema, type TypeMap } from "./_schema.ts"; import type { KindType, Type } from "./_type.ts"; import { isObject, isPlainObject } from "./_utils.ts"; import { YamlSyntaxError } from "./types.ts"; 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_REGEXP = // deno-lint-ignore no-control-regex /[\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_REGEXP = /[\x85\u2028\u2029]/; const PATTERN_FLOW_INDICATORS_REGEXP = /[,\[\]\{\}]/; const PATTERN_TAG_HANDLE_REGEXP = /^(?:!|!!|![a-z\-]+!)$/i; const PATTERN_TAG_URI_REGEXP = /^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i; export interface LoaderStateOptions { /** specifies a schema to use. */ schema?: Schema; /** compatibility with JSON.parse behaviour. */ allowDuplicateKeys?: boolean; /** function to call on warning messages. */ onWarning?(error: YamlSyntaxError): void; } const ESCAPED_HEX_LENGTHS = new Map([ [0x78, 2], // x [0x75, 4], // u [0x55, 8], // U ]); const SIMPLE_ESCAPE_SEQUENCES = new Map([ [0x30, "\x00"], // 0 [0x61, "\x07"], // a [0x62, "\x08"], // b [0x74, "\x09"], // t [0x09, "\x09"], // Tab [0x6e, "\x0A"], // n [0x76, "\x0B"], // v [0x66, "\x0C"], // f [0x72, "\x0D"], // r [0x65, "\x1B"], // e [0x20, " "], // Space [0x22, '"'], // " [0x2f, "/"], // / [0x5c, "\\"], // \ [0x4e, "\x85"], // N [0x5f, "\xA0"], // _ [0x4c, "\u2028"], // L [0x50, "\u2029"], // P ]); /** * Converts a hexadecimal character code to its decimal value. */ function hexCharCodeToNumber(charCode: number) { // Check if the character code is in the range for '0' to '9' if (0x30 <= charCode && charCode <= 0x39) return charCode - 0x30; // Convert '0'-'9' to 0-9 // Normalize the character code to lowercase if it's a letter const lc = charCode | 0x20; // Check if the character code is in the range for 'a' to 'f' if (0x61 <= lc && lc <= 0x66) return lc - 0x61 + 10; // Convert 'a'-'f' to 10-15 return -1; } /** * Converts a decimal character code to its decimal value. */ function decimalCharCodeToNumber(charCode: number): number { // Check if the character code is in the range for '0' to '9' if (0x30 <= charCode && charCode <= 0x39) return charCode - 0x30; // Convert '0'-'9' to 0-9 return -1; } /** * Converts a Unicode code point to a string. */ function codepointToChar(codepoint: number): string { // Check if the code point is within the Basic Multilingual Plane (BMP) if (codepoint <= 0xffff) return String.fromCharCode(codepoint); // Convert BMP code point to character // Encode UTF-16 surrogate pair for code points beyond BMP // Reference: https://en.wikipedia.org/wiki/UTF-16#Code_points_U.2B010000_to_U.2B10FFFF return String.fromCharCode( ((codepoint - 0x010000) >> 10) + 0xd800, // High surrogate ((codepoint - 0x010000) & 0x03ff) + 0xdc00, // Low surrogate ); } const INDENT = 4; const MAX_LENGTH = 75; const DELIMITERS = "\x00\r\n\x85\u2028\u2029"; function getSnippet(buffer: string, position: number): string | null { if (!buffer) return null; let start = position; let end = position; let head = ""; let tail = ""; while (start > 0 && !DELIMITERS.includes(buffer.charAt(start - 1))) { start--; if (position - start > MAX_LENGTH / 2 - 1) { head = " ... "; start += 5; break; } } while (end < buffer.length && !DELIMITERS.includes(buffer.charAt(end))) { end++; if (end - position > MAX_LENGTH / 2 - 1) { tail = " ... "; end -= 5; break; } } const snippet = buffer.slice(start, end); const indent = " ".repeat(INDENT); const caretIndent = " ".repeat(INDENT + position - start + head.length); return `${indent + head + snippet + tail}\n${caretIndent}^`; } function getIndentStatus(lineIndent: number, parentIndent: number) { if (lineIndent > parentIndent) return 1; if (lineIndent < parentIndent) return -1; return 0; } function writeFoldedLines(count: number) { if (count === 1) return " "; if (count > 1) return "\n".repeat(count - 1); return ""; } class Scanner { source: string; #length: number; position = 0; constructor(source: string) { // Use 0 as string terminator. That significantly simplifies bounds check. source += "\0"; this.source = source; this.#length = source.length; } peek(offset = 0) { return this.source.charCodeAt(this.position + offset); } next() { this.position += 1; } eof() { return this.position >= this.#length - 1; } } interface State { tag: string | null; anchor: string | null; kind: KindType | null; result: unknown[] | Record | string | null; } export class LoaderState { #scanner: Scanner; lineIndent = 0; lineStart = 0; line = 0; onWarning: ((error: YamlSyntaxError) => void) | undefined; allowDuplicateKeys: boolean; implicitTypes: Type<"scalar">[]; typeMap: TypeMap; checkLineBreaks = false; tagMap = new Map(); anchorMap = new Map(); constructor( input: string, { schema = DEFAULT_SCHEMA, onWarning, allowDuplicateKeys = false, }: LoaderStateOptions, ) { this.#scanner = new Scanner(input); this.onWarning = onWarning; this.allowDuplicateKeys = allowDuplicateKeys; this.implicitTypes = schema.implicitTypes; this.typeMap = schema.typeMap; this.readIndent(); } skipWhitespaces() { let ch = this.#scanner.peek(); while (isWhiteSpace(ch)) { this.#scanner.next(); ch = this.#scanner.peek(); } } skipComment() { let ch = this.#scanner.peek(); if (ch !== SHARP) return; this.#scanner.next(); ch = this.#scanner.peek(); while (ch !== 0 && !isEOL(ch)) { this.#scanner.next(); ch = this.#scanner.peek(); } } readIndent() { let ch = this.#scanner.peek(); while (ch === SPACE) { this.lineIndent += 1; this.#scanner.next(); ch = this.#scanner.peek(); } } #createError(message: string): YamlSyntaxError { const offset = this.#scanner.position; const snippet = getSnippet(this.#scanner.source, offset) ?? undefined; return new YamlSyntaxError(message, { line: this.line + 1, column: offset - this.lineStart + 1, offset, }, snippet); } dispatchWarning(message: string) { const error = this.#createError(message); this.onWarning?.(error); } yamlDirectiveHandler(args: string[]): string | null { if (args.length !== 1) { throw this.#createError( "Cannot handle YAML directive: YAML directive accepts exactly one argument", ); } const match = /^([0-9]+)\.([0-9]+)$/.exec(args[0]!); if (match === null) { throw this.#createError( "Cannot handle YAML directive: ill-formed argument", ); } const major = parseInt(match[1]!, 10); const minor = parseInt(match[2]!, 10); if (major !== 1) { throw this.#createError( "Cannot handle YAML directive: unacceptable YAML version", ); } this.checkLineBreaks = minor < 2; if (minor !== 1 && minor !== 2) { this.dispatchWarning( "Cannot handle YAML directive: unsupported YAML version", ); } return args[0] ?? null; } tagDirectiveHandler(args: string[]) { if (args.length !== 2) { throw this.#createError( `Cannot handle tag directive: directive accepts exactly two arguments, received ${args.length}`, ); } const handle = args[0]!; const prefix = args[1]!; if (!PATTERN_TAG_HANDLE_REGEXP.test(handle)) { throw this.#createError( `Cannot handle tag directive: ill-formed handle (first argument) in "${handle}"`, ); } if (this.tagMap.has(handle)) { throw this.#createError( `Cannot handle tag directive: previously declared suffix for "${handle}" tag handle`, ); } if (!PATTERN_TAG_URI_REGEXP.test(prefix)) { throw this.#createError( "Cannot handle tag directive: ill-formed tag prefix (second argument) of the TAG directive", ); } this.tagMap.set(handle, prefix); } captureSegment(start: number, end: number, checkJson: boolean) { if (start < end) { const result = this.#scanner.source.slice(start, end); if (checkJson) { for (let position = 0; position < result.length; position++) { const character = result.charCodeAt(position); if ( !(character === 0x09 || (0x20 <= character && character <= 0x10ffff)) ) { throw this.#createError( `Expected valid JSON character: received "${character}"`, ); } } } else if (PATTERN_NON_PRINTABLE_REGEXP.test(result)) { throw this.#createError("Stream contains non-printable characters"); } return result; } } readBlockSequence( tag: string | null, anchor: string | null, nodeIndent: number, ): State | void { let detected = false; const result: unknown[] = []; if (anchor !== null) this.anchorMap.set(anchor, result); let ch = this.#scanner.peek(); while (ch !== 0) { if (ch !== MINUS) { break; } const following = this.#scanner.peek(1); if (!isWhiteSpaceOrEOL(following)) { break; } detected = true; this.#scanner.next(); if (this.skipSeparationSpace(true, -1)) { if (this.lineIndent <= nodeIndent) { result.push(null); ch = this.#scanner.peek(); continue; } } const line = this.line; const newState = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_BLOCK_IN, allowToSeek: false, allowCompact: true, }); if (newState) result.push(newState.result); this.skipSeparationSpace(true, -1); ch = this.#scanner.peek(); if ((this.line === line || this.lineIndent > nodeIndent) && ch !== 0) { throw this.#createError( "Cannot read block sequence: bad indentation of a sequence entry", ); } else if (this.lineIndent < nodeIndent) { break; } } if (detected) return { tag, anchor, kind: "sequence", result }; } mergeMappings( destination: Record, source: Record, overridableKeys: Set, ) { if (!isObject(source)) { throw this.#createError( "Cannot merge mappings: the provided source object is unacceptable", ); } for (const [key, value] of Object.entries(source)) { if (Object.hasOwn(destination, key)) continue; // `Object.defineProperty` is significantly slower than direct // assignment in V8. Direct assignment produces an identical descriptor // (writable/enumerable/configurable) for ordinary keys; the only // sensitive case is `__proto__`, where direct assignment would mutate // the prototype chain instead of creating an own property. if (key === "__proto__") { Object.defineProperty(destination, key, { value, writable: true, enumerable: true, configurable: true, }); } else { destination[key] = value; } overridableKeys.add(key); } } storeMappingPair( result: Record, overridableKeys: Set, keyTag: string | null, keyNode: Record | unknown[] | string | null, valueNode: unknown, startLine?: number, startPos?: number, ): Record { // 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; index < keyNode.length; index++) { if (Array.isArray(keyNode[index])) { throw this.#createError( "Cannot store mapping pair: nested arrays are not supported inside keys", ); } if (typeof keyNode === "object" && isPlainObject(keyNode[index])) { 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" && isPlainObject(keyNode)) { keyNode = "[object Object]"; } keyNode = String(keyNode); if (keyTag === "tag:yaml.org,2002:merge") { if (Array.isArray(valueNode)) { for ( let index = 0; index < valueNode.length; index++ ) { this.mergeMappings(result, valueNode[index], overridableKeys); } } else { this.mergeMappings( result, valueNode as Record, overridableKeys, ); } } else { if ( !this.allowDuplicateKeys && !overridableKeys.has(keyNode) && Object.hasOwn(result, keyNode) ) { this.line = startLine || this.line; this.#scanner.position = startPos || this.#scanner.position; throw this.#createError("Cannot store mapping pair: duplicated key"); } // See `mergeMappings` above for why `Object.defineProperty` is kept // only for the `__proto__` key. if (keyNode === "__proto__") { Object.defineProperty(result, keyNode, { value: valueNode, writable: true, enumerable: true, configurable: true, }); } else { result[keyNode] = valueNode; } overridableKeys.delete(keyNode); } return result; } readLineBreak() { const ch = this.#scanner.peek(); if (ch === LINE_FEED) { this.#scanner.next(); } else if (ch === CARRIAGE_RETURN) { this.#scanner.next(); if (this.#scanner.peek() === LINE_FEED) { this.#scanner.next(); } } else { throw this.#createError("Cannot read line: line break not found"); } this.line += 1; this.lineStart = this.#scanner.position; } skipSeparationSpace(allowComments: boolean, checkIndent: number): number { let lineBreaks = 0; let ch = this.#scanner.peek(); while (ch !== 0) { this.skipWhitespaces(); ch = this.#scanner.peek(); if (allowComments) { this.skipComment(); ch = this.#scanner.peek(); } if (isEOL(ch)) { this.readLineBreak(); ch = this.#scanner.peek(); lineBreaks++; this.lineIndent = 0; this.readIndent(); ch = this.#scanner.peek(); } else { break; } } if ( checkIndent !== -1 && lineBreaks !== 0 && this.lineIndent < checkIndent ) { this.dispatchWarning("deficient indentation"); } return lineBreaks; } testDocumentSeparator(): boolean { let ch = this.#scanner.peek(); // Condition this.#scanner.position === this.lineStart is tested // in parent on each call, for efficiency. No needs to test here again. if ( (ch === MINUS || ch === DOT) && ch === this.#scanner.peek(1) && ch === this.#scanner.peek(2) ) { ch = this.#scanner.peek(3); if (ch === 0 || isWhiteSpaceOrEOL(ch)) { return true; } } return false; } readPlainScalar( tag: string | null, anchor: string | null, nodeIndent: number, withinFlowCollection: boolean, ): State | void { let ch = this.#scanner.peek(); if ( isWhiteSpaceOrEOL(ch) || isFlowIndicator(ch) || ch === SHARP || ch === AMPERSAND || ch === ASTERISK || ch === EXCLAMATION || ch === VERTICAL_LINE || ch === GREATER_THAN || ch === SINGLE_QUOTE || ch === DOUBLE_QUOTE || ch === PERCENT || ch === COMMERCIAL_AT || ch === GRAVE_ACCENT ) { return; } let following: number; if (ch === QUESTION || ch === MINUS) { following = this.#scanner.peek(1); if ( isWhiteSpaceOrEOL(following) || (withinFlowCollection && isFlowIndicator(following)) ) { return; } } let result = ""; let captureEnd = this.#scanner.position; let captureStart = this.#scanner.position; let hasPendingContent = false; let line = 0; while (ch !== 0) { if (ch === COLON) { following = this.#scanner.peek(1); if ( isWhiteSpaceOrEOL(following) || (withinFlowCollection && isFlowIndicator(following)) ) { break; } } else if (ch === SHARP) { const preceding = this.#scanner.peek(-1); if (isWhiteSpaceOrEOL(preceding)) { break; } } else if ( (this.#scanner.position === this.lineStart && this.testDocumentSeparator()) || (withinFlowCollection && isFlowIndicator(ch)) ) { break; } else if (isEOL(ch)) { line = this.line; const lineStart = this.lineStart; const lineIndent = this.lineIndent; this.skipSeparationSpace(false, -1); if (this.lineIndent >= nodeIndent) { hasPendingContent = true; ch = this.#scanner.peek(); continue; } else { this.#scanner.position = captureEnd; this.line = line; this.lineStart = lineStart; this.lineIndent = lineIndent; break; } } if (hasPendingContent) { const segment = this.captureSegment(captureStart, captureEnd, false); if (segment) result += segment; result += writeFoldedLines(this.line - line); captureStart = captureEnd = this.#scanner.position; hasPendingContent = false; } if (!isWhiteSpace(ch)) { captureEnd = this.#scanner.position + 1; } this.#scanner.next(); ch = this.#scanner.peek(); } const segment = this.captureSegment(captureStart, captureEnd, false); if (segment) result += segment; if (anchor !== null) this.anchorMap.set(anchor, result); if (result) return { tag, anchor, kind: "scalar", result }; } readSingleQuotedScalar( tag: string | null, anchor: string | null, nodeIndent: number, ): State | void { let ch = this.#scanner.peek(); if (ch !== SINGLE_QUOTE) return; let result = ""; this.#scanner.next(); let captureStart = this.#scanner.position; let captureEnd = this.#scanner.position; ch = this.#scanner.peek(); while (ch !== 0) { if (ch === SINGLE_QUOTE) { const segment = this.captureSegment( captureStart, this.#scanner.position, true, ); if (segment) result += segment; this.#scanner.next(); ch = this.#scanner.peek(); if (ch === SINGLE_QUOTE) { captureStart = this.#scanner.position; this.#scanner.next(); captureEnd = this.#scanner.position; } else { if (anchor !== null) this.anchorMap.set(anchor, result); return { tag, anchor, kind: "scalar", result }; } } else if (isEOL(ch)) { const segment = this.captureSegment(captureStart, captureEnd, true); if (segment) result += segment; result += writeFoldedLines( this.skipSeparationSpace(false, nodeIndent), ); captureStart = captureEnd = this.#scanner.position; } else if ( this.#scanner.position === this.lineStart && this.testDocumentSeparator() ) { throw this.#createError( "Unexpected end of the document within a single quoted scalar", ); } else { this.#scanner.next(); captureEnd = this.#scanner.position; } ch = this.#scanner.peek(); } throw this.#createError( "Unexpected end of the stream within a single quoted scalar", ); } readDoubleQuotedScalar( tag: string | null, anchor: string | null, nodeIndent: number, ): State | void { let ch = this.#scanner.peek(); if (ch !== DOUBLE_QUOTE) return; let result = ""; this.#scanner.next(); let captureEnd = this.#scanner.position; let captureStart = this.#scanner.position; let tmp: number; ch = this.#scanner.peek(); while (ch !== 0) { if (ch === DOUBLE_QUOTE) { const segment = this.captureSegment( captureStart, this.#scanner.position, true, ); if (segment) result += segment; this.#scanner.next(); if (anchor !== null) this.anchorMap.set(anchor, result); return { tag, anchor, kind: "scalar", result }; } if (ch === BACKSLASH) { const segment = this.captureSegment( captureStart, this.#scanner.position, true, ); if (segment) result += segment; this.#scanner.next(); ch = this.#scanner.peek(); if (isEOL(ch)) { this.skipSeparationSpace(false, nodeIndent); } else if (ch < 256 && SIMPLE_ESCAPE_SEQUENCES.has(ch)) { result += SIMPLE_ESCAPE_SEQUENCES.get(ch); this.#scanner.next(); } else if ((tmp = ESCAPED_HEX_LENGTHS.get(ch) ?? 0) > 0) { let hexLength = tmp; let hexResult = 0; for (; hexLength > 0; hexLength--) { this.#scanner.next(); ch = this.#scanner.peek(); if ((tmp = hexCharCodeToNumber(ch)) >= 0) { hexResult = (hexResult << 4) + tmp; } else { throw this.#createError( "Cannot read double quoted scalar: expected hexadecimal character", ); } } result += codepointToChar(hexResult); this.#scanner.next(); } else { throw this.#createError( "Cannot read double quoted scalar: unknown escape sequence", ); } captureStart = captureEnd = this.#scanner.position; } else if (isEOL(ch)) { const segment = this.captureSegment(captureStart, captureEnd, true); if (segment) result += segment; result += writeFoldedLines( this.skipSeparationSpace(false, nodeIndent), ); captureStart = captureEnd = this.#scanner.position; } else if ( this.#scanner.position === this.lineStart && this.testDocumentSeparator() ) { throw this.#createError( "Unexpected end of the document within a double quoted scalar", ); } else { this.#scanner.next(); captureEnd = this.#scanner.position; } ch = this.#scanner.peek(); } throw this.#createError( "Unexpected end of the stream within a double quoted scalar", ); } readFlowCollection( tag: string | null, anchor: string | null, nodeIndent: number, ): State | void { let ch = this.#scanner.peek(); let terminator: number; let isMapping = true; let result = {}; if (ch === LEFT_SQUARE_BRACKET) { terminator = RIGHT_SQUARE_BRACKET; isMapping = false; result = []; } else if (ch === LEFT_CURLY_BRACKET) { terminator = RIGHT_CURLY_BRACKET; } else { return; } if (anchor !== null) this.anchorMap.set(anchor, result); this.#scanner.next(); ch = this.#scanner.peek(); let readNext = true; let valueNode = null; let keyNode = null; let keyTag: string | null = null; let isExplicitPair = false; let isPair = false; let following = 0; let line = 0; const overridableKeys = new Set(); while (ch !== 0) { this.skipSeparationSpace(true, nodeIndent); ch = this.#scanner.peek(); if (ch === terminator) { this.#scanner.next(); const kind = isMapping ? "mapping" : "sequence"; return { tag, anchor, kind, result }; } if (!readNext) { throw this.#createError( "Cannot read flow collection: missing comma between flow collection entries", ); } keyTag = keyNode = valueNode = null; isPair = isExplicitPair = false; if (ch === QUESTION) { following = this.#scanner.peek(1); if (isWhiteSpaceOrEOL(following)) { isPair = isExplicitPair = true; this.#scanner.next(); this.skipSeparationSpace(true, nodeIndent); } } line = this.line; const newState = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_FLOW_IN, allowToSeek: false, allowCompact: true, }); if (newState) { keyTag = newState.tag || null; keyNode = newState.result; } this.skipSeparationSpace(true, nodeIndent); ch = this.#scanner.peek(); if ((isExplicitPair || this.line === line) && ch === COLON) { isPair = true; this.#scanner.next(); ch = this.#scanner.peek(); this.skipSeparationSpace(true, nodeIndent); const newState = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_FLOW_IN, allowToSeek: false, allowCompact: true, }); if (newState) valueNode = newState.result; } if (isMapping) { this.storeMappingPair( result as Record, overridableKeys, keyTag, keyNode, valueNode, ); } else if (isPair) { (result as Record[]).push( this.storeMappingPair( {}, overridableKeys, keyTag, keyNode, valueNode, ), ); } else { (result as unknown[]).push(keyNode); } this.skipSeparationSpace(true, nodeIndent); ch = this.#scanner.peek(); if (ch === COMMA) { readNext = true; this.#scanner.next(); ch = this.#scanner.peek(); } else { readNext = false; } } throw this.#createError( "Cannot read flow collection: unexpected end of the stream within a flow collection", ); } // Handles block scaler styles: e.g. '|', '>', '|-' and '>-'. // https://yaml.org/spec/1.2.2/#81-block-scalar-styles readBlockScalar( tag: string | null, anchor: string | null, nodeIndent: number, ): State | void { let chomping = CHOMPING_CLIP; let didReadContent = false; let detectedIndent = false; let textIndent = nodeIndent; let emptyLines = 0; let atMoreIndented = false; let ch = this.#scanner.peek(); let folding = false; if (ch === VERTICAL_LINE) { folding = false; } else if (ch === GREATER_THAN) { folding = true; } else { return; } let result = ""; let tmp = 0; while (ch !== 0) { this.#scanner.next(); ch = this.#scanner.peek(); if (ch === PLUS || ch === MINUS) { if (CHOMPING_CLIP === chomping) { chomping = ch === PLUS ? CHOMPING_KEEP : CHOMPING_STRIP; } else { throw this.#createError( "Cannot read block: chomping mode identifier repeated", ); } } else if ((tmp = decimalCharCodeToNumber(ch)) >= 0) { if (tmp === 0) { throw this.#createError( "Cannot read block: indentation width must be greater than 0", ); } else if (!detectedIndent) { textIndent = nodeIndent + tmp - 1; detectedIndent = true; } else { throw this.#createError( "Cannot read block: indentation width identifier repeated", ); } } else { break; } } if (isWhiteSpace(ch)) { this.skipWhitespaces(); this.skipComment(); ch = this.#scanner.peek(); } while (ch !== 0) { this.readLineBreak(); this.lineIndent = 0; ch = this.#scanner.peek(); while ( (!detectedIndent || this.lineIndent < textIndent) && ch === SPACE ) { this.lineIndent++; this.#scanner.next(); ch = this.#scanner.peek(); } if (!detectedIndent && this.lineIndent > textIndent) { textIndent = this.lineIndent; } if (isEOL(ch)) { emptyLines++; continue; } // End of the scalar. if (this.lineIndent < textIndent) { // Perform the chomping. if (chomping === CHOMPING_KEEP) { result += "\n".repeat( didReadContent ? 1 + emptyLines : emptyLines, ); } else if (chomping === CHOMPING_CLIP) { if (didReadContent) { // i.e. only if the scalar is not empty. result += "\n"; } } // Break this `while` cycle and go to the function'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) result += "\n".repeat( didReadContent ? 1 + emptyLines : emptyLines, ); // End of more-indented block. } else if (atMoreIndented) { atMoreIndented = false; result += "\n".repeat(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. result += " "; } // Several line breaks - perceive as different lines. } else { result += "\n".repeat(emptyLines); } // Literal style: just add exact number of line breaks between content lines. } else { // Keep all line breaks except the header line break. result += "\n".repeat( didReadContent ? 1 + emptyLines : emptyLines, ); } didReadContent = true; detectedIndent = true; emptyLines = 0; const captureStart = this.#scanner.position; while (!isEOL(ch) && ch !== 0) { this.#scanner.next(); ch = this.#scanner.peek(); } const segment = this.captureSegment( captureStart, this.#scanner.position, false, ); if (segment) result += segment; } if (anchor !== null) this.anchorMap.set(anchor, result); return { tag, anchor, kind: "scalar", result }; } readBlockMapping( tag: string | null, anchor: string | null, nodeIndent: number, flowIndent: number, ): State | void { const result = {}; const overridableKeys = new Set(); let allowCompact = false; let line: number; let pos: number; let keyTag = null; let keyNode = null; let valueNode = null; let atExplicitKey = false; let detected = false; if (anchor !== null) this.anchorMap.set(anchor, result); let ch = this.#scanner.peek(); while (ch !== 0) { const following = this.#scanner.peek(1); line = this.line; // Save the current line. pos = this.#scanner.position; // // Explicit notation case. There are two separate blocks: // first for the key (denoted by "?") and second for the value (denoted by ":") // if ((ch === QUESTION || ch === COLON) && isWhiteSpaceOrEOL(following)) { if (ch === QUESTION) { if (atExplicitKey) { this.storeMappingPair( result, overridableKeys, keyTag as string, keyNode, null, ); keyTag = null; keyNode = null; valueNode = null; } detected = true; atExplicitKey = true; allowCompact = true; } else if (atExplicitKey) { // i.e. 0x3A/* : */ === character after the explicit key. atExplicitKey = false; allowCompact = true; } else { throw this.#createError( "Cannot read block as explicit mapping pair is incomplete: a key node is missed or followed by a non-tabulated empty line", ); } this.#scanner.next(); ch = following; // // Implicit notation case. Flow-style node as the key first, then ":", and the value. // } else { const newState = this.composeNode({ parentIndent: flowIndent, nodeContext: CONTEXT_FLOW_OUT, allowToSeek: false, allowCompact: true, }); if (!newState) break; // Reading is done. Go to the epilogue. if (this.line === line) { ch = this.#scanner.peek(); this.skipWhitespaces(); ch = this.#scanner.peek(); if (ch === COLON) { this.#scanner.next(); ch = this.#scanner.peek(); if (!isWhiteSpaceOrEOL(ch)) { throw this.#createError( "Cannot read block: a whitespace character is expected after the key-value separator within a block mapping", ); } if (atExplicitKey) { this.storeMappingPair( result, overridableKeys, keyTag as string, keyNode, null, ); keyTag = null; keyNode = null; valueNode = null; } detected = true; atExplicitKey = false; allowCompact = false; keyTag = newState.tag; keyNode = newState.result; } else if (detected) { throw this.#createError( "Cannot read an implicit mapping pair: missing colon", ); } else { const { kind, result } = newState; return { tag, anchor, kind, result }; // Keep the result of `composeNode`. } } else if (detected) { throw this.#createError( "Cannot read a block mapping entry: a multiline key may not be an implicit key", ); } else { const { kind, result } = newState; return { tag, anchor, kind, result }; // Keep the result of `composeNode`. } } // // Common reading code for both explicit and implicit notations. // if (this.line === line || this.lineIndent > nodeIndent) { const newState = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_BLOCK_OUT, allowToSeek: true, allowCompact, }); if (newState) { if (atExplicitKey) { keyNode = newState.result; } else { valueNode = newState.result; } } if (!atExplicitKey) { this.storeMappingPair( result, overridableKeys, keyTag as string, keyNode, valueNode, line, pos, ); keyTag = keyNode = valueNode = null; } this.skipSeparationSpace(true, -1); ch = this.#scanner.peek(); } if (this.lineIndent > nodeIndent && ch !== 0) { throw this.#createError( "Cannot read block: bad indentation of a mapping entry", ); } else if (this.lineIndent < nodeIndent) { break; } } // // Epilogue. // // Special case: last mapping's node contains only the key in explicit notation. if (atExplicitKey) { this.storeMappingPair( result, overridableKeys, keyTag as string, keyNode, null, ); } // Expose the resulting mapping. if (detected) return { tag, anchor, kind: "mapping", result }; } readTagProperty(tag: string | null): string | void { let isVerbatim = false; let isNamed = false; let tagHandle = ""; let tagName: string; let ch = this.#scanner.peek(); if (ch !== EXCLAMATION) return; if (tag !== null) { throw this.#createError( "Cannot read tag property: duplication of a tag property", ); } this.#scanner.next(); ch = this.#scanner.peek(); if (ch === SMALLER_THAN) { isVerbatim = true; this.#scanner.next(); ch = this.#scanner.peek(); } else if (ch === EXCLAMATION) { isNamed = true; tagHandle = "!!"; this.#scanner.next(); ch = this.#scanner.peek(); } else { tagHandle = "!"; } let position = this.#scanner.position; if (isVerbatim) { do { this.#scanner.next(); ch = this.#scanner.peek(); } while (ch !== 0 && ch !== GREATER_THAN); if (!this.#scanner.eof()) { tagName = this.#scanner.source.slice(position, this.#scanner.position); this.#scanner.next(); ch = this.#scanner.peek(); } else { throw this.#createError( "Cannot read tag property: unexpected end of stream", ); } } else { while (ch !== 0 && !isWhiteSpaceOrEOL(ch)) { if (ch === EXCLAMATION) { if (!isNamed) { tagHandle = this.#scanner.source.slice( position - 1, this.#scanner.position + 1, ); if (!PATTERN_TAG_HANDLE_REGEXP.test(tagHandle)) { throw this.#createError( "Cannot read tag property: named tag handle contains invalid characters", ); } isNamed = true; position = this.#scanner.position + 1; } else { throw this.#createError( "Cannot read tag property: tag suffix cannot contain an exclamation mark", ); } } this.#scanner.next(); ch = this.#scanner.peek(); } tagName = this.#scanner.source.slice(position, this.#scanner.position); if (PATTERN_FLOW_INDICATORS_REGEXP.test(tagName)) { throw this.#createError( "Cannot read tag property: tag suffix cannot contain flow indicator characters", ); } } if (tagName && !PATTERN_TAG_URI_REGEXP.test(tagName)) { throw this.#createError( `Cannot read tag property: invalid characters in tag name "${tagName}"`, ); } if (isVerbatim) { return tagName; } else if (this.tagMap.has(tagHandle)) { return this.tagMap.get(tagHandle) + tagName; } else if (tagHandle === "!") { return `!${tagName}`; } else if (tagHandle === "!!") { return `tag:yaml.org,2002:${tagName}`; } throw this.#createError( `Cannot read tag property: undeclared tag handle "${tagHandle}"`, ); } readAnchorProperty(anchor: string | null): string | void { let ch = this.#scanner.peek(); if (ch !== AMPERSAND) return; if (anchor !== null) { throw this.#createError( "Cannot read anchor property: duplicate anchor property", ); } this.#scanner.next(); ch = this.#scanner.peek(); const position = this.#scanner.position; while (ch !== 0 && !isWhiteSpaceOrEOL(ch) && !isFlowIndicator(ch)) { this.#scanner.next(); ch = this.#scanner.peek(); } if (this.#scanner.position === position) { throw this.#createError( "Cannot read anchor property: name of an anchor node must contain at least one character", ); } return this.#scanner.source.slice(position, this.#scanner.position); } readAlias(): string | void { if (this.#scanner.peek() !== ASTERISK) return; this.#scanner.next(); let ch = this.#scanner.peek(); const position = this.#scanner.position; while (ch !== 0 && !isWhiteSpaceOrEOL(ch) && !isFlowIndicator(ch)) { this.#scanner.next(); ch = this.#scanner.peek(); } if (this.#scanner.position === position) { throw this.#createError( "Cannot read alias: alias name must contain at least one character", ); } const alias = this.#scanner.source.slice(position, this.#scanner.position); if (!this.anchorMap.has(alias)) { throw this.#createError( `Cannot read alias: unidentified alias "${alias}"`, ); } this.skipSeparationSpace(true, -1); return this.anchorMap.get(alias); } resolveTag(state: State) { switch (state.tag) { case null: case "!": return state; case "?": { for (const type of this.implicitTypes) { // 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)) continue; // `state.result` updated in resolver if matched const result = type.construct(state.result); state.result = result; state.tag = type.tag; const { anchor } = state; if (anchor !== null) this.anchorMap.set(anchor, result); return state; } return state; } } const kind = state.kind ?? "fallback"; const map = this.typeMap[kind]; const type = map.get(state.tag); if (!type) { throw this.#createError( `Cannot resolve unknown tag !<${state.tag}>`, ); } if (state.result !== null && type.kind !== state.kind) { throw this.#createError( `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 throw this.#createError( `Cannot resolve a node with !<${state.tag}> explicit tag`, ); } const result = type.construct(state.result); state.result = result; const { anchor } = state; if (anchor !== null) this.anchorMap.set(anchor, result); return state; } composeNode({ parentIndent, nodeContext, allowToSeek, allowCompact }: { parentIndent: number; nodeContext: number; allowToSeek: boolean; allowCompact: boolean; }): State | void { let indentStatus = 1; // 1: this>parent, 0: this=parent, -1: this 0 || ch !== PERCENT) { break; } hasDirectives = true; this.#scanner.next(); ch = this.#scanner.peek(); let position = this.#scanner.position; while (ch !== 0 && !isWhiteSpaceOrEOL(ch)) { this.#scanner.next(); ch = this.#scanner.peek(); } const directiveName = this.#scanner.source.slice( position, this.#scanner.position, ); const directiveArgs = []; if (directiveName.length < 1) { throw this.#createError( "Cannot read document: directive name length must be greater than zero", ); } while (ch !== 0) { this.skipWhitespaces(); this.skipComment(); ch = this.#scanner.peek(); if (isEOL(ch)) break; position = this.#scanner.position; while (ch !== 0 && !isWhiteSpaceOrEOL(ch)) { this.#scanner.next(); ch = this.#scanner.peek(); } directiveArgs.push( this.#scanner.source.slice(position, this.#scanner.position), ); } if (ch !== 0) this.readLineBreak(); switch (directiveName) { case "YAML": if (version !== null) { throw this.#createError( "Cannot handle YAML directive: duplication of %YAML directive", ); } version = this.yamlDirectiveHandler(directiveArgs); break; case "TAG": this.tagDirectiveHandler(directiveArgs); break; default: this.dispatchWarning(`unknown document directive "${directiveName}"`); break; } ch = this.#scanner.peek(); } return hasDirectives; } readDocument() { const documentStart = this.#scanner.position; this.checkLineBreaks = false; this.tagMap = new Map(); this.anchorMap = new Map(); const hasDirectives = this.readDirectives(); this.skipSeparationSpace(true, -1); let result = null; if ( this.lineIndent === 0 && this.#scanner.peek() === MINUS && this.#scanner.peek(1) === MINUS && this.#scanner.peek(2) === MINUS ) { this.#scanner.position += 3; this.skipSeparationSpace(true, -1); } else if (hasDirectives) { throw this.#createError( "Cannot read document: directives end mark is expected", ); } const newState = this.composeNode({ parentIndent: this.lineIndent - 1, nodeContext: CONTEXT_BLOCK_OUT, allowToSeek: false, allowCompact: true, }); if (newState) result = newState.result; this.skipSeparationSpace(true, -1); if ( this.checkLineBreaks && PATTERN_NON_ASCII_LINE_BREAKS_REGEXP.test( this.#scanner.source.slice(documentStart, this.#scanner.position), ) ) { this.dispatchWarning("non-ASCII line breaks are interpreted as content"); } if ( this.#scanner.position === this.lineStart && this.testDocumentSeparator() ) { if (this.#scanner.peek() === DOT) { this.#scanner.position += 3; this.skipSeparationSpace(true, -1); } } else if (!this.#scanner.eof()) { throw this.#createError( "Cannot read document: end of the stream or a document separator is expected", ); } return result; } *readDocuments( options: { singleDocument?: boolean } = {}, ): Generator { const { singleDocument = false } = options; let yielded = 0; while (!this.#scanner.eof()) { if (singleDocument && yielded > 0) { throw this.#createError( "Found more than 1 document in the stream: expected a single document", ); } yield this.readDocument(); yielded++; } } }