/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://docs.moderne.io/licensing/moderne-source-available-license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {emptyMarkers, markers, MarkersKind, ParseExceptionResult} from "../markers";
import {omitColon} from "./markers";
import {Parser, ParserInput, parserInputRead, Parsers} from "../parser";
import {randomId} from "../uuid";
import {SourceFile} from "../tree";
import {Yaml, isYamlKey} from "./tree";
import {ParseError, ParseErrorKind} from "../parse-error";
import {Parser as YamlCstParser, CST} from "yaml";
// Types from yaml package CST
type CstToken = CST.Token;
type CstSourceToken = CST.SourceToken;
type CstDocument = CST.Document;
type CstDocumentEnd = CST.DocumentEnd;
type CstBlockMap = CST.BlockMap;
type CstBlockSequence = CST.BlockSequence;
type CstFlowCollection = CST.FlowCollection;
type CstFlowScalar = CST.FlowScalar;
type CstBlockScalar = CST.BlockScalar;
type CstCollectionItem = CST.CollectionItem;
export class YamlParser extends Parser {
async *parse(...sourcePaths: ParserInput[]): AsyncGenerator {
for (const sourcePath of sourcePaths) {
const text = parserInputRead(sourcePath);
try {
yield {
...new YamlCstReader(text).parse(),
sourcePath: this.relativePath(sourcePath)
};
} catch (e: any) {
// Return a ParseError for files that can't be parsed
const parseError: ParseError = {
kind: ParseErrorKind,
id: randomId(),
markers: markers({
kind: MarkersKind.ParseExceptionResult,
id: randomId(),
parserType: "YamlParser",
exceptionType: e.name || "Error",
message: e.message || "Unknown parse error"
} satisfies ParseExceptionResult as ParseExceptionResult),
sourcePath: this.relativePath(sourcePath),
text
};
yield parseError;
}
}
}
}
/**
* Converts YAML CST from the 'yaml' package to our Yaml AST.
* The CST preserves all whitespace, comments, and formatting.
*/
class YamlCstReader {
private readonly cstTokens: CstToken[];
constructor(source: string) {
const parser = new YamlCstParser();
this.cstTokens = [...parser.parse(source)];
}
parse(): Omit {
const documents: Yaml.Document[] = [];
let pendingPrefix = "";
let pendingDirectives: Yaml.Directive[] = [];
for (let i = 0; i < this.cstTokens.length; i++) {
const token = this.cstTokens[i];
if (token.type === 'document') {
const cstDoc = token as CstDocument;
// Check if next token is doc-end
const nextToken = this.cstTokens[i + 1];
let docEnd: CstDocumentEnd | undefined;
if (nextToken?.type === 'doc-end') {
docEnd = nextToken as CstDocumentEnd;
i++; // Skip the doc-end in the main loop
}
const {doc, afterEnd} = this.convertDocument(cstDoc, docEnd, pendingPrefix, pendingDirectives);
documents.push(doc);
pendingPrefix = afterEnd; // Content after ... becomes next doc's prefix
pendingDirectives = []; // Reset directives for next document
} else if (token.type === 'doc-end') {
// Standalone doc-end without preceding document
const docEnd = token as CstDocumentEnd;
pendingPrefix += this.concatenateSources(docEnd.end || []);
} else if (token.type === 'directive') {
// Directive token (e.g., %YAML 1.2 or %TAG !yaml! tag:yaml.org,2002:)
// Look ahead for trailing newline/whitespace to use as suffix
let suffix = "";
while (i + 1 < this.cstTokens.length) {
const nextToken = this.cstTokens[i + 1];
if (nextToken.type === 'newline' || nextToken.type === 'space') {
suffix += (nextToken as CstSourceToken).source;
i++;
} else {
break;
}
}
pendingDirectives.push(this.convertDirective((token as any).source as string, pendingPrefix, suffix));
pendingPrefix = ""; // Prefix consumed by directive
} else if (token.type === 'comment' || token.type === 'newline' || token.type === 'space') {
// Content before document (comments, whitespace) becomes document prefix or directive prefix
pendingPrefix += (token as CstSourceToken).source;
}
}
return {
kind: Yaml.Kind.Documents,
id: randomId(),
prefix: "",
markers: emptyMarkers,
documents,
suffix: undefined
};
}
private convertDirective(source: string, prefix: string, suffix: string): Yaml.Directive {
// Directive source is like "%YAML 1.2" (without trailing newline in CST)
// The suffix is provided from looking ahead at subsequent tokens
// Remove the leading '%'
const value = source.substring(1);
return {
kind: Yaml.Kind.Directive,
id: randomId(),
prefix,
markers: emptyMarkers,
value,
suffix
};
}
private convertDocument(cstDoc: CstDocument, cstDocEnd?: CstDocumentEnd, pendingPrefix: string = "", directives: Yaml.Directive[] = []): {doc: Yaml.Document, afterEnd: string} {
// Extract prefix from document start tokens
// The document prefix is content BEFORE the --- marker
// Content AFTER --- goes into the block's first entry
const startTokens = cstDoc.start || [];
let prefix = pendingPrefix; // Content before this document
let explicit = false;
let afterDocStart = ""; // Content after ---
let seenDocStart = false;
for (const token of startTokens) {
if (token.type === 'doc-start') {
explicit = true;
seenDocStart = true;
} else if (seenDocStart) {
// Content after --- goes into block's prefix
afterDocStart += token.source;
} else {
// Content before --- goes into document prefix
prefix += token.source;
}
}
// Convert the document body and get trailing content
let block: Yaml.Block;
let trailing = "";
if (cstDoc.value) {
const result = this.convertTokenWithTrailing(cstDoc.value);
block = result.node as Yaml.Block;
trailing = result.trailing;
// Prepend the whitespace after --- to the block
if (afterDocStart) {
block = this.prependWhitespaceToValue(block, afterDocStart);
}
} else {
block = this.createEmptyScalar(afterDocStart);
}
// Convert document end, passing the trailing content from block
const {end, afterEnd} = this.convertDocumentEnd(cstDoc.end, cstDocEnd, trailing);
return {
doc: {
kind: Yaml.Kind.Document,
id: randomId(),
prefix,
markers: emptyMarkers,
directives,
explicit,
block,
end
},
afterEnd
};
}
private convertDocumentEnd(docEndTokens?: CstSourceToken[], cstDocEnd?: CstDocumentEnd, trailing: string = ""): {end: Yaml.DocumentEnd, afterEnd: string} {
// Prefix is the trailing content from the block, plus any content before ...
let prefix = trailing;
let explicit = false;
let afterEnd = "";
// Content after the document body (before ... if present)
if (docEndTokens) {
prefix += this.concatenateSources(docEndTokens);
}
// Explicit document end marker (...)
if (cstDocEnd) {
explicit = true;
// cstDocEnd.end contains tokens AFTER the ... marker - these become next doc's prefix
if (cstDocEnd.end) {
afterEnd = this.concatenateSources(cstDocEnd.end);
}
}
return {
end: {
kind: Yaml.Kind.DocumentEnd,
id: randomId(),
prefix,
markers: emptyMarkers,
explicit
},
afterEnd
};
}
private convertTokenWithTrailing(token: CstToken): {node: Yaml, trailing: string} {
switch (token.type) {
case 'block-map':
return this.convertBlockMapWithTrailing(token as CstBlockMap);
case 'block-seq':
return this.convertBlockSequenceWithTrailing(token as CstBlockSequence);
case 'flow-collection':
return this.convertFlowCollectionWithTrailing(token as CstFlowCollection);
case 'scalar':
case 'single-quoted-scalar':
case 'double-quoted-scalar':
return this.convertFlowScalarWithTrailing(token as CstFlowScalar);
case 'block-scalar':
return this.convertBlockScalarWithTrailing(token as CstBlockScalar);
case 'alias':
return this.convertAliasWithTrailing(token as CstFlowScalar);
default:
// For unknown types, create an empty scalar
return {node: this.createEmptyScalar(), trailing: ""};
}
}
private convertBlockMapWithTrailing(cst: CstBlockMap): {node: Yaml.Mapping, trailing: string} {
const entries: Yaml.MappingEntry[] = [];
let pendingPrefix = "";
for (const item of cst.items) {
if (item.key !== undefined || item.value !== undefined) {
const entry = this.convertMappingEntry(item, pendingPrefix);
entries.push(entry.entry);
pendingPrefix = entry.trailingContent;
} else {
// Entry with no key or value - capture start tokens (comments, whitespace)
// into pending prefix for the next entry or as trailing content
pendingPrefix += this.concatenateSources(item.start || []);
}
}
return {
node: {
kind: Yaml.Kind.Mapping,
id: randomId(),
prefix: "",
markers: emptyMarkers,
openingBracePrefix: undefined, // Block map has no braces
entries,
closingBracePrefix: undefined,
anchor: undefined,
tag: undefined
},
trailing: pendingPrefix // Trailing content from last entry
};
}
private convertMappingEntry(item: CstCollectionItem, pendingPrefix: string = ""): {entry: Yaml.MappingEntry, trailingContent: string} {
// Prefix comes from start tokens plus any pending prefix from previous entry's trailing content
const prefix = pendingPrefix + this.concatenateSources(item.start || []);
// Convert key and get its trailing content
let keyTrailing = "";
let key: Yaml.YamlKey;
if (item.key) {
const keyResult = this.convertTokenWithTrailing(item.key);
if (isYamlKey(keyResult.node)) {
key = keyResult.node;
} else {
// Complex keys (mappings or sequences used as keys) are valid YAML
// but our tree model only supports Scalar | Alias as keys.
// Degrade to a Scalar containing the source text.
key = {
kind: Yaml.Kind.Scalar,
id: randomId(),
prefix: keyResult.node.prefix,
markers: emptyMarkers,
style: Yaml.ScalarStyle.PLAIN,
anchor: undefined,
tag: undefined,
value: CST.stringify(item.key).trim()
};
}
keyTrailing = keyResult.trailing;
} else {
key = this.createEmptyScalar();
}
// Extract whitespace before ':' and any anchor/tag after it
let beforeMappingValueIndicator = keyTrailing;
let anchorForValue: Yaml.Anchor | undefined;
let tagForValue: Yaml.Tag | undefined;
let afterColonWhitespace = "";
let seenColon = false;
if (item.sep) {
const sepTokens = item.sep;
for (let i = 0; i < sepTokens.length; i++) {
const sepToken = sepTokens[i];
if (sepToken.type === 'map-value-ind') {
seenColon = true;
} else if (!seenColon) {
// Before the ':'
beforeMappingValueIndicator += sepToken.source;
} else if (sepToken.type === 'anchor') {
// Collect all remaining tokens after anchor as postfix
let postfix = "";
for (let j = i + 1; j < sepTokens.length; j++) {
const nextToken = sepTokens[j];
if (nextToken.type === 'tag') {
tagForValue = this.parseTagTokenWithSuffix(nextToken.source, postfix, sepTokens, j + 1);
postfix = "";
break; // Tag handler consumed remaining tokens
} else {
postfix += nextToken.source;
}
}
anchorForValue = {
kind: Yaml.Kind.Anchor,
id: randomId(),
prefix: afterColonWhitespace,
markers: emptyMarkers,
postfix,
key: sepToken.source.substring(1) // Remove &
};
afterColonWhitespace = "";
break; // We've consumed all remaining tokens
} else if (sepToken.type === 'tag') {
// Parse tag with its suffix (whitespace after the tag)
tagForValue = this.parseTagTokenWithSuffix(sepToken.source, afterColonWhitespace, sepTokens, i + 1);
afterColonWhitespace = ""; // Remaining whitespace is in tag suffix, not value prefix
break; // Tag handler consumed remaining tokens
} else {
// After the ':'
afterColonWhitespace += sepToken.source;
}
}
}
// Convert value
let value: Yaml.Block;
let valueTrailing = "";
if (item.value) {
const valueResult = this.convertTokenWithTrailing(item.value);
value = valueResult.node as Yaml.Block;
valueTrailing = valueResult.trailing;
// Apply anchor and tag if found in separator
if (anchorForValue && 'anchor' in value) {
value = {...value, anchor: anchorForValue} as Yaml.Block;
}
if (tagForValue && 'tag' in value) {
value = {...value, tag: tagForValue} as Yaml.Block;
}
// Prepend accumulated whitespace based on value type
if (afterColonWhitespace) {
value = this.prependWhitespaceToValue(value, afterColonWhitespace);
}
} else {
value = this.createEmptyScalar(afterColonWhitespace);
}
return {
entry: {
kind: Yaml.Kind.MappingEntry,
id: randomId(),
prefix,
markers: emptyMarkers,
key,
beforeMappingValueIndicator,
value
},
trailingContent: valueTrailing
};
}
private convertBlockSequenceWithTrailing(cst: CstBlockSequence): {node: Yaml.Sequence, trailing: string} {
const entries: Yaml.SequenceEntry[] = [];
let pendingPrefix = "";
for (const item of cst.items) {
const result = this.convertSequenceEntry(item, true, pendingPrefix);
entries.push(result.entry);
pendingPrefix = result.trailingContent;
}
return {
node: {
kind: Yaml.Kind.Sequence,
id: randomId(),
prefix: "",
markers: emptyMarkers,
openingBracketPrefix: undefined, // Block sequence has no brackets
entries,
closingBracketPrefix: undefined,
anchor: undefined,
tag: undefined
},
trailing: pendingPrefix // Trailing content from last entry
};
}
private convertSequenceEntry(item: CstCollectionItem, dash: boolean, pendingPrefix: string = ""): {entry: Yaml.SequenceEntry, trailingContent: string} {
// Build prefix from start tokens, but exclude the dash indicator itself
// Also extract anchor and tag if present
let prefix = pendingPrefix;
let afterDashSpace = "";
let seenDash = false;
let anchorForValue: Yaml.Anchor | undefined;
let tagForValue: Yaml.Tag | undefined;
const startTokens = item.start || [];
for (let i = 0; i < startTokens.length; i++) {
const token = startTokens[i];
if (token.type === 'seq-item-ind') {
seenDash = true;
} else if (seenDash) {
if (token.type === 'anchor') {
// Collect all remaining tokens after anchor as postfix
let postfix = "";
for (let j = i + 1; j < startTokens.length; j++) {
const nextToken = startTokens[j];
if (nextToken.type === 'tag') {
tagForValue = this.parseTagTokenWithSuffix(nextToken.source, postfix, startTokens, j + 1);
postfix = "";
break;
} else {
postfix += nextToken.source;
}
}
anchorForValue = {
kind: Yaml.Kind.Anchor,
id: randomId(),
prefix: afterDashSpace,
markers: emptyMarkers,
postfix,
key: token.source.substring(1) // Remove &
};
afterDashSpace = "";
break; // We've consumed all remaining tokens
} else if (token.type === 'tag') {
tagForValue = this.parseTagTokenWithSuffix(token.source, afterDashSpace, startTokens, i + 1);
afterDashSpace = "";
break; // Tag handler consumed remaining tokens
} else {
afterDashSpace += token.source;
}
} else {
prefix += token.source;
}
}
// Convert value
let block: Yaml.Block;
let trailing = "";
if (item.value) {
const valueResult = this.convertTokenWithTrailing(item.value);
block = valueResult.node as Yaml.Block;
trailing = valueResult.trailing;
// Apply anchor and tag if found in start tokens
if (anchorForValue && 'anchor' in block) {
block = {...block, anchor: anchorForValue} as Yaml.Block;
}
if (tagForValue && 'tag' in block) {
block = {...block, tag: tagForValue} as Yaml.Block;
}
// Prepend the space after dash to the block
// Use prependWhitespaceToValue to ensure Mapping/Sequence keep empty prefix
if (afterDashSpace) {
block = this.prependWhitespaceToValue(block, afterDashSpace);
}
} else {
block = this.createEmptyScalar(afterDashSpace);
}
return {
entry: {
kind: Yaml.Kind.SequenceEntry,
id: randomId(),
prefix,
markers: emptyMarkers,
block,
dash,
trailingCommaPrefix: undefined // Block sequence has no commas
},
trailingContent: trailing
};
}
private convertFlowCollectionWithTrailing(cst: CstFlowCollection): {node: Yaml.Mapping | Yaml.Sequence, trailing: string} {
const isMap = cst.start.source === '{';
const openingPrefix = "";
// End tokens include closing bracket/brace plus any trailing content
// We need to separate the closing delimiter from trailing whitespace
const endTokens = cst.end || [];
let closingPrefix = "";
let trailing = "";
let seenClosing = false;
for (const token of endTokens) {
if (token.type === 'flow-map-end' || token.type === 'flow-seq-end') {
seenClosing = true;
} else if (seenClosing) {
trailing += token.source;
} else {
closingPrefix += token.source;
}
}
if (isMap) {
const entries: Yaml.MappingEntry[] = [];
let pendingPrefix = "";
for (const item of cst.items) {
if (item.key !== undefined || item.value !== undefined) {
const result = this.convertFlowMappingEntry(item, pendingPrefix);
entries.push(result.entry);
pendingPrefix = result.trailingContent;
} else {
// Empty item (trailing comma) - capture start tokens including the comma
pendingPrefix += this.concatenateSources(item.start || []);
}
}
// Trailing content from last entry becomes part of closing brace prefix
const finalClosingPrefix = pendingPrefix + closingPrefix;
return {
node: {
kind: Yaml.Kind.Mapping,
id: randomId(),
prefix: "",
markers: emptyMarkers,
openingBracePrefix: openingPrefix,
entries,
closingBracePrefix: finalClosingPrefix,
anchor: undefined,
tag: undefined
},
trailing
};
} else {
// Flow sequence [a, b, c]
const entries: Yaml.SequenceEntry[] = [];
let pendingPrefix = "";
for (let i = 0; i < cst.items.length; i++) {
const item = cst.items[i];
const isLast = i === cst.items.length - 1;
const result = this.convertFlowSequenceEntry(item, isLast, pendingPrefix);
entries.push(result.entry);
pendingPrefix = result.trailingContent;
}
// Trailing content from last entry becomes part of closing bracket prefix
const finalClosingPrefix = pendingPrefix + closingPrefix;
return {
node: {
kind: Yaml.Kind.Sequence,
id: randomId(),
prefix: "",
markers: emptyMarkers,
openingBracketPrefix: openingPrefix,
entries,
closingBracketPrefix: finalClosingPrefix,
anchor: undefined,
tag: undefined
},
trailing
};
}
}
private convertFlowMappingEntry(item: CstCollectionItem, pendingPrefix: string): {entry: Yaml.MappingEntry, trailingContent: string} {
const prefix = pendingPrefix + this.concatenateSources(item.start || []);
let keyTrailing = "";
let key: Yaml.YamlKey;
if (item.key) {
const keyResult = this.convertTokenWithTrailing(item.key);
if (isYamlKey(keyResult.node)) {
key = keyResult.node;
} else {
key = {
kind: Yaml.Kind.Scalar,
id: randomId(),
prefix: keyResult.node.prefix,
markers: emptyMarkers,
style: Yaml.ScalarStyle.PLAIN,
anchor: undefined,
tag: undefined,
value: CST.stringify(item.key).trim()
};
}
keyTrailing = keyResult.trailing;
} else {
key = this.createEmptyScalar();
}
let beforeMappingValueIndicator = keyTrailing;
let afterColonSpace = "";
let seenColon = false;
if (item.sep) {
for (const token of item.sep) {
if (token.type === 'map-value-ind') {
seenColon = true;
} else if (seenColon) {
afterColonSpace += token.source;
} else if (token.type === 'comma') {
// This is the comma after the entry, not before
// Skip it as we handle comma separately
} else {
beforeMappingValueIndicator += token.source;
}
}
}
let value: Yaml.Block;
let valueTrailing = "";
if (item.value) {
const valueResult = this.convertTokenWithTrailing(item.value);
value = valueResult.node as Yaml.Block;
valueTrailing = valueResult.trailing;
if (afterColonSpace) {
value = this.prependWhitespaceToValue(value, afterColonSpace);
}
} else {
value = this.createEmptyScalar(afterColonSpace);
}
// In flow mappings like { "MV7", "7J04" }, entries may lack explicit colons
const entryMarkers = seenColon ? emptyMarkers : markers(omitColon());
return {
entry: {
kind: Yaml.Kind.MappingEntry,
id: randomId(),
prefix,
markers: entryMarkers,
key,
beforeMappingValueIndicator,
value
},
trailingContent: valueTrailing
};
}
private convertFlowSequenceEntry(item: CstCollectionItem, isLast: boolean, pendingPrefix: string): {entry: Yaml.SequenceEntry, trailingContent: string} {
// Start tokens may include comma from previous item
let prefix = pendingPrefix;
let hasComma = false;
for (const token of item.start || []) {
if (token.type === 'comma') {
hasComma = true;
} else {
prefix += token.source;
}
}
let block: Yaml.Block;
let trailing = "";
if (item.value) {
const valueResult = this.convertTokenWithTrailing(item.value);
block = valueResult.node as Yaml.Block;
trailing = valueResult.trailing;
} else if (item.key !== undefined) {
// Flow sequence can contain mapping entries: [a: 1, b: 2]
const entryResult = this.convertFlowMappingEntry(item, "");
block = {
kind: Yaml.Kind.Mapping,
id: randomId(),
prefix: "",
markers: emptyMarkers,
openingBracePrefix: undefined,
entries: [entryResult.entry],
closingBracePrefix: undefined,
anchor: undefined,
tag: undefined
};
trailing = entryResult.trailingContent;
} else {
block = this.createEmptyScalar();
}
// Trailing comma prefix captures space after value before comma
let trailingCommaPrefix: string | undefined;
if (!isLast) {
trailingCommaPrefix = trailing;
trailing = "";
}
return {
entry: {
kind: Yaml.Kind.SequenceEntry,
id: randomId(),
prefix,
markers: emptyMarkers,
block,
dash: false, // Flow sequences don't use dashes
trailingCommaPrefix
},
trailingContent: trailing
};
}
private convertFlowScalarWithTrailing(cst: CstFlowScalar): {node: Yaml.Scalar, trailing: string} {
const resolved = CST.resolveAsScalar(cst);
const value = resolved?.value ?? "";
// Determine scalar style
let style: Yaml.ScalarStyle;
switch (cst.type) {
case 'single-quoted-scalar':
style = Yaml.ScalarStyle.SINGLE_QUOTED;
break;
case 'double-quoted-scalar':
style = Yaml.ScalarStyle.DOUBLE_QUOTED;
break;
default:
style = Yaml.ScalarStyle.PLAIN;
}
// End tokens contain trailing whitespace/newlines
const trailing = this.concatenateSources(cst.end || []);
return {
node: {
kind: Yaml.Kind.Scalar,
id: randomId(),
prefix: "",
markers: emptyMarkers,
style,
anchor: undefined,
tag: undefined,
value
},
trailing
};
}
private convertBlockScalarWithTrailing(cst: CstBlockScalar): {node: Yaml.Scalar, trailing: string} {
// Use CST.stringify to get the exact original source including header
const fullSource = CST.stringify(cst);
// Determine style from the first character (| or >)
const headerChar = fullSource.charAt(0);
const style = headerChar === '|' ? Yaml.ScalarStyle.LITERAL : Yaml.ScalarStyle.FOLDED;
// The value is everything after the header indicator (| or >)
const value = fullSource.substring(1);
// Extract anchor and tag from props if present
let anchor: Yaml.Anchor | undefined;
let tag: Yaml.Tag | undefined;
for (const prop of cst.props || []) {
if ('type' in prop) {
const propTyped = prop as CstSourceToken;
if (propTyped.type === 'anchor') {
anchor = this.parseAnchorToken(propTyped.source, "");
} else if (propTyped.type === 'tag') {
tag = this.parseTagToken(propTyped.source, "");
}
}
}
return {
node: {
kind: Yaml.Kind.Scalar,
id: randomId(),
prefix: "",
markers: emptyMarkers,
style,
anchor,
tag,
value
},
trailing: "" // Block scalars don't have separate end tokens
};
}
private convertAliasWithTrailing(cst: CstFlowScalar): {node: Yaml.Alias, trailing: string} {
// Alias source is like "*anchorName"
const key = cst.source.substring(1); // Remove the *
const anchor: Yaml.Anchor = {
kind: Yaml.Kind.Anchor,
id: randomId(),
prefix: "",
markers: emptyMarkers,
postfix: "",
key
};
// End tokens contain trailing whitespace
const trailing = this.concatenateSources(cst.end || []);
return {
node: {
kind: Yaml.Kind.Alias,
id: randomId(),
prefix: "",
markers: emptyMarkers,
anchor
},
trailing
};
}
private parseAnchorToken(source: string, prefix: string): Yaml.Anchor {
// Anchor source is like "&anchorName"
const key = source.substring(1); // Remove the &
return {
kind: Yaml.Kind.Anchor,
id: randomId(),
prefix,
markers: emptyMarkers,
postfix: "",
key
};
}
private parseTagToken(source: string, prefix: string): Yaml.Tag {
return this.parseTagTokenWithSuffix(source, prefix, [], 0);
}
private parseTagTokenWithSuffix(source: string, prefix: string, remainingTokens: CstSourceToken[], startIndex: number): Yaml.Tag {
let name: string;
let tagKind: Yaml.TagKind;
if (source.startsWith('!<') && source.endsWith('>')) {
// Explicit global tag: !
name = source.substring(2, source.length - 1);
tagKind = Yaml.TagKind.EXPLICIT_GLOBAL;
} else if (source.startsWith('!!')) {
// Implicit global tag: !!str
name = source.substring(2);
tagKind = Yaml.TagKind.IMPLICIT_GLOBAL;
} else {
// Local tag: !custom
name = source.substring(1);
tagKind = Yaml.TagKind.LOCAL;
}
// Collect remaining tokens as suffix (whitespace after the tag)
let suffix = "";
for (let i = startIndex; i < remainingTokens.length; i++) {
suffix += remainingTokens[i].source;
}
return {
kind: Yaml.Kind.Tag,
id: randomId(),
prefix,
markers: emptyMarkers,
name,
suffix,
tagKind
};
}
private createEmptyScalar(prefix: string = ""): Yaml.Scalar {
return {
kind: Yaml.Kind.Scalar,
id: randomId(),
prefix,
markers: emptyMarkers,
style: Yaml.ScalarStyle.PLAIN,
anchor: undefined,
tag: undefined,
value: ""
};
}
/**
* Prepends whitespace to a value node in the appropriate location based on its type.
* - For Scalars and Aliases: prepend to prefix
* - For flow Mappings (with braces): prepend to openingBracePrefix
* - For flow Sequences (with brackets): prepend to openingBracketPrefix
* - For block Mappings/Sequences: prepend to first entry's prefix
*/
private prependWhitespaceToValue(value: Yaml.Block, whitespace: string): Yaml.Block {
if (value.kind === Yaml.Kind.Scalar || value.kind === Yaml.Kind.Alias) {
return {...value, prefix: whitespace + value.prefix};
}
if (value.kind === Yaml.Kind.Mapping) {
const mapping = value as Yaml.Mapping;
if (mapping.openingBracePrefix !== undefined) {
// Flow mapping: prepend to opening brace prefix
return {...mapping, openingBracePrefix: whitespace + mapping.openingBracePrefix};
} else if (mapping.entries.length > 0) {
// Block mapping: prepend to first entry's prefix
const firstEntry = mapping.entries[0];
const updatedFirstEntry = {...firstEntry, prefix: whitespace + firstEntry.prefix};
return {...mapping, entries: [updatedFirstEntry, ...mapping.entries.slice(1)]};
}
}
if (value.kind === Yaml.Kind.Sequence) {
const sequence = value as Yaml.Sequence;
if (sequence.openingBracketPrefix !== undefined) {
// Flow sequence: prepend to opening bracket prefix
return {...sequence, openingBracketPrefix: whitespace + sequence.openingBracketPrefix};
} else if (sequence.entries.length > 0) {
// Block sequence: prepend to first entry's prefix
const firstEntry = sequence.entries[0];
const updatedFirstEntry = {...firstEntry, prefix: whitespace + firstEntry.prefix};
return {...sequence, entries: [updatedFirstEntry, ...sequence.entries.slice(1)]};
}
}
// Fallback: prepend to prefix (shouldn't normally reach here)
return {...value, prefix: whitespace + value.prefix} as Yaml.Block;
}
private concatenateSources(tokens: CstSourceToken[]): string {
return tokens.map(t => t.source).join('');
}
}
Parsers.registerParser("yaml", YamlParser);