buffer-apg-js
Version:
JavaScript APG, an ABNF Parser Generator
1,159 lines (1,151 loc) • 40.5 kB
JavaScript
/* eslint-disable func-names */
/* eslint-disable prefer-destructuring */
/* eslint-disable no-restricted-syntax */
/* eslint-disable guard-for-in */
/* *************************************************************************************
* copyright: Copyright (c) 2021 Lowell D. Thomas, all rights reserved
* license: BSD-2-Clause (https://opensource.org/licenses/BSD-2-Clause)
* ********************************************************************************* */
// This module provides a means of tracing the parser through the parse tree as it goes.
// It is the primary debugging facility for debugging both the SABNF grammar syntax
// and the input strings that are supposed to be valid grammar sentences.
// It is also a very informative and educational tool for understanding
// how a parser actually operates for a given language.
//
// Tracing is the process of generating and saving a record of information for each passage
// of the parser through a parse tree node. And since it traverses each node twice, once down the tree
// and once coming back up, there are two records for each node.
// This, obviously, has the potential of generating lots of records.
// And since these records are normally displayed on a web page
// it is important to have a means to limit the actual number of records generated to
// probably no more that a few thousand. This is almost always enough to find any errors.
// The problem is to get the *right* few thousand records.
// Therefore, this module has a number of ways of limiting and/or filtering, the number and type of records.
// Considerable effort has been made to make this filtering of the trace output as simple
// and intuitive as possible.
//
// However, the ability to filter the trace records, or for that matter even understand what they are
// and the information they contain, does require a minimum amount of understanding of the APG parsing
// method. The parse tree nodes are all represented by APG operators. They break down into two natural groups.
// - The `RNM` operators and `UDT` operators are named phrases.
// These are names chosen by the writer of the SABNF grammar to represent special phrases of interest.
// - All others collect, concatenate and otherwise manipulate various intermediate phrases along the way.
//
// There are separate means of filtering which of these operators in each of these two groups get traced.
// Let `trace` be an instantiated `trace.js` object.
// Prior to parsing the string, filtering the rules and UDTs can be defined as follows:
// ```
// trace.filter.rules["rulename"] = true;
// /* trace rule name "rulename" */
// trace.filter.rules["udtname"] = true;
// /* trace UDT name "udtname" */
// trace.filter.rules["<ALL>"] = true;
// /* trace all rules and UDTs (the default) */
// trace.filter.rules["<NONE>"] = true;
// /* trace no rules or UDTS */
// ```
// If any rule or UDT name other than "<ALL>" or "<NONE>" is specified, all other names are turned off.
// Therefore, to be selective of rule names, a filter statement is required for each rule/UDT name desired.
//
// Filtering of the other operators follows a similar procedure.
// ```
// trace.filter.operators["TRG"] = true;
// /* trace the terminal range, TRG, operators */
// trace.filter.operators["CAT"] = true;
// /* trace the concatenations, CAT, operators */
// trace.filter.operators["<ALL>"] = true;
// /* trace all operators */
// trace.filter.operators["<NONE>"] = true;
// /* trace no operators (the default) */
// ```
// If any operator name other than "<ALL>" or "<NONE>" is specified, all other names are turned off.
// Therefore, to be selective of operator names, a filter statement is required for each name desired.
//
// There is, additionally, a means for limiting the total number of filtered or saved trace records.
// See the function, `setMaxRecords(max)` below. This will result in only the last `max` records being saved.
//
// (See [`apg-examples`](https://github.com/ldthomas/apg-js-examples) for examples of using `trace.js`.)
module.exports = function exportTrace() {
const utils = require('./utilities');
const style = require('./style');
const circular = new (require('./circular-buffer'))();
const id = require('./identifiers');
const thisFileName = 'trace.js: ';
const that = this;
const MODE_HEX = 16;
const MODE_DEC = 10;
const MODE_ASCII = 8;
const MODE_UNICODE = 32;
const MAX_PHRASE = 80;
const MAX_TLS = 5;
const records = [];
let maxRecords = 5000;
let lastRecord = -1;
let filteredRecords = 0;
let treeDepth = 0;
const recordStack = [];
let chars = null;
let rules = null;
let udts = null;
const operatorFilter = [];
const ruleFilter = [];
/* special trace table phrases */
const PHRASE_END = `<span class="${style.CLASS_LINEEND}">•</span>`;
const PHRASE_CONTINUE = `<span class="${style.CLASS_LINEEND}">…</span>`;
const PHRASE_EMPTY = `<span class="${style.CLASS_EMPTY}">𝜺</span>`;
/* filter the non-RNM & non-UDT operators */
const initOperatorFilter = function () {
const setOperators = function (set) {
operatorFilter[id.ALT] = set;
operatorFilter[id.CAT] = set;
operatorFilter[id.REP] = set;
operatorFilter[id.TLS] = set;
operatorFilter[id.TBS] = set;
operatorFilter[id.TRG] = set;
operatorFilter[id.AND] = set;
operatorFilter[id.NOT] = set;
operatorFilter[id.BKR] = set;
operatorFilter[id.BKA] = set;
operatorFilter[id.BKN] = set;
operatorFilter[id.ABG] = set;
operatorFilter[id.AEN] = set;
};
let items = 0;
// eslint-disable-next-line no-unused-vars
for (const name in that.filter.operators) {
items += 1;
}
if (items === 0) {
/* case 1: no operators specified: default: do not trace any operators */
setOperators(false);
return;
}
for (const name in that.filter.operators) {
const upper = name.toUpperCase();
if (upper === '<ALL>') {
/* case 2: <all> operators specified: trace all operators ignore all other operator commands */
setOperators(true);
return;
}
if (upper === '<NONE>') {
/* case 3: <none> operators specified: trace NO operators ignore all other operator commands */
setOperators(false);
return;
}
}
setOperators(false);
for (const name in that.filter.operators) {
const upper = name.toUpperCase();
/* case 4: one or more individual operators specified: trace 'true' operators only */
if (upper === 'ALT') {
operatorFilter[id.ALT] = that.filter.operators[name] === true;
} else if (upper === 'CAT') {
operatorFilter[id.CAT] = that.filter.operators[name] === true;
} else if (upper === 'REP') {
operatorFilter[id.REP] = that.filter.operators[name] === true;
} else if (upper === 'AND') {
operatorFilter[id.AND] = that.filter.operators[name] === true;
} else if (upper === 'NOT') {
operatorFilter[id.NOT] = that.filter.operators[name] === true;
} else if (upper === 'TLS') {
operatorFilter[id.TLS] = that.filter.operators[name] === true;
} else if (upper === 'TBS') {
operatorFilter[id.TBS] = that.filter.operators[name] === true;
} else if (upper === 'TRG') {
operatorFilter[id.TRG] = that.filter.operators[name] === true;
} else if (upper === 'BKR') {
operatorFilter[id.BKR] = that.filter.operators[name] === true;
} else if (upper === 'BKA') {
operatorFilter[id.BKA] = that.filter.operators[name] === true;
} else if (upper === 'BKN') {
operatorFilter[id.BKN] = that.filter.operators[name] === true;
} else if (upper === 'ABG') {
operatorFilter[id.ABG] = that.filter.operators[name] === true;
} else if (upper === 'AEN') {
operatorFilter[id.AEN] = that.filter.operators[name] === true;
} else {
throw new Error(
`${thisFileName}initOpratorFilter: '${name}' not a valid operator name.` +
` Must be <all>, <none>, alt, cat, rep, tls, tbs, trg, and, not, bkr, bka or bkn`
);
}
}
};
/* filter the rule and `UDT` named operators */
const initRuleFilter = function () {
const setRules = function (set) {
operatorFilter[id.RNM] = set;
operatorFilter[id.UDT] = set;
const count = rules.length + udts.length;
ruleFilter.length = 0;
for (let i = 0; i < count; i += 1) {
ruleFilter.push(set);
}
};
let items;
let i;
const list = [];
for (i = 0; i < rules.length; i += 1) {
list.push(rules[i].lower);
}
for (i = 0; i < udts.length; i += 1) {
list.push(udts[i].lower);
}
ruleFilter.length = 0;
items = 0;
// eslint-disable-next-line no-unused-vars
for (const name in that.filter.rules) {
items += 1;
}
if (items === 0) {
/* case 1: default to all rules & udts */
setRules(true);
return;
}
for (const name in that.filter.rules) {
const lower = name.toLowerCase();
if (lower === '<all>') {
/* case 2: trace all rules ignore all other rule commands */
setRules(true);
return;
}
if (lower === '<none>') {
/* case 3: trace no rules */
setRules(false);
return;
}
}
/* case 4: trace only individually specified rules */
setRules(false);
operatorFilter[id.RNM] = true;
operatorFilter[id.UDT] = true;
for (const name in that.filter.rules) {
const lower = name.toLowerCase();
i = list.indexOf(lower);
if (i < 0) {
throw new Error(`${thisFileName}initRuleFilter: '${name}' not a valid rule or udt name`);
}
ruleFilter[i] = that.filter.rules[name] === true;
}
};
/* used by other APG components to verify that they have a valid trace object */
this.traceObject = 'traceObject';
this.filter = {
operators: [],
rules: [],
};
// Set the maximum number of records to keep (default = 5000).
// Each record number larger than `maxRecords`
// will result in deleting the previously oldest record.
// - `max`: maximum number of records to retain (default = 5000)
// - `last`: last record number to retain, (default = -1 for (unknown) actual last record)
this.setMaxRecords = function (max, last) {
lastRecord = -1;
if (typeof max === 'number' && max > 0) {
maxRecords = Math.ceil(max);
} else {
maxRecords = 0;
return;
}
if (typeof last === 'number') {
lastRecord = Math.floor(last);
if (lastRecord < 0) {
lastRecord = -1;
}
}
};
// Returns `maxRecords` to the caller.
this.getMaxRecords = function () {
return maxRecords;
};
// Returns `lastRecord` to the caller.
this.getLastRecord = function () {
return lastRecord;
};
/* Called only by the `parser.js` object. No verification of input. */
this.init = function (rulesIn, udtsIn, charsIn) {
records.length = 0;
recordStack.length = 0;
filteredRecords = 0;
treeDepth = 0;
chars = charsIn;
rules = rulesIn;
udts = udtsIn;
initOperatorFilter();
initRuleFilter();
circular.init(maxRecords);
};
/* returns true if this records passes through the designated filter, false if the record is to be skipped */
const filterOps = function (op) {
let ret = false;
if (op.type === id.RNM) {
if (operatorFilter[op.type] && ruleFilter[op.index]) {
ret = true;
} else {
ret = false;
}
} else if (op.type === id.UDT) {
if (operatorFilter[op.type] && ruleFilter[rules.length + op.index]) {
ret = true;
} else {
ret = false;
}
} else {
ret = operatorFilter[op.type];
}
return ret;
};
const filterRecords = function (record) {
if (lastRecord === -1) {
return true;
}
if (record <= lastRecord) {
return true;
}
return false;
};
/* Collect the "down" record. */
this.down = function (op, state, offset, length, anchor, lookAround) {
if (filterRecords(filteredRecords) && filterOps(op)) {
recordStack.push(filteredRecords);
records[circular.increment()] = {
dirUp: false,
depth: treeDepth,
thisLine: filteredRecords,
thatLine: undefined,
opcode: op,
state,
phraseIndex: offset,
phraseLength: length,
lookAnchor: anchor,
lookAround,
};
filteredRecords += 1;
treeDepth += 1;
}
};
/* Collect the "up" record. */
this.up = function (op, state, offset, length, anchor, lookAround) {
if (filterRecords(filteredRecords) && filterOps(op)) {
const thisLine = filteredRecords;
const thatLine = recordStack.pop();
const thatRecord = circular.getListIndex(thatLine);
if (thatRecord !== -1) {
records[thatRecord].thatLine = thisLine;
}
treeDepth -= 1;
records[circular.increment()] = {
dirUp: true,
depth: treeDepth,
thisLine,
thatLine,
opcode: op,
state,
phraseIndex: offset,
phraseLength: length,
lookAnchor: anchor,
lookAround,
};
filteredRecords += 1;
}
};
/* convert the trace records to a tree of nodes */
const toTreeObj = function () {
/* private helper functions */
function nodeOpcode(node, opcode) {
let name;
let casetype;
let modetype;
if (opcode) {
node.op = { id: opcode.type, name: utils.opcodeToString(opcode.type) };
node.opData = undefined;
switch (opcode.type) {
case id.RNM:
node.opData = rules[opcode.index].name;
break;
case id.UDT:
node.opData = udts[opcode.index].name;
break;
case id.BKR:
if (opcode.index < rules.length) {
name = rules[opcode.index].name;
} else {
name = udts[opcode.index - rules.length].name;
}
casetype = opcode.bkrCase === id.BKR_MODE_CI ? '%i' : '%s';
modetype = opcode.bkrMode === id.BKR_MODE_UM ? '%u' : '%p';
node.opData = `\\\\${casetype}${modetype}${name}`;
break;
case id.TLS:
node.opData = [];
for (let i = 0; i < opcode.string.length; i += 1) {
node.opData.push(opcode.string[i]);
}
break;
case id.TBS:
node.opData = [];
for (let i = 0; i < opcode.string.length; i += 1) {
node.opData.push(opcode.string[i]);
}
break;
case id.TRG:
node.opData = [opcode.min, opcode.max];
break;
case id.REP:
node.opData = [opcode.min, opcode.max];
break;
default:
throw new Error('unrecognized opcode');
}
} else {
node.op = { id: undefined, name: undefined };
node.opData = undefined;
}
}
function nodePhrase(state, index, length) {
if (state === id.MATCH) {
return {
index,
length,
};
}
if (state === id.NOMATCH) {
return {
index,
length: 0,
};
}
if (state === id.EMPTY) {
return {
index,
length: 0,
};
}
return null;
}
let nodeId = -1;
function nodeDown(parent, record, depth) {
const node = {
// eslint-disable-next-line no-plusplus
id: nodeId++,
branch: -1,
parent,
up: false,
down: false,
depth,
children: [],
};
if (record) {
node.down = true;
node.state = { id: record.state, name: utils.stateToString(record.state) };
node.phrase = null;
nodeOpcode(node, record.opcode);
} else {
node.state = { id: undefined, name: undefined };
node.phrase = nodePhrase();
nodeOpcode(node, undefined);
}
return node;
}
function nodeUp(node, record) {
if (record) {
node.up = true;
node.state = { id: record.state, name: utils.stateToString(record.state) };
node.phrase = nodePhrase(record.state, record.phraseIndex, record.phraseLength);
if (!node.down) {
nodeOpcode(node, record.opcode);
}
}
}
/* walk the final tree: label branches and count leaf nodes */
let leafNodes = 0;
let depth = -1;
let branchCount = 1;
function walk(node) {
depth += 1;
node.branch = branchCount;
if (depth > treeDepth) {
treeDepth = depth;
}
if (node.children.length === 0) {
leafNodes += 1;
} else {
for (let i = 0; i < node.children.length; i += 1) {
if (i > 0) {
branchCount += 1;
}
node.children[i].leftMost = false;
node.children[i].rightMost = false;
if (node.leftMost) {
node.children[i].leftMost = i === 0;
}
if (node.rightMost) {
node.children[i].rightMost = i === node.children.length - 1;
}
walk(node.children[i]);
}
}
depth -= 1;
}
function display(node, offset) {
let name;
const obj = {};
obj.id = node.id;
obj.branch = node.branch;
obj.leftMost = node.leftMost;
obj.rightMost = node.rightMost;
name = node.state.name ? node.state.name : 'ACTIVE';
obj.state = { id: node.state.id, name };
name = node.op.name ? node.op.name : '?';
obj.op = { id: node.op.id, name };
if (typeof node.opData === 'string') {
obj.opData = node.opData;
} else if (Array.isArray(node.opData)) {
obj.opData = [];
for (let i = 0; i < node.opData.length; i += 1) {
obj.opData[i] = node.opData[i];
}
} else {
obj.opData = undefined;
}
if (node.phrase) {
obj.phrase = { index: node.phrase.index, length: node.phrase.length };
} else {
obj.phrase = null;
}
obj.depth = node.depth;
obj.children = [];
for (let i = 0; i < node.children.length; i += 1) {
const c = i !== node.children.length - 1;
obj.children[i] = display(node.children[i], offset, c);
}
return obj;
}
/* construct the tree beginning here */
const branch = [];
let root;
let node;
let parent;
let record;
let firstRecord = true;
/* push a dummy node so the root node will have a non-null parent */
const dummy = nodeDown(null, null, -1);
branch.push(dummy);
node = dummy;
circular.forEach((lineIndex) => {
record = records[lineIndex];
if (firstRecord) {
firstRecord = false;
if (record.depth > 0) {
/* push some dummy nodes to fill in for missing records */
const num = record.dirUp ? record.depth + 1 : record.depth;
for (let i = 0; i < num; i += 1) {
parent = node;
node = nodeDown(node, null, i);
branch.push(node);
parent.children.push(node);
}
}
}
if (record.dirUp) {
/* handle the next record up */
node = branch.pop();
nodeUp(node, record);
node = branch[branch.length - 1];
} else {
/* handle the next record down */
parent = node;
node = nodeDown(node, record, record.depth);
branch.push(node);
parent.children.push(node);
}
});
/* if not at root, walk it up to root */
while (branch.length > 1) {
node = branch.pop();
nodeUp(node, null);
}
/* maybe redundant or paranoid tests: these should never happen */
if (dummy.children.length === 0) {
throw new Error('trace.toTree(): parse tree has no nodes');
}
if (branch.length === 0) {
throw new Error('trace.toTree(): integrity check: dummy root node disappeared?');
}
/* if no record for start rule: find the pseudo root node (first dummy node above a real node) */
root = dummy.children[0];
let prev = root;
while (root && !root.down && !root.up) {
prev = root;
root = root.children[0];
}
root = prev;
/* walk the tree of nodes: label brances and count leaves */
root.leftMost = true;
root.rightMost = true;
walk(root);
root.branch = 0;
/* generate the exported object */
const obj = {};
obj.string = [];
for (let i = 0; i < chars.length; i += 1) {
obj.string[i] = chars[i];
}
/* generate the exported rule names */
obj.rules = [];
for (let i = 0; i < rules.length; i += 1) {
obj.rules[i] = rules[i].name;
}
/* generate the exported UDT names */
obj.udts = [];
for (let i = 0; i < udts.length; i += 1) {
obj.udts[i] = udts[i].name;
}
/* generate the ids */
obj.id = {};
obj.id.ALT = { id: id.ALT, name: 'ALT' };
obj.id.CAT = { id: id.CAT, name: 'CAT' };
obj.id.REP = { id: id.REP, name: 'REP' };
obj.id.RNM = { id: id.RNM, name: 'RNM' };
obj.id.TLS = { id: id.TLS, name: 'TLS' };
obj.id.TBS = { id: id.TBS, name: 'TBS' };
obj.id.TRG = { id: id.TRG, name: 'TRG' };
obj.id.UDT = { id: id.UDT, name: 'UDT' };
obj.id.AND = { id: id.AND, name: 'AND' };
obj.id.NOT = { id: id.NOT, name: 'NOT' };
obj.id.BKR = { id: id.BKR, name: 'BKR' };
obj.id.BKA = { id: id.BKA, name: 'BKA' };
obj.id.BKN = { id: id.BKN, name: 'BKN' };
obj.id.ABG = { id: id.ABG, name: 'ABG' };
obj.id.AEN = { id: id.AEN, name: 'AEN' };
obj.id.ACTIVE = { id: id.ACTIVE, name: 'ACTIVE' };
obj.id.MATCH = { id: id.MATCH, name: 'MATCH' };
obj.id.EMPTY = { id: id.EMPTY, name: 'EMPTY' };
obj.id.NOMATCH = { id: id.NOMATCH, name: 'NOMATCH' };
/* generate the max tree depth */
obj.treeDepth = treeDepth;
/* generate the number of leaf nodes (branches) */
obj.leafNodes = leafNodes;
/* generate the types of the left- and right-most branches */
let branchesIncomplete;
if (root.down) {
if (root.up) {
branchesIncomplete = 'none';
} else {
branchesIncomplete = 'right';
}
} else if (root.up) {
branchesIncomplete = 'left';
} else {
branchesIncomplete = 'both';
}
obj.branchesIncomplete = branchesIncomplete;
obj.tree = display(root, root.depth, false);
return obj;
};
// Returns the trace records as JSON parse tree object.
// - stringify: if `true`, the object is 'stringified' before returning, otherwise, the object itself is returned.
this.toTree = function (stringify) {
const obj = toTreeObj();
if (stringify) {
return JSON.stringify(obj);
}
return obj;
};
// Translate the trace records to HTML format and create a complete HTML page for browser display.
this.toHtmlPage = function (mode, caption, title) {
return utils.htmlToPage(this.toHtml(mode, caption), title);
};
/* From here on down, these are just helper functions for `toHtml()`. */
const htmlHeader = function (mode, caption) {
/* open the page */
/* write the HTML5 header with table style */
/* open the <table> tag */
let modeName;
switch (mode) {
case MODE_HEX:
modeName = 'hexadecimal';
break;
case MODE_DEC:
modeName = 'decimal';
break;
case MODE_ASCII:
modeName = 'ASCII';
break;
case MODE_UNICODE:
modeName = 'UNICODE';
break;
default:
throw new Error(`${thisFileName}htmlHeader: unrecognized mode: ${mode}`);
}
let header = '';
header += `<p>display mode: ${modeName}</p>\n`;
header += `<table class="${style.CLASS_TRACE}">\n`;
if (typeof caption === 'string') {
header += `<caption>${caption}</caption>`;
}
return header;
};
const htmlFooter = function () {
let footer = '';
/* close the </table> tag */
footer += '</table>\n';
/* display a table legend */
footer += `<p class="${style.CLASS_MONOSPACE}">legend:<br>\n`;
footer += '(a) - line number<br>\n';
footer += '(b) - matching line number<br>\n';
footer += '(c) - phrase offset<br>\n';
footer += '(d) - phrase length<br>\n';
footer += '(e) - tree depth<br>\n';
footer += '(f) - operator state<br>\n';
footer += ` - <span class="${style.CLASS_ACTIVE}">↓</span> phrase opened<br>\n`;
footer += ` - <span class="${style.CLASS_MATCH}">↑M</span> phrase matched<br>\n`;
footer += ` - <span class="${style.CLASS_EMPTY}">↑E</span> empty phrase matched<br>\n`;
footer += ` - <span class="${style.CLASS_NOMATCH}">↑N</span> phrase not matched<br>\n`;
footer +=
'operator - ALT, CAT, REP, RNM, TRG, TLS, TBS<sup>†</sup>, UDT, AND, NOT, BKA, BKN, BKR, ABG, AEN<sup>‡</sup><br>\n';
footer += `phrase - up to ${MAX_PHRASE} characters of the phrase being matched<br>\n`;
footer += ` - <span class="${style.CLASS_MATCH}">matched characters</span><br>\n`;
footer += ` - <span class="${style.CLASS_LOOKAHEAD}">matched characters in look ahead mode</span><br>\n`;
footer += ` - <span class="${style.CLASS_LOOKBEHIND}">matched characters in look behind mode</span><br>\n`;
footer += ` - <span class="${style.CLASS_REMAINDER}">remainder characters(not yet examined by parser)</span><br>\n`;
footer += ` - <span class="${style.CLASS_CTRLCHAR}">control characters, TAB, LF, CR, etc. (ASCII mode only)</span><br>\n`;
footer += ` - ${PHRASE_EMPTY} empty string<br>\n`;
footer += ` - ${PHRASE_END} end of input string<br>\n`;
footer += ` - ${PHRASE_CONTINUE} input string display truncated<br>\n`;
footer += '</p>\n';
footer += `<p class="${style.CLASS_MONOSPACE}">\n`;
footer += '<sup>†</sup>original ABNF operators:<br>\n';
footer += 'ALT - alternation<br>\n';
footer += 'CAT - concatenation<br>\n';
footer += 'REP - repetition<br>\n';
footer += 'RNM - rule name<br>\n';
footer += 'TRG - terminal range<br>\n';
footer += 'TLS - terminal literal string (case insensitive)<br>\n';
footer += 'TBS - terminal binary string (case sensitive)<br>\n';
footer += '<br>\n';
footer += '<sup>‡</sup>super set SABNF operators:<br>\n';
footer += 'UDT - user-defined terminal<br>\n';
footer += 'AND - positive look ahead<br>\n';
footer += 'NOT - negative look ahead<br>\n';
footer += 'BKA - positive look behind<br>\n';
footer += 'BKN - negative look behind<br>\n';
footer += 'BKR - back reference<br>\n';
footer += 'ABG - anchor - begin of input string<br>\n';
footer += 'AEN - anchor - end of input string<br>\n';
footer += '</p>\n';
return footer;
};
this.indent = function (depth) {
let html = '';
for (let i = 0; i < depth; i += 1) {
html += '.';
}
return html;
};
/* format the TRG operator */
const displayTrg = function (mode, op) {
let html = '';
if (op.type === id.TRG) {
if (mode === MODE_HEX || mode === MODE_UNICODE) {
let hex = op.min.toString(16).toUpperCase();
if (hex.length % 2 !== 0) {
hex = `0${hex}`;
}
html += mode === MODE_HEX ? '%x' : 'U+';
html += hex;
hex = op.max.toString(16).toUpperCase();
if (hex.length % 2 !== 0) {
hex = `0${hex}`;
}
html += `–${hex}`;
} else {
html = `%d${op.min.toString(10)}–${op.max.toString(10)}`;
}
}
return html;
};
/* format the REP operator */
const displayRep = function (mode, op) {
let html = '';
if (op.type === id.REP) {
if (mode === MODE_HEX) {
let hex = op.min.toString(16).toUpperCase();
if (hex.length % 2 !== 0) {
hex = `0${hex}`;
}
html = `x${hex}`;
if (op.max < Infinity) {
hex = op.max.toString(16).toUpperCase();
if (hex.length % 2 !== 0) {
hex = `0${hex}`;
}
} else {
hex = 'inf';
}
html += `–${hex}`;
} else if (op.max < Infinity) {
html = `${op.min.toString(10)}–${op.max.toString(10)}`;
} else {
html = `${op.min.toString(10)}–inf`;
}
}
return html;
};
/* format the TBS operator */
const displayTbs = function (mode, op) {
let html = '';
if (op.type === id.TBS) {
const len = Math.min(op.string.length, MAX_TLS * 2);
if (mode === MODE_HEX || mode === MODE_UNICODE) {
html += mode === MODE_HEX ? '%x' : 'U+';
for (let i = 0; i < len; i += 1) {
let hex;
if (i > 0) {
html += '.';
}
hex = op.string[i].toString(16).toUpperCase();
if (hex.length % 2 !== 0) {
hex = `0${hex}`;
}
html += hex;
}
} else {
html = '%d';
for (let i = 0; i < len; i += 1) {
if (i > 0) {
html += '.';
}
html += op.string[i].toString(10);
}
}
if (len < op.string.length) {
html += PHRASE_CONTINUE;
}
}
return html;
};
/* format the TLS operator */
const displayTls = function (mode, op) {
let html = '';
if (op.type === id.TLS) {
const len = Math.min(op.string.length, MAX_TLS);
if (mode === MODE_HEX || mode === MODE_DEC) {
let charu;
let charl;
let base;
if (mode === MODE_HEX) {
html = '%x';
base = 16;
} else {
html = '%d';
base = 10;
}
for (let i = 0; i < len; i += 1) {
if (i > 0) {
html += '.';
}
charl = op.string[i];
if (charl >= 97 && charl <= 122) {
charu = charl - 32;
html += `${charu.toString(base)}/${charl.toString(base)}`.toUpperCase();
} else if (charl >= 65 && charl <= 90) {
charu = charl;
charl += 32;
html += `${charu.toString(base)}/${charl.toString(base)}`.toUpperCase();
} else {
html += charl.toString(base).toUpperCase();
}
}
if (len < op.string.length) {
html += PHRASE_CONTINUE;
}
} else {
html = '"';
for (let i = 0; i < len; i += 1) {
html += utils.asciiChars[op.string[i]];
}
if (len < op.string.length) {
html += PHRASE_CONTINUE;
}
html += '"';
}
}
return html;
};
const subPhrase = function (mode, charsArg, index, length, prev) {
if (length === 0) {
return '';
}
let phrase = '';
const comma = prev ? ',' : '';
switch (mode) {
case MODE_HEX:
phrase = comma + utils.charsToHex(charsArg, index, length);
break;
case MODE_DEC:
if (prev) {
return `,${utils.charsToDec(charsArg, index, length)}`;
}
phrase = comma + utils.charsToDec(charsArg, index, length);
break;
case MODE_UNICODE:
phrase = utils.charsToUnicode(charsArg, index, length);
break;
case MODE_ASCII:
default:
phrase = utils.charsToAsciiHtml(charsArg, index, length);
break;
}
return phrase;
};
/* display phrases matched in look-behind mode */
const displayBehind = function (mode, charsArg, state, index, length, anchor) {
let html = '';
let beg1;
let len1;
let beg2;
let len2;
let lastchar = PHRASE_END;
const spanBehind = `<span class="${style.CLASS_LOOKBEHIND}">`;
const spanRemainder = `<span class="${style.CLASS_REMAINDER}">`;
const spanend = '</span>';
let prev = false;
switch (state) {
case id.EMPTY:
html += PHRASE_EMPTY;
/* // eslint-disable-next-line no-fallthrough */
case id.NOMATCH:
case id.MATCH:
case id.ACTIVE:
beg1 = index - length;
len1 = anchor - beg1;
beg2 = anchor;
len2 = charsArg.length - beg2;
break;
default:
throw new Error('unrecognized state');
}
lastchar = PHRASE_END;
if (len1 > MAX_PHRASE) {
len1 = MAX_PHRASE;
lastchar = PHRASE_CONTINUE;
len2 = 0;
} else if (len1 + len2 > MAX_PHRASE) {
lastchar = PHRASE_CONTINUE;
len2 = MAX_PHRASE - len1;
}
if (len1 > 0) {
html += spanBehind;
html += subPhrase(mode, charsArg, beg1, len1, prev);
html += spanend;
prev = true;
}
if (len2 > 0) {
html += spanRemainder;
html += subPhrase(mode, charsArg, beg2, len2, prev);
html += spanend;
}
return html + lastchar;
};
const displayForward = function (mode, charsArg, state, index, length, spanAhead) {
let html = '';
let beg1;
let len1;
let beg2;
let len2;
let lastchar = PHRASE_END;
const spanRemainder = `<span class="${style.CLASS_REMAINDER}">`;
const spanend = '</span>';
let prev = false;
switch (state) {
case id.EMPTY:
html += PHRASE_EMPTY;
/* // eslint-disable-next-line no-fallthrough */
case id.NOMATCH:
case id.ACTIVE:
beg1 = index;
len1 = 0;
beg2 = index;
len2 = charsArg.length - beg2;
break;
case id.MATCH:
beg1 = index;
len1 = length;
beg2 = index + len1;
len2 = charsArg.length - beg2;
break;
default:
throw new Error('unrecognized state');
}
lastchar = PHRASE_END;
if (len1 > MAX_PHRASE) {
len1 = MAX_PHRASE;
lastchar = PHRASE_CONTINUE;
len2 = 0;
} else if (len1 + len2 > MAX_PHRASE) {
lastchar = PHRASE_CONTINUE;
len2 = MAX_PHRASE - len1;
}
if (len1 > 0) {
html += spanAhead;
html += subPhrase(mode, charsArg, beg1, len1, prev);
html += spanend;
prev = true;
}
if (len2 > 0) {
html += spanRemainder;
html += subPhrase(mode, charsArg, beg2, len2, prev);
html += spanend;
}
return html + lastchar;
};
/* display phrases matched in look-ahead mode */
const displayAhead = function (mode, charsArg, state, index, length) {
const spanAhead = `<span class="${style.CLASS_LOOKAHEAD}">`;
return displayForward(mode, charsArg, state, index, length, spanAhead);
};
/* display phrases matched in normal parsing mode */
const displayNone = function (mode, charsArg, state, index, length) {
const spanAhead = `<span class="${style.CLASS_MATCH}">`;
return displayForward(mode, charsArg, state, index, length, spanAhead);
};
/* Returns the filtered records, formatted as an HTML table. */
const htmlTable = function (mode) {
if (rules === null) {
return '';
}
let html = '';
let thisLine;
let thatLine;
let lookAhead;
let lookBehind;
let lookAround;
let anchor;
html += '<tr><th>(a)</th><th>(b)</th><th>(c)</th><th>(d)</th><th>(e)</th><th>(f)</th>';
html += '<th>operator</th><th>phrase</th></tr>\n';
circular.forEach((lineIndex) => {
const line = records[lineIndex];
thisLine = line.thisLine;
thatLine = line.thatLine !== undefined ? line.thatLine : '--';
lookAhead = false;
lookBehind = false;
lookAround = false;
if (line.lookAround === id.LOOKAROUND_AHEAD) {
lookAhead = true;
lookAround = true;
anchor = line.lookAnchor;
}
if (line.opcode.type === id.AND || line.opcode.type === id.NOT) {
lookAhead = true;
lookAround = true;
anchor = line.phraseIndex;
}
if (line.lookAround === id.LOOKAROUND_BEHIND) {
lookBehind = true;
lookAround = true;
anchor = line.lookAnchor;
}
if (line.opcode.type === id.BKA || line.opcode.type === id.BKN) {
lookBehind = true;
lookAround = true;
anchor = line.phraseIndex;
}
html += '<tr>';
html += `<td>${thisLine}</td><td>${thatLine}</td>`;
html += `<td>${line.phraseIndex}</td>`;
html += `<td>${line.phraseLength}</td>`;
html += `<td>${line.depth}</td>`;
html += '<td>';
switch (line.state) {
case id.ACTIVE:
html += `<span class="${style.CLASS_ACTIVE}">↓ </span>`;
break;
case id.MATCH:
html += `<span class="${style.CLASS_MATCH}">↑M</span>`;
break;
case id.NOMATCH:
html += `<span class="${style.CLASS_NOMATCH}">↑N</span>`;
break;
case id.EMPTY:
html += `<span class="${style.CLASS_EMPTY}">↑E</span>`;
break;
default:
html += `<span class="${style.CLASS_ACTIVE}">--</span>`;
break;
}
html += '</td>';
html += '<td>';
html += that.indent(line.depth);
if (lookAhead) {
html += `<span class="${style.CLASS_LOOKAHEAD}">`;
} else if (lookBehind) {
html += `<span class="${style.CLASS_LOOKBEHIND}">`;
}
html += utils.opcodeToString(line.opcode.type);
if (line.opcode.type === id.RNM) {
html += `(${rules[line.opcode.index].name}) `;
}
if (line.opcode.type === id.BKR) {
const casetype = line.opcode.bkrCase === id.BKR_MODE_CI ? '%i' : '%s';
const modetype = line.opcode.bkrMode === id.BKR_MODE_UM ? '%u' : '%p';
html += `(\\${casetype}${modetype}${rules[line.opcode.index].name}) `;
}
if (line.opcode.type === id.UDT) {
html += `(${udts[line.opcode.index].name}) `;
}
if (line.opcode.type === id.TRG) {
html += `(${displayTrg(mode, line.opcode)}) `;
}
if (line.opcode.type === id.TBS) {
html += `(${displayTbs(mode, line.opcode)}) `;
}
if (line.opcode.type === id.TLS) {
html += `(${displayTls(mode, line.opcode)}) `;
}
if (line.opcode.type === id.REP) {
html += `(${displayRep(mode, line.opcode)}) `;
}
if (lookAround) {
html += '</span>';
}
html += '</td>';
html += '<td>';
if (lookBehind) {
html += displayBehind(mode, chars, line.state, line.phraseIndex, line.phraseLength, anchor);
} else if (lookAhead) {
html += displayAhead(mode, chars, line.state, line.phraseIndex, line.phraseLength);
} else {
html += displayNone(mode, chars, line.state, line.phraseIndex, line.phraseLength);
}
html += '</td></tr>\n';
});
html += '<tr><th>(a)</th><th>(b)</th><th>(c)</th><th>(d)</th><th>(e)</th><th>(f)</th>';
html += '<th>operator</th><th>phrase</th></tr>\n';
html += '</table>\n';
return html;
};
// Translate the trace records to HTML format.
// - *modearg* - can be `"ascii"`, `"decimal"`, `"hexadecimal"` or `"unicode"`.
// Determines the format of the string character code display.
// - *caption* - optional caption for the HTML table.
this.toHtml = function (modearg, caption) {
/* writes the trace records as a table in a complete html page */
let mode = MODE_ASCII;
if (typeof modearg === 'string' && modearg.length >= 3) {
const modein = modearg.toLowerCase().slice(0, 3);
if (modein === 'hex') {
mode = MODE_HEX;
} else if (modein === 'dec') {
mode = MODE_DEC;
} else if (modein === 'uni') {
mode = MODE_UNICODE;
}
}
let html = '';
html += htmlHeader(mode, caption);
html += htmlTable(mode);
html += htmlFooter();
return html;
};
};