apg-js-examples
Version:
Examples of using the suite of apg-js applications and libraries.
236 lines (218 loc) • 10.3 kB
JavaScript
/* eslint-disable func-names */
/* eslint-disable new-cap */
/* *************************************************************************************
* copyright: Copyright (c) 2021 Lowell D. Thomas, all rights reserved
* license: BSD-2-Clause (https://opensource.org/licenses/BSD-2-Clause)
* ********************************************************************************* */
// This is a demonstration of the bare minimum needed to set up a parser
// and parse a given input string.
module.exports = function main(args) {
/* display the program arguments */
console.log();
console.log('Demonstrate the bare minimum to set up a parser and parse an input string.');
console.log();
console.log('apg-lib args');
console.dir(args);
/* the help screen */
let desc = '';
desc += 'The "apg-lib" example is a simple demonstration using of the JavaScript APG library, apg-lib.\n';
desc += 'It is also a demonstration of the basics of creating and using a JavaScript APG parser.\n';
desc += 'A simple phone number grammar is used to parse an input phone number string.\n';
desc += 'It defines and uses callback functions and generates statistics and a trace of the parse tree.\n';
desc += 'Examine or run a debugger on this module, "apg-examples/src/apg-lib/main.js", to study the example.\n';
desc += '\n';
desc += 'The ABNF grammar is in the file phone-number.bnf. The grammar object, phone-number.js\n';
desc += 'can be regenerated with:\n';
desc += 'npm run apg -- -i src/apg-lib/phone-number.bnf -o src/apg-lib/phone-number\n';
desc += '\n';
desc += 'Note that for web page usage, the generated grammar function needs to be renamed. e.g.\n';
desc += 'npm run apg -- -i src/apg-lib/phone-number.bnf -o src/apg-lib/web-phone-number -n grammarObject\n';
desc += '\n';
let help = 'Usage: npm run simple [-- arg]\n';
help += ' or\n';
help += ' ./src/apg-lib/main.sh [arg]\n';
help += ' arg: help - (or no arg) to display this help screen.\n';
help += ' go - (or any other argument other than "help") to parse a phone number.\n';
if (!args[0]) {
/* display the description and help screen and exit */
console.log(desc);
console.log(help);
return;
}
if (args[0] === 'help') {
/* display the help screen and exit */
console.log(help);
return;
}
const nodeUtil = require('node:util');
const fs = require('node:fs');
const inspectOptions = {
showHidden: true,
depth: null,
};
const doStats = true;
const doTrace = true;
try {
// Get the apg library. The variable `apgLib` has references to all of the library objects
// and object contructors.
const { apgLib } = require('apg-js');
// Create a parser object. This gets further definition later with the
// `grammar` object, the `stats` and `trace` objects and the callback function references.
const parser = new apgLib.parser();
// The `grammar` object defines the SABNF grammar the parser will use to
// parse an input string.
// `phone-number.js` is the output of `apg` for the SABNF grammar
// defined by the `./phone-number.bnf` file.
const obj = require('./phone-number');
const grammar = new obj();
// These identifiers are used by the callback functions to identify the state of the parser
// at the time the callback function was called.
const id = apgLib.ids;
// This creates a `stats` object and attaches it to the parser. When attached,
// the parser will initialize the object and collect parsing statistics with it
// for each parse tree node it visits.
if (doStats) {
parser.stats = new apgLib.stats();
}
// This creates a `trace` object and attaches it to the parser. When attached,
// the parser will initialize the object and collect tracing records
// for each parse tree node it visits
// (see the `./src/trace` example for details on filtering the records).
if (doTrace) {
parser.trace = new apgLib.trace();
}
// The utility library in `apg-lib` has a number of utility functions that are often helpful, even essential
// for handling string, character codes, HTML display of results and other things.
const { utils } = apgLib;
// The next four variables define the parser callback functions for the rule name phrases we are interested in.
// Callback functions are optional and can be defined for all or none of the rule names
// defined by the SABNF grammar.
// Normally, these will be defined in a module of their own to keep the flow of the application clean,
// but are included here to keep things simple.
// The callback function arguments are:
// - *result* - communicates the parsing results to and from the callback functions
// (see the `parser.parse()` function in `apg-lib` for a complete description).
// Here only `result.state` and `result.phraseLength` are of interest.
// - *chars* - the array of character codes for the input string being parsed.
// - *phraseIndex* - index to the first character in *chars* of the phrase the parser is attempting to match
// - *data* - an optional user data object passed to `parser.parse()` by the user.
// For callback function use only. The parser never modifies or uses this in any way.
const phoneNumber = function (result, chars, phraseIndex, data) {
switch (result.state) {
case id.ACTIVE:
if (Array.isArray(data) === false) {
throw new Error("parser's user data must be an array");
}
data.length = 0;
break;
/* the following cases not used in this example */
case id.EMPTY:
break;
case id.MATCH:
break;
case id.NOMATCH:
break;
default:
throw new Error('unrecognized state');
}
};
const areaCode = function (result, chars, phraseIndex, data) {
if (result.state === id.MATCH) {
/* capture the area code */
data['area-code'] = utils.charsToString(chars, phraseIndex, result.phraseLength);
}
};
const office = function (result, chars, phraseIndex, data) {
if (result.state === id.MATCH) {
/* capture the 3-digit central office or exchange number */
data.office = utils.charsToString(chars, phraseIndex, result.phraseLength);
}
};
const subscriber = function (result, chars, phraseIndex, data) {
if (result.state === id.MATCH) {
/* capture the 4-digit subscriber number */
data.subscriber = utils.charsToString(chars, phraseIndex, result.phraseLength);
}
};
// Define which rules the parser will call callback functions for.
// (*NOTE: the generated grammar object,
// `phone-number.js` in this case, will have a pre-defined `callbacks` array for all rule names.*)
parser.callbacks['phone-number'] = phoneNumber;
parser.callbacks['area-code'] = areaCode;
parser.callbacks.office = office;
parser.callbacks.subscriber = subscriber;
/* use a hard-coded input string for this example */
const inputString = '(555)234-5678';
/* convert string to character codes */
const inputCharacterCodes = utils.stringToChars(inputString);
/* set the parser's "start rule" */
const startRule = 'phone-number';
/* the callback function's *data* */
const phoneParts = [];
// This is the call that will finally parse the input string.
const result = parser.parse(grammar, startRule, inputCharacterCodes, phoneParts);
/* display parser results */
console.log();
console.log("the parser's results");
console.dir(result, inspectOptions);
if (result.success === false) {
throw new Error(`input string: '${inputString}' : parse failed`);
}
/* display phone number parts, captured as matched phrases in the callback functions */
console.log();
console.log(`phone number: ${inputString}`);
console.log(` area-code: ${phoneParts['area-code']}`);
console.log(` office: ${phoneParts.office}`);
console.log(` subscriber: ${phoneParts.subscriber}`);
console.log();
const dir = `${process.cwd()}/output`;
if (doStats) {
// This section will demonstrate all of the options for the display of the
// parsing statistics. Finally, all options will be displayed
// on a single web page. See the page `html/simple-stats.html` for the results.
let html = '';
html += parser.stats.toHtml('ops', 'ops-only stats');
html += parser.stats.toHtml('index', 'rules ordered by index');
html += parser.stats.toHtml('alpha', 'rules ordered alphabetically');
html += parser.stats.toHtml('hits', 'rules ordered by hit count');
const name = `${dir}/simple-stats.html`;
try {
fs.mkdirSync(dir);
} catch (e) {
if (e.code !== 'EEXIST') {
throw new Error(`fs.mkdir failed: ${e.message}`);
}
}
html = utils.htmlToPage(html, 'simple-stats');
fs.writeFileSync(name, html);
console.log(`view "${name}" in any browser to display parsing statistics`);
}
if (doTrace) {
// This section will demonstrate the display of the
// parser's trace.
// See the page `html/simple-trace.html` for the results.
const html = parser.trace.toHtmlPage('ascii', 'good phone number, default trace');
const name = `${dir}/simple-trace.html`;
try {
fs.mkdirSync(dir);
} catch (e) {
if (e.code !== 'EEXIST') {
throw new Error(`fs.mkdir failed: ${e.message}`);
}
}
fs.writeFileSync(name, html);
console.log(`view "${name}" in any browser to display parser's trace`);
}
} 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);
}
console.log(msg);
throw e;
}
};