VexFlow - Copyright (c) Mohit Muthanna 2010.

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

/* eslint max-classes-per-file: "off" */

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

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 const X = Vex.MakeException('EasyScoreError');

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

  begin() { return this.LINE; }

  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]),
    };
  }
  OPTS() {
    return {
      expect: [this.LBRACKET, this.KEYVAL, this.KEYVALS, this.RBRACKET],
      maybe: true,
    };
  }
  KEYVALS() {
    return {
      expect: [this.COMMA, this.KEYVAL],
      zeroOrMore: true,
    };
  }
  KEYVAL() {
    const unquote = (str) => str.slice(1, -1);

    return {
      expect: [this.KEY, this.EQUALS, this.VAL],
      run: (state) => this.builder.addNoteOption(state.matches[0], unquote(state.matches[2])),
    };
  }
  VAL() {
    return {
      expect: [this.SVAL, this.DVAL],
      or: true,
    };
  }

  KEY() { return { token: '[a-zA-Z][a-zA-Z0-9]*' }; }
  DVAL() { return { token: '["][^"]*["]' }; }
  SVAL() { return { token: "['][^']*[']" }; }
  NOTENAME() { return { token: '[a-gA-G]' }; }
  OCTAVE() { return { token: '[0-9]+' }; }
  ACCIDENTALS() { return { token: 'bbs|bb|bss|bs|b|db|d|##|#|n|\\+\\+-|\\+-|\\+\\+|\\+|k|o' }; }
  DURATIONS() { return { token: '[0-9whq]+' }; }
  TYPES() { return { token: '[rRsSxX]' }; }
  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.commitHooks = [];
    this.reset();
  }

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

  getFactory() { return this.factory; }

  getElements() { return this.elements; }

  addCommitHook(commitHook) {
    this.commitHooks.push(commitHook);
  }

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

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

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

  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, accid, octave) {
    L('addNote:', key, accid, octave);
    this.piece.chord.push({ key, accid, octave });
  }

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

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

  commitPiece() {
    L('commitPiece');
    const { factory } = this;

    if (!factory) return;

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

Build StaveNotes.

    const { chord, duration, dots, type } = this.piece;
    const keys = chord.map(note => note.key + '/' + note.octave);
    const note = factory.StaveNote({
      keys,
      duration,
      dots,
      type,
      clef,
      auto_stem: autoStem,
    });
    if (!autoStem) note.setStemDirection(stemDirection);

Attach accidentals.

    const accids = chord.map(note => note.accid || null);
    accids.forEach((accid, i) => {
      if (accid) note.addAccidental(i, factory.Accidental({ type: accid }));
    });

Attach dots.

    for (let i = 0; i < dots; i++) note.addDotToAll();

    this.commitHooks.forEach(fn => fn(options, note, this));

    this.elements.notes.push(note);
    this.elements.accidentals.concat(accids);
    this.resetPiece();
  }
}

function setId({ id }, note) {
  if (id === undefined) return;

  note.setAttribute('id', id);
}


function setClass(options, note) {
  if (!options.class) return;

  const commaSeparatedRegex = /\s*,\s*/;

  options.class
    .split(commaSeparatedRegex)
    .forEach(className => note.addClass(className));
}

export class EasyScore {
  constructor(options = {}) {
    this.setOptions(options);
    this.defaults = {
      clef: 'treble',
      time: '4/4',
      stem: 'auto',
    };
  }

  set(defaults) {
    Object.assign(this.defaults, defaults);
    return this;
  }

  setOptions(options) {
    this.options = {
      factory: null,
      builder: null,
      commitHooks: [
        setId,
        setClass,
        Articulation.easyScoreHook,
      ],
      throwOnError: false, ...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);
    this.options.commitHooks.forEach(commitHook => this.addCommitHook(commitHook));
    return this;
  }

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

  parse(line, options = {}) {
    this.builder.reset(options);
    const result = this.parser.parse(line);
    if (!result.success && this.options.throwOnError) {
      throw new X('Error parsing line: ' + line, result);
    }
    return result;
  }

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

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

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

  voice(notes, voiceOptions) {
    voiceOptions = { time: this.defaults.time, ...voiceOptions };
    return this.factory.Voice(voiceOptions).addTickables(notes);
  }

  addCommitHook(commitHook) {
    return this.builder.addCommitHook(commitHook);
  }
}
h