UNPKG

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
"use strict"; 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