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
JavaScript
;
/**
* 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