• Jump To … +
    api.js attributes.js node-exports.js parser.js rule-attributes.js rule-dependencies.js sabnf-grammar.js scanner-callbacks.js scanner-grammar.js scanner.js semantic-callbacks.js show-rules.js syntax-callbacks.js web-exports.js converter.js node-exports.js transformers.js web-exports.js apg-conv.js help.js apg-exp.js exec.js flags.js parse-replacement.js replace-grammar.js replace.js result.js sabnf-generator.js split.js web-exports.js ast.js circular-buffer.js emitcss.js identifiers.js node-exports.js parser.js stats.js style.js trace.js utilities.js web-exports.js apg.js command-line.js LICENSE.md README.md index.md
  • trace.js

  • §
    /* 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 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}">&bull;</span>`;
      const PHRASE_CONTINUE = `<span class="${style.CLASS_LINEEND}">&hellip;</span>`;
      const PHRASE_EMPTY = `<span class="${style.CLASS_EMPTY}">&#120634;</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)&nbsp;-&nbsp;line number<br>\n';
        footer += '(b)&nbsp;-&nbsp;matching line number<br>\n';
        footer += '(c)&nbsp;-&nbsp;phrase offset<br>\n';
        footer += '(d)&nbsp;-&nbsp;phrase length<br>\n';
        footer += '(e)&nbsp;-&nbsp;tree depth<br>\n';
        footer += '(f)&nbsp;-&nbsp;operator state<br>\n';
        footer += `&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;<span class="${style.CLASS_ACTIVE}">&darr;</span>&nbsp;&nbsp;phrase opened<br>\n`;
        footer += `&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;<span class="${style.CLASS_MATCH}">&uarr;M</span> phrase matched<br>\n`;
        footer += `&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;<span class="${style.CLASS_EMPTY}">&uarr;E</span> empty phrase matched<br>\n`;
        footer += `&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;<span class="${style.CLASS_NOMATCH}">&uarr;N</span> phrase not matched<br>\n`;
        footer +=
          'operator&nbsp;-&nbsp;ALT, CAT, REP, RNM, TRG, TLS, TBS<sup>&dagger;</sup>, UDT, AND, NOT, BKA, BKN, BKR, ABG, AEN<sup>&Dagger;</sup><br>\n';
        footer += `phrase&nbsp;&nbsp;&nbsp;-&nbsp;up to ${MAX_PHRASE} characters of the phrase being matched<br>\n`;
        footer += `&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;<span class="${style.CLASS_MATCH}">matched characters</span><br>\n`;
        footer += `&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;<span class="${style.CLASS_LOOKAHEAD}">matched characters in look ahead mode</span><br>\n`;
        footer += `&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;<span class="${style.CLASS_LOOKBEHIND}">matched characters in look behind mode</span><br>\n`;
        footer += `&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;<span class="${style.CLASS_REMAINDER}">remainder characters(not yet examined by parser)</span><br>\n`;
        footer += `&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;<span class="${style.CLASS_CTRLCHAR}">control characters, TAB, LF, CR, etc. (ASCII mode only)</span><br>\n`;
        footer += `&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;${PHRASE_EMPTY} empty string<br>\n`;
        footer += `&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;${PHRASE_END} end of input string<br>\n`;
        footer += `&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;-&nbsp;${PHRASE_CONTINUE} input string display truncated<br>\n`;
        footer += '</p>\n';
        footer += `<p class="${style.CLASS_MONOSPACE}">\n`;
        footer += '<sup>&dagger;</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>&Dagger;</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 += `&ndash;${hex}`;
          } else {
            html = `%d${op.min.toString(10)}&ndash;${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 += `&ndash;${hex}`;
          } else if (op.max < Infinity) {
            html = `${op.min.toString(10)}&ndash;${op.max.toString(10)}`;
          } else {
            html = `${op.min.toString(10)}&ndash;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}">&darr;&nbsp;</span>`;
              break;
            case id.MATCH:
              html += `<span class="${style.CLASS_MATCH}">&uarr;M</span>`;
              break;
            case id.NOMATCH:
              html += `<span class="${style.CLASS_NOMATCH}">&uarr;N</span>`;
              break;
            case id.EMPTY:
              html += `<span class="${style.CLASS_EMPTY}">&uarr;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;
      };
    };