UNPKG

abcjs

Version:

Renderer for abc music notation

1,231 lines (1,191 loc) 48.1 kB
var parseCommon = require('./abc_common'); var parseDirective = {}; (function() { "use strict"; var tokenizer; var warn; var multilineVars; var tune; var tuneBuilder; parseDirective.initialize = function(tokenizer_, warn_, multilineVars_, tune_, tuneBuilder_) { tokenizer = tokenizer_; warn = warn_; multilineVars = multilineVars_; tune = tune_; tuneBuilder = tuneBuilder_; initializeFonts(); }; function initializeFonts() { multilineVars.annotationfont = { face: "Helvetica", size: 12, weight: "normal", style: "normal", decoration: "none" }; multilineVars.gchordfont = { face: "Helvetica", size: 12, weight: "normal", style: "normal", decoration: "none" }; multilineVars.historyfont = { face: "\"Times New Roman\"", size: 16, weight: "normal", style: "normal", decoration: "none" }; multilineVars.infofont = { face: "\"Times New Roman\"", size: 14, weight: "normal", style: "italic", decoration: "none" }; multilineVars.measurefont = { face: "\"Times New Roman\"", size: 14, weight: "normal", style: "italic", decoration: "none" }; multilineVars.partsfont = { face: "\"Times New Roman\"", size: 15, weight: "normal", style: "normal", decoration: "none" }; multilineVars.repeatfont = { face: "\"Times New Roman\"", size: 13, weight: "normal", style: "normal", decoration: "none" }; multilineVars.textfont = { face: "\"Times New Roman\"", size: 16, weight: "normal", style: "normal", decoration: "none" }; multilineVars.tripletfont = {face: "Times", size: 11, weight: "normal", style: "italic", decoration: "none"}; multilineVars.vocalfont = { face: "\"Times New Roman\"", size: 13, weight: "bold", style: "normal", decoration: "none" }; multilineVars.wordsfont = { face: "\"Times New Roman\"", size: 16, weight: "normal", style: "normal", decoration: "none" }; // These fonts are global for the entire tune. tune.formatting.composerfont = { face: "\"Times New Roman\"", size: 14, weight: "normal", style: "italic", decoration: "none" }; tune.formatting.subtitlefont = { face: "\"Times New Roman\"", size: 16, weight: "normal", style: "normal", decoration: "none" }; tune.formatting.tempofont = { face: "\"Times New Roman\"", size: 15, weight: "bold", style: "normal", decoration: "none" }; tune.formatting.titlefont = { face: "\"Times New Roman\"", size: 20, weight: "normal", style: "normal", decoration: "none" }; tune.formatting.footerfont = { face: "\"Times New Roman\"", size: 12, weight: "normal", style: "normal", decoration: "none" }; tune.formatting.headerfont = { face: "\"Times New Roman\"", size: 12, weight: "normal", style: "normal", decoration: "none" }; tune.formatting.voicefont = { face: "\"Times New Roman\"", size: 13, weight: "bold", style: "normal", decoration: "none" }; tune.formatting.tablabelfont = { face: "\"Trebuchet MS\"", size: 16, weight: "normal", style: "normal", decoration: "none" }; tune.formatting.tabnumberfont = { face: "\"Arial\"", size: 11, weight: "normal", style: "normal", decoration: "none" }; tune.formatting.tabgracefont = { face: "\"Arial\"", size: 8, weight: "normal", style: "normal", decoration: "none" }; // these are the default fonts for these element types. In the printer, these fonts might change as the tune progresses. tune.formatting.annotationfont = multilineVars.annotationfont; tune.formatting.gchordfont = multilineVars.gchordfont; tune.formatting.historyfont = multilineVars.historyfont; tune.formatting.infofont = multilineVars.infofont; tune.formatting.measurefont = multilineVars.measurefont; tune.formatting.partsfont = multilineVars.partsfont; tune.formatting.repeatfont = multilineVars.repeatfont; tune.formatting.textfont = multilineVars.textfont; tune.formatting.tripletfont = multilineVars.tripletfont; tune.formatting.vocalfont = multilineVars.vocalfont; tune.formatting.wordsfont = multilineVars.wordsfont; } var fontTypeCanHaveBox = { gchordfont: true, measurefont: true, partsfont: true, annotationfont: true, composerfont: true, historyfont: true, infofont: true, subtitlefont: true, textfont: true, titlefont: true, voicefont: true }; var fontTranslation = function(fontFace) { // This translates Postscript fonts for a web alternative. // Note that the postscript fonts contain italic and bold info in them, so what is returned is a hash. switch (fontFace) { case "Arial-Italic": return { face: "Arial", weight: "normal", style: "italic", decoration: "none" }; case "Arial-Bold": return { face: "Arial", weight: "bold", style: "normal", decoration: "none" }; case "Bookman-Demi": return { face: "Bookman,serif", weight: "bold", style: "normal", decoration: "none" }; case "Bookman-DemiItalic": return { face: "Bookman,serif", weight: "bold", style: "italic", decoration: "none" }; case "Bookman-Light": return { face: "Bookman,serif", weight: "normal", style: "normal", decoration: "none" }; case "Bookman-LightItalic": return { face: "Bookman,serif", weight: "normal", style: "italic", decoration: "none" }; case "Courier": return { face: "\"Courier New\"", weight: "normal", style: "normal", decoration: "none" }; case "Courier-Oblique": return { face: "\"Courier New\"", weight: "normal", style: "italic", decoration: "none" }; case "Courier-Bold": return { face: "\"Courier New\"", weight: "bold", style: "normal", decoration: "none" }; case "Courier-BoldOblique": return { face: "\"Courier New\"", weight: "bold", style: "italic", decoration: "none" }; case "AvantGarde-Book": return { face: "AvantGarde,Arial", weight: "normal", style: "normal", decoration: "none" }; case "AvantGarde-BookOblique": return { face: "AvantGarde,Arial", weight: "normal", style: "italic", decoration: "none" }; case "AvantGarde-Demi": case "Avant-Garde-Demi": return { face: "AvantGarde,Arial", weight: "bold", style: "normal", decoration: "none" }; case "AvantGarde-DemiOblique": return { face: "AvantGarde,Arial", weight: "bold", style: "italic", decoration: "none" }; case "Helvetica-Oblique": return { face: "Helvetica", weight: "normal", style: "italic", decoration: "none" }; case "Helvetica-Bold": return { face: "Helvetica", weight: "bold", style: "normal", decoration: "none" }; case "Helvetica-BoldOblique": return { face: "Helvetica", weight: "bold", style: "italic", decoration: "none" }; case "Helvetica-Narrow": return { face: "\"Helvetica Narrow\",Helvetica", weight: "normal", style: "normal", decoration: "none" }; case "Helvetica-Narrow-Oblique": return { face: "\"Helvetica Narrow\",Helvetica", weight: "normal", style: "italic", decoration: "none" }; case "Helvetica-Narrow-Bold": return { face: "\"Helvetica Narrow\",Helvetica", weight: "bold", style: "normal", decoration: "none" }; case "Helvetica-Narrow-BoldOblique": return { face: "\"Helvetica Narrow\",Helvetica", weight: "bold", style: "italic", decoration: "none" }; case "Palatino-Roman": return { face: "Palatino", weight: "normal", style: "normal", decoration: "none" }; case "Palatino-Italic": return { face: "Palatino", weight: "normal", style: "italic", decoration: "none" }; case "Palatino-Bold": return { face: "Palatino", weight: "bold", style: "normal", decoration: "none" }; case "Palatino-BoldItalic": return { face: "Palatino", weight: "bold", style: "italic", decoration: "none" }; case "NewCenturySchlbk-Roman": return { face: "\"New Century\",serif", weight: "normal", style: "normal", decoration: "none" }; case "NewCenturySchlbk-Italic": return { face: "\"New Century\",serif", weight: "normal", style: "italic", decoration: "none" }; case "NewCenturySchlbk-Bold": return { face: "\"New Century\",serif", weight: "bold", style: "normal", decoration: "none" }; case "NewCenturySchlbk-BoldItalic": return { face: "\"New Century\",serif", weight: "bold", style: "italic", decoration: "none" }; case "Times": case "Times-Roman": case "Times-Narrow": case "Times-Courier": case "Times-New-Roman": return { face: "\"Times New Roman\"", weight: "normal", style: "normal", decoration: "none" }; case "Times-Italic": case "Times-Italics": return { face: "\"Times New Roman\"", weight: "normal", style: "italic", decoration: "none" }; case "Times-Bold": return { face: "\"Times New Roman\"", weight: "bold", style: "normal", decoration: "none" }; case "Times-BoldItalic": return { face: "\"Times New Roman\"", weight: "bold", style: "italic", decoration: "none" }; case "ZapfChancery-MediumItalic": return { face: "\"Zapf Chancery\",cursive,serif", weight: "normal", style: "normal", decoration: "none" }; default: return null; } }; var getFontParameter = function(tokens, currentSetting, str, position, cmd) { // Every font parameter has the following format: // <face> <utf8> <size> <modifiers> <box> // Where: // face: either a standard web font name, or a postscript font, enumerated in fontTranslation. This could also be an * or be missing if the face shouldn't change. // utf8: This is optional, and specifies utf8. That's all that is supported so the field is just silently ignored. // size: The size, in pixels. This may be omitted if the size is not changing. // modifiers: zero or more of "bold", "italic", "underline" // box: Only applies to the measure numbers, gchords, and the parts. If present, then a box is drawn around the characters. // If face is present, then all the modifiers are cleared. If face is absent, then the modifiers are illegal. // The face can be a single word, a set of words separated by hyphens, or a quoted string. // // So, in practicality, there are three types of font definitions: a number only, an asterisk and a number only, or the full definition (with an optional size). function processNumberOnly() { var size = parseInt(tokens[0].token); tokens.shift(); if (!currentSetting) { warn("Can't set just the size of the font since there is no default value.", str, position); return { face: "\"Times New Roman\"", weight: "normal", style: "normal", decoration: "none", size: size}; } if (tokens.length === 0) { return { face: currentSetting.face, weight: currentSetting.weight, style: currentSetting.style, decoration: currentSetting.decoration, size: size}; } if (tokens.length === 1 && tokens[0].token === "box" && fontTypeCanHaveBox[cmd]) return { face: currentSetting.face, weight: currentSetting.weight, style: currentSetting.style, decoration: currentSetting.decoration, size: size, box: true}; warn("Extra parameters in font definition.", str, position); return { face: currentSetting.face, weight: currentSetting.weight, style: currentSetting.style, decoration: currentSetting.decoration, size: size}; } // format 1: asterisk and number only if (tokens[0].token === '*') { tokens.shift(); if (tokens[0].type === 'number') return processNumberOnly(); else { warn("Expected font size number after *.", str, position); } } // format 2: number only if (tokens[0].type === 'number') { return processNumberOnly(); } // format 3: whole definition var face = []; var size; var weight = "normal"; var style = "normal"; var decoration = "none"; var box = false; var state = 'face'; var hyphenLast = false; while (tokens.length) { var currToken = tokens.shift(); var word = currToken.token.toLowerCase(); switch (state) { case 'face': if (hyphenLast || (word !== 'utf' && currToken.type !== 'number' && word !== "bold" && word !== "italic" && word !== "underline" && word !== "box")) { if (face.length > 0 && currToken.token === '-') { hyphenLast = true; face[face.length-1] = face[face.length-1] + currToken.token; } else { if (hyphenLast) { hyphenLast = false; face[face.length-1] = face[face.length-1] + currToken.token; } else face.push(currToken.token); } } else { if (currToken.type === 'number') { if (size) { warn("Font size specified twice in font definition.", str, position); } else { size = currToken.token; } state = 'modifier'; } else if (word === "bold") weight = "bold"; else if (word === "italic") style = "italic"; else if (word === "underline") decoration = "underline"; else if (word === "box") { if (fontTypeCanHaveBox[cmd]) box = true; else warn("This font style doesn't support \"box\"", str, position); state = "finished"; } else if (word === "utf") { currToken = tokens.shift(); // this gets rid of the "8" after "utf" state = "size"; } else warn("Unknown parameter " + currToken.token + " in font definition.", str, position); } break; case "size": if (currToken.type === 'number') { if (size) { warn("Font size specified twice in font definition.", str, position); } else { size = currToken.token; } } else { warn("Expected font size in font definition.", str, position); } state = 'modifier'; break; case "modifier": if (word === "bold") weight = "bold"; else if (word === "italic") style = "italic"; else if (word === "underline") decoration = "underline"; else if (word === "box") { if (fontTypeCanHaveBox[cmd]) box = true; else warn("This font style doesn't support \"box\"", str, position); state = "finished"; } else warn("Unknown parameter " + currToken.token + " in font definition.", str, position); break; case "finished": warn("Extra characters found after \"box\" in font definition.", str, position); break; } } if (size === undefined) { if (!currentSetting) { warn("Must specify the size of the font since there is no default value.", str, position); size = 12; } else size = currentSetting.size; } else size = parseFloat(size); face = face.join(' '); if (face === '') { if (!currentSetting) { warn("Must specify the name of the font since there is no default value.", str, position); face = "sans-serif"; } else face = currentSetting.face; } var psFont = fontTranslation(face); var font = {}; if (psFont) { font.face = psFont.face; font.weight = psFont.weight; font.style = psFont.style; font.decoration = psFont.decoration; font.size = size; if (box) font.box = true; return font; } font.face = face; font.weight = weight; font.style = style; font.decoration = decoration; font.size = size; if (box) font.box = true; return font; }; var getChangingFont = function(cmd, tokens, str) { if (tokens.length === 0) return "Directive \"" + cmd + "\" requires a font as a parameter."; multilineVars[cmd] = getFontParameter(tokens, multilineVars[cmd], str, 0, cmd); if (multilineVars.is_in_header) // If the font appears in the header, then it becomes the default font. tune.formatting[cmd] = multilineVars[cmd]; return null; }; var getGlobalFont = function(cmd, tokens, str) { if (tokens.length === 0) return "Directive \"" + cmd + "\" requires a font as a parameter."; tune.formatting[cmd] = getFontParameter(tokens, tune.formatting[cmd], str, 0, cmd); return null; }; var setScale = function(cmd, tokens) { var scratch = ""; tokens.forEach(function(tok) { scratch += tok.token; }); var num = parseFloat(scratch); if (isNaN(num) || num === 0) return "Directive \"" + cmd + "\" requires a number as a parameter."; tune.formatting.scale = num; }; // starts at 35 var drumNames = [ "acoustic-bass-drum", "bass-drum-1", "side-stick", "acoustic-snare", "hand-clap", "electric-snare", "low-floor-tom", "closed-hi-hat", "high-floor-tom", "pedal-hi-hat", "low-tom", "open-hi-hat", "low-mid-tom", "hi-mid-tom", "crash-cymbal-1", "high-tom", "ride-cymbal-1", "chinese-cymbal", "ride-bell", "tambourine", "splash-cymbal", "cowbell", "crash-cymbal-2", "vibraslap", "ride-cymbal-2", "hi-bongo", "low-bongo", "mute-hi-conga", "open-hi-conga", "low-conga", "high-timbale", "low-timbale", "high-agogo", "low-agogo", "cabasa", "maracas", "short-whistle", "long-whistle", "short-guiro", "long-guiro", "claves", "hi-wood-block", "low-wood-block", "mute-cuica", "open-cuica", "mute-triangle", "open-triangle", ]; var interpretPercMap = function(restOfString) { var tokens = restOfString.split(/\s+/); // Allow multiple spaces. if (tokens.length !== 2 && tokens.length !== 3) return { error: 'Expected parameters "abc-note", "drum-sound", and optionally "note-head"'}; var key = tokens[0]; // The percussion sound can either be a MIDI number or a drum name. If it is not a number then check for a name. var pitch = parseInt(tokens[1], 10); if ((isNaN(pitch) || pitch < 35 || pitch > 81) && tokens[1]) { pitch = drumNames.indexOf(tokens[1].toLowerCase()) + 35; } if ((isNaN(pitch) || pitch < 35 || pitch > 81)) return { error: 'Expected drum name, received "' + tokens[1] + '"' }; var value = { sound: pitch }; if (tokens.length === 3) value.noteHead = tokens[2]; return { key: key, value: value }; }; var getRequiredMeasurement = function(cmd, tokens) { var points = tokenizer.getMeasurement(tokens); if (points.used === 0 || tokens.length !== 0) return { error: "Directive \"" + cmd + "\" requires a measurement as a parameter."}; return points.value; }; var oneParameterMeasurement = function(cmd, tokens) { var points = tokenizer.getMeasurement(tokens); if (points.used === 0 || tokens.length !== 0) return "Directive \"" + cmd + "\" requires a measurement as a parameter."; tune.formatting[cmd] = points.value; return null; }; var addMultilineVar = function(key, cmd, tokens, min, max) { if (tokens.length !== 1 || tokens[0].type !== 'number') return "Directive \"" + cmd + "\" requires a number as a parameter."; var i = tokens[0].intt; if (min !== undefined && i < min) return "Directive \"" + cmd + "\" requires a number greater than or equal to " + min + " as a parameter."; if (max !== undefined && i > max) return "Directive \"" + cmd + "\" requires a number less than or equal to " + max + " as a parameter."; multilineVars[key] = i; return null; }; var addMultilineVarBool = function(key, cmd, tokens) { if (tokens.length === 1 && (tokens[0].token === 'true' || tokens[0].token === 'false')) { multilineVars[key] = tokens[0].token === 'true'; return null; } var str = addMultilineVar(key, cmd, tokens, 0, 1); if (str !== null) return str; multilineVars[key] = (multilineVars[key] === 1); return null; }; var addMultilineVarOneParamChoice = function(key, cmd, tokens, choices) { if (tokens.length !== 1) return "Directive \"" + cmd + "\" requires one of [ " + choices.join(", ") + " ] as a parameter."; var choice = tokens[0].token; var found = false; for (var i = 0; !found && i < choices.length; i++) { if (choices[i] === choice) found = true; } if (!found) return "Directive \"" + cmd + "\" requires one of [ " + choices.join(", ") + " ] as a parameter."; multilineVars[key] = choice; return null; }; var midiCmdParam0 = [ "nobarlines", "barlines", "beataccents", "nobeataccents", "droneon", "droneoff", "drumon", "drumoff", "fermatafixed", "fermataproportional", "gchordon", "gchordoff", "controlcombo", "temperamentnormal", "noportamento" ]; var midiCmdParam1String = [ "gchord", "ptstress", "beatstring" ]; var midiCmdParam1Integer = [ "bassvol", "chordvol", "bassprog", "chordprog", "c", "channel", "beatmod", "deltaloudness", "drumbars", "gracedivider", "makechordchannels", "randomchordattack", "chordattack", "stressmodel", "transpose", "rtranspose", "vol", "volinc" ]; var midiCmdParam1Integer1OptionalInteger = [ "program" ]; var midiCmdParam2Integer = [ "ratio", "snt", "bendvelocity", "pitchbend", "control", "temperamentlinear" ]; var midiCmdParam4Integer = [ "beat" ]; var midiCmdParam5Integer = [ "drone" ]; var midiCmdParam1String1Integer = [ "portamento" ]; var midiCmdParamFraction = [ "expand", "grace", "trim" ]; var midiCmdParam1StringVariableIntegers = [ "drum", "chordname" ]; var parseMidiCommand = function(midi, tune, restOfString) { var midi_cmd = midi.shift().token; var midi_params = []; if (midiCmdParam0.indexOf(midi_cmd) >= 0) { // NO PARAMETERS if (midi.length !== 0) warn("Unexpected parameter in MIDI " + midi_cmd, restOfString, 0); } else if (midiCmdParam1String.indexOf(midi_cmd) >= 0) { // ONE STRING PARAMETER if (midi.length !== 1) warn("Expected one parameter in MIDI " + midi_cmd, restOfString, 0); else midi_params.push(midi[0].token); } else if (midiCmdParam1Integer.indexOf(midi_cmd) >= 0) { // ONE INT PARAMETER if (midi.length !== 1) warn("Expected one parameter in MIDI " + midi_cmd, restOfString, 0); else if (midi[0].type !== "number") warn("Expected one integer parameter in MIDI " + midi_cmd, restOfString, 0); else midi_params.push(midi[0].intt); } else if (midiCmdParam1Integer1OptionalInteger.indexOf(midi_cmd) >= 0) { // ONE INT PARAMETER, ONE OPTIONAL PARAMETER if (midi.length !== 1 && midi.length !== 2) warn("Expected one or two parameters in MIDI " + midi_cmd, restOfString, 0); else if (midi[0].type !== "number") warn("Expected integer parameter in MIDI " + midi_cmd, restOfString, 0); else if (midi.length === 2 && midi[1].type !== "number") warn("Expected integer parameter in MIDI " + midi_cmd, restOfString, 0); else { midi_params.push(midi[0].intt); if (midi.length === 2) midi_params.push(midi[1].intt); } } else if (midiCmdParam2Integer.indexOf(midi_cmd) >= 0) { // TWO INT PARAMETERS if (midi.length !== 2) warn("Expected two parameters in MIDI " + midi_cmd, restOfString, 0); else if (midi[0].type !== "number" || midi[1].type !== "number") warn("Expected two integer parameters in MIDI " + midi_cmd, restOfString, 0); else { midi_params.push(midi[0].intt); midi_params.push(midi[1].intt); } } else if (midiCmdParam1String1Integer.indexOf(midi_cmd) >= 0) { // ONE STRING PARAMETER, ONE INT PARAMETER if (midi.length !== 2) warn("Expected two parameters in MIDI " + midi_cmd, restOfString, 0); else if (midi[0].type !== "alpha" || midi[1].type !== "number") warn("Expected one string and one integer parameters in MIDI " + midi_cmd, restOfString, 0); else { midi_params.push(midi[0].token); midi_params.push(midi[1].intt); } } else if (midi_cmd === 'drummap') { // BUILD AN OBJECT OF ABC NOTE => MIDI NOTE if (midi.length === 2 && midi[0].type === 'alpha' && midi[1].type === 'number') { if (!tune.formatting) tune.formatting = {}; if (!tune.formatting.midi) tune.formatting.midi = {}; if (!tune.formatting.midi.drummap) tune.formatting.midi.drummap = {}; tune.formatting.midi.drummap[midi[0].token] = midi[1].intt; midi_params = tune.formatting.midi.drummap; } else if (midi.length === 3 && midi[0].type === 'punct' && midi[1].type === 'alpha' && midi[2].type === 'number') { if (!tune.formatting) tune.formatting = {}; if (!tune.formatting.midi) tune.formatting.midi = {}; if (!tune.formatting.midi.drummap) tune.formatting.midi.drummap = {}; tune.formatting.midi.drummap[midi[0].token+midi[1].token] = midi[2].intt; midi_params = tune.formatting.midi.drummap; } else { warn("Expected one note name and one integer parameter in MIDI " + midi_cmd, restOfString, 0); } } else if (midiCmdParamFraction.indexOf(midi_cmd) >= 0) { // ONE FRACTION PARAMETER if (midi.length !== 3) warn("Expected fraction parameter in MIDI " + midi_cmd, restOfString, 0); else if (midi[0].type !== "number" || midi[1].token !== "/" || midi[2].type !== "number") warn("Expected fraction parameter in MIDI " + midi_cmd, restOfString, 0); else { midi_params.push(midi[0].intt); midi_params.push(midi[2].intt); } } else if (midiCmdParam4Integer.indexOf(midi_cmd) >= 0) { // FOUR INT PARAMETERS if (midi.length !== 4) warn("Expected four parameters in MIDI " + midi_cmd, restOfString, 0); else if (midi[0].type !== "number" || midi[1].type !== "number" || midi[2].type !== "number" || midi[3].type !== "number") warn("Expected four integer parameters in MIDI " + midi_cmd, restOfString, 0); else { midi_params.push(midi[0].intt); midi_params.push(midi[1].intt); midi_params.push(midi[2].intt); midi_params.push(midi[3].intt); } } else if (midiCmdParam5Integer.indexOf(midi_cmd) >= 0) { // FIVE INT PARAMETERS if (midi.length !== 5) warn("Expected five parameters in MIDI " + midi_cmd, restOfString, 0); else if (midi[0].type !== "number" || midi[1].type !== "number" || midi[2].type !== "number" || midi[3].type !== "number" || midi[4].type !== "number") warn("Expected five integer parameters in MIDI " + midi_cmd, restOfString, 0); else { midi_params.push(midi[0].intt); midi_params.push(midi[1].intt); midi_params.push(midi[2].intt); midi_params.push(midi[3].intt); midi_params.push(midi[4].intt); } } else if (midiCmdParam1Integer1OptionalInteger.indexOf(midi_cmd) >= 0) { // ONE INT PARAMETER, ONE OPTIONAL OCTAVE PARAMETER if (midi.length !== 1 || midi.length !== 4) warn("Expected one or two parameters in MIDI " + midi_cmd, restOfString, 0); else if (midi[0].type !== "number") warn("Expected integer parameter in MIDI " + midi_cmd, restOfString, 0); else if (midi.length === 4) { if (midi[1].token !== "octave") warn("Expected octave parameter in MIDI " + midi_cmd, restOfString, 0); if (midi[2].token !== "=") warn("Expected octave parameter in MIDI " + midi_cmd, restOfString, 0); if (midi[3].type !== "number") warn("Expected integer parameter for octave in MIDI " + midi_cmd, restOfString, 0); } else { midi_params.push(midi[0].intt); if (midi.length === 4) midi_params.push(midi[3].intt); } } else if (midiCmdParam1StringVariableIntegers.indexOf(midi_cmd) >= 0) { // ONE STRING, VARIABLE INT PARAMETERS if (midi.length < 2) warn("Expected string parameter and at least one integer parameter in MIDI " + midi_cmd, restOfString, 0); else if (midi[0].type !== "alpha") warn("Expected string parameter and at least one integer parameter in MIDI " + midi_cmd, restOfString, 0); else { var p = midi.shift(); midi_params.push(p.token); while (midi.length > 0) { p = midi.shift(); if (p.type !== "number") warn("Expected integer parameter in MIDI " + midi_cmd, restOfString, 0); midi_params.push(p.intt); } } } if (tuneBuilder.hasBeginMusic()) tuneBuilder.appendElement('midi', -1, -1, { cmd: midi_cmd, params: midi_params }); else { if (tune.formatting['midi'] === undefined) tune.formatting['midi'] = {}; tune.formatting['midi'][midi_cmd] = midi_params; } }; parseDirective.parseFontChangeLine = function(textstr) { var textParts = textstr.split('$'); if (textParts.length > 1 && multilineVars.setfont) { var textarr = [ { text: textParts[0] }]; for (var i = 1; i < textParts.length; i++) { if (textParts[i][0] === '0') textarr.push({ text: textParts[i].substring(1) }); else if (textParts[i][0] === '1' && multilineVars.setfont[1]) textarr.push({font: multilineVars.setfont[1], text: textParts[i].substring(1) }); else if (textParts[i][0] === '2' && multilineVars.setfont[2]) textarr.push({font: multilineVars.setfont[2], text: textParts[i].substring(1) }); else if (textParts[i][0] === '3' && multilineVars.setfont[3]) textarr.push({font: multilineVars.setfont[3], text: textParts[i].substring(1) }); else if (textParts[i][0] === '4' && multilineVars.setfont[4]) textarr.push({font: multilineVars.setfont[4], text: textParts[i].substring(1) }); else textarr[textarr.length-1].text += '$' + textParts[i]; } if (textarr.length > 1) return textarr; } return textstr; }; var positionChoices = [ 'auto', 'above', 'below', 'hidden' ]; parseDirective.addDirective = function(str) { var tokens = tokenizer.tokenize(str, 0, str.length); // 3 or more % in a row, or just spaces after %% is just a comment if (tokens.length === 0 || tokens[0].type !== 'alpha') return null; var restOfString = str.substring(str.indexOf(tokens[0].token)+tokens[0].token.length); restOfString = tokenizer.stripComment(restOfString); var cmd = tokens.shift().token.toLowerCase(); var scratch = ""; var line; switch (cmd) { // The following directives were added to abc_parser_lint, but haven't been implemented here. // Most of them are direct translations from the directives that will be parsed in. See abcm2ps's format.txt for info on each of these. // alignbars: { type: "number", optional: true }, // aligncomposer: { type: "string", Enum: [ 'left', 'center','right' ], optional: true }, // bstemdown: { type: "boolean", optional: true }, // continueall: { type: "boolean", optional: true }, // dynalign: { type: "boolean", optional: true }, // exprabove: { type: "boolean", optional: true }, // exprbelow: { type: "boolean", optional: true }, // gchordbox: { type: "boolean", optional: true }, // gracespacebefore: { type: "number", optional: true }, // gracespaceinside: { type: "number", optional: true }, // gracespaceafter: { type: "number", optional: true }, // infospace: { type: "number", optional: true }, // lineskipfac: { type: "number", optional: true }, // maxshrink: { type: "number", optional: true }, // maxstaffsep: { type: "number", optional: true }, // maxsysstaffsep: { type: "number", optional: true }, // notespacingfactor: { type: "number", optional: true }, // parskipfac: { type: "number", optional: true }, // slurheight: { type: "number", optional: true }, // splittune: { type: "boolean", optional: true }, // squarebreve: { type: "boolean", optional: true }, // stemheight: { type: "number", optional: true }, // straightflags: { type: "boolean", optional: true }, // stretchstaff: { type: "boolean", optional: true }, // titleformat: { type: "string", optional: true }, case "bagpipes":tune.formatting.bagpipes = true;break; case "flatbeams":tune.formatting.flatbeams = true;break; case "jazzchords":tune.formatting.jazzchords = true;break; case "germanAlphabet":tune.formatting.germanAlphabet = true;break; case "landscape":multilineVars.landscape = true;break; case "papersize":multilineVars.papersize = restOfString;break; case "graceslurs": if (tokens.length !== 1) return "Directive graceslurs requires one parameter: 0 or 1"; if (tokens[0].token === '0' || tokens[0].token === 'false') tune.formatting.graceSlurs = false; else if (tokens[0].token === '1' || tokens[0].token === 'true') tune.formatting.graceSlurs = true; else return "Directive graceslurs requires one parameter: 0 or 1 (received " + tokens[0].token + ')'; break; case "lineThickness": var lt = parseStretchLast(tokens); if (lt.value !== undefined) tune.formatting.lineThickness = lt.value; if (lt.error) return lt.error; break; case "stretchlast": var sl = parseStretchLast(tokens); if (sl.value !== undefined) tune.formatting.stretchlast = sl.value; if (sl.error) return sl.error; break; case "titlecaps":multilineVars.titlecaps = true;break; case "titleleft":tune.formatting.titleleft = true;break; case "measurebox":tune.formatting.measurebox = true;break; case "vocal": return addMultilineVarOneParamChoice("vocalPosition", cmd, tokens, positionChoices); case "dynamic": return addMultilineVarOneParamChoice("dynamicPosition", cmd, tokens, positionChoices); case "gchord": return addMultilineVarOneParamChoice("chordPosition", cmd, tokens, positionChoices); case "ornament": return addMultilineVarOneParamChoice("ornamentPosition", cmd, tokens, positionChoices); case "volume": return addMultilineVarOneParamChoice("volumePosition", cmd, tokens, positionChoices); case "botmargin": case "botspace": case "composerspace": case "indent": case "leftmargin": case "linesep": case "musicspace": case "partsspace": case "pageheight": case "pagewidth": case "rightmargin": case "staffsep": case "staffwidth": case "subtitlespace": case "sysstaffsep": case "systemsep": case "textspace": case "titlespace": case "topmargin": case "topspace": case "vocalspace": case "wordsspace": return oneParameterMeasurement(cmd, tokens); case "voicescale": if (tokens.length !== 1 || tokens[0].type !== 'number') return "voicescale requires one float as a parameter"; var voiceScale = tokens.shift(); if (multilineVars.currentVoice) { multilineVars.currentVoice.scale = voiceScale.floatt; tuneBuilder.changeVoiceScale(multilineVars.currentVoice.scale); } return null; case "voicecolor": if (tokens.length !== 1) // this could either be of type alpha or quote, but it's ok if it is a number return "voicecolor requires one string as a parameter"; var voiceColor = tokens.shift(); if (multilineVars.currentVoice) { multilineVars.currentVoice.color = voiceColor.token; tuneBuilder.changeVoiceColor(multilineVars.currentVoice.color); } return null; case "vskip": var vskip = Math.round(getRequiredMeasurement(cmd, tokens)); if (vskip.error) return vskip.error; tuneBuilder.addSpacing(vskip); return null; case "scale": setScale(cmd, tokens); break; case "sep": if (tokens.length === 0) tuneBuilder.addSeparator(14,14,85, { startChar: multilineVars.iChar, endChar: multilineVars.iChar+5}); // If no parameters are given, then there is a default size. else { var points = tokenizer.getMeasurement(tokens); if (points.used === 0) return "Directive \"" + cmd + "\" requires 3 numbers: space above, space below, length of line"; var spaceAbove = points.value; points = tokenizer.getMeasurement(tokens); if (points.used === 0) return "Directive \"" + cmd + "\" requires 3 numbers: space above, space below, length of line"; var spaceBelow = points.value; points = tokenizer.getMeasurement(tokens); if (points.used === 0 || tokens.length !== 0) return "Directive \"" + cmd + "\" requires 3 numbers: space above, space below, length of line"; var lenLine = points.value; tuneBuilder.addSeparator(spaceAbove, spaceBelow, lenLine, { startChar: multilineVars.iChar, endChar: multilineVars.iChar+restOfString.length}); } break; case "barsperstaff": scratch = addMultilineVar('barsperstaff', cmd, tokens); if (scratch !== null) return scratch; break; case "staffnonote": // The sense of the boolean is opposite here. "0" means true. if (tokens.length !== 1) return "Directive staffnonote requires one parameter: 0 or 1"; if (tokens[0].token === '0') multilineVars.staffnonote = true; else if (tokens[0].token === '1') multilineVars.staffnonote = false; else return "Directive staffnonote requires one parameter: 0 or 1 (received " + tokens[0].token + ')'; break; case "printtempo": scratch = addMultilineVarBool('printTempo', cmd, tokens); if (scratch !== null) return scratch; break; case "partsbox": scratch = addMultilineVarBool('partsBox', cmd, tokens); if (scratch !== null) return scratch; multilineVars.partsfont.box = multilineVars.partsBox; break; case "freegchord": scratch = addMultilineVarBool('freegchord', cmd, tokens); if (scratch !== null) return scratch; break; case "measurenb": case "barnumbers": scratch = addMultilineVar('barNumbers', cmd, tokens); if (scratch !== null) return scratch; break; case "setbarnb": if (tokens.length !== 1 || tokens[0].type !== 'number') { return 'Directive setbarnb requires a number as a parameter.'; } multilineVars.currBarNumber = tuneBuilder.setBarNumberImmediate(tokens[0].intt); break; case "begintext": var textBlock = ''; line = tokenizer.nextLine(); while(line && line.indexOf('%%endtext') !== 0) { if (parseCommon.startsWith(line, "%%")) textBlock += line.substring(2) + "\n"; else textBlock += line + "\n"; line = tokenizer.nextLine(); } tuneBuilder.addText(textBlock, { startChar: multilineVars.iChar, endChar: multilineVars.iChar+textBlock.length+7}); break; case "continueall": multilineVars.continueall = true; break; case "beginps": line = tokenizer.nextLine(); while(line && line.indexOf('%%endps') !== 0) { tokenizer.nextLine(); } warn("Postscript ignored", str, 0); break; case "deco": if (restOfString.length > 0) multilineVars.ignoredDecorations.push(restOfString.substring(0, restOfString.indexOf(' '))); warn("Decoration redefinition ignored", str, 0); break; case "text": var textstr = tokenizer.translateString(restOfString); tuneBuilder.addText(parseDirective.parseFontChangeLine(textstr), { startChar: multilineVars.iChar, endChar: multilineVars.iChar+restOfString.length+7}); break; case "center": var centerstr = tokenizer.translateString(restOfString); tuneBuilder.addCentered(parseDirective.parseFontChangeLine(centerstr)); break; case "font": // don't need to do anything for this; it is a useless directive break; case "setfont": var sfTokens = tokenizer.tokenize(restOfString, 0, restOfString.length); // var sfDone = false; if (sfTokens.length >= 4) { if (sfTokens[0].token === '-' && sfTokens[1].type === 'number') { var sfNum = parseInt(sfTokens[1].token); if (sfNum >= 1 && sfNum <= 4) { if (!multilineVars.setfont) multilineVars.setfont = []; sfTokens.shift(); sfTokens.shift(); multilineVars.setfont[sfNum] = getFontParameter(sfTokens, multilineVars.setfont[sfNum], str, 0, 'setfont'); // var sfSize = sfTokens.pop(); // if (sfSize.type === 'number') { // sfSize = parseInt(sfSize.token); // var sfFontName = ''; // for (var sfi = 2; sfi < sfTokens.length; sfi++) // sfFontName += sfTokens[sfi].token; // multilineVars.setfont[sfNum] = { face: sfFontName, size: sfSize }; // sfDone = true; // } } } } // if (!sfDone) // return "Bad parameters: " + cmd; break; case "gchordfont": case "partsfont": case "tripletfont": case "vocalfont": case "textfont": case "annotationfont": case "historyfont": case "infofont": case "measurefont": case "repeatfont": case "wordsfont": return getChangingFont(cmd, tokens, str); case "composerfont": case "subtitlefont": case "tempofont": case "titlefont": case "voicefont": case "footerfont": case "headerfont": return getGlobalFont(cmd, tokens, str); case "barlabelfont": case "barnumberfont": case "barnumfont": return getChangingFont("measurefont", tokens, str); case "staves": case "score": multilineVars.score_is_present = true; var addVoice = function(id, newStaff, bracket, brace, continueBar) { if (newStaff || multilineVars.staves.length === 0) { multilineVars.staves.push({index: multilineVars.staves.length, numVoices: 0}); } var staff = parseCommon.last(multilineVars.staves); if (bracket !== undefined && staff.bracket === undefined) staff.bracket = bracket; if (brace !== undefined && staff.brace === undefined) staff.brace = brace; if (continueBar) staff.connectBarLines = 'end'; if (multilineVars.voices[id] === undefined) { multilineVars.voices[id] = {staffNum: staff.index, index: staff.numVoices}; staff.numVoices++; } }; var openParen = false; var openBracket = false; var openBrace = false; var justOpenParen = false; var justOpenBracket = false; var justOpenBrace = false; var continueBar = false; var lastVoice; var addContinueBar = function() { continueBar = true; if (lastVoice) { var ty = 'start'; if (lastVoice.staffNum > 0) { if (multilineVars.staves[lastVoice.staffNum-1].connectBarLines === 'start' || multilineVars.staves[lastVoice.staffNum-1].connectBarLines === 'continue') ty = 'continue'; } multilineVars.staves[lastVoice.staffNum].connectBarLines = ty; } }; while (tokens.length) { var t = tokens.shift(); switch (t.token) { case '(': if (openParen) warn("Can't nest parenthesis in %%score", str, t.start); else {openParen = true;justOpenParen = true;} break; case ')': if (!openParen || justOpenParen) warn("Unexpected close parenthesis in %%score", str, t.start); else openParen = false; break; case '[': if (openBracket) warn("Can't nest brackets in %%score", str, t.start); else {openBracket = true;justOpenBracket = true;} break; case ']': if (!openBracket || justOpenBracket) warn("Unexpected close bracket in %%score", str, t.start); else {openBracket = false;multilineVars.staves[lastVoice.staffNum].bracket = 'end';} break; case '{': if (openBrace ) warn("Can't nest braces in %%score", str, t.start); else {openBrace = true;justOpenBrace = true;} break; case '}': if (!openBrace || justOpenBrace) warn("Unexpected close brace in %%score", str, t.start); else {openBrace = false;multilineVars.staves[lastVoice.staffNum].brace = 'end';} break; case '|': addContinueBar(); break; default: var vc = ""; while (t.type === 'alpha' || t.type === 'number') { vc += t.token; if (t.continueId) t = tokens.shift(); else break; } var newStaff = !openParen || justOpenParen; var bracket = justOpenBracket ? 'start' : openBracket ? 'continue' : undefined; var brace = justOpenBrace ? 'start' : openBrace ? 'continue' : undefined; addVoice(vc, newStaff, bracket, brace, continueBar); justOpenParen = false; justOpenBracket = false; justOpenBrace = false; continueBar = false; lastVoice = multilineVars.voices[vc]; if (cmd === 'staves') addContinueBar(); break; } } break; case "newpage": var pgNum = tokenizer.getInt(restOfString); tuneBuilder.addNewPage(pgNum.digits === 0 ? -1 : pgNum.value); break; case "abc": var arr = restOfString.split(' '); switch (arr[0]) { case "-copyright": case "-creator": case "-edited-by": case "-version": case "-charset": var subCmd = arr.shift(); tuneBuilder.addMetaText(cmd+subCmd, arr.join(' '), { startChar: multilineVars.iChar, endChar: multilineVars.iChar+restOfString.length+5}); break; default: return "Unknown directive: " + cmd+arr[0]; } break; case "header": case "footer": var footerStr = tokenizer.getMeat(restOfString, 0, restOfString.length); footerStr = restOfString.substring(footerStr.start, footerStr.end); if (footerStr[0] === '"' && footerStr[footerStr.length-1] === '"' ) footerStr = footerStr.substring(1, footerStr.length-1); var footerArr = footerStr.split('\t'); var footer = {}; if (footerArr.length === 1) footer = { left: "", center: footerArr[0], right: "" }; else if (footerArr.length === 2) footer = { left: footerArr[0], center: footerArr[1], right: "" }; else footer = { left: footerArr[0], center: footerArr[1], right: footerArr[2] }; if (footerArr.length > 3) warn("Too many tabs in " + cmd + ": " + footerArr.length + " found.", restOfString, 0); tuneBuilder.addMetaTextObj(cmd, footer, { startChar: multilineVars.iChar, endChar: multilineVars.iChar+str.length}); break; case "midi": var midi = tokenizer.tokenize(restOfString, 0, restOfString.length, true); if (midi.length > 0 && midi[0].token === '=') midi.shift(); if (midi.length === 0) warn("Expected midi command", restOfString, 0); else parseMidiCommand(midi, tune, restOfString); break; case "percmap": var percmap = interpretPercMap(restOfString); if (percmap.error) warn(percmap.error, str, 8); else { if (!tune.formatting.percmap) tune.formatting.percmap = {}; tune.formatting.percmap[percmap.key] = percmap.value; } break; case "map": case "playtempo": case "auquality": case "continuous": case "nobarcheck": // TODO-PER: Actually handle the parameters of these tune.formatting[cmd] = restOfString; break; default: return "Unknown directive: " + cmd; } return null; }; parseDirective.globalFormatting = function(formatHash) { for (var cmd in formatHash) { if (formatHash.hasOwnProperty(cmd)) { var value = ''+formatHash[cmd]; var tokens = tokenizer.tokenize(value, 0, value.length); var scratch; switch (cmd) { case "titlefont": case "gchordfont": case "composerfont": case "footerfont": case "headerfont": case "historyfont": case "infofont": case "measurefont": case "partsfont": case "repeatfont": case "subtitlefont": case "tempofont": case "textfont": case "voicefont": case "tripletfont": case "vocalfont": case "wordsfont": case "annotationfont": case "tablabelfont": case "tabnumberfont": case "tabgracefont": getChangingFont(cmd, tokens, value); break; case "scale": setScale(cmd, tokens); break; case "partsbox": scratch = addMultilineVarBool('partsBox', cmd, tokens); if (scratch !== null) warn(scratch); multilineVars.partsfont.box = multilineVars.partsBox; break; case "freegchord": scratch = addMultilineVarBool('freegchord', cmd, tokens); if (scratch !== null) warn(scratch); break; case "fontboxpadding": if (tokens.length !== 1 || tokens[0].type !== 'number') warn("Directive \"" + cmd + "\" requires a number as a parameter."); tune.formatting.fontboxpadding = tokens[0].floatt; break; case "stretchlast": var sl = parseStretchLast(tokens); if (sl.value !== undefined) tune.formatting.stretchlast = sl.value; if (sl.error) return sl.error; break; default: warn("Formatting directive unrecognized: ", cmd, 0); } } } }; function parseStretchLast(tokens) { if (tokens.length === 0) return { value: 1 }; // if there is no value then the presence of this is the same as "true" else if (tokens.length === 1) { if (tokens[0].type === "number") { if (tokens[0].floatt >= 0 || tokens[0].floatt <= 1) return {value: tokens[0].floatt}; } else if (tokens[0].token === 'false') { return { value: 0 }; } else if (tokens[0].token === 'true') { return {value: 1}; } } return { error: "Directive stretchlast requires zero or one parameter: false, true, or number between 0 and 1 (received " + tokens[0].token + ')' }; } })(); module.exports = parseDirective;