yini-parser
Version:
Readable configuration without YAML foot-guns or JSON noise. The official Node.js parser for YINI config format — An INI-inspired configuration format with clear nesting, explicit types, and predictable parsing.
840 lines (838 loc) • 45 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const env_1 = require("../config/env");
const YiniParserVisitor_1 = __importDefault(require("../grammar/generated/YiniParserVisitor"));
const extractSignificantYiniLine_1 = require("../parsers/extractSignificantYiniLine");
const parseBoolean_1 = __importDefault(require("../parsers/parseBoolean"));
const parseNumber_1 = __importDefault(require("../parsers/parseNumber"));
// import parseNumber from '../parsers/parseNumber'
const parseSectionHeader_1 = __importDefault(require("../parsers/parseSectionHeader"));
const number_1 = require("../utils/number");
const print_1 = require("../utils/print");
const string_1 = require("../utils/string");
const yiniHelpers_1 = require("../utils/yiniHelpers");
const errorDataHandler_1 = require("./errorDataHandler");
// -----------------------
// --- Helpers -------------------------------------------------------------
// let _subjectType: TSubjectType = 'None'
let _sourceType;
/**
* @param {string | undefined} [tag]
* Debugging only. Its contents may change at any time and
* must not be relied upon for any functional purpose.
*/
const makeScalarValue = (type, value = null, tag = undefined) => {
switch (type) {
case 'String':
return { type, value: value, tag };
case 'Number':
return { type, value: value, tag };
case 'Boolean':
return { type, value: !!value, tag };
case 'Null':
return { type: 'Null', value: null, tag };
case 'Undefined':
return { type: 'Undefined', value: undefined, tag };
default:
new errorDataHandler_1.ErrorDataHandler(_sourceType).pushOrBail(null, 'Fatal-Error', `No such type in makeValue(..), type: ${type}, value: ${value}`, 'Something in the code is done incorrectly in order for this to happen... :S');
}
return { type: 'Null', value: null, tag };
};
/**
* @param {string | undefined} [tag]
* Debugging only. Its contents may change at any time and
* must not be relied upon for any functional purpose.
*/
const makeListValue = (elems = [], tag = undefined) => {
return { type: 'List', elems, tag };
};
/**
* @param {string | undefined} [tag]
* Debugging only. Its contents may change at any time and
* must not be relied upon for any functional purpose.
*/
const makeObjectValue = (entries = {}, tag = undefined) => {
return { type: 'Object', entries, tag };
};
function trimQuotes(text) {
// STRING token already excludes quotes; the rule returns the literal with quotes present.
// We’ll reliably strip the outer quote(s) and leave contents as-is (concat pieces handled below).
const q = text[0];
if ((q === '"' || q === "'") &&
text.length >= 2 &&
text[text.length - 1] === q) {
return text.slice(1, -1);
}
// Triple-quoted cases are handled by the lexer too; same stripping works since token text begins with quotes.
if (text.startsWith('"""') && text.endsWith('"""') && text.length >= 6) {
return text.slice(3, -3);
}
return text;
}
function makeSection(name, level) {
return { sectionName: name, level, members: new Map(), children: [] };
}
/** Parse SECTION_HEAD token text → {level, name}.
* Supports repeated markers (^^^^) and shorthand (^7) (Spec 5.2–5.3.1). :contentReference[oaicite:5]{index=5}:contentReference[oaicite:6]{index=6}
*/
// function parseSectionHeadToken(raw: string): { level: number; name: string } {
// // SECTION_HEAD token text includes: optional WS, marker(s) or shorthand, WS, IDENT (possibly backticked), NL+
// // We only need the visible line content up to NL.
// const line = raw.split(/\r?\n/)[0]
// // Extract marker block and name
// // Examples: "^^ Section", "^7 `Section name`", "< MySection"
// const m = line.match(/^\s*([\^<§€]+|\^|\<|§|€)(\d+)?[ \t]+(.+?)\s*$/)
// if (m) {
// const markerRun = m[1]
// const numeric = m[2]
// let level: number
// if (numeric) {
// level = parseInt(numeric, 10)
// } else {
// // count repeated marker chars (^^^^)
// level = markerRun.length
// }
// // Section name may be backticked: `Name with spaces`
// let name = m[3]
// if (name.startsWith('`') && name.endsWith('`')) {
// name = name.slice(1, -1)
// }
// return { level, name }
// }
// // Fallback: be defensive
// return { level: 1, name: line.trim() }
// }
// --- Builder Visitor -----------------------------------------------------
/**
* This interface defines a complete generic visitor for a parse tree produced
* by `YiniParser`.
*
* @param <Result> The return type of the visit operation. Use `void` for
* operations with no return type.
*/
// export default class YINIVisitor<IResult> extends YiniParserVisitor<IResult> {
class ASTBuilder extends YiniParserVisitor_1.default {
/**
* @param metaFileName If parsing from a file, provide the file name here so the meta information can be updated accordingly.
* @param metaLineCount Provide the line-count here so the meta information can be updated accordingly.
*/
constructor(errorHandler, options, sourceType, metaFileName) {
super();
this.errorHandler = null;
this.meta_hasYiniMarker = false; // For stats.
// private meta_numOfSections = 0 // For stats.
this._numOfMembers = 0; // For error checking and stats.
// private meta_numOfChains = 0 // For stats.
this.meta_maxLevel = 0; // For stats.
this.mapSectionNamePaths = new Map();
// --- Private helper methods --------------------------------
this.hasDefinedSectionTitle = (keyPath) => {
return this.mapSectionNamePaths?.has(keyPath);
};
this.setDefineSectionTitle = (keyPath, level) => {
this.mapSectionNamePaths.set(keyPath, level);
};
/**
* Visit a parse tree produced by `YiniParser.yini`.
* @param ctx the parse tree
* @return the visitor result
*/
// visitYini?: (ctx: YiniContext) => Result
// visitYini?: (ctx: YiniContext) => any
this.visitYini = (ctx) => {
// children: prolog?, stmt*, terminal?, EOF
ctx.children?.forEach((c) => this.visit?.(c));
return this.ast;
};
/**
* Visit a parse tree produced by `YiniParser.prolog`.
* @param ctx the parse tree
* @return the visitor result
*/
// visitProlog?: (ctx: PrologContext) => Result
this.visitProlog = (ctx) => {
// Ignored for structure; keeps column rules stable.
return null;
};
/**
* Visit a parse tree produced by `YiniParser.terminal`.
* @param ctx the parse tree
* @return the visitor result
*/
// visitTerminal_stmt?: (ctx: Terminal_stmtContext) => Result;
this.visitTerminal_stmt = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitTerminal_stmt(..)');
let rawText = ctx.getText().trim();
(0, print_1.debugPrint)('rawText = "' + rawText + '"');
// rawText = extractYiniLine(rawText) // Remove possible comments.
// rawText = stripCommentsAndAfter(rawText.split('\n', 1)[0]).trim() // Remove possible comments.
rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(rawText); // Remove possible comments.
(0, print_1.debugPrint)('rawText2 = "' + rawText + '"');
if (rawText.toLowerCase() === '/end') {
// NOTE: Below, maybe not reached at all.
if (this.ast.terminatorSeen) {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Warning', 'Hit a duplicate terminator in document', `'${rawText}' already exists in this file, there must only be one terminator at the end of file ('/END'). Also note that the terminator is optional in both lenient and strict mode, unless the option 'isRequireDocTerminator' is enabled.`);
}
}
else {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Error', 'Encountered unknow syntax for terminator', `Got '${rawText}', but expected '/END' (case insensitive).`);
}
this.ast.terminatorSeen = true;
return null;
};
/**
* Visit a parse tree produced by `YiniParser.stmt`.
* @param ctx the parse tree
* @grammarRule eol | SECTION_HEAD | assignment | colon_list_decl | marker_stmt | bad_member
* @return the visitor result
*/
// visitStmt?: (ctx: StmtContext) => Result
this.visitStmt = (ctx) => {
const child = ctx.getChild(0);
const ruleName = child?.constructor?.name ?? '';
if (ruleName.includes('EolContext'))
return this.visitEol?.(child);
if (ruleName.includes('AssignmentContext'))
return this.visitAssignment?.(child);
if (ruleName.includes('Colon_list_declContext'))
return this.visitColon_list_decl?.(child);
if (ruleName.includes('Meta_stmtContext'))
return this.visitMeta_stmt?.(child);
(0, print_1.debugPrint)('S1');
// let headerAlt = child.getText?.() ?? ''
// let header = ctx.SECTION_HEAD()?.getText().trim() || ''
let header = ctx.SECTION_HEAD()?.getText().trim() || '';
// debugPrint('S2, lineAlt: >>>' + lineAlt + '<<<')
(0, print_1.debugPrint)('S2, header: >>>' + header + '<<<');
header = (0, extractSignificantYiniLine_1.extractYiniLine)(header);
(0, print_1.debugPrint)('S3, header: >>>' + header + '<<<');
if (!!header) {
const { sectionName, sectionLevel } = (0, parseSectionHeader_1.default)(header, this.errorHandler, ctx);
// Validate level sequencing per spec 5.3 (no skipping upward)
const currentLevel = this.sectionStack[this.sectionStack.length - 1].level;
if (sectionLevel > currentLevel + 1) {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Error', 'Invalid section level transition', `Cannot skip levels: from ${currentLevel} to ${sectionLevel}.`, `Section headers may not start directly at level ${sectionLevel}, skipping previous section levels. Please start with one level further down.`);
}
const section = makeSection((0, string_1.trimBackticks)(sectionName), sectionLevel);
this.attachSection(ctx, this.sectionStack, section, this.ast); // respects up/down nesting
return null;
}
// bad_member fallback
return this.visitBad_member?.(ctx.getChild(0));
};
/**
* Visit a parse tree produced by `YiniParser.meta_stmt`.
* @param ctx the parse tree
*/
this.visitMeta_stmt = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitMeta_stmt(..)');
let rawText = ctx.getText().trim();
(0, print_1.debugPrint)('rawText = "' + rawText + '"');
ctx.children?.forEach((c) => {
let rawText = c?.getText().trim();
(0, print_1.debugPrint)('visitMeta_stmt:child = "' + rawText + '"');
this.visit?.(c);
});
return null;
};
/**
* Visit a parse tree produced by `YiniParser.directive`.
* @param ctx the parse tree
* @note Directive statements in YINI are special top-level commands that
* appear only at the beginning of a document, before any sections
* or members. Each directive may occur at most once per file.
*/
this.visitDirective = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitDirective(..)');
let rawText = ctx.getText().trim();
(0, print_1.debugPrint)('rawText = "' + rawText + '"');
// NOTE: Important to strip any possible comments on the same line.
rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(rawText); // Remove possible comments.
(0, print_1.debugPrint)('rawText2 = "' + rawText + '"');
if (this.mapSectionNamePaths.size || this._numOfMembers) {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', `Found a directive statement in the wrong place ${this.isStrict ? '(strict mode)' : '(lenient mode)'}`, `Directive '${rawText}' must appear only at the beginning of the document, before any sections or members.`, `Tip: Move the line with '${rawText}' to the very top of the file (but after a possible #! line or comments).`);
}
if (rawText.toLowerCase().startsWith('@include')) {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Notice', `Detected unsupported directive '@include'`, `This directive is not currently supported by the parser.`);
}
else if (rawText.toLowerCase() === '@yini') {
if (this.ast.yiniMarkerSeen) {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, this.isStrict ? 'Syntax-Error' : 'Syntax-Warning', `Hit a duplicate YINI Marker in document`, `'${rawText}' already exists in this file, it's enough with only one YINI Marker ('@YINI').`);
}
}
else {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Error', 'Encountered unknow directive statement', `Got '${rawText}', but expected '@YINI' (case insensitive).`);
}
// @yini marker is advisory (no semantic value) per spec. We ignore it. (Spec 2.4) :contentReference[oaicite:9]{index=9}
this.ast.yiniMarkerSeen = true;
return null;
};
/**
* Visit a parse tree produced by `YiniParser.annotation`.
* @param ctx the parse tree
* @return the visitor result
*/
this.visitAnnotation = (ctx) => {
/*
Experimental / for future.
*/
(0, print_1.debugPrint)('-> Entered visitAnnotation(..)');
let rawText = ctx.getText().trim();
(0, print_1.debugPrint)('rawText = "' + rawText + '"');
// NOTE: Important to strip any possible comments on the same line.
rawText = (0, yiniHelpers_1.stripCommentsAndAfter)(rawText); // Remove possible comments.
(0, print_1.debugPrint)('rawText2 = "' + rawText + '"');
if (rawText.toLowerCase().startsWith('@deprecated')) {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Notice', `Detected unsupported annotation '@deprecated'`, `This annotation is not currently supported by the parser.`);
}
// NOTE: Don't implement! Only experimental / testing for future.
return null;
};
/**
* Visit a parse tree produced by `YiniParser.eol`.
* @param ctx the parse tree
* @return the visitor result
*/
// visitEol?: (ctx: EolContext) => Result
this.visitEol = (ctx) => null;
/**
* Visit a parse tree produced by `YiniParser.assignment`.
* @param ctx the parse tree
* @return the visitor result
*/
// visitAssignment?: (ctx: AssignmentContext) => Result
this.visitAssignment = (ctx) => {
(0, env_1.isDebug)() && console.log();
(0, print_1.debugPrint)('-> Entered visitAssignment(..)');
// assignment : member eol
const mem = ctx.member();
this.visitMember?.(mem);
return null;
};
/**
* Visit a parse tree produced by `YiniParser.member`.
* @param ctx the parse tree
* @grammarRule KEY WS? EQ WS? value?
* @return the visitor result
*/
// visitMember?: (ctx: MemberContext) => Result
this.visitMember = (ctx) => {
(0, env_1.isDebug)() && console.log();
(0, print_1.debugPrint)('-> Entered visitMember(..)');
// member: KEY WS? EQ WS? value?
const rawKey = ctx.getChild(0).getText();
(0, print_1.debugPrint)(`visitMember(..): rawKey = '${rawKey}'`);
if (rawKey) {
(0, print_1.debugPrint)();
(0, print_1.debugPrint)('Has a key... Validate it either as a simple or a backticked ident...');
if ((0, string_1.isEnclosedInBackticks)(rawKey)) {
if (!(0, yiniHelpers_1.isValidBacktickedIdent)(rawKey)) {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Error', `Invalid (backticked) key/identifier: '${rawKey}'`, 'Backticked key/identifier should be like e.g. `My section name`.');
}
}
else {
if (!(0, yiniHelpers_1.isValidSimpleIdent)(rawKey)) {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Error', `Invalid key/identifier name: '${rawKey}'`, `Key/identifier names must start with: A-Z, a-z, or _, unless enclosed in backticks e.g.: \`${rawKey}\` or \`My key name\`.`);
}
}
}
const resultKey = (0, string_1.trimBackticks)(rawKey);
const rawValue = ctx.value?.()?.getText();
(0, print_1.debugPrint)(`visitMember(..): rawValue = ` + ctx.value?.()?.getText());
let valueContext = ctx.value?.();
let valueNode;
if (!rawValue) {
// treatEmptyValueAsNull = 'allow' (default in lenient mode, empty value => Null in lenient mode)
// if (!this.isStrict) {
switch (this.options.rules.treatEmptyValueAsNull) {
case 'allow':
// Lenient mode: implicit null, no warning (treatEmptyValueAsNull = 'allow').
valueNode = makeScalarValue('Null', null, 'Implicit null (empty value)');
break;
case 'allow-with-warning':
valueNode = makeScalarValue('Null', null, 'Implicit null (empty value)');
this.errorHandler.pushOrBail(ctx, 'Syntax-Warning', `Empty value treated as null for key '${resultKey}'.`, `An empty value after '=' was encountered. Per 'treatEmptyValueAsNull = allow-with-warning', interpreted as null.`, `If you intended null, write it explicitly: ${resultKey} = null. Otherwise provide a non-empty value or set 'treatEmptyValueAsNull' to 'disallow'.`);
break;
case 'disallow':
// treatEmptyValueAsNull = 'disallow' (default in strict mode)
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Error', `Missing value for key '${resultKey}'.`, `Expected a value after '=' but found none. Implicit nulls are disallowed by 'treatEmptyValueAsNull = disallow'.`, `Write 'null' explicitly (${resultKey} = null) if that is intended, or provide a concrete value.`);
break;
}
}
else {
valueNode = this.visitValue?.(valueContext);
}
(0, print_1.debugPrint)('visitMember(..): valueNode:');
if ((0, env_1.isDebug)()) {
(0, print_1.printObject)(valueNode);
}
// const resultType = valueLiteral?.type
// const resultValue = valueLiteral?.type
if (!valueNode || valueNode.type === 'Undefined') {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Error', 'Invalid value', `Invalid value for key '${resultKey} in member (<key> = <value> pair)'.`, `Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`);
}
const current = this.sectionStack[this.sectionStack.length - 1];
if (valueNode !== undefined) {
this.putMember(this.errorHandler, ctx, current, resultKey, valueNode,
// this.ast,
this.onDuplicateKey);
}
return valueNode;
};
/**
* Visit a parse tree produced by `YiniParser.value`.
* @param ctx the parse tree
* @return the visitor result
*/
// visitValue?: (ctx: ValueContext) => Result
this.visitValue = (ctx) => {
(0, print_1.debugPrint)('----------------------------');
(0, print_1.debugPrint)('-> Entered visitValue(..)');
let valueNode = undefined;
if (ctx.null_literal()) {
(0, print_1.debugPrint)(' visiting visitNull_literal(..)');
valueNode = this.visitNull_literal(ctx.null_literal());
}
else if (ctx.string_literal()) {
(0, print_1.debugPrint)(' visiting visitString_literal(..)');
valueNode = this.visitString_literal(ctx.string_literal());
}
else if (ctx.number_literal()) {
(0, print_1.debugPrint)(' visiting visitNumber_literal(..)');
valueNode = this.visitNumber_literal(ctx.number_literal());
}
else if (ctx.boolean_literal()) {
(0, print_1.debugPrint)(' visiting visitBoolean_literal(..)');
valueNode = this.visitBoolean_literal(ctx.boolean_literal());
}
else if (ctx.list_literal()) {
(0, print_1.debugPrint)(' visiting visitList_literal(..)');
valueNode = this.visitList_literal(ctx.list_literal());
}
else if (ctx.object_literal()) {
(0, print_1.debugPrint)(' visiting visitObject_literal(..)');
valueNode = this.visitObject_literal(ctx.object_literal());
}
else {
(0, print_1.debugPrint)(' Entered else case in visitValue(..)');
valueNode = makeScalarValue('Undefined', undefined, 'Invalid value');
}
(0, print_1.debugPrint)('<- About to exit visitValue(..): returning:');
if ((0, env_1.isDebug)()) {
(0, print_1.printObject)(valueNode);
}
(0, print_1.debugPrint)('----------------------------\n');
return valueNode;
};
/**
* Visit a parse tree produced by `YiniParser.string_literal`.
* @param ctx the parse tree
* @return the visitor result
*/
// visitString_literal?: (ctx: String_literalContext) => Result
this.visitString_literal = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitString_literal(..)');
// STRING (string_concat)*
// Concatenate pieces with PLUS (Spec 6.6). Each piece is a STRING token; '+' is structural. :contentReference[oaicite:17]{index=17}
let text = trimQuotes(ctx.STRING().getText());
// for (const c of ctx.string_concat() ?? []) {
for (const c of ctx.string_concat_list() ?? []) {
(0, print_1.debugPrint)('c of ctx.string_concat():');
(0, env_1.isDebug)() && (0, print_1.printObject)(c);
text += this.visitString_concat?.(c);
}
return makeScalarValue('String', text);
};
/**
* Visit a parse tree produced by `YiniParser.number_literal`.
* @param ctx the parse tree
* @return the visitor result
*/
// visitNumber_literal?: (ctx: Number_literalContext) => Result
this.visitNumber_literal = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitNumber_literal(..)');
const rawText = ctx.getText();
const parsedNum = (0, parseNumber_1.default)(rawText);
(0, print_1.debugPrint)('parseNumberLiteral(..) returned, parsedNum:');
if ((0, env_1.isDebug)()) {
(0, print_1.printObject)(parsedNum);
}
// Check if number is JS special type NaN, Infinity or -Infinity.
// Note: Value with a zero (0) should pass OK!
if (parsedNum?.value !== 0 &&
(!parsedNum?.value ||
(0, number_1.isNaNValue)(parsedNum.value) ||
(0, number_1.isInfinityValue)(parsedNum.value))) {
// **************************************************
// NOTE: (!) Currently a bit unsure if to return
// option 1 or 2..!?, 2025-08-23
// Option 1.
// return makeScalarValue('Undefined', undefined, parsedNum?.tag)
// Option 2.
return undefined;
// **************************************************
}
const value = makeScalarValue('Number', parsedNum.value, parsedNum.tag);
if ((0, env_1.isDebug)()) {
console.log(' rawText = ' + rawText);
console.log('parsedNum = ' + parsedNum.value);
console.log('Number literal:');
(0, yiniHelpers_1.printLiteral)(value);
// return parseNumber(ctx.getText())
}
return value;
};
/**
* Visit a parse tree produced by `YiniParser.boolean_literal`.
* @param ctx the parse tree
* @return the visitor result
*/
// visitBoolean_literal?: (ctx: Boolean_literalContext) => Result
this.visitBoolean_literal = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitBoolean_literal(..)');
const raw = ctx.getText();
(0, print_1.debugPrint)('raw: "' + raw + '"');
// Case-insensitive true/false/on/off/yes/no (Spec section, 8.1).
return makeScalarValue('Boolean', (0, parseBoolean_1.default)(raw));
};
/**
* Visit a parse tree produced by `YiniParser.null_literal`.
* @param ctx the parse tree
* @return the visitor result
*/
// visitNull_literal?: (ctx: Null_literalContext) => Result
this.visitNull_literal = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitNull_literal(..)');
(0, print_1.debugPrint)('raw = ' + ctx.getText());
return makeScalarValue('Null', null, 'Explicit Null');
};
/**
* Visit a parse tree produced by `YiniParser.list_literal`.
* @param ctx the parse tree
* @grammarRule OB NL* elements? NL* CB NL* | EMPTY_LIST NL*
* @return the visitor result
*/
// visitList_literal?: (ctx: List_literalContext) => Result
this.visitList_literal = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitList_literal(..)');
// '[' elements? ']' ; empty_list handled by lexer (Spec section, 10.1). :contentReference[oaicite:14]{index=14}
const elems = this.visitElements(ctx.elements());
const value = makeListValue(elems, 'From bracketed list');
(0, print_1.debugPrint)('<- About to exit visitList_literal(..)...');
if ((0, env_1.isDebug)()) {
console.log('List literal:');
(0, print_1.printObject)(value);
}
return value;
};
/**
* Visit a parse tree produced by `YiniParser.elements`.
* @param ctx the parse tree
* @grammarRule value (NL* COMMA NL* value)* COMMA?
* @return the visitor result
*/
this.visitElements = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitElements(..)');
(0, print_1.debugPrint)(' elements.length = ' + ctx?.value_list().length);
const elems = !ctx?.value_list()
? []
: ctx.value_list().map((elem) => {
const valueNode = this.visitValue(elem);
if (!valueNode) {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Error', 'Invalid list element', `Invalid list element: '${elem?.getText()}'`, `Expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`);
}
return valueNode;
});
(0, print_1.debugPrint)('<- About to exit visitElements(..)');
if ((0, env_1.isDebug)()) {
console.log('Mapped value_list of elements in a list:');
(0, print_1.printObject)(elems);
}
return elems;
};
/**
* Visit a parse tree produced by `YiniParser.object_literal`.
* @param ctx the parse tree
* @grammarRule OC NL* object_members? NL* CC NL* | EMPTY_OBJECT NL*
* @return the visitor result
*/
this.visitObject_literal = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitObject_literal(..)');
// debugPrint('entries.EMPTY_OBJECT = ' + ctx?.EMPTY_OBJECT())
// debugPrint('entries.length = ' + ctx?.object_members())
// printObject(ctx)
// const entries = this.visitObject_members(ctx?.object_members())
const entries = ctx.object_members()
? this.visitObject_members(ctx.object_members())
: {};
const value = makeObjectValue(entries);
(0, print_1.debugPrint)('<- About to exit visitObject_literal(..)...');
if ((0, env_1.isDebug)()) {
console.log('Object literal:');
(0, print_1.printObject)(value);
}
return value;
};
/**
* Visit a parse tree produced by `YiniParser.object_members`.
* @param ctx the parse tree
* @grammarRule object_member (COMMA NL* object_member)* COMMA?
* @return the visitor result
*/
this.visitObject_members = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitObject_members(..)');
(0, print_1.debugPrint)('entries.length = ' + ctx?.object_member_list().length);
const entries = [];
ctx.object_member_list().forEach((member) => {
const { key, value } = this.visitObject_member(member);
(0, print_1.debugPrint)(' key = ' + key);
entries[key] = value;
});
(0, print_1.debugPrint)('<- About to exit visitObject_members(..)');
return entries;
};
/**
* Visit a parse tree produced by `YiniParser.object_member`.
* @param ctx the parse tree
* @grammarRule KEY WS? COLON NL* value
* @return the visitor result
*/
// visitObject_member?: (ctx: Object_memberContext) => Result
this.visitObject_member = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitObject_member(..)');
const rawKey = ctx.KEY().getText();
const key = (0, string_1.trimBackticks)(rawKey);
const rawValue = ctx.value().getText();
const valueNode = ctx.value()
? this.visitValue(ctx.value())
: makeScalarValue('Null', 'Implicit Null');
(0, print_1.debugPrint)(' rawKey = ' + rawKey);
(0, print_1.debugPrint)(' key = ' + key);
(0, print_1.debugPrint)('rawValue = ' + rawValue);
if (!valueNode) {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Error', 'Invalid object entry', `Invalid object entry for key '${key}'.`, `Got '${rawValue}', but expected a valid value/literal (string, number, boolean, null, list, or object). Optionally with a single leading minus sign '-'.`);
}
(0, print_1.debugPrint)('<- About to exit visitObject_member(..)');
if ((0, env_1.isDebug)()) {
console.log('Returning:');
(0, print_1.printObject)({ key, value: valueNode });
}
return { key, value: valueNode };
};
/**
* Visit a parse tree produced by `YiniParser.colon_list_decl`.
* @param ctx the parse tree
* @grammarRule KEY WS? COLON (eol | WS+)* elements (eol | WS+)* eol
* @return the visitor result
*/
// visitColon_list_decl?: (ctx: ListAfterColonContext) => Result
this.visitColon_list_decl = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitColon_list_decl(..)');
const key = ctx.getChild(0).getText();
(0, print_1.debugPrint)(`visitColon_list_decl(..): key = '${key}'`);
const elems = this.visitElements(ctx.elements());
const value = makeListValue(elems, 'From colon-list');
const current = this.sectionStack[this.sectionStack.length - 1];
// putMember(current, key, list, this.ast, this.onDuplicateKey)
this.putMember(this.errorHandler, ctx, current, key, value,
// this.ast,
this.onDuplicateKey);
(0, print_1.debugPrint)('<- About to exit visitColon_list_decl(..)...');
if ((0, env_1.isDebug)()) {
console.log('List literal: (from a Colon-list)');
(0, print_1.printObject)(value);
}
return value;
};
/**
* Visit a parse tree produced by `YiniParser.string_concat`.
* @param ctx the parse tree
* @return the visitor result
*/
// visitString_concat?: (ctx: String_concatContext) => Result
this.visitString_concat = (ctx) => {
// PLUS STRING
return trimQuotes(ctx.STRING().getText());
};
/**
* Visit a parse tree produced by `YiniParser.bad_member`.
* @param ctx the parse tree
* @return the visitor result
*/
// visitBad_member?: (ctx: Bad_memberContext) => Result
this.visitBad_member = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitBad_member(..)');
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Error', 'Invalid or malformed member (key-value pair) found.', `Offending text: ${ctx?.getText()?.trim()}`, 'Members must have the form: <key> = <value>, where <key> is a name/identifier and <value> is a value/literal.');
return null;
};
/**
* Visit a parse tree produced by `YiniParser.bad_meta_text`.
* @param ctx the parse tree
* @return the visitor result
*/
this.visitBad_meta_text = (ctx) => {
(0, print_1.debugPrint)('-> Entered visitBad_meta_text(..)');
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Error', 'Invalid or malformed directive or annotation statement', `Offending statement: ${ctx?.getText()?.trim()}`);
return null;
};
if (!errorHandler) {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
new errorDataHandler_1.ErrorDataHandler('None/Ignore').pushOrBail(null, 'Fatal-Error', 'Has no ErrorDataHandler instance when calling visitYini(..)', 'Something in the code is done incorrectly in order for this to happen... :S');
}
_sourceType = sourceType;
this.options = options;
this.errorHandler = errorHandler;
this.isStrict = options?.rules?.initialMode === 'strict';
this.onDuplicateKey = options?.rules?.onDuplicateKey ?? 'error'; // Different setting depending on mode.
// if (options.isStrict) {
// this.onDuplicateKey = 'error'
// } else {
// this.onDuplicateKey = 'warn'
// }
const root = makeSection('(root)', 0);
// this.mapSectionNamePaths.set('(root)', 0)
this.ast = {
root,
isStrict: this.isStrict,
sourceType: metaFileName ? 'File' : 'Inline',
fileName: !!metaFileName ? metaFileName : undefined,
terminatorSeen: false,
yiniMarkerSeen: false,
maxDepth: null,
numOfSections: 0,
numOfMembers: 0,
sectionNamePaths: null,
};
this.sectionStack = [root];
}
/** Attach a section to the stack respecting up/down moves (Spec 5.3). :contentReference[oaicite:7]{index=7} */
attachSection(ctx, stack, section, ast) {
const targetLevel = section.level;
const sectionName = section.sectionName;
if (targetLevel <= 0) {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Warning', `Invalid section level: ${targetLevel}`);
return;
}
// ------------------------------
// Construct section name path.
let keyPath = '';
let i = targetLevel - 1;
for (const [key, value] of Array.from(this.mapSectionNamePaths.entries()).reverse()) {
if (value === i) {
keyPath += key + '.';
break;
}
}
keyPath += sectionName; // Append current section name last.
(0, print_1.debugPrint)('section full path: keyPath = ' + keyPath);
// ------------------------------
if (this.hasDefinedSectionTitle(keyPath)) {
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(ctx, 'Syntax-Error', 'Duplicate section name', `Section name: '${sectionName}' at level ${targetLevel} is already defined and cannot be redefined.`);
}
else {
if (section.members === undefined) {
(0, print_1.debugPrint)('This sReslult does not hold any valid members (=undefined)');
}
else {
// this.existingSectionTitles.set(key, true)
this.setDefineSectionTitle(keyPath, targetLevel);
// printObject(this.existingSectionTitles)
}
}
// ------------------------------
// Ensure stack top has level targetLevel-1 (implicit root is level 0).
while (stack.length > 0 &&
stack[stack.length - 1].level >= targetLevel) {
stack.pop();
}
const parent = stack[stack.length - 1]; // root or higher-level section
parent.children.push(section);
stack.push(section);
if (targetLevel > this.meta_maxLevel) {
this.meta_maxLevel = targetLevel;
}
}
/** Insert a key/value into current section (duplicate handling per options). */
putMember(errorHandler, ctx, sec, key, value, mode = 'warn-and-keep-first') {
(0, env_1.isDebug)() && console.log();
(0, print_1.debugPrint)('-> Entered putMember(..)');
(0, print_1.debugPrint)(`putMember(..): key: '${key}', value: ${value}`);
if (sec.members.has(key)) {
switch (mode) {
case 'error':
errorHandler.pushOrBail(ctx, 'Syntax-Error', 'Hit a duplicate key in this section and scope', `Key '${key}' already exists in section '${sec.sectionName}' on level ${sec.level}.`);
break;
case 'warn-and-keep-first':
errorHandler.pushOrBail(ctx, 'Syntax-Warning', `Hit a duplicate key (will keep first value) in this section and scope`, `Key '${key}' already exists in section '${sec.sectionName}' on level ${sec.level}.`);
return; // Keep first, don't overwrite.
case 'warn-and-overwrite':
errorHandler.pushOrBail(ctx, 'Syntax-Warning', `Overwrote a duplicate key (will keep last value) in this section and scope`, `Key '${key}' was overwritten in section '${sec.sectionName}' on level ${sec.level}.`);
break; // Overwrite, replace value.
case 'keep-first':
return; // Keep first, don't overwrite.
case 'overwrite':
break; // Overwrite, replace value.
}
}
else {
this._numOfMembers++;
}
sec.members.set(key, value);
}
// --------------------------------
// Public entry
buildAST(ctx) {
this.visitYini?.(ctx);
// The document terminator is optional by default.
// If the option `isRequireDocTerminator` is set to true,
// the '/END' terminator at the end of the document becomes required.
if (!this.ast.terminatorSeen &&
this.options.rules.requireDocTerminator === 'required') {
const msgWhat = `Missing '/END' at end of document (option requireDocTerminator is ${this.options.rules.requireDocTerminator}).`;
const msgWhy = `The terminator '/END' (case insensitive) is required and must appear at the end of the document.`;
const msgHint = `This is option can be overriden by the option requireDocTerminator.`;
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(null, 'Syntax-Error', msgWhat, msgWhy, msgHint);
}
else if (!this.ast.terminatorSeen &&
this.options.rules.requireDocTerminator === 'warn-if-missing') {
const msgWhat = `Missing '/END' at end of document (option requireDocTerminator is ${this.options.rules.requireDocTerminator}).`;
const msgWhy = `The terminator '/END' (case insensitive) might be missing at the end of the document.`;
const msgHint = `This is option can be overriden by the option requireDocTerminator.`;
// Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold.
this.errorHandler.pushOrBail(null, 'Syntax-Warning', msgWhat, msgWhy, msgHint);
}
// Note: Below is important for error checking as well as for meta data.
this.ast.numOfSections = this.mapSectionNamePaths.size;
this.ast.numOfMembers = this._numOfMembers;
if (this.options.isIncludeMeta) {
// Attach collected meta information.
this.ast.maxDepth = this.meta_maxLevel;
this.ast.sectionNamePaths = [...this.mapSectionNamePaths.keys()];
}
return this.ast;
}
}
exports.default = ASTBuilder;
//# sourceMappingURL=astBuilder.js.map