From e51ae2a77252fc3e86192d005d4e02ee7ac04f54 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Dec 2025 08:54:18 +0100 Subject: [PATCH 1/9] initial commit --- yaml/_loader_state.ts | 567 ++++++++++++++++++++++++------------------ 1 file changed, 320 insertions(+), 247 deletions(-) diff --git a/yaml/_loader_state.ts b/yaml/_loader_state.ts index 6208dc8d0dd1..ec451f05e839 100644 --- a/yaml/_loader_state.ts +++ b/yaml/_loader_state.ts @@ -134,6 +134,18 @@ function codepointToChar(codepoint: number): string { ); } +function writeFoldedLines( + data: unknown[] | Record | string | null, + count: number, +) { + if (count === 1) { + data += " "; + } else if (count > 1) { + data += "\n".repeat(count - 1); + } + return data; +} + const INDENT = 4; const MAX_LENGTH = 75; const DELIMITERS = "\x00\r\n\x85\u2028\u2029"; @@ -181,6 +193,13 @@ function markToString( return where; } +interface Node { + tag: string | null | undefined; + anchor: string | null | undefined; + kind: string | null | undefined; + result: unknown[] | Record | string | null; +} + export class LoaderState { input: string; length: number; @@ -196,10 +215,6 @@ export class LoaderState { checkLineBreaks = false; tagMap = new Map(); anchorMap = new Map(); - tag: string | null | undefined; - anchor: string | null | undefined; - kind: string | null | undefined; - result: unknown[] | Record | string | null = ""; constructor( input: string, @@ -325,17 +340,22 @@ export class LoaderState { this.tagMap.set(handle, prefix); } - captureSegment(start: number, end: number, checkJson: boolean) { + captureSegment( + result: unknown[] | Record | string | null, + start: number, + end: number, + checkJson: boolean, + ) { if (start < end) { - const result = this.input.slice(start, end); + const newResult = this.input.slice(start, end); if (checkJson) { for ( let position = 0; - position < result.length; + position < newResult.length; position++ ) { - const character = result.charCodeAt(position); + const character = newResult.charCodeAt(position); if ( !(character === 0x09 || (0x20 <= character && character <= 0x10ffff)) @@ -345,22 +365,23 @@ export class LoaderState { ); } } - } else if (PATTERN_NON_PRINTABLE.test(result)) { + } else if (PATTERN_NON_PRINTABLE.test(newResult)) { throw this.#createError("Stream contains non-printable characters"); } - this.result += result; + result += newResult; } + return result; } - readBlockSequence(nodeIndent: number): boolean { + readBlockSequence(node: Node, nodeIndent: number): Node | void { let detected = false; - const tag = this.tag; - const anchor = this.anchor; + const tag = node.tag; + const anchor = node.anchor; const result: unknown[] = []; - if (this.anchor !== null && typeof this.anchor !== "undefined") { - this.anchorMap.set(this.anchor, result); + if (node.anchor !== null && typeof node.anchor !== "undefined") { + this.anchorMap.set(node.anchor, result); } let ch = this.peek(); @@ -388,13 +409,13 @@ export class LoaderState { } const line = this.line; - this.composeNode({ + const composedNode = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_BLOCK_IN, allowToSeek: false, allowCompact: true, }); - result.push(this.result); + result.push(composedNode?.result); this.skipSeparationSpace(true, -1); ch = this.peek(); @@ -409,13 +430,8 @@ export class LoaderState { } if (detected) { - this.tag = tag; - this.anchor = anchor; - this.kind = "sequence"; - this.result = result; - return true; + return { tag, anchor, kind: "sequence", result }; } - return false; } mergeMappings( destination: Record, @@ -586,16 +602,12 @@ export class LoaderState { return false; } - writeFoldedLines(count: number) { - if (count === 1) { - this.result += " "; - } else if (count > 1) { - this.result += "\n".repeat(count - 1); - } - } - readPlainScalar(nodeIndent: number, withinFlowCollection: boolean): boolean { - const kind = this.kind; - const result = this.result; + + readPlainScalar( + node: Node, + nodeIndent: number, + withinFlowCollection: boolean, + ): Node | void { let ch = this.peek(); if ( @@ -613,7 +625,7 @@ export class LoaderState { ch === COMMERCIAL_AT || ch === GRAVE_ACCENT ) { - return false; + return; } let following: number; @@ -624,12 +636,12 @@ export class LoaderState { isWhiteSpaceOrEOL(following) || (withinFlowCollection && isFlowIndicator(following)) ) { - return false; + return; } } - this.kind = "scalar"; - this.result = ""; + let result: string | unknown[] | Record | null = ""; + let captureEnd = this.position; let captureStart = this.position; let hasPendingContent = false; @@ -675,8 +687,13 @@ export class LoaderState { } if (hasPendingContent) { - this.captureSegment(captureStart, captureEnd, false); - this.writeFoldedLines(this.line - line); + node.result = this.captureSegment( + node.result, + captureStart, + captureEnd, + false, + ); + node.result = writeFoldedLines(node.result, this.line - line); captureStart = captureEnd = this.position; hasPendingContent = false; } @@ -688,33 +705,27 @@ export class LoaderState { ch = this.next(); } - this.captureSegment(captureStart, captureEnd, false); - - if (this.result) { - return true; - } + result = this.captureSegment(result, captureStart, captureEnd, false); - this.kind = kind; - this.result = result; - return false; + if (result) return { ...node, kind: "scalar", result }; } - readSingleQuotedScalar(nodeIndent: number): boolean { + readSingleQuotedScalar(node: Node, nodeIndent: number): Node | undefined { let ch = this.peek(); if (ch !== SINGLE_QUOTE) { - return false; + return; } - this.kind = "scalar"; - this.result = ""; this.position++; let captureStart = this.position; let captureEnd = this.position; + let result: string | unknown[] | Record | null = ""; + ch = this.peek(); while (ch !== 0) { if (ch === SINGLE_QUOTE) { - this.captureSegment(captureStart, this.position, true); + result = this.captureSegment(result, captureStart, this.position, true); ch = this.next(); if (ch === SINGLE_QUOTE) { @@ -722,11 +733,14 @@ export class LoaderState { this.position++; captureEnd = this.position; } else { - return true; + return { ...node, kind: "scalar", result }; } } else if (isEOL(ch)) { - this.captureSegment(captureStart, captureEnd, true); - this.writeFoldedLines(this.skipSeparationSpace(false, nodeIndent)); + result = this.captureSegment(result, captureStart, captureEnd, true); + result = writeFoldedLines( + result, + this.skipSeparationSpace(false, nodeIndent), + ); captureStart = captureEnd = this.position; } else if ( this.position === this.lineStart && @@ -746,15 +760,15 @@ export class LoaderState { "Unexpected end of the stream within a single quoted scalar", ); } - readDoubleQuotedScalar(nodeIndent: number): boolean { + readDoubleQuotedScalar(node: Node, nodeIndent: number): Node | undefined { let ch = this.peek(); if (ch !== DOUBLE_QUOTE) { - return false; + return; } - this.kind = "scalar"; - this.result = ""; + let result: string | unknown[] | Record | null = ""; + this.position++; let captureEnd = this.position; let captureStart = this.position; @@ -762,18 +776,18 @@ export class LoaderState { ch = this.peek(); while (ch !== 0) { if (ch === DOUBLE_QUOTE) { - this.captureSegment(captureStart, this.position, true); + result = this.captureSegment(result, captureStart, this.position, true); this.position++; - return true; + return { ...node, kind: "scalar", result }; } if (ch === BACKSLASH) { - this.captureSegment(captureStart, this.position, true); + result = this.captureSegment(result, captureStart, this.position, true); ch = this.next(); if (isEOL(ch)) { this.skipSeparationSpace(false, nodeIndent); } else if (ch < 256 && SIMPLE_ESCAPE_SEQUENCES.has(ch)) { - this.result += SIMPLE_ESCAPE_SEQUENCES.get(ch); + (result as string) += SIMPLE_ESCAPE_SEQUENCES.get(ch); this.position++; } else if ((tmp = ESCAPED_HEX_LENGTHS.get(ch) ?? 0) > 0) { let hexLength = tmp; @@ -791,7 +805,7 @@ export class LoaderState { } } - this.result += codepointToChar(hexResult); + result += codepointToChar(hexResult); this.position++; } else { @@ -802,8 +816,11 @@ export class LoaderState { captureStart = captureEnd = this.position; } else if (isEOL(ch)) { - this.captureSegment(captureStart, captureEnd, true); - this.writeFoldedLines(this.skipSeparationSpace(false, nodeIndent)); + result = this.captureSegment(result, captureStart, captureEnd, true); + result = writeFoldedLines( + result, + this.skipSeparationSpace(false, nodeIndent), + ); captureStart = captureEnd = this.position; } else if ( this.position === this.lineStart && @@ -823,7 +840,7 @@ export class LoaderState { "Unexpected end of the stream within a double quoted scalar", ); } - readFlowCollection(nodeIndent: number): boolean { + readFlowCollection(node: Node, nodeIndent: number): Node | undefined { let ch = this.peek(); let terminator: number; let isMapping = true; @@ -835,17 +852,17 @@ export class LoaderState { } else if (ch === LEFT_CURLY_BRACKET) { terminator = RIGHT_CURLY_BRACKET; } else { - return false; + return; } - if (this.anchor !== null && typeof this.anchor !== "undefined") { - this.anchorMap.set(this.anchor, result); + const { tag, anchor } = node; + + if (node.anchor !== null && typeof node.anchor !== "undefined") { + this.anchorMap.set(node.anchor, result); } ch = this.next(); - const tag = this.tag; - const anchor = this.anchor; let readNext = true; let valueNode = null; let keyNode = null; @@ -862,11 +879,8 @@ export class LoaderState { if (ch === terminator) { this.position++; - this.tag = tag; - this.anchor = anchor; - this.kind = isMapping ? "mapping" : "sequence"; - this.result = result; - return true; + const kind = isMapping ? "mapping" : "sequence"; + return { tag, anchor, kind, result }; } if (!readNext) { throw this.#createError( @@ -888,14 +902,15 @@ export class LoaderState { } line = this.line; - this.composeNode({ + const composedNode = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_FLOW_IN, allowToSeek: false, allowCompact: true, }); - keyTag = this.tag || null; - keyNode = this.result; + if (composedNode) node = composedNode; + keyTag = node.tag || null; + keyNode = node.result; this.skipSeparationSpace(true, nodeIndent); ch = this.peek(); @@ -904,13 +919,14 @@ export class LoaderState { isPair = true; ch = this.next(); this.skipSeparationSpace(true, nodeIndent); - this.composeNode({ + const composedNode = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_FLOW_IN, allowToSeek: false, allowCompact: true, }); - valueNode = this.result; + if (composedNode) node = composedNode; + valueNode = node.result; } if (isMapping) { @@ -953,7 +969,7 @@ export class LoaderState { } // Handles block scaler styles: e.g. '|', '>', '|-' and '>-'. // https://yaml.org/spec/1.2.2/#81-block-scalar-styles - readBlockScalar(nodeIndent: number): boolean { + readBlockScalar(node: Node, nodeIndent: number): Node | undefined { let chomping = CHOMPING_CLIP; let didReadContent = false; let detectedIndent = false; @@ -969,12 +985,9 @@ export class LoaderState { } else if (ch === GREATER_THAN) { folding = true; } else { - return false; + return; } - this.kind = "scalar"; - this.result = ""; - let tmp = 0; while (ch !== 0) { ch = this.next(); @@ -1011,6 +1024,8 @@ export class LoaderState { ch = this.peek(); } + let result: string | unknown[] | Record | null = ""; + while (ch !== 0) { this.readLineBreak(); this.lineIndent = 0; @@ -1038,13 +1053,13 @@ export class LoaderState { if (this.lineIndent < textIndent) { // Perform the chomping. if (chomping === CHOMPING_KEEP) { - this.result += "\n".repeat( + result += "\n".repeat( didReadContent ? 1 + emptyLines : emptyLines, ); } else if (chomping === CHOMPING_CLIP) { if (didReadContent) { // i.e. only if the scalar is not empty. - this.result += "\n"; + result += "\n"; } } @@ -1058,31 +1073,31 @@ export class LoaderState { if (isWhiteSpace(ch)) { atMoreIndented = true; // except for the first content line (cf. Example 8.1) - this.result += "\n".repeat( + result += "\n".repeat( didReadContent ? 1 + emptyLines : emptyLines, ); // End of more-indented block. } else if (atMoreIndented) { atMoreIndented = false; - this.result += "\n".repeat(emptyLines + 1); + 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. - this.result += " "; + result += " "; } // Several line breaks - perceive as different lines. } else { - this.result += "\n".repeat(emptyLines); + 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. - this.result += "\n".repeat( + result += "\n".repeat( didReadContent ? 1 + emptyLines : emptyLines, ); } @@ -1096,14 +1111,17 @@ export class LoaderState { ch = this.next(); } - this.captureSegment(captureStart, this.position, false); + result = this.captureSegment(result, captureStart, this.position, false); } - - return true; + return { ...node, kind: "scalar", result }; } - readBlockMapping(nodeIndent: number, flowIndent: number): boolean { - const tag = this.tag; - const anchor = this.anchor; + readBlockMapping( + node: Node, + nodeIndent: number, + flowIndent: number, + ): Node | undefined { + const tag = node.tag; + const anchor = node.anchor; const result = {}; const overridableKeys = new Set(); @@ -1116,8 +1134,8 @@ export class LoaderState { let atExplicitKey = false; let detected = false; - if (this.anchor !== null && typeof this.anchor !== "undefined") { - this.anchorMap.set(this.anchor, result); + if (node.anchor !== null && typeof node.anchor !== "undefined") { + this.anchorMap.set(node.anchor, result); } let ch = this.peek(); @@ -1165,85 +1183,81 @@ export class LoaderState { // // Implicit notation case. Flow-style node as the key first, then ":", and the value. // - } else if ( - this.composeNode({ + } else { + const composedNode = this.composeNode({ parentIndent: flowIndent, nodeContext: CONTEXT_FLOW_OUT, allowToSeek: false, allowCompact: true, - }) - ) { - if (this.line === line) { - ch = this.peek(); - - this.skipWhitespaces(); - ch = this.peek(); - - if (ch === COLON) { - ch = this.next(); - - if (!isWhiteSpaceOrEOL(ch)) { + }); + if (composedNode) { + if (this.line === line) { + ch = this.peek(); + + this.skipWhitespaces(); + ch = this.peek(); + + if (ch === COLON) { + ch = this.next(); + + 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 = composedNode.tag; + keyNode = composedNode.result; + } else if (detected) { 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, + "Cannot read an implicit mapping pair: missing colon", ); - keyTag = null; - keyNode = null; - valueNode = null; + } else { + return { ...composedNode, tag, anchor }; } - - detected = true; - atExplicitKey = false; - allowCompact = false; - keyTag = this.tag; - keyNode = this.result; } else if (detected) { throw this.#createError( - "Cannot read an implicit mapping pair: missing colon", + "Cannot read a block mapping entry: a multiline key may not be an implicit key", ); } else { - this.tag = tag; - this.anchor = anchor; - return true; // Keep the result of `composeNode`. + return { ...composedNode, tag, anchor }; } - } else if (detected) { - throw this.#createError( - "Cannot read a block mapping entry: a multiline key may not be an implicit key", - ); } else { - this.tag = tag; - this.anchor = anchor; - return true; // Keep the result of `composeNode`. + break; // Reading is done. Go to the epilogue. } - } else { - break; // Reading is done. Go to the epilogue. } // // Common reading code for both explicit and implicit notations. // if (this.line === line || this.lineIndent > nodeIndent) { - if ( - this.composeNode({ - parentIndent: nodeIndent, - nodeContext: CONTEXT_BLOCK_OUT, - allowToSeek: true, - allowCompact, - }) - ) { + const composedNode = this.composeNode({ + parentIndent: nodeIndent, + nodeContext: CONTEXT_BLOCK_OUT, + allowToSeek: true, + allowCompact, + }); + if (composedNode) { if (atExplicitKey) { - keyNode = this.result; + keyNode = composedNode.result; } else { - valueNode = this.result; + valueNode = composedNode.result; } } @@ -1290,15 +1304,10 @@ export class LoaderState { // Expose the resulting mapping. if (detected) { - this.tag = tag; - this.anchor = anchor; - this.kind = "mapping"; - this.result = result; + return { tag, anchor, kind: "mapping", result }; } - - return detected; } - readTagProperty(): boolean { + readTagProperty(tag: string | null | undefined): string | null | undefined { let isVerbatim = false; let isNamed = false; let tagHandle = ""; @@ -1306,9 +1315,9 @@ export class LoaderState { let ch = this.peek(); - if (ch !== EXCLAMATION) return false; + if (ch !== EXCLAMATION) return; - if (this.tag !== null) { + if (tag !== null) { throw this.#createError( "Cannot read tag property: duplication of a tag property", ); @@ -1382,26 +1391,28 @@ export class LoaderState { } if (isVerbatim) { - this.tag = tagName; - } else if (this.tagMap.has(tagHandle)) { - this.tag = this.tagMap.get(tagHandle) + tagName; - } else if (tagHandle === "!") { - this.tag = `!${tagName}`; - } else if (tagHandle === "!!") { - this.tag = `tag:yaml.org,2002:${tagName}`; - } else { - throw this.#createError( - `Cannot read tag property: undeclared tag handle "${tagHandle}"`, - ); + return tagName; } - - return true; + if (this.tagMap.has(tagHandle)) { + return this.tagMap.get(tagHandle) + tagName; + } + if (tagHandle === "!") { + return `!${tagName}`; + } + if (tagHandle === "!!") { + return `tag:yaml.org,2002:${tagName}`; + } + throw this.#createError( + `Cannot read tag property: undeclared tag handle "${tagHandle}"`, + ); } - readAnchorProperty(): boolean { + readAnchorProperty( + anchor: string | null | undefined, + ): string | null | undefined { let ch = this.peek(); - if (ch !== AMPERSAND) return false; + if (ch !== AMPERSAND) return; - if (this.anchor !== null) { + if (anchor !== null) { throw this.#createError( "Cannot read anchor property: duplicate anchor property", ); @@ -1419,10 +1430,9 @@ export class LoaderState { ); } - this.anchor = this.input.slice(position, this.position); - return true; + return this.input.slice(position, this.position); } - readAlias(): boolean { + readAlias() { if (this.peek() !== ASTERISK) return false; let ch = this.next(); @@ -1446,9 +1456,8 @@ export class LoaderState { ); } - this.result = this.anchorMap.get(alias); this.skipSeparationSpace(true, -1); - return true; + return this.anchorMap.get(alias); } composeNode( @@ -1458,16 +1467,18 @@ export class LoaderState { allowToSeek: boolean; allowCompact: boolean; }, - ): boolean { + ): Node | void { let indentStatus = 1; // 1: this>parent, 0: this=parent, -1: this; - this.tag = null; - this.anchor = null; - this.kind = null; - this.result = null; + let resultNode: Node = { + tag: null, + anchor: null, + kind: null, + result: null, + }; const allowBlockScalars = CONTEXT_BLOCK_OUT === nodeContext || CONTEXT_BLOCK_IN === nodeContext; @@ -1490,7 +1501,16 @@ export class LoaderState { } if (indentStatus === 1) { - while (this.readTagProperty() || this.readAnchorProperty()) { + while (true) { + const tag = this.readTagProperty(resultNode.tag); + if (tag) { + resultNode.tag = tag; + } else { + const anchor = this.readAnchorProperty(resultNode.anchor); + if (!anchor) break; + resultNode.anchor = anchor; + } + if (this.skipSeparationSpace(true, -1)) { atNewLine = true; allowBlockCollections = allowBlockStyles; @@ -1520,52 +1540,96 @@ export class LoaderState { const blockIndent = this.position - this.lineStart; if (indentStatus === 1) { - if ( - (allowBlockCollections && - (this.readBlockSequence(blockIndent) || - this.readBlockMapping(blockIndent, flowIndent))) || - this.readFlowCollection(flowIndent) - ) { - hasContent = true; - } else { - if ( - (allowBlockScalars && this.readBlockScalar(flowIndent)) || - this.readSingleQuotedScalar(flowIndent) || - this.readDoubleQuotedScalar(flowIndent) - ) { - hasContent = true; - } else if (this.readAlias()) { + const node = this.readBlockSequence(resultNode, blockIndent); + if (node) resultNode = node; + if (allowBlockCollections) { + if (node) { hasContent = true; - - if (this.tag !== null || this.anchor !== null) { - throw this.#createError( - "Cannot compose node: alias node should not have any properties", - ); + } else { + const node = this.readBlockMapping( + resultNode, + blockIndent, + flowIndent, + ); + if (node) { + resultNode = node; + hasContent = true; } - } else if ( - this.readPlainScalar(flowIndent, CONTEXT_FLOW_IN === nodeContext) - ) { + } + } else { + const node = this.readFlowCollection(resultNode, flowIndent); + if (node) { + resultNode = node; hasContent = true; + } else { + const node = this.readBlockScalar(resultNode, flowIndent); + const didReadBlock = allowBlockScalars && node; + if (node) resultNode = node; - if (this.tag === null) { - this.tag = "?"; + if (didReadBlock) { + hasContent = true; + } else { + const node = this.readSingleQuotedScalar( + resultNode, + flowIndent, + ); + if (node) { + resultNode = node; + hasContent = true; + } else { + const node = this.readDoubleQuotedScalar( + resultNode, + flowIndent, + ); + if (node) { + resultNode = node; + hasContent = true; + } else { + const result = this.readAlias(); + if (result) { + resultNode.result = result; + hasContent = true; + + if (resultNode.tag !== null || resultNode.anchor !== null) { + throw this.#createError( + "Cannot compose node: alias node should not have any properties", + ); + } + } else { + const node = this.readPlainScalar( + resultNode, + flowIndent, + CONTEXT_FLOW_IN === nodeContext, + ); + if (node) { + resultNode = node; + hasContent = true; + + if (node.tag === null) { + node.tag = "?"; + } + } + } + } + + if (resultNode.anchor !== null) { + this.anchorMap.set(resultNode.anchor, resultNode.result); + } + } } } - - if (this.anchor !== null) { - this.anchorMap.set(this.anchor, this.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 && - this.readBlockSequence(blockIndent); + const node = this.readBlockSequence(resultNode, blockIndent); + hasContent = allowBlockCollections && !!node; + if (node) resultNode = node; } } - if (this.tag !== null && this.tag !== "!") { - if (this.tag === "?") { + if (resultNode.tag !== null && resultNode.tag !== "!") { + if (resultNode.tag === "?") { for ( let typeIndex = 0; typeIndex < this.implicitTypes.length; @@ -1577,43 +1641,52 @@ export class LoaderState { // non-specific tag is only assigned to plain scalars. So, it isn't // needed to check for 'kind' conformity. - if (type.resolve(this.result)) { + if (type.resolve(resultNode.result)) { // `state.result` updated in resolver if matched - this.result = type.construct(this.result); - this.tag = type.tag; - if (this.anchor !== null) { - this.anchorMap.set(this.anchor, this.result); + resultNode.result = type.construct(resultNode.result); + resultNode.tag = type.tag; + if (resultNode.anchor !== null) { + this.anchorMap.set(resultNode.anchor, resultNode.result); } break; } } - } else if (this.typeMap[this.kind ?? "fallback"].has(this.tag)) { - const map = this.typeMap[this.kind ?? "fallback"]; - type = map.get(this.tag)!; + } else if ( + this.typeMap[(resultNode.kind ?? "fallback") as KindType].has( + resultNode.tag!, + ) + ) { + const map = this.typeMap[(resultNode.kind ?? "fallback") as KindType]; + type = map.get(resultNode.tag!)!; - if (this.result !== null && type.kind !== this.kind) { + if (resultNode.result !== null && type.kind !== resultNode.kind) { throw this.#createError( - `Unacceptable node kind for !<${this.tag}> tag: it should be "${type.kind}", not "${this.kind}"`, + `Unacceptable node kind for !<${resultNode.tag}> tag: it should be "${type.kind}", not "${resultNode.kind}"`, ); } - if (!type.resolve(this.result)) { + if (!type.resolve(resultNode.result)) { // `state.result` updated in resolver if matched throw this.#createError( - `Cannot resolve a node with !<${this.tag}> explicit tag`, + `Cannot resolve a node with !<${resultNode.tag}> explicit tag`, ); } else { - this.result = type.construct(this.result); - if (this.anchor !== null) { - this.anchorMap.set(this.anchor, this.result); + resultNode.result = type.construct(resultNode.result); + if (resultNode.anchor !== null) { + this.anchorMap.set(resultNode.anchor, resultNode.result); } } } else { - throw this.#createError(`Cannot resolve unknown tag !<${this.tag}>`); + throw this.#createError( + `Cannot resolve unknown tag !<${resultNode.tag}>`, + ); } } - return this.tag !== null || this.anchor !== null || hasContent; + const success = resultNode.tag !== null || resultNode.anchor !== null || + hasContent; + if (!success) return; + return resultNode; } readDirectives() { @@ -1712,7 +1785,7 @@ export class LoaderState { ); } - this.composeNode({ + const composedNode = this.composeNode({ parentIndent: this.lineIndent - 1, nodeContext: CONTEXT_BLOCK_OUT, allowToSeek: false, @@ -1740,7 +1813,7 @@ export class LoaderState { ); } - return this.result; + return composedNode?.result; } *readDocuments() { From 89686e156f207076d57d59838652d211373f5d90 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Dec 2025 09:12:20 +0100 Subject: [PATCH 2/9] rename node to state --- yaml/_loader_state.ts | 222 +++++++++++++++++++++--------------------- 1 file changed, 112 insertions(+), 110 deletions(-) diff --git a/yaml/_loader_state.ts b/yaml/_loader_state.ts index ec451f05e839..1be387ef9637 100644 --- a/yaml/_loader_state.ts +++ b/yaml/_loader_state.ts @@ -193,7 +193,7 @@ function markToString( return where; } -interface Node { +interface State { tag: string | null | undefined; anchor: string | null | undefined; kind: string | null | undefined; @@ -373,15 +373,15 @@ export class LoaderState { } return result; } - readBlockSequence(node: Node, nodeIndent: number): Node | void { + readBlockSequence(state: State, nodeIndent: number): State | void { let detected = false; - const tag = node.tag; - const anchor = node.anchor; + const tag = state.tag; + const anchor = state.anchor; const result: unknown[] = []; - if (node.anchor !== null && typeof node.anchor !== "undefined") { - this.anchorMap.set(node.anchor, result); + if (state.anchor !== null && typeof state.anchor !== "undefined") { + this.anchorMap.set(state.anchor, result); } let ch = this.peek(); @@ -409,13 +409,13 @@ export class LoaderState { } const line = this.line; - const composedNode = this.composeNode({ + const newState = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_BLOCK_IN, allowToSeek: false, allowCompact: true, }); - result.push(composedNode?.result); + result.push(newState?.result); this.skipSeparationSpace(true, -1); ch = this.peek(); @@ -604,10 +604,10 @@ export class LoaderState { } readPlainScalar( - node: Node, + state: State, nodeIndent: number, withinFlowCollection: boolean, - ): Node | void { + ): State | void { let ch = this.peek(); if ( @@ -687,13 +687,13 @@ export class LoaderState { } if (hasPendingContent) { - node.result = this.captureSegment( - node.result, + state.result = this.captureSegment( + state.result, captureStart, captureEnd, false, ); - node.result = writeFoldedLines(node.result, this.line - line); + state.result = writeFoldedLines(state.result, this.line - line); captureStart = captureEnd = this.position; hasPendingContent = false; } @@ -707,9 +707,9 @@ export class LoaderState { result = this.captureSegment(result, captureStart, captureEnd, false); - if (result) return { ...node, kind: "scalar", result }; + if (result) return { ...state, kind: "scalar", result }; } - readSingleQuotedScalar(node: Node, nodeIndent: number): Node | undefined { + readSingleQuotedScalar(state: State, nodeIndent: number): State | void { let ch = this.peek(); if (ch !== SINGLE_QUOTE) { @@ -733,7 +733,7 @@ export class LoaderState { this.position++; captureEnd = this.position; } else { - return { ...node, kind: "scalar", result }; + return { ...state, kind: "scalar", result }; } } else if (isEOL(ch)) { result = this.captureSegment(result, captureStart, captureEnd, true); @@ -760,7 +760,7 @@ export class LoaderState { "Unexpected end of the stream within a single quoted scalar", ); } - readDoubleQuotedScalar(node: Node, nodeIndent: number): Node | undefined { + readDoubleQuotedScalar(state: State, nodeIndent: number): State | void { let ch = this.peek(); if (ch !== DOUBLE_QUOTE) { @@ -778,7 +778,7 @@ export class LoaderState { if (ch === DOUBLE_QUOTE) { result = this.captureSegment(result, captureStart, this.position, true); this.position++; - return { ...node, kind: "scalar", result }; + return { ...state, kind: "scalar", result }; } if (ch === BACKSLASH) { result = this.captureSegment(result, captureStart, this.position, true); @@ -840,7 +840,7 @@ export class LoaderState { "Unexpected end of the stream within a double quoted scalar", ); } - readFlowCollection(node: Node, nodeIndent: number): Node | undefined { + readFlowCollection(state: State, nodeIndent: number): State | void { let ch = this.peek(); let terminator: number; let isMapping = true; @@ -855,10 +855,10 @@ export class LoaderState { return; } - const { tag, anchor } = node; + const { tag, anchor } = state; - if (node.anchor !== null && typeof node.anchor !== "undefined") { - this.anchorMap.set(node.anchor, result); + if (state.anchor !== null && typeof state.anchor !== "undefined") { + this.anchorMap.set(state.anchor, result); } ch = this.next(); @@ -902,15 +902,15 @@ export class LoaderState { } line = this.line; - const composedNode = this.composeNode({ + const newState = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_FLOW_IN, allowToSeek: false, allowCompact: true, }); - if (composedNode) node = composedNode; - keyTag = node.tag || null; - keyNode = node.result; + if (newState) state = newState; + keyTag = state.tag || null; + keyNode = state.result; this.skipSeparationSpace(true, nodeIndent); ch = this.peek(); @@ -919,14 +919,14 @@ export class LoaderState { isPair = true; ch = this.next(); this.skipSeparationSpace(true, nodeIndent); - const composedNode = this.composeNode({ + const newState = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_FLOW_IN, allowToSeek: false, allowCompact: true, }); - if (composedNode) node = composedNode; - valueNode = node.result; + if (newState) state = newState; + valueNode = state.result; } if (isMapping) { @@ -969,7 +969,7 @@ export class LoaderState { } // Handles block scaler styles: e.g. '|', '>', '|-' and '>-'. // https://yaml.org/spec/1.2.2/#81-block-scalar-styles - readBlockScalar(node: Node, nodeIndent: number): Node | undefined { + readBlockScalar(state: State, nodeIndent: number): State | void { let chomping = CHOMPING_CLIP; let didReadContent = false; let detectedIndent = false; @@ -1113,15 +1113,15 @@ export class LoaderState { result = this.captureSegment(result, captureStart, this.position, false); } - return { ...node, kind: "scalar", result }; + return { ...state, kind: "scalar", result }; } readBlockMapping( - node: Node, + state: State, nodeIndent: number, flowIndent: number, - ): Node | undefined { - const tag = node.tag; - const anchor = node.anchor; + ): State | void { + const tag = state.tag; + const anchor = state.anchor; const result = {}; const overridableKeys = new Set(); @@ -1134,8 +1134,8 @@ export class LoaderState { let atExplicitKey = false; let detected = false; - if (node.anchor !== null && typeof node.anchor !== "undefined") { - this.anchorMap.set(node.anchor, result); + if (state.anchor !== null && typeof state.anchor !== "undefined") { + this.anchorMap.set(state.anchor, result); } let ch = this.peek(); @@ -1184,13 +1184,13 @@ export class LoaderState { // Implicit notation case. Flow-style node as the key first, then ":", and the value. // } else { - const composedNode = this.composeNode({ + const newState = this.composeNode({ parentIndent: flowIndent, nodeContext: CONTEXT_FLOW_OUT, allowToSeek: false, allowCompact: true, }); - if (composedNode) { + if (newState) { if (this.line === line) { ch = this.peek(); @@ -1222,21 +1222,21 @@ export class LoaderState { detected = true; atExplicitKey = false; allowCompact = false; - keyTag = composedNode.tag; - keyNode = composedNode.result; + keyTag = newState.tag; + keyNode = newState.result; } else if (detected) { throw this.#createError( "Cannot read an implicit mapping pair: missing colon", ); } else { - return { ...composedNode, tag, anchor }; + return { ...newState, tag, anchor }; } } else if (detected) { throw this.#createError( "Cannot read a block mapping entry: a multiline key may not be an implicit key", ); } else { - return { ...composedNode, tag, anchor }; + return { ...newState, tag, anchor }; } } else { break; // Reading is done. Go to the epilogue. @@ -1247,17 +1247,17 @@ export class LoaderState { // Common reading code for both explicit and implicit notations. // if (this.line === line || this.lineIndent > nodeIndent) { - const composedNode = this.composeNode({ + const newState = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_BLOCK_OUT, allowToSeek: true, allowCompact, }); - if (composedNode) { + if (newState) { if (atExplicitKey) { - keyNode = composedNode.result; + keyNode = newState.result; } else { - valueNode = composedNode.result; + valueNode = newState.result; } } @@ -1467,13 +1467,13 @@ export class LoaderState { allowToSeek: boolean; allowCompact: boolean; }, - ): Node | void { + ): State | void { let indentStatus = 1; // 1: this>parent, 0: this=parent, -1: this; - let resultNode: Node = { + let resultState: State = { tag: null, anchor: null, kind: null, @@ -1502,13 +1502,13 @@ export class LoaderState { if (indentStatus === 1) { while (true) { - const tag = this.readTagProperty(resultNode.tag); + const tag = this.readTagProperty(resultState.tag); if (tag) { - resultNode.tag = tag; + resultState.tag = tag; } else { - const anchor = this.readAnchorProperty(resultNode.anchor); + const anchor = this.readAnchorProperty(resultState.anchor); if (!anchor) break; - resultNode.anchor = anchor; + resultState.anchor = anchor; } if (this.skipSeparationSpace(true, -1)) { @@ -1540,80 +1540,82 @@ export class LoaderState { const blockIndent = this.position - this.lineStart; if (indentStatus === 1) { - const node = this.readBlockSequence(resultNode, blockIndent); - if (node) resultNode = node; + const state = this.readBlockSequence(resultState, blockIndent); + if (state) resultState = state; if (allowBlockCollections) { - if (node) { + if (state) { hasContent = true; } else { - const node = this.readBlockMapping( - resultNode, + const state = this.readBlockMapping( + resultState, blockIndent, flowIndent, ); - if (node) { - resultNode = node; + if (state) { + resultState = state; hasContent = true; } } } else { - const node = this.readFlowCollection(resultNode, flowIndent); - if (node) { - resultNode = node; + const state = this.readFlowCollection(resultState, flowIndent); + if (state) { + resultState = state; hasContent = true; } else { - const node = this.readBlockScalar(resultNode, flowIndent); - const didReadBlock = allowBlockScalars && node; - if (node) resultNode = node; + const state = this.readBlockScalar(resultState, flowIndent); + const didReadBlock = allowBlockScalars && state; + if (state) resultState = state; if (didReadBlock) { hasContent = true; } else { - const node = this.readSingleQuotedScalar( - resultNode, + const state = this.readSingleQuotedScalar( + resultState, flowIndent, ); - if (node) { - resultNode = node; + if (state) { + resultState = state; hasContent = true; } else { - const node = this.readDoubleQuotedScalar( - resultNode, + const state = this.readDoubleQuotedScalar( + resultState, flowIndent, ); - if (node) { - resultNode = node; + if (state) { + resultState = state; hasContent = true; } else { const result = this.readAlias(); if (result) { - resultNode.result = result; + resultState.result = result; hasContent = true; - if (resultNode.tag !== null || resultNode.anchor !== null) { + if ( + resultState.tag !== null || resultState.anchor !== null + ) { throw this.#createError( "Cannot compose node: alias node should not have any properties", ); } } else { - const node = this.readPlainScalar( - resultNode, + const state = this.readPlainScalar( + resultState, flowIndent, CONTEXT_FLOW_IN === nodeContext, ); - if (node) { - resultNode = node; + if (state) { + resultState = state; hasContent = true; - if (node.tag === null) { - node.tag = "?"; + if (state.tag === null) { + state.tag = "?"; } } } } - if (resultNode.anchor !== null) { - this.anchorMap.set(resultNode.anchor, resultNode.result); + if (resultState.anchor !== null) { + this.anchorMap.set(resultState.anchor, resultState.result); } } } @@ -1622,14 +1624,14 @@ export class LoaderState { } 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 - const node = this.readBlockSequence(resultNode, blockIndent); - hasContent = allowBlockCollections && !!node; - if (node) resultNode = node; + const state = this.readBlockSequence(resultState, blockIndent); + hasContent = allowBlockCollections && !!state; + if (state) resultState = state; } } - if (resultNode.tag !== null && resultNode.tag !== "!") { - if (resultNode.tag === "?") { + if (resultState.tag !== null && resultState.tag !== "!") { + if (resultState.tag === "?") { for ( let typeIndex = 0; typeIndex < this.implicitTypes.length; @@ -1641,52 +1643,52 @@ export class LoaderState { // non-specific tag is only assigned to plain scalars. So, it isn't // needed to check for 'kind' conformity. - if (type.resolve(resultNode.result)) { + if (type.resolve(resultState.result)) { // `state.result` updated in resolver if matched - resultNode.result = type.construct(resultNode.result); - resultNode.tag = type.tag; - if (resultNode.anchor !== null) { - this.anchorMap.set(resultNode.anchor, resultNode.result); + resultState.result = type.construct(resultState.result); + resultState.tag = type.tag; + if (resultState.anchor !== null) { + this.anchorMap.set(resultState.anchor, resultState.result); } break; } } } else if ( - this.typeMap[(resultNode.kind ?? "fallback") as KindType].has( - resultNode.tag!, + this.typeMap[(resultState.kind ?? "fallback") as KindType].has( + resultState.tag!, ) ) { - const map = this.typeMap[(resultNode.kind ?? "fallback") as KindType]; - type = map.get(resultNode.tag!)!; + const map = this.typeMap[(resultState.kind ?? "fallback") as KindType]; + type = map.get(resultState.tag!)!; - if (resultNode.result !== null && type.kind !== resultNode.kind) { + if (resultState.result !== null && type.kind !== resultState.kind) { throw this.#createError( - `Unacceptable node kind for !<${resultNode.tag}> tag: it should be "${type.kind}", not "${resultNode.kind}"`, + `Unacceptable node kind for !<${resultState.tag}> tag: it should be "${type.kind}", not "${resultState.kind}"`, ); } - if (!type.resolve(resultNode.result)) { + if (!type.resolve(resultState.result)) { // `state.result` updated in resolver if matched throw this.#createError( - `Cannot resolve a node with !<${resultNode.tag}> explicit tag`, + `Cannot resolve a node with !<${resultState.tag}> explicit tag`, ); } else { - resultNode.result = type.construct(resultNode.result); - if (resultNode.anchor !== null) { - this.anchorMap.set(resultNode.anchor, resultNode.result); + resultState.result = type.construct(resultState.result); + if (resultState.anchor !== null) { + this.anchorMap.set(resultState.anchor, resultState.result); } } } else { throw this.#createError( - `Cannot resolve unknown tag !<${resultNode.tag}>`, + `Cannot resolve unknown tag !<${resultState.tag}>`, ); } } - const success = resultNode.tag !== null || resultNode.anchor !== null || + const success = resultState.tag !== null || resultState.anchor !== null || hasContent; if (!success) return; - return resultNode; + return resultState; } readDirectives() { @@ -1785,7 +1787,7 @@ export class LoaderState { ); } - const composedNode = this.composeNode({ + const newState = this.composeNode({ parentIndent: this.lineIndent - 1, nodeContext: CONTEXT_BLOCK_OUT, allowToSeek: false, @@ -1813,7 +1815,7 @@ export class LoaderState { ); } - return composedNode?.result; + return newState?.result; } *readDocuments() { From 2b1ddebccd6cfb6bfabd33e98adf24e04deee0b8 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Dec 2025 09:19:42 +0100 Subject: [PATCH 3/9] cleanup --- yaml/_loader_state.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/yaml/_loader_state.ts b/yaml/_loader_state.ts index 1be387ef9637..1e360cb8b9ca 100644 --- a/yaml/_loader_state.ts +++ b/yaml/_loader_state.ts @@ -1624,9 +1624,13 @@ export class LoaderState { } 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 - const state = this.readBlockSequence(resultState, blockIndent); - hasContent = allowBlockCollections && !!state; - if (state) resultState = state; + if (allowBlockCollections) { + const state = this.readBlockSequence(resultState, blockIndent); + if (state) { + hasContent = true; + resultState = state; + } + } } } @@ -1687,8 +1691,7 @@ export class LoaderState { const success = resultState.tag !== null || resultState.anchor !== null || hasContent; - if (!success) return; - return resultState; + if (success) return resultState; } readDirectives() { From 8eb6c111546bc9823deb6cf711f3603dd35f4672 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Dec 2025 09:23:17 +0100 Subject: [PATCH 4/9] cleanup --- yaml/_loader_state.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/yaml/_loader_state.ts b/yaml/_loader_state.ts index 1e360cb8b9ca..41f1cdbe41e7 100644 --- a/yaml/_loader_state.ts +++ b/yaml/_loader_state.ts @@ -341,21 +341,21 @@ export class LoaderState { this.tagMap.set(handle, prefix); } captureSegment( - result: unknown[] | Record | string | null, + data: unknown[] | Record | string | null, start: number, end: number, checkJson: boolean, ) { if (start < end) { - const newResult = this.input.slice(start, end); + const result = this.input.slice(start, end); if (checkJson) { for ( let position = 0; - position < newResult.length; + position < result.length; position++ ) { - const character = newResult.charCodeAt(position); + const character = result.charCodeAt(position); if ( !(character === 0x09 || (0x20 <= character && character <= 0x10ffff)) @@ -365,13 +365,13 @@ export class LoaderState { ); } } - } else if (PATTERN_NON_PRINTABLE.test(newResult)) { + } else if (PATTERN_NON_PRINTABLE.test(result)) { throw this.#createError("Stream contains non-printable characters"); } - result += newResult; + data += result; } - return result; + return data; } readBlockSequence(state: State, nodeIndent: number): State | void { let detected = false; From 0a3f23dfe28b985ae8da7fc443d1a3cf898b9afd Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Dec 2025 12:35:33 +0100 Subject: [PATCH 5/9] cleanup --- yaml/_loader_state.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/yaml/_loader_state.ts b/yaml/_loader_state.ts index 41f1cdbe41e7..38c4a98c42d5 100644 --- a/yaml/_loader_state.ts +++ b/yaml/_loader_state.ts @@ -134,18 +134,6 @@ function codepointToChar(codepoint: number): string { ); } -function writeFoldedLines( - data: unknown[] | Record | string | null, - count: number, -) { - if (count === 1) { - data += " "; - } else if (count > 1) { - data += "\n".repeat(count - 1); - } - return data; -} - const INDENT = 4; const MAX_LENGTH = 75; const DELIMITERS = "\x00\r\n\x85\u2028\u2029"; @@ -603,6 +591,18 @@ export class LoaderState { return false; } + writeFoldedLines( + data: unknown[] | Record | string | null, + count: number, + ) { + if (count === 1) { + data += " "; + } else if (count > 1) { + data += "\n".repeat(count - 1); + } + return data; + } + readPlainScalar( state: State, nodeIndent: number, @@ -693,7 +693,7 @@ export class LoaderState { captureEnd, false, ); - state.result = writeFoldedLines(state.result, this.line - line); + state.result = this.writeFoldedLines(state.result, this.line - line); captureStart = captureEnd = this.position; hasPendingContent = false; } @@ -737,7 +737,7 @@ export class LoaderState { } } else if (isEOL(ch)) { result = this.captureSegment(result, captureStart, captureEnd, true); - result = writeFoldedLines( + result = this.writeFoldedLines( result, this.skipSeparationSpace(false, nodeIndent), ); @@ -817,7 +817,7 @@ export class LoaderState { captureStart = captureEnd = this.position; } else if (isEOL(ch)) { result = this.captureSegment(result, captureStart, captureEnd, true); - result = writeFoldedLines( + result = this.writeFoldedLines( result, this.skipSeparationSpace(false, nodeIndent), ); From 3224881336b5f31ee71f61a3846e7400b68f362b Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 27 Dec 2025 02:56:48 +0100 Subject: [PATCH 6/9] update --- yaml/_loader_state.ts | 560 ++++++++++++++++++------------------------ 1 file changed, 237 insertions(+), 323 deletions(-) diff --git a/yaml/_loader_state.ts b/yaml/_loader_state.ts index 38c4a98c42d5..2328ac5484a3 100644 --- a/yaml/_loader_state.ts +++ b/yaml/_loader_state.ts @@ -181,13 +181,24 @@ function markToString( return where; } +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 ""; +} + interface State { - tag: string | null | undefined; - anchor: string | null | undefined; - kind: string | null | undefined; + tag: string | null; + anchor: string | null; + kind: string | null; result: unknown[] | Record | string | null; } - export class LoaderState { input: string; length: number; @@ -328,12 +339,7 @@ export class LoaderState { this.tagMap.set(handle, prefix); } - captureSegment( - data: unknown[] | Record | string | null, - start: number, - end: number, - checkJson: boolean, - ) { + captureSegment(start: number, end: number, checkJson: boolean) { if (start < end) { const result = this.input.slice(start, end); @@ -357,20 +363,17 @@ export class LoaderState { throw this.#createError("Stream contains non-printable characters"); } - data += result; + return result; } - return data; } readBlockSequence(state: State, nodeIndent: number): State | void { let detected = false; - const tag = state.tag; - const anchor = state.anchor; + const { tag, anchor } = state; + const result: unknown[] = []; - if (state.anchor !== null && typeof state.anchor !== "undefined") { - this.anchorMap.set(state.anchor, result); - } + if (anchor !== null) this.anchorMap.set(anchor, result); let ch = this.peek(); @@ -403,7 +406,7 @@ export class LoaderState { allowToSeek: false, allowCompact: true, }); - result.push(newState?.result); + if (newState) result.push(newState.result); this.skipSeparationSpace(true, -1); ch = this.peek(); @@ -417,9 +420,7 @@ export class LoaderState { } } - if (detected) { - return { tag, anchor, kind: "sequence", result }; - } + if (detected) return { tag, anchor, kind: "sequence", result }; } mergeMappings( destination: Record, @@ -591,18 +592,6 @@ export class LoaderState { return false; } - writeFoldedLines( - data: unknown[] | Record | string | null, - count: number, - ) { - if (count === 1) { - data += " "; - } else if (count > 1) { - data += "\n".repeat(count - 1); - } - return data; - } - readPlainScalar( state: State, nodeIndent: number, @@ -640,7 +629,7 @@ export class LoaderState { } } - let result: string | unknown[] | Record | null = ""; + let result = ""; let captureEnd = this.position; let captureStart = this.position; @@ -687,13 +676,9 @@ export class LoaderState { } if (hasPendingContent) { - state.result = this.captureSegment( - state.result, - captureStart, - captureEnd, - false, - ); - state.result = this.writeFoldedLines(state.result, this.line - line); + const segment = this.captureSegment(captureStart, captureEnd, false); + if (segment) result += segment; + result += writeFoldedLines(this.line - line); captureStart = captureEnd = this.position; hasPendingContent = false; } @@ -705,27 +690,27 @@ export class LoaderState { ch = this.next(); } - result = this.captureSegment(result, captureStart, captureEnd, false); - - if (result) return { ...state, kind: "scalar", result }; + const segment = this.captureSegment(captureStart, captureEnd, false); + if (segment) result += segment; + const { tag, anchor } = state; + if (anchor !== null) this.anchorMap.set(anchor, result); + if (result) return { tag, anchor, kind: "scalar", result }; } readSingleQuotedScalar(state: State, nodeIndent: number): State | void { let ch = this.peek(); - if (ch !== SINGLE_QUOTE) { - return; - } + if (ch !== SINGLE_QUOTE) return; + let result = ""; this.position++; let captureStart = this.position; let captureEnd = this.position; - let result: string | unknown[] | Record | null = ""; - ch = this.peek(); while (ch !== 0) { if (ch === SINGLE_QUOTE) { - result = this.captureSegment(result, captureStart, this.position, true); + const segment = this.captureSegment(captureStart, this.position, true); + if (segment) result += segment; ch = this.next(); if (ch === SINGLE_QUOTE) { @@ -733,12 +718,14 @@ export class LoaderState { this.position++; captureEnd = this.position; } else { - return { ...state, kind: "scalar", result }; + const { tag, anchor } = state; + if (anchor !== null) this.anchorMap.set(anchor, result); + return { tag, anchor, kind: "scalar", result }; } } else if (isEOL(ch)) { - result = this.captureSegment(result, captureStart, captureEnd, true); - result = this.writeFoldedLines( - result, + const segment = this.captureSegment(captureStart, captureEnd, true); + if (segment) result += segment; + result += writeFoldedLines( this.skipSeparationSpace(false, nodeIndent), ); captureStart = captureEnd = this.position; @@ -763,12 +750,9 @@ export class LoaderState { readDoubleQuotedScalar(state: State, nodeIndent: number): State | void { let ch = this.peek(); - if (ch !== DOUBLE_QUOTE) { - return; - } - - let result: string | unknown[] | Record | null = ""; + if (ch !== DOUBLE_QUOTE) return; + let result = ""; this.position++; let captureEnd = this.position; let captureStart = this.position; @@ -776,18 +760,22 @@ export class LoaderState { ch = this.peek(); while (ch !== 0) { if (ch === DOUBLE_QUOTE) { - result = this.captureSegment(result, captureStart, this.position, true); + const segment = this.captureSegment(captureStart, this.position, true); + if (segment) result += segment; this.position++; - return { ...state, kind: "scalar", result }; + const { tag, anchor } = state; + if (anchor !== null) this.anchorMap.set(anchor, result); + return { tag, anchor, kind: "scalar", result }; } if (ch === BACKSLASH) { - result = this.captureSegment(result, captureStart, this.position, true); + const segment = this.captureSegment(captureStart, this.position, true); + if (segment) result += segment; ch = this.next(); if (isEOL(ch)) { this.skipSeparationSpace(false, nodeIndent); } else if (ch < 256 && SIMPLE_ESCAPE_SEQUENCES.has(ch)) { - (result as string) += SIMPLE_ESCAPE_SEQUENCES.get(ch); + result += SIMPLE_ESCAPE_SEQUENCES.get(ch); this.position++; } else if ((tmp = ESCAPED_HEX_LENGTHS.get(ch) ?? 0) > 0) { let hexLength = tmp; @@ -816,9 +804,9 @@ export class LoaderState { captureStart = captureEnd = this.position; } else if (isEOL(ch)) { - result = this.captureSegment(result, captureStart, captureEnd, true); - result = this.writeFoldedLines( - result, + const segment = this.captureSegment(captureStart, captureEnd, true); + if (segment) result += segment; + result += writeFoldedLines( this.skipSeparationSpace(false, nodeIndent), ); captureStart = captureEnd = this.position; @@ -857,9 +845,7 @@ export class LoaderState { const { tag, anchor } = state; - if (state.anchor !== null && typeof state.anchor !== "undefined") { - this.anchorMap.set(state.anchor, result); - } + if (anchor !== null) this.anchorMap.set(anchor, result); ch = this.next(); @@ -908,9 +894,10 @@ export class LoaderState { allowToSeek: false, allowCompact: true, }); - if (newState) state = newState; - keyTag = state.tag || null; - keyNode = state.result; + if (newState) { + keyTag = newState.tag || null; + keyNode = newState.result; + } this.skipSeparationSpace(true, nodeIndent); ch = this.peek(); @@ -925,8 +912,7 @@ export class LoaderState { allowToSeek: false, allowCompact: true, }); - if (newState) state = newState; - valueNode = state.result; + if (newState) valueNode = newState.result; } if (isMapping) { @@ -988,6 +974,8 @@ export class LoaderState { return; } + let result = ""; + let tmp = 0; while (ch !== 0) { ch = this.next(); @@ -1024,8 +1012,6 @@ export class LoaderState { ch = this.peek(); } - let result: string | unknown[] | Record | null = ""; - while (ch !== 0) { this.readLineBreak(); this.lineIndent = 0; @@ -1111,17 +1097,19 @@ export class LoaderState { ch = this.next(); } - result = this.captureSegment(result, captureStart, this.position, false); + const segment = this.captureSegment(captureStart, this.position, false); + if (segment) result += segment; } - return { ...state, kind: "scalar", result }; + + const { tag, anchor } = state; + if (anchor !== null) this.anchorMap.set(anchor, result); + return { tag, anchor, kind: "scalar", result }; } readBlockMapping( state: State, nodeIndent: number, flowIndent: number, ): State | void { - const tag = state.tag; - const anchor = state.anchor; const result = {}; const overridableKeys = new Set(); @@ -1134,9 +1122,9 @@ export class LoaderState { let atExplicitKey = false; let detected = false; - if (state.anchor !== null && typeof state.anchor !== "undefined") { - this.anchorMap.set(state.anchor, result); - } + const { tag, anchor } = state; + + if (anchor !== null) this.anchorMap.set(anchor, result); let ch = this.peek(); @@ -1190,56 +1178,55 @@ export class LoaderState { allowToSeek: false, allowCompact: true, }); - if (newState) { - if (this.line === line) { - ch = this.peek(); - - this.skipWhitespaces(); - ch = this.peek(); - - if (ch === COLON) { - ch = this.next(); - - 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) { + if (!newState) break; // Reading is done. Go to the epilogue. + if (this.line === line) { + ch = this.peek(); + + this.skipWhitespaces(); + ch = this.peek(); + + if (ch === COLON) { + ch = this.next(); + + if (!isWhiteSpaceOrEOL(ch)) { throw this.#createError( - "Cannot read an implicit mapping pair: missing colon", + "Cannot read block: a whitespace character is expected after the key-value separator within a block mapping", ); - } else { - return { ...newState, tag, anchor }; } + + 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 a block mapping entry: a multiline key may not be an implicit key", + "Cannot read an implicit mapping pair: missing colon", ); } else { - return { ...newState, tag, anchor }; + 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 { - break; // Reading is done. Go to the epilogue. + const { kind, result } = newState; + return { tag, anchor, kind, result }; // Keep the result of `composeNode`. } } @@ -1303,11 +1290,9 @@ export class LoaderState { } // Expose the resulting mapping. - if (detected) { - return { tag, anchor, kind: "mapping", result }; - } + if (detected) return { tag, anchor, kind: "mapping", result }; } - readTagProperty(tag: string | null | undefined): string | null | undefined { + readTagProperty(tag: string | null): string | void { let isVerbatim = false; let isNamed = false; let tagHandle = ""; @@ -1392,23 +1377,19 @@ export class LoaderState { if (isVerbatim) { return tagName; - } - if (this.tagMap.has(tagHandle)) { + } else if (this.tagMap.has(tagHandle)) { return this.tagMap.get(tagHandle) + tagName; - } - if (tagHandle === "!") { + } else if (tagHandle === "!") { return `!${tagName}`; - } - if (tagHandle === "!!") { + } else if (tagHandle === "!!") { return `tag:yaml.org,2002:${tagName}`; } + throw this.#createError( `Cannot read tag property: undeclared tag handle "${tagHandle}"`, ); } - readAnchorProperty( - anchor: string | null | undefined, - ): string | null | undefined { + readAnchorProperty(anchor: string | null): string | void { let ch = this.peek(); if (ch !== AMPERSAND) return; @@ -1432,8 +1413,8 @@ export class LoaderState { return this.input.slice(position, this.position); } - readAlias() { - if (this.peek() !== ASTERISK) return false; + readAlias(): string | void { + if (this.peek() !== ASTERISK) return; let ch = this.next(); @@ -1457,28 +1438,69 @@ export class LoaderState { } 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. - composeNode( - { parentIndent, nodeContext, allowToSeek, allowCompact }: { - parentIndent: number; - nodeContext: number; - allowToSeek: boolean; - allowCompact: boolean; - }, - ): State | void { + if (!type.resolve(state.result)) continue; + // `state.result` updated in resolver if matched + const result = type.construct(state.result); + const tag = type.tag; + const { anchor, kind } = state; + if (anchor !== null) this.anchorMap.set(anchor, result); + return { tag, anchor, kind, result }; + } + return state; + } + } + + const kind = (state.kind ?? "fallback") as KindType; + + 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); + const { tag, anchor } = state; + if (anchor !== null) this.anchorMap.set(anchor, result); + return { tag, anchor, kind, result }; + } + 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; - - let resultState: State = { - tag: null, - anchor: null, - kind: null, - result: null, - }; const allowBlockScalars = CONTEXT_BLOCK_OUT === nodeContext || CONTEXT_BLOCK_IN === nodeContext; @@ -1489,39 +1511,26 @@ export class LoaderState { if (allowToSeek) { if (this.skipSeparationSpace(true, -1)) { atNewLine = true; - - if (this.lineIndent > parentIndent) { - indentStatus = 1; - } else if (this.lineIndent === parentIndent) { - indentStatus = 0; - } else if (this.lineIndent < parentIndent) { - indentStatus = -1; - } + indentStatus = getIndentStatus(this.lineIndent, parentIndent); } } + const state: State = { tag: null, anchor: null, kind: null, result: null }; + if (indentStatus === 1) { while (true) { - const tag = this.readTagProperty(resultState.tag); + const tag = this.readTagProperty(state.tag); if (tag) { - resultState.tag = tag; + state.tag = tag; } else { - const anchor = this.readAnchorProperty(resultState.anchor); + const anchor = this.readAnchorProperty(state.anchor); if (!anchor) break; - resultState.anchor = anchor; + state.anchor = anchor; } - if (this.skipSeparationSpace(true, -1)) { atNewLine = true; allowBlockCollections = allowBlockStyles; - - if (this.lineIndent > parentIndent) { - indentStatus = 1; - } else if (this.lineIndent === parentIndent) { - indentStatus = 0; - } else if (this.lineIndent < parentIndent) { - indentStatus = -1; - } + indentStatus = getIndentStatus(this.lineIndent, parentIndent); } else { allowBlockCollections = false; } @@ -1532,166 +1541,69 @@ export class LoaderState { allowBlockCollections = atNewLine || allowCompact; } - if (indentStatus === 1 || CONTEXT_BLOCK_OUT === nodeContext) { + if (indentStatus === 1) { const cond = CONTEXT_FLOW_IN === nodeContext || CONTEXT_FLOW_OUT === nodeContext; const flowIndent = cond ? parentIndent : parentIndent + 1; - const blockIndent = this.position - this.lineStart; - - if (indentStatus === 1) { - const state = this.readBlockSequence(resultState, blockIndent); - if (state) resultState = state; - if (allowBlockCollections) { - if (state) { - hasContent = true; - } else { - const state = this.readBlockMapping( - resultState, - blockIndent, - flowIndent, - ); - if (state) { - resultState = state; - hasContent = true; - } - } - } else { - const state = this.readFlowCollection(resultState, flowIndent); - if (state) { - resultState = state; - hasContent = true; - } else { - const state = this.readBlockScalar(resultState, flowIndent); - const didReadBlock = allowBlockScalars && state; - if (state) resultState = state; + if (allowBlockCollections) { + const blockIndent = this.position - this.lineStart; + const blockSequenceState = this.readBlockSequence(state, blockIndent); + if (blockSequenceState) return this.resolveTag(blockSequenceState); - if (didReadBlock) { - hasContent = true; - } else { - const state = this.readSingleQuotedScalar( - resultState, - flowIndent, - ); - if (state) { - resultState = state; - hasContent = true; - } else { - const state = this.readDoubleQuotedScalar( - resultState, - flowIndent, - ); - if (state) { - resultState = state; - hasContent = true; - } else { - const result = this.readAlias(); - if (result) { - resultState.result = result; - hasContent = true; - - if ( - resultState.tag !== null || resultState.anchor !== null - ) { - throw this.#createError( - "Cannot compose node: alias node should not have any properties", - ); - } - } else { - const state = this.readPlainScalar( - resultState, - flowIndent, - CONTEXT_FLOW_IN === nodeContext, - ); - if (state) { - resultState = state; - hasContent = true; - - if (state.tag === null) { - state.tag = "?"; - } - } - } - } - - if (resultState.anchor !== null) { - this.anchorMap.set(resultState.anchor, resultState.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 - if (allowBlockCollections) { - const state = this.readBlockSequence(resultState, blockIndent); - if (state) { - hasContent = true; - resultState = state; - } - } + const blockMappingState = this.readBlockMapping( + state, + blockIndent, + flowIndent, + ); + if (blockMappingState) return this.resolveTag(blockMappingState); } - } - - if (resultState.tag !== null && resultState.tag !== "!") { - if (resultState.tag === "?") { - for ( - let typeIndex = 0; - typeIndex < this.implicitTypes.length; - typeIndex++ - ) { - type = this.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. + const flowCollectionState = this.readFlowCollection(state, flowIndent); + if (flowCollectionState) return this.resolveTag(flowCollectionState); - if (type.resolve(resultState.result)) { - // `state.result` updated in resolver if matched - resultState.result = type.construct(resultState.result); - resultState.tag = type.tag; - if (resultState.anchor !== null) { - this.anchorMap.set(resultState.anchor, resultState.result); - } - break; - } - } - } else if ( - this.typeMap[(resultState.kind ?? "fallback") as KindType].has( - resultState.tag!, - ) - ) { - const map = this.typeMap[(resultState.kind ?? "fallback") as KindType]; - type = map.get(resultState.tag!)!; + if (allowBlockScalars) { + const blockScalarState = this.readBlockScalar(state, flowIndent); + if (blockScalarState) return this.resolveTag(blockScalarState); + } + const singleQuoteState = this.readSingleQuotedScalar(state, flowIndent); + if (singleQuoteState) return this.resolveTag(singleQuoteState); - if (resultState.result !== null && type.kind !== resultState.kind) { - throw this.#createError( - `Unacceptable node kind for !<${resultState.tag}> tag: it should be "${type.kind}", not "${resultState.kind}"`, - ); - } + const doubleQuoteState = this.readDoubleQuotedScalar(state, flowIndent); + if (doubleQuoteState) return this.resolveTag(doubleQuoteState); - if (!type.resolve(resultState.result)) { - // `state.result` updated in resolver if matched + const alias = this.readAlias(); + if (alias) { + if (state.tag !== null || state.anchor !== null) { throw this.#createError( - `Cannot resolve a node with !<${resultState.tag}> explicit tag`, + "Cannot compose node: alias node should not have any properties", ); - } else { - resultState.result = type.construct(resultState.result); - if (resultState.anchor !== null) { - this.anchorMap.set(resultState.anchor, resultState.result); - } } - } else { - throw this.#createError( - `Cannot resolve unknown tag !<${resultState.tag}>`, - ); + state.result = alias; + return this.resolveTag(state); } + const plainScalarState = this.readPlainScalar( + state, + flowIndent, + CONTEXT_FLOW_IN === nodeContext, + ); + if (plainScalarState) { + plainScalarState.tag ??= "?"; + return this.resolveTag(plainScalarState); + } + } else if ( + indentStatus === 0 && + CONTEXT_BLOCK_OUT === nodeContext && + allowBlockCollections + ) { + // Special case: block sequences are allowed to have same indentation level as the parent. + // http://www.yaml.org/spec/1.2/spec.html#id2799784 + const blockIndent = this.position - this.lineStart; + const newState = this.readBlockSequence(state, blockIndent); + if (newState) return this.resolveTag(state); } - const success = resultState.tag !== null || resultState.anchor !== null || - hasContent; - if (success) return resultState; + const newState = this.resolveTag(state); + if (newState.tag !== null || newState.anchor !== null) return newState; } readDirectives() { @@ -1775,6 +1687,7 @@ export class LoaderState { const hasDirectives = this.readDirectives(); this.skipSeparationSpace(true, -1); + let state: State = { tag: null, anchor: null, kind: null, result: null }; if ( this.lineIndent === 0 && @@ -1796,6 +1709,7 @@ export class LoaderState { allowToSeek: false, allowCompact: true, }); + if (newState) state = newState; this.skipSeparationSpace(true, -1); if ( @@ -1818,7 +1732,7 @@ export class LoaderState { ); } - return newState?.result; + return state.result; } *readDocuments() { From 59fda5b5cfee97e303a831692cbd90eea9860a65 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 27 Dec 2025 03:00:01 +0100 Subject: [PATCH 7/9] update --- yaml/_loader_state.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/yaml/_loader_state.ts b/yaml/_loader_state.ts index 2328ac5484a3..8b830fc935bd 100644 --- a/yaml/_loader_state.ts +++ b/yaml/_loader_state.ts @@ -1687,7 +1687,8 @@ export class LoaderState { const hasDirectives = this.readDirectives(); this.skipSeparationSpace(true, -1); - let state: State = { tag: null, anchor: null, kind: null, result: null }; + + let result = null; if ( this.lineIndent === 0 && @@ -1709,7 +1710,7 @@ export class LoaderState { allowToSeek: false, allowCompact: true, }); - if (newState) state = newState; + if (newState) result = newState.result; this.skipSeparationSpace(true, -1); if ( @@ -1732,7 +1733,7 @@ export class LoaderState { ); } - return state.result; + return result; } *readDocuments() { From 16399019e76fea8d6d3b5cca5a3bfafd89455122 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Jan 2026 00:47:21 +0100 Subject: [PATCH 8/9] update --- yaml/_loader_state.ts | 108 +++++++++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 37 deletions(-) diff --git a/yaml/_loader_state.ts b/yaml/_loader_state.ts index 9f0d43f948e2..c329ca469f67 100644 --- a/yaml/_loader_state.ts +++ b/yaml/_loader_state.ts @@ -366,11 +366,13 @@ export class LoaderState { return result; } } - readBlockSequence(state: State, nodeIndent: number): State | void { + readBlockSequence( + tag: string | null, + anchor: string | null, + nodeIndent: number, + ): State | void { let detected = false; - const { tag, anchor } = state; - const result: unknown[] = []; if (anchor !== null) this.anchorMap.set(anchor, result); @@ -593,7 +595,8 @@ export class LoaderState { } readPlainScalar( - state: State, + tag: string | null, + anchor: string | null, nodeIndent: number, withinFlowCollection: boolean, ): State | void { @@ -692,11 +695,14 @@ export class LoaderState { const segment = this.captureSegment(captureStart, captureEnd, false); if (segment) result += segment; - const { tag, anchor } = state; if (anchor !== null) this.anchorMap.set(anchor, result); if (result) return { tag, anchor, kind: "scalar", result }; } - readSingleQuotedScalar(state: State, nodeIndent: number): State | void { + readSingleQuotedScalar( + tag: string | null, + anchor: string | null, + nodeIndent: number, + ): State | void { let ch = this.peek(); if (ch !== SINGLE_QUOTE) return; @@ -718,7 +724,6 @@ export class LoaderState { this.position++; captureEnd = this.position; } else { - const { tag, anchor } = state; if (anchor !== null) this.anchorMap.set(anchor, result); return { tag, anchor, kind: "scalar", result }; } @@ -747,7 +752,11 @@ export class LoaderState { "Unexpected end of the stream within a single quoted scalar", ); } - readDoubleQuotedScalar(state: State, nodeIndent: number): State | void { + readDoubleQuotedScalar( + tag: string | null, + anchor: string | null, + nodeIndent: number, + ): State | void { let ch = this.peek(); if (ch !== DOUBLE_QUOTE) return; @@ -763,7 +772,6 @@ export class LoaderState { const segment = this.captureSegment(captureStart, this.position, true); if (segment) result += segment; this.position++; - const { tag, anchor } = state; if (anchor !== null) this.anchorMap.set(anchor, result); return { tag, anchor, kind: "scalar", result }; } @@ -828,7 +836,11 @@ export class LoaderState { "Unexpected end of the stream within a double quoted scalar", ); } - readFlowCollection(state: State, nodeIndent: number): State | void { + readFlowCollection( + tag: string | null, + anchor: string | null, + nodeIndent: number, + ): State | void { let ch = this.peek(); let terminator: number; let isMapping = true; @@ -843,8 +855,6 @@ export class LoaderState { return; } - const { tag, anchor } = state; - if (anchor !== null) this.anchorMap.set(anchor, result); ch = this.next(); @@ -955,7 +965,11 @@ export class LoaderState { } // Handles block scaler styles: e.g. '|', '>', '|-' and '>-'. // https://yaml.org/spec/1.2.2/#81-block-scalar-styles - readBlockScalar(state: State, nodeIndent: number): State | void { + readBlockScalar( + tag: string | null, + anchor: string | null, + nodeIndent: number, + ): State | void { let chomping = CHOMPING_CLIP; let didReadContent = false; let detectedIndent = false; @@ -1101,12 +1115,12 @@ export class LoaderState { if (segment) result += segment; } - const { tag, anchor } = state; if (anchor !== null) this.anchorMap.set(anchor, result); return { tag, anchor, kind: "scalar", result }; } readBlockMapping( - state: State, + tag: string | null, + anchor: string | null, nodeIndent: number, flowIndent: number, ): State | void { @@ -1122,8 +1136,6 @@ export class LoaderState { let atExplicitKey = false; let detected = false; - const { tag, anchor } = state; - if (anchor !== null) this.anchorMap.set(anchor, result); let ch = this.peek(); @@ -1515,17 +1527,18 @@ export class LoaderState { } } - const state: State = { tag: null, anchor: null, kind: null, result: null }; + let tag: string | null = null; + let anchor: string | null = null; if (indentStatus === 1) { while (true) { - const tag = this.readTagProperty(state.tag); - if (tag) { - state.tag = tag; + const newTag = this.readTagProperty(tag); + if (newTag) { + tag = newTag; } else { - const anchor = this.readAnchorProperty(state.anchor); - if (!anchor) break; - state.anchor = anchor; + const newAnchor = this.readAnchorProperty(anchor); + if (!newAnchor) break; + anchor = newAnchor; } if (this.skipSeparationSpace(true, -1)) { atNewLine = true; @@ -1548,41 +1561,62 @@ export class LoaderState { if (allowBlockCollections) { const blockIndent = this.position - this.lineStart; - const blockSequenceState = this.readBlockSequence(state, blockIndent); + const blockSequenceState = this.readBlockSequence( + tag, + anchor, + blockIndent, + ); if (blockSequenceState) return this.resolveTag(blockSequenceState); const blockMappingState = this.readBlockMapping( - state, + tag, + anchor, blockIndent, flowIndent, ); if (blockMappingState) return this.resolveTag(blockMappingState); } - const flowCollectionState = this.readFlowCollection(state, flowIndent); + const flowCollectionState = this.readFlowCollection( + tag, + anchor, + flowIndent, + ); if (flowCollectionState) return this.resolveTag(flowCollectionState); if (allowBlockScalars) { - const blockScalarState = this.readBlockScalar(state, flowIndent); + const blockScalarState = this.readBlockScalar( + tag, + anchor, + flowIndent, + ); if (blockScalarState) return this.resolveTag(blockScalarState); } - const singleQuoteState = this.readSingleQuotedScalar(state, flowIndent); + const singleQuoteState = this.readSingleQuotedScalar( + tag, + anchor, + flowIndent, + ); if (singleQuoteState) return this.resolveTag(singleQuoteState); - const doubleQuoteState = this.readDoubleQuotedScalar(state, flowIndent); + const doubleQuoteState = this.readDoubleQuotedScalar( + tag, + anchor, + flowIndent, + ); if (doubleQuoteState) return this.resolveTag(doubleQuoteState); const alias = this.readAlias(); if (alias) { - if (state.tag !== null || state.anchor !== null) { + if (tag !== null || anchor !== null) { throw this.#createError( "Cannot compose node: alias node should not have any properties", ); } - state.result = alias; - return this.resolveTag(state); + return this.resolveTag({ tag, anchor, kind: null, result: alias }); } const plainScalarState = this.readPlainScalar( - state, + tag, + anchor, flowIndent, CONTEXT_FLOW_IN === nodeContext, ); @@ -1598,11 +1632,11 @@ export class LoaderState { // Special case: block sequences are allowed to have same indentation level as the parent. // http://www.yaml.org/spec/1.2/spec.html#id2799784 const blockIndent = this.position - this.lineStart; - const newState = this.readBlockSequence(state, blockIndent); - if (newState) return this.resolveTag(state); + const newState = this.readBlockSequence(tag, anchor, blockIndent); + if (newState) return this.resolveTag(newState); } - const newState = this.resolveTag(state); + const newState = this.resolveTag({ tag, anchor, kind: null, result: null }); if (newState.tag !== null || newState.anchor !== null) return newState; } From e945690607372a8bfd1a2b254acf04335c1672a5 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 8 Jan 2026 00:54:32 +0100 Subject: [PATCH 9/9] update --- yaml/_loader_state.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/yaml/_loader_state.ts b/yaml/_loader_state.ts index c329ca469f67..3b637ff1530c 100644 --- a/yaml/_loader_state.ts +++ b/yaml/_loader_state.ts @@ -1467,10 +1467,11 @@ export class LoaderState { if (!type.resolve(state.result)) continue; // `state.result` updated in resolver if matched const result = type.construct(state.result); - const tag = type.tag; - const { anchor, kind } = state; + state.result = result; + state.tag = type.tag; + const { anchor } = state; if (anchor !== null) this.anchorMap.set(anchor, result); - return { tag, anchor, kind, result }; + return state; } return state; } @@ -1501,9 +1502,10 @@ export class LoaderState { } const result = type.construct(state.result); - const { tag, anchor } = state; + state.result = result; + const { anchor } = state; if (anchor !== null) this.anchorMap.set(anchor, result); - return { tag, anchor, kind, result }; + return state; } composeNode({ parentIndent, nodeContext, allowToSeek, allowCompact }: { parentIndent: number;