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.

244 lines (243 loc) 12.9 kB
"use strict"; /** * This file is the orchestrator that wires up the pipeline (lexer → parser → * astBuilder → objectBuilder, etc.) */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.runPipeline = void 0; const perf_hooks_1 = require("perf_hooks"); const antlr4_1 = require("antlr4"); const env_1 = require("../../config/env"); const YiniLexer_1 = __importDefault(require("../../grammar/generated/YiniLexer")); const YiniParser_1 = __importDefault(require("../../grammar/generated/YiniParser")); const object_1 = require("../../utils/object"); const print_1 = require("../../utils/print"); const astBuilder_1 = __importDefault(require("../astBuilder")); const errorDataHandler_1 = require("../errorDataHandler"); const objectBuilder_1 = require("../objectBuilder"); const resultMetadataBuilder_1 = require("../resultMetadataBuilder"); const errorListeners_1 = require("./errorListeners"); /** * @internal Single source of truth. * * Entrypoint for the YINI parsing pipeline: * tokenization → grammar parse → AST build → object build → result. * * @param _meta_userOpts The user options from which the core options were * derived/resolved from. This object is provided only for debugging * and metadata purposes and should not be relied upon in * application logic. */ const runPipeline = (yiniContent, coreOptions, runtimeInfo, _meta_userOpts) => { (0, print_1.debugPrint)(); (0, print_1.debugPrint)('-> Entered runPipeline(..) in pipeline.ts'); (0, print_1.debugPrint)(' isStrict initialMode = ' + coreOptions.rules.initialMode); (0, print_1.debugPrint)(' bailSensitivity = ' + coreOptions.bailSensitivity); (0, print_1.debugPrint)(' isIncludeMeta = ' + coreOptions.isIncludeMeta); (0, print_1.debugPrint)(' isWithDiagnostics = ' + coreOptions.isWithDiagnostics); (0, print_1.debugPrint)(' isWithTiming = ' + coreOptions.isWithTiming); (0, print_1.debugPrint)(' isKeepUndefinedInMeta = ' + coreOptions.isKeepUndefinedInMeta); (0, print_1.debugPrint)('isQuiet = ' + coreOptions.isQuiet); (0, print_1.debugPrint)(' onDuplicateKey = ' + coreOptions.rules.onDuplicateKey); (0, print_1.debugPrint)(' requireDocTerminator = ' + coreOptions.rules.requireDocTerminator); (0, print_1.debugPrint)(' treatEmptyValueAsNull = ' + coreOptions.rules.treatEmptyValueAsNull); (0, print_1.debugPrint)(); (0, print_1.debugPrint)(' runtimeInfo.sourceType = ' + runtimeInfo.sourceType); (0, print_1.debugPrint)(' runtimeInfo.fileName = ' + runtimeInfo.fileName); // let persistThreshold: TBailSensitivityLevel // switch (coreOptions.bailSensitivity) { // case '0-Ignore-Errors': // persistThreshold = '0-Ignore-Errors' // break // case 1: // persistThreshold = '1-Abort-on-Errors' // break // default: // persistThreshold = '2-Abort-Even-on-Warnings' // } const errorHandler = new errorDataHandler_1.ErrorDataHandler(runtimeInfo.sourceType, runtimeInfo.fileName, coreOptions.bailSensitivity, coreOptions.isQuiet, coreOptions.isSilent, coreOptions.isThrowOnError); if (yiniContent.trim() === '') { const isFileSourceType = runtimeInfo?.sourceType === 'File'; // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold. errorHandler.pushOrBail(null, 'Syntax-Error', 'Empty YINI document.', `The input is blank or contains only whitespace in the ${isFileSourceType ? 'YINI file' : 'YINI inline content'}.`, `Tip: Add at least one section '^ SectionName' or a key-value pair 'key = value' to make it a valid YINI file.`); } //--------------------------------------------- // Note: Only computed when isWithTiming. let timeStartMs = 0; let timeEnd1Ms = 0; let timeEnd2Ms = 0; let timeEnd3Ms = 0; let timeEnd4Ms = 0; //--------------------------------------------- //--------------------------------------------- // Note: Should ALWAYS be computed. let runStartedAt = ''; let runFinishedAt = ''; let durationMs = 0; //--------------------------------------------- (0, env_1.isDebug)() && console.log(); (0, print_1.debugPrint)('=== Phase 1 - Lexing ==================================================='); // ----------------------------- // Below block should always be done despite isWithTiming to compute // total time and runStartedAt that should always be computed. { timeStartMs = perf_hooks_1.performance.now(); runStartedAt = new Date().toISOString(); } // ----------------------------- const inputStream = antlr4_1.CharStreams.fromString(yiniContent); const lexer = new YiniLexer_1.default(inputStream); // Remove the default ConsoleErrorListener lexer.removeErrorListeners(); // Removes the default lexer console error output. // const lexerErrorListener = new MyLexerErrorListener(errorHandler) const lexerErrorListener = (0, errorListeners_1.createLexerErrorListener)(errorHandler); lexer.addErrorListener(lexerErrorListener); const tokenStream = new antlr4_1.CommonTokenStream(lexer); // Important: force tokenization here so lexing is measured separately. tokenStream.fill(); (0, print_1.debugPrint)('--- Parsing done. ---'); (0, print_1.debugPrint)('=== Ended phase 1 ============================================='); (0, env_1.isDebug)() && console.log(); (0, print_1.debugPrint)('=== Phase 2 - Parsing ==================================================='); if (coreOptions.isWithTiming) { timeEnd1Ms = perf_hooks_1.performance.now(); } const parser = new YiniParser_1.default(tokenStream); // const errorListener = new MyParserErrorListener(errorHandler) parser.removeErrorListeners(); // Removes the default parser console error output. // const parserErrorListener = new MyParserErrorListener(errorHandler) const parserErrorListener = (0, errorListeners_1.createParserErrorListener)(errorHandler); parser.addErrorListener(parserErrorListener); const parseTree = parser.yini(); // The function yini() is the start rule. // if ( // parserErrorListener.errors.length > 0 || // lexerErrorListener.errors.length > 0 // ) { // debugPrint('*** ERROR detected ***') // if (isDebug()) { // // Handle or display syntax errors // console.error( // 'Syntax errors detected:', // parserErrorListener.errors, // lexerErrorListener.errors, // ) // } // } (0, print_1.debugPrint)('=== Ended phase 2 ============================================='); (0, env_1.isDebug)() && console.log(); (0, print_1.debugPrint)('=== Phase 3 - AST Model build & validation ==================================================='); if (coreOptions.isWithTiming) { timeEnd2Ms = perf_hooks_1.performance.now(); } const builder = new astBuilder_1.default(errorHandler, coreOptions, runtimeInfo.sourceType, runtimeInfo.fileName || null); const ast = builder.buildAST(parseTree); if (ast.numOfMembers === 0 && ast.numOfSections === 0) { // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold. errorHandler.pushOrBail(null, 'Syntax-Error', 'No meaningful content.', `No sections or members found in the ${ast.sourceType === 'File' ? 'YINI file' : 'YINI inline content'}.`, `${ast.sourceType === 'File' ? 'A valid YINI file' : 'Any valid YINI inline content'} must contain at least one section '^ SectionName' or a key–value pair 'key = value' to make it a valid YINI file.`); } if ((0, env_1.isDebug)()) { console.log(); console.log('**************************************************************************'); console.log('*** AST *************************************************'); (0, print_1.printObject)(ast); console.log('**************************************************************************'); console.log('**************************************************************************'); console.log(); } (0, print_1.debugPrint)('=== Ended phase 3 ============================================='); (0, env_1.isDebug)() && console.log(); (0, print_1.debugPrint)('=== Phase 4 - Object Building Construction / Binding / Evaluation) ==================================================='); if (coreOptions.isWithTiming) { timeEnd3Ms = perf_hooks_1.performance.now(); } // Construct. // const finalJSResult = constructFinalObject(syntaxTreeC, errorHandler) // const finalJSResult = builder.build(parseTree) // const finalJSResult = ast //NOTE: ONLY TEMP so code runs const finalJSResult = (0, objectBuilder_1.astToObject)(ast, errorHandler); (0, print_1.debugPrint)('=== Ended phase 4 ============================================='); // ----------------------------- // Below block should always be done despite isWithTiming to compute // total time and runStartedAt that should always be computed. { timeEnd4Ms = perf_hooks_1.performance.now(); durationMs = timeEnd4Ms - timeStartMs; runFinishedAt = new Date().toISOString(); } // ----------------------------- (0, print_1.debugPrint)('visitor.visit(..): finalJSResult:'); (0, env_1.isDebug)() && console.debug(finalJSResult); (0, print_1.debugPrint)(); if (coreOptions.rules.initialMode === 'strict') { // Note, after pushing processing may continue or exit, depending on the error and/or the bail threshold. errorHandler.pushOrBail(null, 'Syntax-Warning', 'Warning: Strict initialMode is not yet fully implemented.', 'Some validation rules may still be missing or incomplete.'); if (coreOptions.bailSensitivity === '0-Ignore-Errors') { // IMPORTANT: If "silent" option is set, do not log anything to console! if (!coreOptions.isQuiet && !coreOptions.isSilent) { console.warn(`Warning: The initial mode was set to strict mode, but fail level is set to 'ignore-errors'. This combination is contradictory and might be a mistake.`); } } } else { (0, print_1.debugPrint)('visitor.visit(..): finalJSResult:'); (0, env_1.isDebug)() && console.debug(finalJSResult); } const params = { ast, coreOptions, runtimeInfo, _meta_userOpts, errorHandler, runStartedAt, runFinishedAt, durationMs, timeStartMs, timeEnd1Ms, timeEnd2Ms, timeEnd3Ms, timeEnd4Ms, }; const constructedMetadata = (0, resultMetadataBuilder_1.buildResultMetadata)(params); (0, print_1.debugPrint)('getNumOfErrors(): ' + errorHandler.getNumOfErrors()); // Print a summary line at the end if any errors or warnings. if (!coreOptions.isQuiet && !coreOptions.isSilent) { const errors = errorHandler.getNumOfErrors(); const warnings = errorHandler.getNumOfWarnings(); // Notes: // - if any errors, print to console **ERROR**. // - if no errors but warnings, print to console **WARN**. // Otherwise, adds a lot more complexity to auto testing (especially options testing), etc. // // Also, output one concise summary line (according to "best practices"). if (coreOptions.bailSensitivity !== '0-Ignore-Errors') { /* '1-Abort-on-Errors': Show summary if: errors >= 1 or warnings >= 3. '2-Abort-Even-on-Warnings': Show summary if: errors >= 1 or warnings >= 1 */ const numOfWarningsToTrigger = coreOptions.bailSensitivity === '1-Abort-on-Errors' ? 3 : 1; if (errors) { console.error(`Parsing completed with ${errors} error(s), ${warnings} warning(s). Please see details above.`); } else if (warnings >= numOfWarningsToTrigger && !coreOptions.isQuiet) { console.warn(`Parsing completed with ${errors} error(s), ${warnings} warning(s).`); } } } if (coreOptions.isIncludeMeta) { return { result: finalJSResult, meta: !coreOptions.isKeepUndefinedInMeta ? (0, object_1.removeUndefinedDeep)(constructedMetadata) : constructedMetadata, }; } return finalJSResult; }; exports.runPipeline = runPipeline; //# sourceMappingURL=pipeline.js.map