VexFlow - Copyright (c) Mohit Muthanna 2010.

This class implements a parser for a simple language to generate VexFlow objects.

import { Vex } from './vex';
import { StaveNote } from './stavenote';
import { Parser } from './parser';

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

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

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

function unquote(str) { return str.slice(1, -1); }

class Grammar {
  constructor(builder) {
    this.builder = builder;
  }

  begin() { return this.LINE; }

Notation grammar for EasyScore.

  LINE()   {
    return { expect: [this.PIECE, this.PIECES, this.EOL] };
  }
  PIECE()  {
    return { expect: [this.CHORDORNOTE, this.PARAMS],
             run: () => this.builder.commitPiece() };
  }
  PIECES() {
    return { expect: [this.COMMA, this.PIECE], zeroOrMore: true };
  }
  PARAMS()  {
    return { expect: [this.DURATION, this.TYPE, this.DOTS, this.OPTS] };
  }
  CHORDORNOTE() {
    return { expect: [this.CHORD, this.SINGLENOTE], or: true };
  }
  CHORD()  {
    return { expect: [this.LPAREN, this.NOTES, this.RPAREN],
             run: (state) => this.builder.addChord(state.matches[1]) };
  }
  NOTES()  {
    return { expect: [this.NOTE], oneOrMore: true };
  }
  NOTE()   {
    return { expect: [this.NOTENAME, this.ACCIDENTAL, this.OCTAVE] };
  }
  SINGLENOTE()   {
    return { expect: [this.NOTENAME, this.ACCIDENTAL, this.OCTAVE],
             run: (state) => this.builder.addSingleNote(
               state.matches[0], state.matches[1], state.matches[2]) };
  }
  ACCIDENTAL() {
    return { expect: [this.ACCIDENTALS], maybe: true };
  }
  DOTS()   {
    return { expect: [this.DOT], zeroOrMore: true,
             run: (state) => this.builder.setNoteDots(state.matches[0]) };
  }
  TYPE()   {
    return { expect: [this.SLASH, this.MAYBESLASH, this.TYPES], maybe: true,
             run: (state) => this.builder.setNoteType(state.matches[2]) };
  }
  DURATION()   {
    return { expect: [this.SLASH, this.DURATIONS], maybe: true,
             run: (state) => this.builder.setNoteDuration(state.matches[1]) };
  }

Options can be key=value pairs, with single or double quoted values.

  OPTS() {
    return { expect: [this.LBRACKET, this.KEYVAL, this.KEYVALS, this.RBRACKET], maybe: true };
  }
  KEYVALS() { return { expect: [this.COMMA, this.KEYVAL], zeroOrMore: true }; }
  KEYVAL()  {
    return { expect: [this.KEY, this.EQUALS, this.VAL],
             run: (state) => this.builder.addNoteOption(
               state.matches[0], unquote(state.matches[2])) };
  }
  KEY()     { return { token: '[a-zA-Z][a-zA-Z0-9]*' }; }
  VAL()     { return { expect: [this.SVAL, this.DVAL], or: true }; }
  DVAL()    { return { token: '["][^"]*["]' }; }
  SVAL()    { return { token: "['][^']*[']" }; }

Valid notational symbols.

  NOTENAME() { return { token: '[a-gA-G]' }; }
  OCTAVE()   { return { token: '[0-9]+' }; }
  ACCIDENTALS() { return { token: '[b#n]+' }; }
  DURATIONS() { return { token: '[0-9whq]+' }; }
  TYPES() { return { token: '[rRsSxX]' }; }

Raw tokens used by grammar.

  LPAREN()   { return { token: '[(]' }; }
  RPAREN()   { return { token: '[)]' }; }
  COMMA()    { return { token: '[,]' }; }
  DOT()      { return { token: '[.]' }; }
  SLASH()    { return { token: '[/]' }; }
  MAYBESLASH()    { return { token: '[/]?' }; }
  EQUALS()   { return { token: '[=]' }; }
  LBRACKET() { return { token: '\\[' }; }
  RBRACKET() { return { token: '\\]' }; }
  EOL()      { return { token: '$' }; }
}

class Builder {
  constructor(factory) {
    this.factory = factory;
    this.reset();
  }

  reset(options = {}) {
    this.options = {
      stem: 'auto',
    };
    this.elements = {
      notes: [],
      accidentals: [],
    };
    this.resetPiece();
    Object.assign(this.options, options);
  }

  getElements() { return this.elements; }

  resetPiece() {
    this.piece = {
      chord: [],
      duration: '8',
      dots: 0,
      type: undefined,
      options: {},
    };
  }

  setNoteDots(dots) {
    L('setNoteDots:', dots);
    if (dots) this.piece.dots = dots.length;
  }

  setNoteDuration(duration) {
    L('setNoteDuration:', duration);
    if (duration) this.piece.duration = duration;
  }

  setNoteType(type) {
    L('setNoteType:', type);
    if (type) this.piece.type = type;
  }

  addNoteOption(key, value) {
    L('addNoteOption: key:', key, 'value:', value);
    this.piece.options[key] = value;
  }

  addNote(key, acc, octave) {
    L('addNote:', key, acc, octave);
    this.piece.chord.push({ key, acc, octave });
  }

  addSingleNote(key, acc, octave) {
    L('addSingleNote:', key, acc, octave);
    this.addNote(key, acc, octave);
  }

  addChord(notes) {
    L('startChord');
    if (typeof(notes[0]) !== 'object') {
      this.addSingleNote(notes[0]);
    } else {
      notes.forEach(n => {
        if (n) this.addNote(n[0], n[1], n[2]);
      });
    }
    L('endChord');
  }

  commitPiece() {
    L('commitPiece');
    if (!this.factory) return;

    const autoStem = this.options.stem.toLowerCase() === 'auto';
    const stemDirection = !autoStem &&
      (this.options.stem.toLowerCase() === 'up') ? StaveNote.STEM_UP : StaveNote.STEM_DOWN;

Build the note.

    const keys = this.piece.chord.map(note => note.key + '/' + note.octave);
    const accs = this.piece.chord.map(note => note.acc || null);
    const note = this.factory.StaveNote(
      { keys,
        duration: this.piece.duration,
        dots: this.piece.dots,
        auto_stem: autoStem,
        type: this.piece.type });

    if (!autoStem) note.setStemDirection(stemDirection);

    for (let i = 0; i < this.piece.dots; i++) note.addDotToAll();
    this.elements.notes.push(note);

Build and attach the accidentals.

    accs.forEach((acc, i) => {
      if (acc) note.addAccidental(i, this.factory.Accidental({ type: acc }));
    });
    this.elements.accidentals.concat(accs);

Set attributes. this.piece.options.forEach(o => note.setAttribute(o[0], o[1]));

    this.resetPiece();
  }
}

export class EasyScore {
  constructor(options = {}) {
    this.setOptions(options);
  }

  setOptions(options) {
    this.options = Object.assign({
      factory: null,
      builder: null,
    }, options);

    this.factory = this.options.factory;
    this.builder = this.options.builder || new Builder(this.factory);
    this.grammar = new Grammar(this.builder);
    this.parser = new Parser(this.grammar);
  }

  setContext(context) {
    if (this.factory) this.factory.setContext(context);
    return this;
  }

  parse(line, options = {}) {
    this.builder.reset(options);
    return this.parser.parse(line);
  }

  beam(notes, options = {}) {
    this.factory.Beam({ notes, options });
    return notes;
  }

  tuplet(notes, options = {}) {
    this.factory.Tuplet({ notes, options });
    return notes;
  }

  notes(line, options = {}) {
    this.parse(line, options);
    return this.builder.getElements().notes;
  }

  voice(notes, options = { voiceOptions: null }) {
    return this.factory.Voice(options.voiceOptions).addTickables(notes);
  }
}
h