VexFlow - Copyright (c) Mohit Muthanna 2010.

A generic text parsing class for VexFlow.

import { Vex } from './vex';

To enable logging for this class. Set Vex.Flow.Parser.DEBUG to true.

function L(...args) { if (Parser.DEBUG) Vex.L('Vex.Flow.Parser', args); }

export class X extends Error {
  constructor(message, data) {
    super(message);
    this.message = message;
    this.data = data;
    this.name = 'Parser';
  }
}

Converts parser results into an easy to reference list that can be used in triggers.

function flattenMatches(results) {
  if (results.matchedString !== undefined) return results.matchedString;
  if (results.results) return flattenMatches(results.results);
  if (results.length === 1) return flattenMatches(results[0]);
  if (results.length === 0) return null;
  return results.map(flattenMatches);
}

This is the base parser class. Given an arbitrary context-free grammar, it can parse any line and execute code when specific rules are met (e.g., when a string is terminated.)

export class Parser {

For an example of a simple grammar, take a look at tests/parser_tests.js or the EasyScore grammar in easyscore.js.

  constructor(grammar) {
    this.grammar = grammar;
  }

Parse line using current grammar. Returns {success: true} if the line parsed correctly, otherwise returns {success: false, errorPos: N} where errorPos is the location of the error in the string.

  parse(line) {
    this.line = line;
    this.pos = 0;
    this.errorPos = -1;
    const results = this.expect(this.grammar.begin());
    results.errorPos = this.errorPos;
    return results;
  }

  matchFail(returnPos) {
    if (this.errorPos === -1) this.errorPos = this.pos;
    this.pos = returnPos;
  }

  matchSuccess() {
    this.errorPos = -1;
  }

Look for token in this.line[this.pos], and return success if one is found. token is specified as a regular expression.

  matchToken(token, noSpace = false) {
    const re = noSpace ? new RegExp('^((' + token + '))') : new RegExp('^((' + token + ')\\s*)');
    const workingLine = this.line.slice(this.pos);
    const result = workingLine.match(re);
    if (result !== null) {
      return {
        success: true,
        matchedString: result[2],
        incrementPos: result[1].length,
        pos: this.pos,
      };
    } else {
      return {
        success: false,
        pos: this.pos,
      };
    }
  }

Execute rule to match a sequence of tokens (or rules). If maybe is set, then return success even if the token is not found, but reset the position before exiting.

  expectOne(rule, maybe = false) {
    const results = [];
    const pos = this.pos;

    let allMatches = true;
    let oneMatch = false;
    maybe = (maybe === true) || (rule.maybe === true);

Execute all sub rules in sequence.

    for (let i = 0; i < rule.expect.length; i++) {
      const next = rule.expect[i];
      const localPos = this.pos;
      const result = this.expect(next);

If rule.or is set, then return success if any one of the subrules match, else all subrules must match.

      if (result.success) {
        results.push(result);
        oneMatch = true;
        if (rule.or) break;
      } else {
        allMatches = false;
        if (!rule.or) {
          this.pos = localPos;
          break;
        }
      }
    }

    const gotOne = (rule.or && oneMatch) || allMatches;
    const success = gotOne || (maybe === true);
    if (maybe && !gotOne) this.pos = pos;
    if (success) this.matchSuccess(); else this.matchFail(pos);
    return { success, results, numMatches: gotOne ? 1 : 0 };
  }

Try to match multiple (one or more) instances of the rule. If maybe is set, then a failed match is also a success (but the position is reset).

  expectOneOrMore(rule, maybe = false) {
    const results = [];
    const pos = this.pos;
    let numMatches = 0;
    let more = true;

    do {
      const result = this.expectOne(rule);
      if (result.success) {
        numMatches++;
        results.push(result.results);
      } else {
        more = false;
      }
    } while (more);

    const success = (numMatches > 0) || (maybe === true);
    if (maybe && !(numMatches > 0)) this.pos = pos;
    if (success) this.matchSuccess(); else this.matchFail(pos);
    return { success, results, numMatches };
  }

Match zero or more instances of rule. Offloads to expectOneOrMore.

  expectZeroOrMore(rule) {
    return this.expectOneOrMore(rule, true);
  }

Execute the rule produced by the provided the rules function. This ofloads to one of the above matchers and consolidates the results. It is also responsible for executing any code triggered by the rule (in rule.run.)

  expect(rules) {
    L('Evaluating rules:', rules);
    let result;
    if (!rules) {
      throw new X('Invalid Rule: ' + rules, rules);
    }

Get rule from Grammar class.

    const rule = rules.bind(this.grammar)();

    if (rule.token) {

Base case: parse the regex and throw an error if the line doesn’t match.

      result = this.matchToken(rule.token, (rule.noSpace === true));
      if (result.success) {

Token match! Update position and throw away parsed portion of string.

        this.pos += result.incrementPos;
      }
    } else if (rule.expect) {
      if (rule.oneOrMore) {
        result = this.expectOneOrMore(rule);
      } else if (rule.zeroOrMore) {
        result = this.expectZeroOrMore(rule);
      } else {
        result = this.expectOne(rule);
      }
    } else {
      throw new X('Bad grammar! No `token` or `expect` property', rule);
    }

If there’s a trigger attached to this rule, then pull it.

    result.matches = [];
    if (result.results) result.results.forEach(r => result.matches.push(flattenMatches(r)));
    if (rule.run && result.success) rule.run(result);
    return result;
  }
}
h