This module provides an example of how a more “real world” parser might be built.
It parses the INI file format, commonly
used as a configuration file for many types of applications.
The grammar (ini-file.bnf) presented here includes “error productions”.
That is, rules are defined to catch errors in the configuration file format.
This way, instead of simply having the parser fail on bad input (a very frustrating user experience)
the application can provide callback functions to catch the errors and report them to the user.
To keep things modular the syntax and AST
callback functions are defined in separate modules,
parser-callbacks.js
and ast-callbacks.js
, resectively,
and “require()”ed by the application as needed.
- input - the input string to parse
- trace - if
true
display the trace
- stats - if
true
display the stats
module.exports = function setup(input, traceArg, statsArg) {
let trace = traceArg;
let stats = statsArg;
const thisFileName = 'setup.js: ';
const nodeUtil = require('node:util');
const writeHtml = require('../writeHtml');
const inspectOptions = {
showHidden: true,
depth: null,
};
const sortNames = function sortNames(obj) {
const names = [];
for (const name in obj) {
names.push(name);
}
return names.sort();
};
try {
if (typeof input !== 'string') {
throw new Error(`${thisFileName}invalid input string`);
}
if (trace === null || typeof trace !== 'object' || trace.traceObject !== 'traceObject') {
trace = null;
}
if (stats === null || typeof stats !== 'object' || stats.statsObject !== 'statsObject') {
stats = null;
}
const { apgLib } = require('apg-js');
const parser = new apgLib.parser();
const grammar = new (require('./ini-file'))();
const parserCallbacks = new (require('./parser-callbacks'))();
const astCallbacks = new (require('./ast-callbacks'))();
parser.ast = new apgLib.ast();
parser.callbacks = parserCallbacks.callbacks;
parser.ast.callbacks = astCallbacks.callbacks;
parser.trace = trace;
parser.stats = stats;
const inputCharacterCodes = apgLib.utils.stringToChars(input);
const syntaxData = {};
const result = parser.parse(grammar, 0, inputCharacterCodes, syntaxData);
console.log();
console.log("the parser's results");
console.dir(result, inspectOptions);
if (result.success !== true) {
throw new Error(`${thisFileName}parse failed`);
}
if (syntaxData.errors.length > 0) {
console.log();
console.log('syntax errors found:');
syntaxData.errors.forEach((error) => {
console.log(error);
});
}
let html;
if (parser.stats !== null) {
html = parser.stats.toHtmlPage('hits', 'rules ordered by hit count', 'IniFile Stats');
writeHtml(html, 'ini-file-stats');
}
if (parser.trace !== null) {
html = parser.trace.toHtmlPage('ascii', 'IniFile Trace', 'IniFile Trace');
writeHtml(html, 'ini-file-trace');
}
const data = {};
parser.ast.translate(data);
console.log();
console.log('alphabetized AST translation data:');
console.log();
const sectionNames = sortNames(data);
sectionNames.forEach((sectionName) => {
if (sectionName !== '0') {
console.log();
console.log(`[${sectionName}]`);
}
const keys = sortNames(data[sectionName]);
keys.forEach((keyName) => {
console.log(`${keyName}: ${data[sectionName][keyName]}`);
});
});
} catch (e) {
let msg = '\nEXCEPTION THROWN: \n';
if (e instanceof Error) {
msg += `${e.name}: ${e.message}`;
} else if (typeof e === 'string') {
msg += e;
} else {
msg += nodeUtil.inspect(e, inspectOptions);
}
process.exitCode = 1;
console.log(msg);
throw e;
}
};