diff --git a/yaml/_loader_state.ts b/yaml/_loader_state.ts index 987502a80d09..3b637ff1530c 100644 --- a/yaml/_loader_state.ts +++ b/yaml/_loader_state.ts @@ -187,6 +187,18 @@ function getIndentStatus(lineIndent: number, parentIndent: number) { return 0; } +function writeFoldedLines(count: number) { + if (count === 1) return " "; + if (count > 1) return "\n".repeat(count - 1); + return ""; +} + +interface State { + tag: string | null; + anchor: string | null; + kind: string | null; + result: unknown[] | Record | string | null; +} export class LoaderState { input: string; length: number; @@ -202,10 +214,6 @@ export class LoaderState { checkLineBreaks = false; tagMap = new Map(); anchorMap = new Map(); - tag: string | null = null; - anchor: string | null = null; - kind: string | null = null; - result: unknown[] | Record | string | null = ""; constructor( input: string, @@ -355,17 +363,19 @@ export class LoaderState { throw this.#createError("Stream contains non-printable characters"); } - this.result += result; + return result; } } - readBlockSequence(nodeIndent: number): boolean { + readBlockSequence( + tag: string | null, + anchor: string | null, + nodeIndent: number, + ): State | void { let detected = false; - const tag = this.tag; - const anchor = this.anchor; const result: unknown[] = []; - if (this.anchor !== null) this.anchorMap.set(this.anchor, result); + if (anchor !== null) this.anchorMap.set(anchor, result); let ch = this.peek(); @@ -392,13 +402,13 @@ export class LoaderState { } const line = this.line; - this.composeNode({ + const newState = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_BLOCK_IN, allowToSeek: false, allowCompact: true, }); - result.push(this.result); + if (newState) result.push(newState.result); this.skipSeparationSpace(true, -1); ch = this.peek(); @@ -412,14 +422,7 @@ export class LoaderState { } } - if (detected) { - this.tag = tag; - this.anchor = anchor; - this.kind = "sequence"; - this.result = result; - return true; - } - return false; + if (detected) return { tag, anchor, kind: "sequence", result }; } mergeMappings( destination: Record, @@ -590,16 +593,13 @@ 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( + tag: string | null, + anchor: string | null, + nodeIndent: number, + withinFlowCollection: boolean, + ): State | void { let ch = this.peek(); if ( @@ -617,7 +617,7 @@ export class LoaderState { ch === COMMERCIAL_AT || ch === GRAVE_ACCENT ) { - return false; + return; } let following: number; @@ -628,12 +628,12 @@ export class LoaderState { isWhiteSpaceOrEOL(following) || (withinFlowCollection && isFlowIndicator(following)) ) { - return false; + return; } } - this.kind = "scalar"; - this.result = ""; + let result = ""; + let captureEnd = this.position; let captureStart = this.position; let hasPendingContent = false; @@ -679,8 +679,9 @@ export class LoaderState { } if (hasPendingContent) { - this.captureSegment(captureStart, captureEnd, false); - this.writeFoldedLines(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; } @@ -692,25 +693,21 @@ export class LoaderState { ch = this.next(); } - this.captureSegment(captureStart, captureEnd, false); - - if (this.result) { - return true; - } - - this.kind = kind; - this.result = result; - return false; + 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(nodeIndent: number): boolean { + readSingleQuotedScalar( + tag: string | null, + anchor: string | null, + nodeIndent: number, + ): State | void { let ch = this.peek(); - if (ch !== SINGLE_QUOTE) { - return false; - } + if (ch !== SINGLE_QUOTE) return; - this.kind = "scalar"; - this.result = ""; + let result = ""; this.position++; let captureStart = this.position; let captureEnd = this.position; @@ -718,7 +715,8 @@ export class LoaderState { ch = this.peek(); while (ch !== 0) { if (ch === SINGLE_QUOTE) { - this.captureSegment(captureStart, this.position, true); + const segment = this.captureSegment(captureStart, this.position, true); + if (segment) result += segment; ch = this.next(); if (ch === SINGLE_QUOTE) { @@ -726,11 +724,15 @@ export class LoaderState { this.position++; captureEnd = this.position; } else { - return true; + if (anchor !== null) this.anchorMap.set(anchor, result); + return { tag, anchor, kind: "scalar", result }; } } else if (isEOL(ch)) { - this.captureSegment(captureStart, captureEnd, true); - this.writeFoldedLines(this.skipSeparationSpace(false, nodeIndent)); + const segment = this.captureSegment(captureStart, captureEnd, true); + if (segment) result += segment; + result += writeFoldedLines( + this.skipSeparationSpace(false, nodeIndent), + ); captureStart = captureEnd = this.position; } else if ( this.position === this.lineStart && @@ -750,15 +752,16 @@ export class LoaderState { "Unexpected end of the stream within a single quoted scalar", ); } - readDoubleQuotedScalar(nodeIndent: number): boolean { + readDoubleQuotedScalar( + tag: string | null, + anchor: string | null, + nodeIndent: number, + ): State | void { let ch = this.peek(); - if (ch !== DOUBLE_QUOTE) { - return false; - } + if (ch !== DOUBLE_QUOTE) return; - this.kind = "scalar"; - this.result = ""; + let result = ""; this.position++; let captureEnd = this.position; let captureStart = this.position; @@ -766,18 +769,21 @@ export class LoaderState { ch = this.peek(); while (ch !== 0) { if (ch === DOUBLE_QUOTE) { - this.captureSegment(captureStart, this.position, true); + const segment = this.captureSegment(captureStart, this.position, true); + if (segment) result += segment; this.position++; - return true; + if (anchor !== null) this.anchorMap.set(anchor, result); + return { tag, anchor, kind: "scalar", result }; } if (ch === BACKSLASH) { - this.captureSegment(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)) { - this.result += 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; @@ -795,7 +801,7 @@ export class LoaderState { } } - this.result += codepointToChar(hexResult); + result += codepointToChar(hexResult); this.position++; } else { @@ -806,8 +812,11 @@ export class LoaderState { captureStart = captureEnd = this.position; } else if (isEOL(ch)) { - this.captureSegment(captureStart, captureEnd, true); - this.writeFoldedLines(this.skipSeparationSpace(false, nodeIndent)); + const segment = this.captureSegment(captureStart, captureEnd, true); + if (segment) result += segment; + result += writeFoldedLines( + this.skipSeparationSpace(false, nodeIndent), + ); captureStart = captureEnd = this.position; } else if ( this.position === this.lineStart && @@ -827,7 +836,11 @@ export class LoaderState { "Unexpected end of the stream within a double quoted scalar", ); } - readFlowCollection(nodeIndent: number): boolean { + readFlowCollection( + tag: string | null, + anchor: string | null, + nodeIndent: number, + ): State | void { let ch = this.peek(); let terminator: number; let isMapping = true; @@ -839,15 +852,13 @@ export class LoaderState { } else if (ch === LEFT_CURLY_BRACKET) { terminator = RIGHT_CURLY_BRACKET; } else { - return false; + return; } - if (this.anchor !== null) this.anchorMap.set(this.anchor, result); + if (anchor !== null) this.anchorMap.set(anchor, result); ch = this.next(); - const tag = this.tag; - const anchor = this.anchor; let readNext = true; let valueNode = null; let keyNode = null; @@ -864,11 +875,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( @@ -890,14 +898,16 @@ export class LoaderState { } line = this.line; - this.composeNode({ + const newState = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_FLOW_IN, allowToSeek: false, allowCompact: true, }); - keyTag = this.tag || null; - keyNode = this.result; + if (newState) { + keyTag = newState.tag || null; + keyNode = newState.result; + } this.skipSeparationSpace(true, nodeIndent); ch = this.peek(); @@ -906,13 +916,13 @@ export class LoaderState { isPair = true; ch = this.next(); this.skipSeparationSpace(true, nodeIndent); - this.composeNode({ + const newState = this.composeNode({ parentIndent: nodeIndent, nodeContext: CONTEXT_FLOW_IN, allowToSeek: false, allowCompact: true, }); - valueNode = this.result; + if (newState) valueNode = newState.result; } if (isMapping) { @@ -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(nodeIndent: number): boolean { + readBlockScalar( + tag: string | null, + anchor: string | null, + nodeIndent: number, + ): State | void { let chomping = CHOMPING_CLIP; let didReadContent = false; let detectedIndent = false; @@ -971,11 +985,10 @@ export class LoaderState { } else if (ch === GREATER_THAN) { folding = true; } else { - return false; + return; } - this.kind = "scalar"; - this.result = ""; + let result = ""; let tmp = 0; while (ch !== 0) { @@ -1040,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"; } } @@ -1060,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, ); } @@ -1098,14 +1111,19 @@ export class LoaderState { ch = this.next(); } - this.captureSegment(captureStart, this.position, false); + const segment = this.captureSegment(captureStart, this.position, false); + if (segment) result += segment; } - return true; + if (anchor !== null) this.anchorMap.set(anchor, result); + return { tag, anchor, kind: "scalar", result }; } - readBlockMapping(nodeIndent: number, flowIndent: number): boolean { - const tag = this.tag; - const anchor = this.anchor; + readBlockMapping( + tag: string | null, + anchor: string | null, + nodeIndent: number, + flowIndent: number, + ): State | void { const result = {}; const overridableKeys = new Set(); @@ -1118,7 +1136,7 @@ export class LoaderState { let atExplicitKey = false; let detected = false; - if (this.anchor !== null) this.anchorMap.set(this.anchor, result); + if (anchor !== null) this.anchorMap.set(anchor, result); let ch = this.peek(); @@ -1165,14 +1183,14 @@ export class LoaderState { // // Implicit notation case. Flow-style node as the key first, then ":", and the value. // - } else if ( - this.composeNode({ + } 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.peek(); @@ -1204,46 +1222,41 @@ export class LoaderState { detected = true; atExplicitKey = false; allowCompact = false; - keyTag = this.tag; - keyNode = this.result; + keyTag = newState.tag; + keyNode = newState.result; } else if (detected) { throw this.#createError( "Cannot read an implicit mapping pair: missing colon", ); } else { - this.tag = tag; - this.anchor = anchor; - return true; // Keep the result of `composeNode`. + 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 { - this.tag = tag; - this.anchor = anchor; - return true; // Keep the result of `composeNode`. + const { kind, result } = newState; + return { tag, anchor, kind, result }; // Keep the result of `composeNode`. } - } 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 newState = this.composeNode({ + parentIndent: nodeIndent, + nodeContext: CONTEXT_BLOCK_OUT, + allowToSeek: true, + allowCompact, + }); + if (newState) { if (atExplicitKey) { - keyNode = this.result; + keyNode = newState.result; } else { - valueNode = this.result; + valueNode = newState.result; } } @@ -1289,16 +1302,9 @@ export class LoaderState { } // Expose the resulting mapping. - if (detected) { - this.tag = tag; - this.anchor = anchor; - this.kind = "mapping"; - this.result = result; - } - - return detected; + if (detected) return { tag, anchor, kind: "mapping", result }; } - readTagProperty(): boolean { + readTagProperty(tag: string | null): string | void { let isVerbatim = false; let isNamed = false; let tagHandle = ""; @@ -1306,9 +1312,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 +1388,24 @@ export class LoaderState { } if (isVerbatim) { - this.tag = tagName; + return tagName; } else if (this.tagMap.has(tagHandle)) { - this.tag = this.tagMap.get(tagHandle) + tagName; + return this.tagMap.get(tagHandle) + tagName; } else if (tagHandle === "!") { - this.tag = `!${tagName}`; + return `!${tagName}`; } else if (tagHandle === "!!") { - this.tag = `tag:yaml.org,2002:${tagName}`; - } else { - throw this.#createError( - `Cannot read tag property: undeclared tag handle "${tagHandle}"`, - ); + return `tag:yaml.org,2002:${tagName}`; } - return true; + throw this.#createError( + `Cannot read tag property: undeclared tag handle "${tagHandle}"`, + ); } - readAnchorProperty(): boolean { + readAnchorProperty(anchor: string | null): string | void { 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,11 +1423,10 @@ export class LoaderState { ); } - this.anchor = this.input.slice(position, this.position); - return true; + return this.input.slice(position, this.position); } - readAlias(): boolean { - if (this.peek() !== ASTERISK) return false; + readAlias(): string | void { + if (this.peek() !== ASTERISK) return; let ch = this.next(); @@ -1446,74 +1449,73 @@ export class LoaderState { ); } - this.result = this.anchorMap.get(alias); this.skipSeparationSpace(true, -1); - return true; + + return this.anchorMap.get(alias); } - resolveTag() { - switch (this.tag) { + resolveTag(state: State) { + switch (state.tag) { case null: case "!": - return; - 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(this.result)) continue; + if (!type.resolve(state.result)) continue; // `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); - } - break; + 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; + return state; + } } - const kind = (this.kind ?? "fallback") as KindType; + const kind = (state.kind ?? "fallback") as KindType; const map = this.typeMap[kind]; - const type = map.get(this.tag); + const type = map.get(state.tag); if (!type) { - throw this.#createError(`Cannot resolve unknown tag !<${this.tag}>`); + throw this.#createError( + `Cannot resolve unknown tag !<${state.tag}>`, + ); } - if (this.result !== null && type.kind !== this.kind) { + if (state.result !== null && type.kind !== state.kind) { throw this.#createError( - `Unacceptable node kind for !<${this.tag}> tag: it should be "${type.kind}", not "${this.kind}"`, + `Unacceptable node kind for !<${state.tag}> tag: it should be "${type.kind}", not "${state.kind}"`, ); } - if (!type.resolve(this.result)) { + if (!type.resolve(state.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 !<${state.tag}> explicit tag`, ); } - this.result = type.construct(this.result); - if (this.anchor !== null) this.anchorMap.set(this.anchor, this.result); + 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; - }, - ): boolean { + 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