abcjs-rails 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,76 @@
1
+ // abc_parse.js: parses a string representing ABC Music Notation into a usable internal structure.
2
+ // Copyright (C) 2010 Paul Rosen (paul at paulrosen dot net)
3
+ //
4
+ // This program is free software: you can redistribute it and/or modify
5
+ // it under the terms of the GNU General Public License as published by
6
+ // the Free Software Foundation, either version 3 of the License, or
7
+ // (at your option) any later version.
8
+ //
9
+ // This program is distributed in the hope that it will be useful,
10
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ // GNU General Public License for more details.
13
+ //
14
+ // You should have received a copy of the GNU General Public License
15
+ // along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ /*global window */
18
+
19
+ if (!window.ABCJS)
20
+ window.ABCJS = {};
21
+
22
+ if (!window.ABCJS.parse)
23
+ window.ABCJS.parse = {};
24
+
25
+ window.ABCJS.parse.clone = function(source) {
26
+ var destination = {};
27
+ for (var property in source)
28
+ if (source.hasOwnProperty(property))
29
+ destination[property] = source[property];
30
+ return destination;
31
+ };
32
+
33
+ window.ABCJS.parse.gsub = function(source, pattern, replacement) {
34
+ return source.split(pattern).join(replacement);
35
+ };
36
+
37
+ window.ABCJS.parse.strip = function(str) {
38
+ return str.replace(/^\s+/, '').replace(/\s+$/, '');
39
+ };
40
+
41
+ window.ABCJS.parse.startsWith = function(str, pattern) {
42
+ return str.indexOf(pattern) === 0;
43
+ };
44
+
45
+ window.ABCJS.parse.endsWith = function(str, pattern) {
46
+ var d = str.length - pattern.length;
47
+ return d >= 0 && str.lastIndexOf(pattern) === d;
48
+ };
49
+
50
+ window.ABCJS.parse.each = function(arr, iterator, context) {
51
+ for (var i = 0, length = arr.length; i < length; i++)
52
+ iterator.apply(context, [arr[i],i]);
53
+ };
54
+
55
+ window.ABCJS.parse.last = function(arr) {
56
+ if (arr.length === 0)
57
+ return null;
58
+ return arr[arr.length-1];
59
+ };
60
+
61
+ window.ABCJS.parse.compact = function(arr) {
62
+ var output = [];
63
+ for (var i = 0; i < arr.length; i++) {
64
+ if (arr[i])
65
+ output.push(arr[i]);
66
+ }
67
+ return output;
68
+ };
69
+
70
+ window.ABCJS.parse.detect = function(arr, iterator) {
71
+ for (var i = 0; i < arr.length; i++) {
72
+ if (iterator(arr[i]))
73
+ return true;
74
+ }
75
+ return false;
76
+ };
@@ -0,0 +1,1385 @@
1
+ // abc_parse.js: parses a string representing ABC Music Notation into a usable internal structure.
2
+ // Copyright (C) 2010 Paul Rosen (paul at paulrosen dot net)
3
+ //
4
+ // This program is free software: you can redistribute it and/or modify
5
+ // it under the terms of the GNU General Public License as published by
6
+ // the Free Software Foundation, either version 3 of the License, or
7
+ // (at your option) any later version.
8
+ //
9
+ // This program is distributed in the hope that it will be useful,
10
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ // GNU General Public License for more details.
13
+ //
14
+ // You should have received a copy of the GNU General Public License
15
+ // along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ /*global window */
18
+
19
+ if (!window.ABCJS)
20
+ window.ABCJS = {};
21
+
22
+ if (!window.ABCJS.parse)
23
+ window.ABCJS.parse = {};
24
+
25
+ window.ABCJS.parse.Parse = function() {
26
+ var tune = new window.ABCJS.data.Tune();
27
+ var tokenizer = new window.ABCJS.parse.tokenizer();
28
+
29
+ this.getTune = function() {
30
+ return tune;
31
+ };
32
+
33
+ var multilineVars = {
34
+ reset: function() {
35
+ for (var property in this) {
36
+ if (this.hasOwnProperty(property) && typeof this[property] !== "function") {
37
+ delete this[property];
38
+ }
39
+ }
40
+ this.iChar = 0;
41
+ this.key = {accidentals: [], root: 'none', acc: '', mode: '' };
42
+ this.meter = {type: 'specified', value: [{num: '4', den: '4'}]}; // if no meter is specified, there is an implied one.
43
+ this.origMeter = {type: 'specified', value: [{num: '4', den: '4'}]}; // this is for new voices that are created after we set the meter.
44
+ this.hasMainTitle = false;
45
+ this.default_length = 0.125;
46
+ this.clef = { type: 'treble', verticalPos: 0 };
47
+ this.next_note_duration = 0;
48
+ this.start_new_line = true;
49
+ this.is_in_header = true;
50
+ this.is_in_history = false;
51
+ this.partForNextLine = "";
52
+ this.havent_set_length = true;
53
+ this.voices = {};
54
+ this.staves = [];
55
+ this.macros = {};
56
+ this.currBarNumber = 1;
57
+ this.inTextBlock = false;
58
+ this.inPsBlock = false;
59
+ this.ignoredDecorations = [];
60
+ this.textBlock = "";
61
+ this.score_is_present = false; // Can't have original V: lines when there is the score directive
62
+ this.inEnding = false;
63
+ this.inTie = false;
64
+ this.inTieChord = {};
65
+ }
66
+ };
67
+
68
+ var addWarning = function(str) {
69
+ if (!multilineVars.warnings)
70
+ multilineVars.warnings = [];
71
+ multilineVars.warnings.push(str);
72
+ };
73
+
74
+ var encode = function(str) {
75
+ var ret = window.ABCJS.parse.gsub(str, '\x12', ' ');
76
+ ret = window.ABCJS.parse.gsub(ret, '&', '&amp;');
77
+ ret = window.ABCJS.parse.gsub(ret, '<', '&lt;');
78
+ return window.ABCJS.parse.gsub(ret, '>', '&gt;');
79
+ };
80
+
81
+ var warn = function(str, line, col_num) {
82
+ var bad_char = line.charAt(col_num);
83
+ if (bad_char === ' ')
84
+ bad_char = "SPACE";
85
+ var clean_line = encode(line.substring(0, col_num)) +
86
+ '<span style="text-decoration:underline;font-size:1.3em;font-weight:bold;">' + bad_char + '</span>' +
87
+ encode(line.substring(col_num+1));
88
+ addWarning("Music Line:" + tune.getNumLines() + ":" + (col_num+1) + ': ' + str + ": " + clean_line);
89
+ };
90
+ var header = new window.ABCJS.parse.ParseHeader(tokenizer, warn, multilineVars, tune);
91
+
92
+ this.getWarnings = function() {
93
+ return multilineVars.warnings;
94
+ };
95
+
96
+ var letter_to_chord = function(line, i)
97
+ {
98
+ if (line.charAt(i) === '"')
99
+ {
100
+ var chord = tokenizer.getBrackettedSubstring(line, i, 5);
101
+ if (!chord[2])
102
+ warn("Missing the closing quote while parsing the chord symbol", line , i);
103
+ // If it starts with ^, then the chord appears above.
104
+ // If it starts with _ then the chord appears below.
105
+ // (note that the 2.0 draft standard defines them as not chords, but annotations and also defines @.)
106
+ if (chord[0] > 0 && chord[1].length > 0 && chord[1].charAt(0) === '^') {
107
+ chord[1] = chord[1].substring(1);
108
+ chord[2] = 'above';
109
+ } else if (chord[0] > 0 && chord[1].length > 0 && chord[1].charAt(0) === '_') {
110
+ chord[1] = chord[1].substring(1);
111
+ chord[2] = 'below';
112
+ } else if (chord[0] > 0 && chord[1].length > 0 && chord[1].charAt(0) === '<') {
113
+ chord[1] = chord[1].substring(1);
114
+ chord[2] = 'left';
115
+ } else if (chord[0] > 0 && chord[1].length > 0 && chord[1].charAt(0) === '>') {
116
+ chord[1] = chord[1].substring(1);
117
+ chord[2] = 'right';
118
+ } else if (chord[0] > 0 && chord[1].length > 0 && chord[1].charAt(0) === '@') {
119
+ // @-15,5.7
120
+ chord[1] = chord[1].substring(1);
121
+ var x = tokenizer.getFloat(chord[1]);
122
+ if (x.digits === 0)
123
+ warn("Missing first position in absolutely positioned annotation.", line , i);
124
+ chord[1] = chord[1].substring(x.digits);
125
+ if (chord[1][0] !== ',')
126
+ warn("Missing comma absolutely positioned annotation.", line , i);
127
+ chord[1] = chord[1].substring(1);
128
+ var y = tokenizer.getFloat(chord[1]);
129
+ if (y.digits === 0)
130
+ warn("Missing second position in absolutely positioned annotation.", line , i);
131
+ chord[1] = chord[1].substring(y.digits);
132
+ var ws = tokenizer.skipWhiteSpace(chord[1]);
133
+ chord[1] = chord[1].substring(ws);
134
+ chord[2] = null;
135
+ chord[3] = { x: x.value, y: y.value };
136
+ } else {
137
+ chord[1] = chord[1].replace(/([ABCDEFG])b/g, "$1♭");
138
+ chord[1] = chord[1].replace(/([ABCDEFG])#/g, "$1♯");
139
+ chord[2] = 'default';
140
+ }
141
+ return chord;
142
+ }
143
+ return [0, ""];
144
+ };
145
+
146
+ var legalAccents = [ "trill", "lowermordent", "uppermordent", "mordent", "pralltriller", "accent",
147
+ "fermata", "invertedfermata", "tenuto", "0", "1", "2", "3", "4", "5", "+", "wedge",
148
+ "open", "thumb", "snap", "turn", "roll", "breath", "shortphrase", "mediumphrase", "longphrase",
149
+ "segno", "coda", "D.S.", "D.C.", "fine", "crescendo(", "crescendo)", "diminuendo(", "diminuendo)",
150
+ "p", "pp", "f", "ff", "mf", "mp", "ppp", "pppp", "fff", "ffff", "sfz", "repeatbar", "repeatbar2", "slide",
151
+ "upbow", "downbow", "/", "//", "///", "////", "trem1", "trem2", "trem3", "trem4",
152
+ "turnx", "invertedturn", "invertedturnx", "trill(", "trill)", "arpeggio", "xstem", "mark",
153
+ "style=normal", "style=harmonic", "style=rhythm", "style=x"
154
+ ];
155
+ var accentPsuedonyms = [ ["<", "accent"], [">", "accent"], ["tr", "trill"], ["<(", "crescendo("], ["<)", "crescendo)"],
156
+ [">(", "diminuendo("], [">)", "diminuendo)"], ["plus", "+"], [ "emphasis", "accent"] ];
157
+ var letter_to_accent = function(line, i)
158
+ {
159
+ var macro = multilineVars.macros[line.charAt(i)];
160
+
161
+ if (macro !== undefined) {
162
+ if (macro.charAt(0) === '!' || macro.charAt(0) === '+')
163
+ macro = macro.substring(1);
164
+ if (macro.charAt(macro.length-1) === '!' || macro.charAt(macro.length-1) === '+')
165
+ macro = macro.substring(0, macro.length-1);
166
+ if (window.ABCJS.parse.detect(legalAccents, function(acc) {
167
+ return (macro === acc);
168
+ }))
169
+ return [ 1, macro ];
170
+ else {
171
+ if (!window.ABCJS.parse.detect(multilineVars.ignoredDecorations, function(dec) {
172
+ return (macro === dec);
173
+ }))
174
+ warn("Unknown macro: " + macro, line, i);
175
+ return [1, '' ];
176
+ }
177
+ }
178
+ switch (line.charAt(i))
179
+ {
180
+ case '.':return [1, 'staccato'];
181
+ case 'u':return [1, 'upbow'];
182
+ case 'v':return [1, 'downbow'];
183
+ case '~':return [1, 'irishroll'];
184
+ case '!':
185
+ case '+':
186
+ var ret = tokenizer.getBrackettedSubstring(line, i, 5);
187
+ // Be sure that the accent is recognizable.
188
+ if (ret[1].length > 0 && (ret[1].charAt(0) === '^' || ret[1].charAt(0) ==='_'))
189
+ ret[1] = ret[1].substring(1); // TODO-PER: The test files have indicators forcing the ornament to the top or bottom, but that isn't in the standard. We'll just ignore them.
190
+ if (window.ABCJS.parse.detect(legalAccents, function(acc) {
191
+ return (ret[1] === acc);
192
+ }))
193
+ return ret;
194
+
195
+ if (window.ABCJS.parse.detect(accentPsuedonyms, function(acc) {
196
+ if (ret[1] === acc[0]) {
197
+ ret[1] = acc[1];
198
+ return true;
199
+ } else
200
+ return false;
201
+ }))
202
+ return ret;
203
+
204
+ // We didn't find the accent in the list, so consume the space, but don't return an accent.
205
+ // Although it is possible that ! was used as a line break, so accept that.
206
+ if (line.charAt(i) === '!' && (ret[0] === 1 || line.charAt(i+ret[0]-1) !== '!'))
207
+ return [1, null ];
208
+ warn("Unknown decoration: " + ret[1], line, i);
209
+ ret[1] = "";
210
+ return ret;
211
+ case 'H':return [1, 'fermata'];
212
+ case 'J':return [1, 'slide'];
213
+ case 'L':return [1, 'accent'];
214
+ case 'M':return [1, 'mordent'];
215
+ case 'O':return[1, 'coda'];
216
+ case 'P':return[1, 'pralltriller'];
217
+ case 'R':return [1, 'roll'];
218
+ case 'S':return [1, 'segno'];
219
+ case 'T':return [1, 'trill'];
220
+ }
221
+ return [0, 0];
222
+ };
223
+
224
+ var letter_to_spacer = function(line, i)
225
+ {
226
+ var start = i;
227
+ while (tokenizer.isWhiteSpace(line.charAt(i)))
228
+ i++;
229
+ return [ i-start ];
230
+ };
231
+
232
+ // returns the class of the bar line
233
+ // the number of the repeat
234
+ // and the number of characters used up
235
+ // if 0 is returned, then the next element was not a bar line
236
+ var letter_to_bar = function(line, curr_pos)
237
+ {
238
+ var ret = tokenizer.getBarLine(line, curr_pos);
239
+ if (ret.len === 0)
240
+ return [0,""];
241
+ if (ret.warn) {
242
+ warn(ret.warn, line, curr_pos);
243
+ return [ret.len,""];
244
+ }
245
+
246
+ // Now see if this is a repeated ending
247
+ // A repeated ending is all of the characters 1,2,3,4,5,6,7,8,9,0,-, and comma
248
+ // It can also optionally start with '[', which is ignored.
249
+ // Also, it can have white space before the '['.
250
+ for (var ws = 0; ws < line.length; ws++)
251
+ if (line.charAt(curr_pos+ret.len+ws) !== ' ')
252
+ break;
253
+ var orig_bar_len = ret.len;
254
+ if (line.charAt(curr_pos+ret.len+ws) === '[') {
255
+ ret.len += ws + 1;
256
+ }
257
+
258
+ // It can also be a quoted string. It is unclear whether that construct requires '[', but it seems like it would. otherwise it would be confused with a regular chord.
259
+ if (line.charAt(curr_pos+ret.len) === '"' && line.charAt(curr_pos+ret.len-1) === '[') {
260
+ var ending = tokenizer.getBrackettedSubstring(line, curr_pos+ret.len, 5);
261
+ return [ret.len+ending[0], ret.token, ending[1]];
262
+ }
263
+ var retRep = tokenizer.getTokenOf(line.substring(curr_pos+ret.len), "1234567890-,");
264
+ if (retRep.len === 0 || retRep.token[0] === '-')
265
+ return [orig_bar_len, ret.token];
266
+
267
+ return [ret.len+retRep.len, ret.token, retRep.token];
268
+ };
269
+
270
+ var letter_to_open_slurs_and_triplets = function(line, i) {
271
+ // consume spaces, and look for all the open parens. If there is a number after the open paren,
272
+ // that is a triplet. Otherwise that is a slur. Collect all the slurs and the first triplet.
273
+ var ret = {};
274
+ var start = i;
275
+ while (line.charAt(i) === '(' || tokenizer.isWhiteSpace(line.charAt(i))) {
276
+ if (line.charAt(i) === '(') {
277
+ if (i+1 < line.length && (line.charAt(i+1) >= '2' && line.charAt(i+1) <= '9')) {
278
+ if (ret.triplet !== undefined)
279
+ warn("Can't nest triplets", line, i);
280
+ else {
281
+ ret.triplet = line.charAt(i+1) - '0';
282
+ if (i+2 < line.length && line.charAt(i+2) === ':') {
283
+ // We are expecting "(p:q:r" or "(p:q" or "(p::r" we are only interested in the first number (p) and the number of notes (r)
284
+ // if r is missing, then it is equal to p.
285
+ if (i+3 < line.length && line.charAt(i+3) === ':') {
286
+ if (i+4 < line.length && (line.charAt(i+4) >= '1' && line.charAt(i+4) <= '9')) {
287
+ ret.num_notes = line.charAt(i+4) - '0';
288
+ i += 3;
289
+ } else
290
+ warn("expected number after the two colons after the triplet to mark the duration", line, i);
291
+ } else if (i+3 < line.length && (line.charAt(i+3) >= '1' && line.charAt(i+3) <= '9')) {
292
+ // ignore this middle number
293
+ if (i+4 < line.length && line.charAt(i+4) === ':') {
294
+ if (i+5 < line.length && (line.charAt(i+5) >= '1' && line.charAt(i+5) <= '9')) {
295
+ ret.num_notes = line.charAt(i+5) - '0';
296
+ i += 4;
297
+ }
298
+ } else {
299
+ ret.num_notes = ret.triplet;
300
+ i += 3;
301
+ }
302
+ } else
303
+ warn("expected number after the triplet to mark the duration", line, i);
304
+ }
305
+ }
306
+ i++;
307
+ }
308
+ else {
309
+ if (ret.startSlur === undefined)
310
+ ret.startSlur = 1;
311
+ else
312
+ ret.startSlur++;
313
+ }
314
+ }
315
+ i++;
316
+ }
317
+ ret.consumed = i-start;
318
+ return ret;
319
+ };
320
+
321
+ var addWords = function(line, words) {
322
+ if (!line) { warn("Can't add words before the first line of mulsic", line, 0); return; }
323
+ words = window.ABCJS.parse.strip(words);
324
+ if (words.charAt(words.length-1) !== '-')
325
+ words = words + ' '; // Just makes it easier to parse below, since every word has a divider after it.
326
+ var word_list = [];
327
+ // first make a list of words from the string we are passed. A word is divided on either a space or dash.
328
+ var last_divider = 0;
329
+ var replace = false;
330
+ var addWord = function(i) {
331
+ var word = window.ABCJS.parse.strip(words.substring(last_divider, i));
332
+ last_divider = i+1;
333
+ if (word.length > 0) {
334
+ if (replace)
335
+ word = window.ABCJS.parse.gsub(word,'~', ' ');
336
+ var div = words.charAt(i);
337
+ if (div !== '_' && div !== '-')
338
+ div = ' ';
339
+ word_list.push({syllable: tokenizer.translateString(word), divider: div});
340
+ replace = false;
341
+ return true;
342
+ }
343
+ return false;
344
+ };
345
+ for (var i = 0; i < words.length; i++) {
346
+ switch (words.charAt(i)) {
347
+ case ' ':
348
+ case '\x12':
349
+ addWord(i);
350
+ break;
351
+ case '-':
352
+ if (!addWord(i) && word_list.length > 0) {
353
+ window.ABCJS.parse.last(word_list).divider = '-';
354
+ word_list.push({skip: true, to: 'next'});
355
+ }
356
+ break;
357
+ case '_':
358
+ addWord(i);
359
+ word_list.push({skip: true, to: 'slur'});
360
+ break;
361
+ case '*':
362
+ addWord(i);
363
+ word_list.push({skip: true, to: 'next'});
364
+ break;
365
+ case '|':
366
+ addWord(i);
367
+ word_list.push({skip: true, to: 'bar'});
368
+ break;
369
+ case '~':
370
+ replace = true;
371
+ break;
372
+ }
373
+ }
374
+
375
+ var inSlur = false;
376
+ window.ABCJS.parse.each(line, function(el) {
377
+ if (word_list.length !== 0) {
378
+ if (word_list[0].skip) {
379
+ switch (word_list[0].to) {
380
+ case 'next': if (el.el_type === 'note' && el.pitches !== null && !inSlur) word_list.shift(); break;
381
+ case 'slur': if (el.el_type === 'note' && el.pitches !== null) word_list.shift(); break;
382
+ case 'bar': if (el.el_type === 'bar') word_list.shift(); break;
383
+ }
384
+ } else {
385
+ if (el.el_type === 'note' && el.rest === undefined && !inSlur) {
386
+ var lyric = word_list.shift();
387
+ if (el.lyric === undefined)
388
+ el.lyric = [ lyric ];
389
+ else
390
+ el.lyric.push(lyric);
391
+ }
392
+ }
393
+ }
394
+ });
395
+ };
396
+
397
+ var addSymbols = function(line, words) {
398
+ // TODO-PER: Currently copied from w: line. This needs to be read as symbols instead.
399
+ if (!line) { warn("Can't add symbols before the first line of mulsic", line, 0); return; }
400
+ words = window.ABCJS.parse.strip(words);
401
+ if (words.charAt(words.length-1) !== '-')
402
+ words = words + ' '; // Just makes it easier to parse below, since every word has a divider after it.
403
+ var word_list = [];
404
+ // first make a list of words from the string we are passed. A word is divided on either a space or dash.
405
+ var last_divider = 0;
406
+ var replace = false;
407
+ var addWord = function(i) {
408
+ var word = window.ABCJS.parse.strip(words.substring(last_divider, i));
409
+ last_divider = i+1;
410
+ if (word.length > 0) {
411
+ if (replace)
412
+ word = window.ABCJS.parse.gsub(word, '~', ' ');
413
+ var div = words.charAt(i);
414
+ if (div !== '_' && div !== '-')
415
+ div = ' ';
416
+ word_list.push({syllable: tokenizer.translateString(word), divider: div});
417
+ replace = false;
418
+ return true;
419
+ }
420
+ return false;
421
+ };
422
+ for (var i = 0; i < words.length; i++) {
423
+ switch (words.charAt(i)) {
424
+ case ' ':
425
+ case '\x12':
426
+ addWord(i);
427
+ break;
428
+ case '-':
429
+ if (!addWord(i) && word_list.length > 0) {
430
+ window.ABCJS.parse.last(word_list).divider = '-';
431
+ word_list.push({skip: true, to: 'next'});
432
+ }
433
+ break;
434
+ case '_':
435
+ addWord(i);
436
+ word_list.push({skip: true, to: 'slur'});
437
+ break;
438
+ case '*':
439
+ addWord(i);
440
+ word_list.push({skip: true, to: 'next'});
441
+ break;
442
+ case '|':
443
+ addWord(i);
444
+ word_list.push({skip: true, to: 'bar'});
445
+ break;
446
+ case '~':
447
+ replace = true;
448
+ break;
449
+ }
450
+ }
451
+
452
+ var inSlur = false;
453
+ window.ABCJS.parse.each(line, function(el) {
454
+ if (word_list.length !== 0) {
455
+ if (word_list[0].skip) {
456
+ switch (word_list[0].to) {
457
+ case 'next': if (el.el_type === 'note' && el.pitches !== null && !inSlur) word_list.shift(); break;
458
+ case 'slur': if (el.el_type === 'note' && el.pitches !== null) word_list.shift(); break;
459
+ case 'bar': if (el.el_type === 'bar') word_list.shift(); break;
460
+ }
461
+ } else {
462
+ if (el.el_type === 'note' && el.rest === undefined && !inSlur) {
463
+ var lyric = word_list.shift();
464
+ if (el.lyric === undefined)
465
+ el.lyric = [ lyric ];
466
+ else
467
+ el.lyric.push(lyric);
468
+ }
469
+ }
470
+ }
471
+ });
472
+ };
473
+
474
+ var getBrokenRhythm = function(line, index) {
475
+ switch (line.charAt(index)) {
476
+ case '>':
477
+ if (index < line.length - 1 && line.charAt(index+1) === '>') // double >>
478
+ return [2, 1.75, 0.25];
479
+ else
480
+ return [1, 1.5, 0.5];
481
+ break;
482
+ case '<':
483
+ if (index < line.length - 1 && line.charAt(index+1) === '<') // double <<
484
+ return [2, 0.25, 1.75];
485
+ else
486
+ return [1, 0.5, 1.5];
487
+ break;
488
+ }
489
+ return null;
490
+ };
491
+
492
+ // TODO-PER: make this a method in el.
493
+ var addEndBeam = function(el) {
494
+ if (el.duration !== undefined && el.duration < 0.25)
495
+ el.end_beam = true;
496
+ return el;
497
+ };
498
+
499
+ var pitches = {A: 5, B: 6, C: 0, D: 1, E: 2, F: 3, G: 4, a: 12, b: 13, c: 7, d: 8, e: 9, f: 10, g: 11};
500
+ var rests = {x: 'invisible', y: 'spacer', z: 'rest', Z: 'multimeasure' };
501
+ var getCoreNote = function(line, index, el, canHaveBrokenRhythm) {
502
+ //var el = { startChar: index };
503
+ var isComplete = function(state) {
504
+ return (state === 'octave' || state === 'duration' || state === 'Zduration' || state === 'broken_rhythm' || state === 'end_slur');
505
+ };
506
+ var state = 'startSlur';
507
+ var durationSetByPreviousNote = false;
508
+ while (1) {
509
+ switch(line.charAt(index)) {
510
+ case '(':
511
+ if (state === 'startSlur') {
512
+ if (el.startSlur === undefined) el.startSlur = 1; else el.startSlur++;
513
+ } else if (isComplete(state)) {el.endChar = index;return el;}
514
+ else return null;
515
+ break;
516
+ case ')':
517
+ if (isComplete(state)) {
518
+ if (el.endSlur === undefined) el.endSlur = 1; else el.endSlur++;
519
+ } else return null;
520
+ break;
521
+ case '^':
522
+ if (state === 'startSlur') {el.accidental = 'sharp';state = 'sharp2';}
523
+ else if (state === 'sharp2') {el.accidental = 'dblsharp';state = 'pitch';}
524
+ else if (isComplete(state)) {el.endChar = index;return el;}
525
+ else return null;
526
+ break;
527
+ case '_':
528
+ if (state === 'startSlur') {el.accidental = 'flat';state = 'flat2';}
529
+ else if (state === 'flat2') {el.accidental = 'dblflat';state = 'pitch';}
530
+ else if (isComplete(state)) {el.endChar = index;return el;}
531
+ else return null;
532
+ break;
533
+ case '=':
534
+ if (state === 'startSlur') {el.accidental = 'natural';state = 'pitch';}
535
+ else if (isComplete(state)) {el.endChar = index;return el;}
536
+ else return null;
537
+ break;
538
+ case 'A':
539
+ case 'B':
540
+ case 'C':
541
+ case 'D':
542
+ case 'E':
543
+ case 'F':
544
+ case 'G':
545
+ case 'a':
546
+ case 'b':
547
+ case 'c':
548
+ case 'd':
549
+ case 'e':
550
+ case 'f':
551
+ case 'g':
552
+ if (state === 'startSlur' || state === 'sharp2' || state === 'flat2' || state === 'pitch') {
553
+ el.pitch = pitches[line.charAt(index)];
554
+ state = 'octave';
555
+ // At this point we have a valid note. The rest is optional. Set the duration in case we don't get one below
556
+ if (canHaveBrokenRhythm && multilineVars.next_note_duration !== 0) {
557
+ el.duration = multilineVars.next_note_duration;
558
+ multilineVars.next_note_duration = 0;
559
+ durationSetByPreviousNote = true;
560
+ } else
561
+ el.duration = multilineVars.default_length;
562
+ } else if (isComplete(state)) {el.endChar = index;return el;}
563
+ else return null;
564
+ break;
565
+ case ',':
566
+ if (state === 'octave') {el.pitch -= 7;}
567
+ else if (isComplete(state)) {el.endChar = index;return el;}
568
+ else return null;
569
+ break;
570
+ case '\'':
571
+ if (state === 'octave') {el.pitch += 7;}
572
+ else if (isComplete(state)) {el.endChar = index;return el;}
573
+ else return null;
574
+ break;
575
+ case 'x':
576
+ case 'y':
577
+ case 'z':
578
+ case 'Z':
579
+ if (state === 'startSlur') {
580
+ el.rest = { type: rests[line.charAt(index)] };
581
+ // There shouldn't be some of the properties that notes have. If some sneak in due to bad syntax in the abc file,
582
+ // just nix them here.
583
+ delete el.accidental;
584
+ delete el.startSlur;
585
+ delete el.startTie;
586
+ delete el.endSlur;
587
+ delete el.endTie;
588
+ delete el.end_beam;
589
+ delete el.grace_notes;
590
+ // At this point we have a valid note. The rest is optional. Set the duration in case we don't get one below
591
+ if (el.rest.type === 'multimeasure') {
592
+ el.duration = 1;
593
+ state = 'Zduration';
594
+ } else {
595
+ if (canHaveBrokenRhythm && multilineVars.next_note_duration !== 0) {
596
+ el.duration = multilineVars.next_note_duration;
597
+ multilineVars.next_note_duration = 0;
598
+ durationSetByPreviousNote = true;
599
+ } else
600
+ el.duration = multilineVars.default_length;
601
+ state = 'duration';
602
+ }
603
+ } else if (isComplete(state)) {el.endChar = index;return el;}
604
+ else return null;
605
+ break;
606
+ case '1':
607
+ case '2':
608
+ case '3':
609
+ case '4':
610
+ case '5':
611
+ case '6':
612
+ case '7':
613
+ case '8':
614
+ case '9':
615
+ case '0':
616
+ case '/':
617
+ if (state === 'octave' || state === 'duration') {
618
+ var fraction = tokenizer.getFraction(line, index);
619
+ if (!durationSetByPreviousNote)
620
+ el.duration = el.duration * fraction.value;
621
+ // TODO-PER: We can test the returned duration here and give a warning if it isn't the one expected.
622
+ el.endChar = fraction.index;
623
+ while (fraction.index < line.length && (tokenizer.isWhiteSpace(line.charAt(fraction.index)) || line.charAt(fraction.index) === '-')) {
624
+ if (line.charAt(fraction.index) === '-')
625
+ el.startTie = {};
626
+ else
627
+ el = addEndBeam(el);
628
+ fraction.index++;
629
+ }
630
+ index = fraction.index-1;
631
+ state = 'broken_rhythm';
632
+ } else if (state === 'sharp2') {
633
+ el.accidental = 'quartersharp';state = 'pitch';
634
+ } else if (state === 'flat2') {
635
+ el.accidental = 'quarterflat';state = 'pitch';
636
+ } else if (state === 'Zduration') {
637
+ var num = tokenizer.getNumber(line, index);
638
+ el.duration = num.num;
639
+ el.endChar = num.index;
640
+ return el;
641
+ } else return null;
642
+ break;
643
+ case '-':
644
+ if (state === 'startSlur') {
645
+ // This is the first character, so it must have been meant for the previous note. Correct that here.
646
+ tune.addTieToLastNote();
647
+ el.endTie = true;
648
+ } else if (state === 'octave' || state === 'duration' || state === 'end_slur') {
649
+ el.startTie = {};
650
+ if (!durationSetByPreviousNote && canHaveBrokenRhythm)
651
+ state = 'broken_rhythm';
652
+ else {
653
+ // Peek ahead to the next character. If it is a space, then we have an end beam.
654
+ if (tokenizer.isWhiteSpace(line.charAt(index+1)))
655
+ addEndBeam(el);
656
+ el.endChar = index+1;
657
+ return el;
658
+ }
659
+ } else if (state === 'broken_rhythm') {el.endChar = index;return el;}
660
+ else return null;
661
+ break;
662
+ case ' ':
663
+ case '\t':
664
+ if (isComplete(state)) {
665
+ el.end_beam = true;
666
+ // look ahead to see if there is a tie
667
+ do {
668
+ if (line.charAt(index) === '-')
669
+ el.startTie = {};
670
+ index++;
671
+ } while (index < line.length && (tokenizer.isWhiteSpace(line.charAt(index)) || line.charAt(index) === '-'));
672
+ el.endChar = index;
673
+ if (!durationSetByPreviousNote && canHaveBrokenRhythm && (line.charAt(index) === '<' || line.charAt(index) === '>')) { // TODO-PER: Don't need the test for < and >, but that makes the endChar work out for the regression test.
674
+ index--;
675
+ state = 'broken_rhythm';
676
+ } else
677
+ return el;
678
+ }
679
+ else return null;
680
+ break;
681
+ case '>':
682
+ case '<':
683
+ if (isComplete(state)) {
684
+ if (canHaveBrokenRhythm) {
685
+ var br2 = getBrokenRhythm(line, index);
686
+ index += br2[0] - 1; // index gets incremented below, so we'll let that happen
687
+ multilineVars.next_note_duration = br2[2]*el.duration;
688
+ el.duration = br2[1]*el.duration;
689
+ state = 'end_slur';
690
+ } else {
691
+ el.endChar = index;
692
+ return el;
693
+ }
694
+ } else
695
+ return null;
696
+ break;
697
+ default:
698
+ if (isComplete(state)) {
699
+ el.endChar = index;
700
+ return el;
701
+ }
702
+ return null;
703
+ }
704
+ index++;
705
+ if (index === line.length) {
706
+ if (isComplete(state)) {el.endChar = index;return el;}
707
+ else return null;
708
+ }
709
+ }
710
+ return null;
711
+ };
712
+
713
+ function startNewLine() {
714
+ var params = { startChar: -1, endChar: -1};
715
+ if (multilineVars.partForNextLine.length)
716
+ params.part = multilineVars.partForNextLine;
717
+ params.clef = multilineVars.currentVoice && multilineVars.staves[multilineVars.currentVoice.staffNum].clef !== undefined ? window.ABCJS.parse.clone(multilineVars.staves[multilineVars.currentVoice.staffNum].clef) : window.ABCJS.parse.clone(multilineVars.clef) ;
718
+ params.key = window.ABCJS.parse.parseKeyVoice.deepCopyKey(multilineVars.key);
719
+ window.ABCJS.parse.parseKeyVoice.addPosToKey(params.clef, params.key);
720
+ if (multilineVars.meter !== null) {
721
+ if (multilineVars.currentVoice) {
722
+ window.ABCJS.parse.each(multilineVars.staves, function(st) {
723
+ st.meter = multilineVars.meter;
724
+ });
725
+ params.meter = multilineVars.staves[multilineVars.currentVoice.staffNum].meter;
726
+ multilineVars.staves[multilineVars.currentVoice.staffNum].meter = null;
727
+ } else
728
+ params.meter = multilineVars.meter;
729
+ multilineVars.meter = null;
730
+ } else if (multilineVars.currentVoice && multilineVars.staves[multilineVars.currentVoice.staffNum].meter) {
731
+ // Make sure that each voice gets the meter marking.
732
+ params.meter = multilineVars.staves[multilineVars.currentVoice.staffNum].meter;
733
+ multilineVars.staves[multilineVars.currentVoice.staffNum].meter = null;
734
+ }
735
+ if (multilineVars.currentVoice && multilineVars.currentVoice.name)
736
+ params.name = multilineVars.currentVoice.name;
737
+ if (multilineVars.vocalfont)
738
+ params.vocalfont = multilineVars.vocalfont;
739
+ if (multilineVars.style)
740
+ params.style = multilineVars.style;
741
+ if (multilineVars.currentVoice) {
742
+ var staff = multilineVars.staves[multilineVars.currentVoice.staffNum];
743
+ if (staff.brace) params.brace = staff.brace;
744
+ if (staff.bracket) params.bracket = staff.bracket;
745
+ if (staff.connectBarLines) params.connectBarLines = staff.connectBarLines;
746
+ if (staff.name) params.name = staff.name[multilineVars.currentVoice.index];
747
+ if (staff.subname) params.subname = staff.subname[multilineVars.currentVoice.index];
748
+ if (multilineVars.currentVoice.stem)
749
+ params.stem = multilineVars.currentVoice.stem;
750
+ if (multilineVars.currentVoice.scale)
751
+ params.scale = multilineVars.currentVoice.scale;
752
+ if (multilineVars.currentVoice.style)
753
+ params.style = multilineVars.currentVoice.style;
754
+ }
755
+ tune.startNewLine(params);
756
+
757
+ multilineVars.partForNextLine = "";
758
+ if (multilineVars.currentVoice === undefined || (multilineVars.currentVoice.staffNum === multilineVars.staves.length-1 && multilineVars.staves[multilineVars.currentVoice.staffNum].numVoices-1 === multilineVars.currentVoice.index)) {
759
+ //multilineVars.meter = null;
760
+ if (multilineVars.barNumbers === 0)
761
+ multilineVars.barNumOnNextNote = multilineVars.currBarNumber;
762
+ }
763
+ }
764
+
765
+ var letter_to_grace = function(line, i) {
766
+ // Grace notes are an array of: startslur, note, endslur, space; where note is accidental, pitch, duration
767
+ if (line.charAt(i) === '{') {
768
+ // fetch the gracenotes string and consume that into the array
769
+ var gra = tokenizer.getBrackettedSubstring(line, i, 1, '}');
770
+ if (!gra[2])
771
+ warn("Missing the closing '}' while parsing grace note", line, i);
772
+ // If there is a slur after the grace construction, then move it to the last note inside the grace construction
773
+ if (line[i+gra[0]] === ')') {
774
+ gra[0]++;
775
+ gra[1] += ')';
776
+ }
777
+
778
+ var gracenotes = [];
779
+ var ii = 0;
780
+ var inTie = false;
781
+ while (ii < gra[1].length) {
782
+ var acciaccatura = false;
783
+ if (gra[1].charAt(ii) === '/') {
784
+ acciaccatura = true;
785
+ ii++;
786
+ }
787
+ var note = getCoreNote(gra[1], ii, {}, false);
788
+ if (note !== null) {
789
+ if (acciaccatura)
790
+ note.acciaccatura = true;
791
+ gracenotes.push(note);
792
+
793
+ if (inTie) {
794
+ note.endTie = true;
795
+ inTie = false;
796
+ }
797
+ if (note.startTie)
798
+ inTie = true;
799
+
800
+ ii = note.endChar;
801
+ delete note.endChar;
802
+ }
803
+ else {
804
+ // We shouldn't get anything but notes or a space here, so report an error
805
+ if (gra[1].charAt(ii) === ' ') {
806
+ if (gracenotes.length > 0)
807
+ gracenotes[gracenotes.length-1].end_beam = true;
808
+ } else
809
+ warn("Unknown character '" + gra[1].charAt(ii) + "' while parsing grace note", line, i);
810
+ ii++;
811
+ }
812
+ }
813
+ if (gracenotes.length)
814
+ return [gra[0], gracenotes];
815
+ // for (var ret = letter_to_pitch(gra[1], ii); ret[0]>0 && ii<gra[1].length;
816
+ // ret = letter_to_pitch(gra[1], ii)) {
817
+ // //todo get other stuff that could be in a grace note
818
+ // ii += ret[0];
819
+ // gracenotes.push({el_type:"gracenote",pitch:ret[1]});
820
+ // }
821
+ // return [ gra[0], gracenotes ];
822
+ }
823
+ return [ 0 ];
824
+ };
825
+
826
+ //
827
+ // Parse line of music
828
+ //
829
+ // This is a stream of <(bar-marking|header|note-group)...> in any order, with optional spaces between each element
830
+ // core-note is <open-slur, accidental, pitch:required, octave, duration, close-slur&|tie> with no spaces within that
831
+ // chord is <open-bracket:required, core-note:required... close-bracket:required duration> with no spaces within that
832
+ // grace-notes is <open-brace:required, (open-slur|core-note:required|close-slur)..., close-brace:required> spaces are allowed
833
+ // note-group is <grace-notes, chord symbols&|decorations..., grace-notes, slur&|triplet, chord|core-note, end-slur|tie> spaces are allowed between items
834
+ // bar-marking is <ampersand> or <chord symbols&|decorations..., bar:required> spaces allowed
835
+ // header is <open-bracket:required, K|M|L|V:required, colon:required, field:required, close-bracket:required> spaces can occur between the colon, in the field, and before the close bracket
836
+ // header can also be the only thing on a line. This is true even if it is a continuation line. In this case the brackets are not required.
837
+ // a space is a back-tick, a space, or a tab. If it is a back-tick, then there is no end-beam.
838
+
839
+ // Line preprocessing: anything after a % is ignored (the double %% should have been taken care of before this)
840
+ // Then, all leading and trailing spaces are ignored.
841
+ // If there was a line continuation, the \n was replaced by a \r and the \ was replaced by a space. This allows the construct
842
+ // of having a header mid-line conceptually, but actually be at the start of the line. This is equivolent to putting the header in [ ].
843
+
844
+ // TODO-PER: How to handle ! for line break?
845
+ // TODO-PER: dots before bar, dots before slur
846
+ // TODO-PER: U: redefinable symbols.
847
+
848
+ // Ambiguous symbols:
849
+ // "[" can be the start of a chord, the start of a header element or part of a bar line.
850
+ // --- if it is immediately followed by "|", it is a bar line
851
+ // --- if it is immediately followed by K: L: M: V: it is a header (note: there are other headers mentioned in the standard, but I'm not sure how they would be used.)
852
+ // --- otherwise it is the beginning of a chord
853
+ // "(" can be the start of a slur or a triplet
854
+ // --- if it is followed by a number from 2-9, then it is a triplet
855
+ // --- otherwise it is a slur
856
+ // "]"
857
+ // --- if there is a chord open, then this is the close
858
+ // --- if it is after a [|, then it is an invisible bar line
859
+ // --- otherwise, it is par of a bar
860
+ // "." can be a bar modifier or a slur modifier, or a decoration
861
+ // --- if it comes immediately before a bar, it is a bar modifier
862
+ // --- if it comes immediately before a slur, it is a slur modifier
863
+ // --- otherwise it is a decoration for the next note.
864
+ // number:
865
+ // --- if it is after a bar, with no space, it is an ending marker
866
+ // --- if it is after a ( with no space, it is a triplet count
867
+ // --- if it is after a pitch or octave or slash, then it is a duration
868
+
869
+ // Unambiguous symbols (except inside quoted strings):
870
+ // vertical-bar, colon: part of a bar
871
+ // ABCDEFGabcdefg: pitch
872
+ // xyzZ: rest
873
+ // comma, prime: octave
874
+ // close-paren: end-slur
875
+ // hyphen: tie
876
+ // tilde, v, u, bang, plus, THLMPSO: decoration
877
+ // carat, underscore, equal: accidental
878
+ // ampersand: time reset
879
+ // open-curly, close-curly: grace notes
880
+ // double-quote: chord symbol
881
+ // less-than, greater-than, slash: duration
882
+ // back-tick, space, tab: space
883
+ var nonDecorations = "ABCDEFGabcdefgxyzZ[]|^_{"; // use this to prescreen so we don't have to look for a decoration at every note.
884
+
885
+ var parseRegularMusicLine = function(line) {
886
+ header.resolveTempo();
887
+ //multilineVars.havent_set_length = false; // To late to set this now.
888
+ multilineVars.is_in_header = false; // We should have gotten a key header by now, but just in case, this is definitely out of the header.
889
+ var i = 0;
890
+ var startOfLine = multilineVars.iChar;
891
+ // see if there is nothing but a comment on this line. If so, just ignore it. A full line comment is optional white space followed by %
892
+ while (tokenizer.isWhiteSpace(line.charAt(i)) && i < line.length)
893
+ i++;
894
+ if (i === line.length || line.charAt(i) === '%')
895
+ return;
896
+
897
+ // Start with the standard staff, clef and key symbols on each line
898
+ var delayStartNewLine = multilineVars.start_new_line;
899
+ // if (multilineVars.start_new_line) {
900
+ // startNewLine();
901
+ // }
902
+ if (multilineVars.continueall === undefined)
903
+ multilineVars.start_new_line = true;
904
+ else
905
+ multilineVars.start_new_line = false;
906
+ var tripletNotesLeft = 0;
907
+ //var tripletMultiplier = 0;
908
+ // var inTie = false;
909
+ // var inTieChord = {};
910
+
911
+ // See if the line starts with a header field
912
+ var retHeader = header.letter_to_body_header(line, i);
913
+ if (retHeader[0] > 0) {
914
+ i += retHeader[0];
915
+ // TODO-PER: Handle inline headers
916
+ }
917
+ var el = { };
918
+
919
+ while (i < line.length)
920
+ {
921
+ var startI = i;
922
+ if (line.charAt(i) === '%')
923
+ break;
924
+
925
+ var retInlineHeader = header.letter_to_inline_header(line, i);
926
+ if (retInlineHeader[0] > 0) {
927
+ i += retInlineHeader[0];
928
+ // TODO-PER: Handle inline headers
929
+ //multilineVars.start_new_line = false;
930
+ } else {
931
+ // Wait until here to actually start the line because we know we're past the inline statements.
932
+ if (delayStartNewLine) {
933
+ startNewLine();
934
+ delayStartNewLine = false;
935
+ }
936
+ // var el = { };
937
+
938
+ // We need to decide if the following characters are a bar-marking or a note-group.
939
+ // Unfortunately, that is ambiguous. Both can contain chord symbols and decorations.
940
+ // If there is a grace note either before or after the chord symbols and decorations, then it is definitely a note-group.
941
+ // If there is a bar marker, it is definitely a bar-marking.
942
+ // If there is either a core-note or chord, it is definitely a note-group.
943
+ // So, loop while we find grace-notes, chords-symbols, or decorations. [It is an error to have more than one grace-note group in a row; the others can be multiple]
944
+ // Then, if there is a grace-note, we know where to go.
945
+ // Else see if we have a chord, core-note, slur, triplet, or bar.
946
+
947
+ var ret;
948
+ while (1) {
949
+ ret = tokenizer.eatWhiteSpace(line, i);
950
+ if (ret > 0) {
951
+ i += ret;
952
+ }
953
+ if (i > 0 && line.charAt(i-1) === '\x12') {
954
+ // there is one case where a line continuation isn't the same as being on the same line, and that is if the next character after it is a header.
955
+ ret = header.letter_to_body_header(line, i);
956
+ if (ret[0] > 0) {
957
+ // TODO: insert header here
958
+ i = ret[0];
959
+ multilineVars.start_new_line = false;
960
+ }
961
+ }
962
+ // gather all the grace notes, chord symbols and decorations
963
+ ret = letter_to_spacer(line, i);
964
+ if (ret[0] > 0) {
965
+ i += ret[0];
966
+ }
967
+
968
+ ret = letter_to_chord(line, i);
969
+ if (ret[0] > 0) {
970
+ // There could be more than one chord here if they have different positions.
971
+ // If two chords have the same position, then connect them with newline.
972
+ if (!el.chord)
973
+ el.chord = [];
974
+ var chordName = tokenizer.translateString(ret[1]);
975
+ chordName = chordName.replace(/;/g, "\n");
976
+ var addedChord = false;
977
+ for (var ci = 0; ci < el.chord.length; ci++) {
978
+ if (el.chord[ci].position === ret[2]) {
979
+ addedChord = true;
980
+ el.chord[ci].name += "\n" + chordName;
981
+ }
982
+ }
983
+ if (addedChord === false) {
984
+ if (ret[2] === null && ret[3])
985
+ el.chord.push({name: chordName, rel_position: ret[3]});
986
+ else
987
+ el.chord.push({name: chordName, position: ret[2]});
988
+ }
989
+
990
+ i += ret[0];
991
+ var ii = tokenizer.skipWhiteSpace(line.substring(i));
992
+ if (ii > 0)
993
+ el.force_end_beam_last = true;
994
+ i += ii;
995
+ } else {
996
+ if (nonDecorations.indexOf(line.charAt(i)) === -1)
997
+ ret = letter_to_accent(line, i);
998
+ else ret = [ 0 ];
999
+ if (ret[0] > 0) {
1000
+ if (ret[1] === null) {
1001
+ if (i+1 < line.length)
1002
+ startNewLine(); // There was a ! in the middle of the line. Start a new line if there is anything after it.
1003
+ } else if (ret[1].length > 0) {
1004
+ if (el.decoration === undefined)
1005
+ el.decoration = [];
1006
+ el.decoration.push(ret[1]);
1007
+ }
1008
+ i += ret[0];
1009
+ } else {
1010
+ ret = letter_to_grace(line, i);
1011
+ // TODO-PER: Be sure there aren't already grace notes defined. That is an error.
1012
+ if (ret[0] > 0) {
1013
+ el.gracenotes = ret[1];
1014
+ i += ret[0];
1015
+ } else
1016
+ break;
1017
+ }
1018
+ }
1019
+ }
1020
+
1021
+ ret = letter_to_bar(line, i);
1022
+ if (ret[0] > 0) {
1023
+ // This is definitely a bar
1024
+ if (el.gracenotes !== undefined) {
1025
+ // Attach the grace note to an invisible note
1026
+ el.rest = { type: 'spacer' };
1027
+ el.duration = 0.125; // TODO-PER: I don't think the duration of this matters much, but figure out if it does.
1028
+ tune.appendElement('note', startOfLine+i, startOfLine+i+ret[0], el);
1029
+ multilineVars.measureNotEmpty = true;
1030
+ el = {};
1031
+ }
1032
+ var bar = {type: ret[1]};
1033
+ if (bar.type.length === 0)
1034
+ warn("Unknown bar type", line, i);
1035
+ else {
1036
+ if (multilineVars.inEnding && bar.type !== 'bar_thin') {
1037
+ bar.endEnding = true;
1038
+ multilineVars.inEnding = false;
1039
+ }
1040
+ if (ret[2]) {
1041
+ bar.startEnding = ret[2];
1042
+ if (multilineVars.inEnding)
1043
+ bar.endEnding = true;
1044
+ multilineVars.inEnding = true;
1045
+ }
1046
+ if (el.decoration !== undefined)
1047
+ bar.decoration = el.decoration;
1048
+ if (el.chord !== undefined)
1049
+ bar.chord = el.chord;
1050
+ if (bar.startEnding && multilineVars.barFirstEndingNum === undefined)
1051
+ multilineVars.barFirstEndingNum = multilineVars.currBarNumber;
1052
+ else if (bar.startEnding && bar.endEnding && multilineVars.barFirstEndingNum)
1053
+ multilineVars.currBarNumber = multilineVars.barFirstEndingNum;
1054
+ else if (bar.endEnding)
1055
+ multilineVars.barFirstEndingNum = undefined;
1056
+ if (bar.type !== 'bar_invisible' && multilineVars.measureNotEmpty) {
1057
+ multilineVars.currBarNumber++;
1058
+ if (multilineVars.barNumbers && multilineVars.currBarNumber % multilineVars.barNumbers === 0)
1059
+ multilineVars.barNumOnNextNote = multilineVars.currBarNumber;
1060
+ }
1061
+ tune.appendElement('bar', startOfLine+i, startOfLine+i+ret[0], bar);
1062
+ multilineVars.measureNotEmpty = false;
1063
+ el = {};
1064
+ }
1065
+ i += ret[0];
1066
+ } else if (line[i] === '&') { // backtrack to beginning of measure
1067
+ warn("Overlay not yet supported", line, i);
1068
+ i++;
1069
+
1070
+ } else {
1071
+ // This is definitely a note group
1072
+ //
1073
+ // Look for as many open slurs and triplets as there are. (Note: only the first triplet is valid.)
1074
+ ret = letter_to_open_slurs_and_triplets(line, i);
1075
+ if (ret.consumed > 0) {
1076
+ if (ret.startSlur !== undefined)
1077
+ el.startSlur = ret.startSlur;
1078
+ if (ret.triplet !== undefined) {
1079
+ if (tripletNotesLeft > 0)
1080
+ warn("Can't nest triplets", line, i);
1081
+ else {
1082
+ el.startTriplet = ret.triplet;
1083
+ tripletNotesLeft = ret.num_notes === undefined ? ret.triplet : ret.num_notes;
1084
+ }
1085
+ }
1086
+ i += ret.consumed;
1087
+ }
1088
+
1089
+ // handle chords.
1090
+ if (line.charAt(i) === '[') {
1091
+ i++;
1092
+ var chordDuration = null;
1093
+
1094
+ var done = false;
1095
+ while (!done) {
1096
+ var chordNote = getCoreNote(line, i, {}, false);
1097
+ if (chordNote !== null) {
1098
+ if (chordNote.end_beam) {
1099
+ el.end_beam = true;
1100
+ delete chordNote.end_beam;
1101
+ }
1102
+ if (el.pitches === undefined) {
1103
+ el.duration = chordNote.duration;
1104
+ el.pitches = [ chordNote ];
1105
+ } else // Just ignore the note lengths of all but the first note. The standard isn't clear here, but this seems less confusing.
1106
+ el.pitches.push(chordNote);
1107
+ delete chordNote.duration;
1108
+
1109
+ if (multilineVars.inTieChord[el.pitches.length]) {
1110
+ chordNote.endTie = true;
1111
+ multilineVars.inTieChord[el.pitches.length] = undefined;
1112
+ }
1113
+ if (chordNote.startTie)
1114
+ multilineVars.inTieChord[el.pitches.length] = true;
1115
+
1116
+ i = chordNote.endChar;
1117
+ delete chordNote.endChar;
1118
+ } else if (line.charAt(i) === ' ') {
1119
+ // Spaces are not allowed in chords, but we can recover from it by ignoring it.
1120
+ warn("Spaces are not allowed in chords", line, i);
1121
+ i++;
1122
+ } else {
1123
+ if (i < line.length && line.charAt(i) === ']') {
1124
+ // consume the close bracket
1125
+ i++;
1126
+
1127
+ if (multilineVars.next_note_duration !== 0) {
1128
+ el.duration = el.duration * multilineVars.next_note_duration;
1129
+ // window.ABCJS.parse.each(el.pitches, function(p) {
1130
+ // p.duration = p.duration * multilineVars.next_note_duration;
1131
+ // });
1132
+ multilineVars.next_note_duration = 0;
1133
+ }
1134
+
1135
+ if (multilineVars.inTie) {
1136
+ window.ABCJS.parse.each(el.pitches, function(pitch) { pitch.endTie = true; });
1137
+ multilineVars.inTie = false;
1138
+ }
1139
+
1140
+ if (tripletNotesLeft > 0) {
1141
+ tripletNotesLeft--;
1142
+ if (tripletNotesLeft === 0) {
1143
+ el.endTriplet = true;
1144
+ }
1145
+ }
1146
+
1147
+ // if (el.startSlur !== undefined) {
1148
+ // window.ABCJS.parse.each(el.pitches, function(pitch) { if (pitch.startSlur === undefined) pitch.startSlur = el.startSlur; else pitch.startSlur += el.startSlur; });
1149
+ // delete el.startSlur;
1150
+ // }
1151
+
1152
+ var postChordDone = false;
1153
+ while (i < line.length && !postChordDone) {
1154
+ switch (line.charAt(i)) {
1155
+ case ' ':
1156
+ case '\t':
1157
+ addEndBeam(el);
1158
+ break;
1159
+ case ')':
1160
+ if (el.endSlur === undefined) el.endSlur = 1; else el.endSlur++;
1161
+ //window.ABCJS.parse.each(el.pitches, function(pitch) { if (pitch.endSlur === undefined) pitch.endSlur = 1; else pitch.endSlur++; });
1162
+ break;
1163
+ case '-':
1164
+ window.ABCJS.parse.each(el.pitches, function(pitch) { pitch.startTie = {}; });
1165
+ multilineVars.inTie = true;
1166
+ break;
1167
+ case '>':
1168
+ case '<':
1169
+ var br2 = getBrokenRhythm(line, i);
1170
+ i += br2[0] - 1; // index gets incremented below, so we'll let that happen
1171
+ multilineVars.next_note_duration = br2[2];
1172
+ chordDuration = br2[1];
1173
+ break;
1174
+ case '1':
1175
+ case '2':
1176
+ case '3':
1177
+ case '4':
1178
+ case '5':
1179
+ case '6':
1180
+ case '7':
1181
+ case '8':
1182
+ case '9':
1183
+ case '/':
1184
+ var fraction = tokenizer.getFraction(line, i);
1185
+ chordDuration = fraction.value;
1186
+ i = fraction.index;
1187
+ postChordDone = true;
1188
+ break;
1189
+ default:
1190
+ postChordDone = true;
1191
+ break;
1192
+ }
1193
+ if (!postChordDone) {
1194
+ i++;
1195
+ }
1196
+ }
1197
+ } else
1198
+ warn("Expected ']' to end the chords", line, i);
1199
+
1200
+ if (el.pitches !== undefined) {
1201
+ if (chordDuration !== null) {
1202
+ el.duration = el.duration * chordDuration;
1203
+ // window.ABCJS.parse.each(el.pitches, function(p) {
1204
+ // p.duration = p.duration * chordDuration;
1205
+ // });
1206
+ }
1207
+ if (multilineVars.barNumOnNextNote) {
1208
+ el.barNumber = multilineVars.barNumOnNextNote;
1209
+ multilineVars.barNumOnNextNote = null;
1210
+ }
1211
+ tune.appendElement('note', startOfLine+i, startOfLine+i, el);
1212
+ multilineVars.measureNotEmpty = true;
1213
+ el = {};
1214
+ }
1215
+ done = true;
1216
+ }
1217
+ }
1218
+
1219
+ } else {
1220
+ // Single pitch
1221
+ var el2 = {};
1222
+ var core = getCoreNote(line, i, el2, true);
1223
+ if (el2.endTie !== undefined) multilineVars.inTie = true;
1224
+ if (core !== null) {
1225
+ if (core.pitch !== undefined) {
1226
+ el.pitches = [ { } ];
1227
+ // TODO-PER: straighten this out so there is not so much copying: getCoreNote shouldn't change e'
1228
+ if (core.accidental !== undefined) el.pitches[0].accidental = core.accidental;
1229
+ el.pitches[0].pitch = core.pitch;
1230
+ if (core.endSlur !== undefined) el.pitches[0].endSlur = core.endSlur;
1231
+ if (core.endTie !== undefined) el.pitches[0].endTie = core.endTie;
1232
+ if (core.startSlur !== undefined) el.pitches[0].startSlur = core.startSlur;
1233
+ if (el.startSlur !== undefined) el.pitches[0].startSlur = el.startSlur;
1234
+ if (core.startTie !== undefined) el.pitches[0].startTie = core.startTie;
1235
+ if (el.startTie !== undefined) el.pitches[0].startTie = el.startTie;
1236
+ } else {
1237
+ el.rest = core.rest;
1238
+ if (core.endSlur !== undefined) el.endSlur = core.endSlur;
1239
+ if (core.endTie !== undefined) el.rest.endTie = core.endTie;
1240
+ if (core.startSlur !== undefined) el.startSlur = core.startSlur;
1241
+ //if (el.startSlur !== undefined) el.startSlur = el.startSlur;
1242
+ if (core.startTie !== undefined) el.rest.startTie = core.startTie;
1243
+ if (el.startTie !== undefined) el.rest.startTie = el.startTie;
1244
+ }
1245
+
1246
+ if (core.chord !== undefined) el.chord = core.chord;
1247
+ if (core.duration !== undefined) el.duration = core.duration;
1248
+ if (core.decoration !== undefined) el.decoration = core.decoration;
1249
+ if (core.graceNotes !== undefined) el.graceNotes = core.graceNotes;
1250
+ delete el.startSlur;
1251
+ if (multilineVars.inTie) {
1252
+ if (el.pitches !== undefined)
1253
+ el.pitches[0].endTie = true;
1254
+ else
1255
+ el.rest.endTie = true;
1256
+ multilineVars.inTie = false;
1257
+ }
1258
+ if (core.startTie || el.startTie)
1259
+ multilineVars.inTie = true;
1260
+ i = core.endChar;
1261
+
1262
+ if (tripletNotesLeft > 0) {
1263
+ tripletNotesLeft--;
1264
+ if (tripletNotesLeft === 0) {
1265
+ el.endTriplet = true;
1266
+ }
1267
+ }
1268
+
1269
+ if (core.end_beam)
1270
+ addEndBeam(el);
1271
+
1272
+ if (multilineVars.barNumOnNextNote) {
1273
+ el.barNumber = multilineVars.barNumOnNextNote;
1274
+ multilineVars.barNumOnNextNote = null;
1275
+ }
1276
+ tune.appendElement('note', startOfLine+startI, startOfLine+i, el);
1277
+ multilineVars.measureNotEmpty = true;
1278
+ el = {};
1279
+ }
1280
+ }
1281
+
1282
+ if (i === startI) { // don't know what this is, so ignore it.
1283
+ if (line.charAt(i) !== ' ' && line.charAt(i) !== '`')
1284
+ warn("Unknown character ignored", line, i);
1285
+ // warn("Unknown character ignored (" + line.charCodeAt(i) + ")", line, i);
1286
+ i++;
1287
+ }
1288
+ }
1289
+ }
1290
+ }
1291
+ };
1292
+
1293
+ var parseLine = function(line) {
1294
+ var ret = header.parseHeader(line);
1295
+ if (ret.regular)
1296
+ parseRegularMusicLine(ret.str);
1297
+ if (ret.newline && multilineVars.continueall === undefined)
1298
+ startNewLine();
1299
+ if (ret.words)
1300
+ addWords(tune.getCurrentVoice(), line.substring(2));
1301
+ if (ret.symbols)
1302
+ addSymbols(tune.getCurrentVoice(), line.substring(2));
1303
+ if (ret.recurse)
1304
+ parseLine(ret.str);
1305
+ };
1306
+
1307
+ this.parse = function(strTune, switches) {
1308
+ // the switches are optional and cause a difference in the way the tune is parsed.
1309
+ // switches.header_only : stop parsing when the header is finished
1310
+ // switches.stop_on_warning : stop at the first warning encountered.
1311
+ // switches.print: format for the page instead of the browser.
1312
+ tune.reset();
1313
+ if (switches && switches.print)
1314
+ tune.media = 'print';
1315
+ multilineVars.reset();
1316
+ // Take care of whatever line endings come our way
1317
+ strTune = window.ABCJS.parse.gsub(strTune, '\r\n', '\n');
1318
+ strTune = window.ABCJS.parse.gsub(strTune, '\r', '\n');
1319
+ strTune += '\n'; // Tacked on temporarily to make the last line continuation work
1320
+ strTune = strTune.replace(/\n\\.*\n/g, "\n"); // get rid of latex commands.
1321
+ var continuationReplacement = function(all, backslash, comment){
1322
+ var spaces = " ";
1323
+ var padding = comment ? spaces.substring(0, comment.length) : "";
1324
+ return backslash + " \x12" + padding;
1325
+ };
1326
+ strTune = strTune.replace(/\\([ \t]*)(%.*)*\n/g, continuationReplacement); // take care of line continuations right away, but keep the same number of characters
1327
+ var lines = strTune.split('\n');
1328
+ if (window.ABCJS.parse.last(lines).length === 0) // remove the blank line we added above.
1329
+ lines.pop();
1330
+ try {
1331
+ window.ABCJS.parse.each(lines, function(line) {
1332
+ if (switches) {
1333
+ if (switches.header_only && multilineVars.is_in_header === false)
1334
+ throw "normal_abort";
1335
+ if (switches.stop_on_warning && multilineVars.warnings)
1336
+ throw "normal_abort";
1337
+ }
1338
+ if (multilineVars.is_in_history) {
1339
+ if (line.charAt(1) === ':') {
1340
+ multilineVars.is_in_history = false;
1341
+ parseLine(line);
1342
+ } else
1343
+ tune.addMetaText("history", tokenizer.translateString(tokenizer.stripComment(line)));
1344
+ } else if (multilineVars.inTextBlock) {
1345
+ if (window.ABCJS.parse.startsWith(line, "%%endtext")) {
1346
+ //tune.addMetaText("textBlock", multilineVars.textBlock);
1347
+ tune.addText(multilineVars.textBlock);
1348
+ multilineVars.inTextBlock = false;
1349
+ }
1350
+ else {
1351
+ if (window.ABCJS.parse.startsWith(line, "%%"))
1352
+ multilineVars.textBlock += ' ' + line.substring(2);
1353
+ else
1354
+ multilineVars.textBlock += ' ' + line;
1355
+ }
1356
+ } else if (multilineVars.inPsBlock) {
1357
+ if (window.ABCJS.parse.startsWith(line, "%%endps")) {
1358
+ // Just ignore postscript
1359
+ multilineVars.inPsBlock = false;
1360
+ }
1361
+ else
1362
+ multilineVars.textBlock += ' ' + line;
1363
+ } else
1364
+ parseLine(line);
1365
+ multilineVars.iChar += line.length + 1;
1366
+ });
1367
+ var ph = 11*72;
1368
+ var pl = 8.5*72;
1369
+ switch (multilineVars.papersize) {
1370
+ //case "letter": ph = 11*72; pl = 8.5*72; break;
1371
+ case "legal": ph = 14*72; pl = 8.5*72; break;
1372
+ case "A4": ph = 11.7*72; pl = 8.3*72; break;
1373
+ }
1374
+ if (multilineVars.landscape) {
1375
+ var x = ph;
1376
+ ph = pl;
1377
+ pl = x;
1378
+ }
1379
+ tune.cleanUp(pl, ph, multilineVars.barsperstaff, multilineVars.staffnonote);
1380
+ } catch (err) {
1381
+ if (err !== "normal_abort")
1382
+ throw err;
1383
+ }
1384
+ };
1385
+ };