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,521 @@
1
+ // abc_parse_header.js: parses a the header fields from 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.ParseHeader = function(tokenizer, warn, multilineVars, tune) {
26
+ window.ABCJS.parse.parseKeyVoice.initialize(tokenizer, warn, multilineVars, tune);
27
+ window.ABCJS.parse.parseDirective.initialize(tokenizer, warn, multilineVars, tune);
28
+
29
+
30
+ this.setTitle = function(title) {
31
+ if (multilineVars.hasMainTitle)
32
+ tune.addSubtitle(tokenizer.translateString(tokenizer.stripComment(title))); // display secondary title
33
+ else
34
+ {
35
+ tune.addMetaText("title", tokenizer.translateString(tokenizer.theReverser(tokenizer.stripComment(title))));
36
+ multilineVars.hasMainTitle = true;
37
+ }
38
+ };
39
+
40
+ this.setMeter = function(line) {
41
+ line = tokenizer.stripComment(line);
42
+ if (line === 'C') {
43
+ if (multilineVars.havent_set_length === true)
44
+ multilineVars.default_length = 0.125;
45
+ return {type: 'common_time'};
46
+ } else if (line === 'C|') {
47
+ if (multilineVars.havent_set_length === true)
48
+ multilineVars.default_length = 0.125;
49
+ return {type: 'cut_time'};
50
+ } else if (line === 'o') {
51
+ if (multilineVars.havent_set_length === true)
52
+ multilineVars.default_length = 0.125;
53
+ return {type: 'tempus_perfectum'};
54
+ } else if (line === 'c') {
55
+ if (multilineVars.havent_set_length === true)
56
+ multilineVars.default_length = 0.125;
57
+ return {type: 'tempus_imperfectum'};
58
+ } else if (line === 'o.') {
59
+ if (multilineVars.havent_set_length === true)
60
+ multilineVars.default_length = 0.125;
61
+ return {type: 'tempus_perfectum_prolatio'};
62
+ } else if (line === 'c.') {
63
+ if (multilineVars.havent_set_length === true)
64
+ multilineVars.default_length = 0.125;
65
+ return {type: 'tempus_imperfectum_prolatio'};
66
+ } else if (line.length === 0 || line.toLowerCase() === 'none') {
67
+ if (multilineVars.havent_set_length === true)
68
+ multilineVars.default_length = 0.125;
69
+ return null;
70
+ }
71
+ else
72
+ {
73
+ var tokens = tokenizer.tokenize(line, 0, line.length);
74
+ // the form is [open_paren] decimal [ plus|dot decimal ]... [close_paren] slash decimal [plus same_as_before]
75
+ try {
76
+ var parseNum = function() {
77
+ // handles this much: [open_paren] decimal [ plus|dot decimal ]... [close_paren]
78
+ var ret = {value: 0, num: ""};
79
+
80
+ var tok = tokens.shift();
81
+ if (tok.token === '(')
82
+ tok = tokens.shift();
83
+ while (1) {
84
+ if (tok.type !== 'number') throw "Expected top number of meter";
85
+ ret.value += parseInt(tok.token);
86
+ ret.num += tok.token;
87
+ if (tokens.length === 0 || tokens[0].token === '/') return ret;
88
+ tok = tokens.shift();
89
+ if (tok.token === ')') {
90
+ if (tokens.length === 0 || tokens[0].token === '/') return ret;
91
+ throw "Unexpected paren in meter";
92
+ }
93
+ if (tok.token !== '.' && tok.token !== '+') throw "Expected top number of meter";
94
+ ret.num += tok.token;
95
+ if (tokens.length === 0) throw "Expected top number of meter";
96
+ tok = tokens.shift();
97
+ }
98
+ return ret; // just to suppress warning
99
+ };
100
+
101
+ var parseFraction = function() {
102
+ // handles this much: parseNum slash decimal
103
+ var ret = parseNum();
104
+ if (tokens.length === 0) return ret;
105
+ var tok = tokens.shift();
106
+ if (tok.token !== '/') throw "Expected slash in meter";
107
+ tok = tokens.shift();
108
+ if (tok.type !== 'number') throw "Expected bottom number of meter";
109
+ ret.den = tok.token;
110
+ ret.value = ret.value / parseInt(ret.den);
111
+ return ret;
112
+ };
113
+
114
+ if (tokens.length === 0) throw "Expected meter definition in M: line";
115
+ var meter = {type: 'specified', value: [ ]};
116
+ var totalLength = 0;
117
+ while (1) {
118
+ var ret = parseFraction();
119
+ totalLength += ret.value;
120
+ var mv = { num: ret.num };
121
+ if (ret.den !== undefined)
122
+ mv.den = ret.den;
123
+ meter.value.push(mv);
124
+ if (tokens.length === 0) break;
125
+ //var tok = tokens.shift();
126
+ //if (tok.token !== '+') throw "Extra characters in M: line";
127
+ }
128
+
129
+ if (multilineVars.havent_set_length === true) {
130
+ multilineVars.default_length = totalLength < 0.75 ? 0.0625 : 0.125;
131
+ }
132
+ return meter;
133
+ } catch (e) {
134
+ warn(e, line, 0);
135
+ }
136
+ }
137
+ return null;
138
+ };
139
+
140
+ this.calcTempo = function(relTempo) {
141
+ var dur = 1/4;
142
+ if (multilineVars.meter && multilineVars.meter.type === 'specified') {
143
+ dur = 1 / parseInt(multilineVars.meter.value[0].den);
144
+ } else if (multilineVars.origMeter && multilineVars.origMeter.type === 'specified') {
145
+ dur = 1 / parseInt(multilineVars.origMeter.value[0].den);
146
+ }
147
+ //var dur = multilineVars.default_length ? multilineVars.default_length : 1;
148
+ for (var i = 0; i < relTempo.duration; i++)
149
+ relTempo.duration[i] = dur * relTempo.duration[i];
150
+ return relTempo;
151
+ };
152
+
153
+ this.resolveTempo = function() {
154
+ if (multilineVars.tempo) { // If there's a tempo waiting to be resolved
155
+ this.calcTempo(multilineVars.tempo);
156
+ tune.metaText.tempo = multilineVars.tempo;
157
+ delete multilineVars.tempo;
158
+ }
159
+ };
160
+
161
+ this.addUserDefinition = function(line, start, end) {
162
+ var equals = line.indexOf('=', start);
163
+ if (equals === -1) {
164
+ warn("Need an = in a macro definition", line, start);
165
+ return;
166
+ }
167
+
168
+ var before = window.ABCJS.parse.strip(line.substring(start, equals));
169
+ var after = window.ABCJS.parse.strip(line.substring(equals+1));
170
+
171
+ if (before.length !== 1) {
172
+ warn("Macro definitions can only be one character", line, start);
173
+ return;
174
+ }
175
+ var legalChars = "HIJKLMNOPQRSTUVWXYhijklmnopqrstuvw~";
176
+ if (legalChars.indexOf(before) === -1) {
177
+ warn("Macro definitions must be H-Y, h-w, or tilde", line, start);
178
+ return;
179
+ }
180
+ if (after.length === 0) {
181
+ warn("Missing macro definition", line, start);
182
+ return;
183
+ }
184
+ if (multilineVars.macros === undefined)
185
+ multilineVars.macros = {};
186
+ multilineVars.macros[before] = after;
187
+ };
188
+
189
+ this.setDefaultLength = function(line, start, end) {
190
+ var len = window.ABCJS.parse.gsub(line.substring(start, end), " ", "");
191
+ var len_arr = len.split('/');
192
+ if (len_arr.length === 2) {
193
+ var n = parseInt(len_arr[0]);
194
+ var d = parseInt(len_arr[1]);
195
+ if (d > 0) {
196
+ multilineVars.default_length = n / d; // a whole note is 1
197
+ multilineVars.havent_set_length = false;
198
+ }
199
+ }
200
+ };
201
+
202
+ this.setTempo = function(line, start, end) {
203
+ //Q - tempo; can be used to specify the notes per minute, e.g. If
204
+ //the meter denominator is a 4 note then Q:120 or Q:C=120
205
+ //is 120 quarter notes per minute. Similarly Q:C3=40 would be 40
206
+ //dotted half notes per minute. An absolute tempo may also be
207
+ //set, e.g. Q:1/8=120 is 120 eighth notes per minute,
208
+ //irrespective of the meter's denominator.
209
+ //
210
+ // This is either a number, "C=number", "Cnumber=number", or fraction [fraction...]=number
211
+ // It depends on the M: field, which may either not be present, or may appear after this.
212
+ // If M: is not present, an eighth note is used.
213
+ // That means that this field can't be calculated until the end, if it is the first three types, since we don't know if we'll see an M: field.
214
+ // So, if it is the fourth type, set it here, otherwise, save the info in the multilineVars.
215
+ // The temporary variables we keep are the duration and the bpm. In the first two forms, the duration is 1.
216
+ // In addition, a quoted string may both precede and follow. If a quoted string is present, then the duration part is optional.
217
+ try {
218
+ var tokens = tokenizer.tokenize(line, start, end);
219
+
220
+ if (tokens.length === 0) throw "Missing parameter in Q: field";
221
+
222
+ var tempo = {};
223
+ var delaySet = true;
224
+ var token = tokens.shift();
225
+ if (token.type === 'quote') {
226
+ tempo.preString = token.token;
227
+ token = tokens.shift();
228
+ if (tokens.length === 0) { // It's ok to just get a string for the tempo
229
+ return {type: 'immediate', tempo: tempo};
230
+ }
231
+ }
232
+ if (token.type === 'alpha' && token.token === 'C') { // either type 2 or type 3
233
+ if (tokens.length === 0) throw "Missing tempo after C in Q: field";
234
+ token = tokens.shift();
235
+ if (token.type === 'punct' && token.token === '=') {
236
+ // This is a type 2 format. The duration is an implied 1
237
+ if (tokens.length === 0) throw "Missing tempo after = in Q: field";
238
+ token = tokens.shift();
239
+ if (token.type !== 'number') throw "Expected number after = in Q: field";
240
+ tempo.duration = [1];
241
+ tempo.bpm = parseInt(token.token);
242
+ } else if (token.type === 'number') {
243
+ // This is a type 3 format.
244
+ tempo.duration = [parseInt(token.token)];
245
+ if (tokens.length === 0) throw "Missing = after duration in Q: field";
246
+ token = tokens.shift();
247
+ if (token.type !== 'punct' || token.token !== '=') throw "Expected = after duration in Q: field";
248
+ if (tokens.length === 0) throw "Missing tempo after = in Q: field";
249
+ token = tokens.shift();
250
+ if (token.type !== 'number') throw "Expected number after = in Q: field";
251
+ tempo.bpm = parseInt(token.token);
252
+ } else throw "Expected number or equal after C in Q: field";
253
+
254
+ } else if (token.type === 'number') { // either type 1 or type 4
255
+ var num = parseInt(token.token);
256
+ if (tokens.length === 0 || tokens[0].type === 'quote') {
257
+ // This is type 1
258
+ tempo.duration = [1];
259
+ tempo.bpm = num;
260
+ } else { // This is type 4
261
+ delaySet = false;
262
+ token = tokens.shift();
263
+ if (token.type !== 'punct' && token.token !== '/') throw "Expected fraction in Q: field";
264
+ token = tokens.shift();
265
+ if (token.type !== 'number') throw "Expected fraction in Q: field";
266
+ var den = parseInt(token.token);
267
+ tempo.duration = [num/den];
268
+ // We got the first fraction, keep getting more as long as we find them.
269
+ while (tokens.length > 0 && tokens[0].token !== '=' && tokens[0].type !== 'quote') {
270
+ token = tokens.shift();
271
+ if (token.type !== 'number') throw "Expected fraction in Q: field";
272
+ num = parseInt(token.token);
273
+ token = tokens.shift();
274
+ if (token.type !== 'punct' && token.token !== '/') throw "Expected fraction in Q: field";
275
+ token = tokens.shift();
276
+ if (token.type !== 'number') throw "Expected fraction in Q: field";
277
+ den = parseInt(token.token);
278
+ tempo.duration.push(num/den);
279
+ }
280
+ token = tokens.shift();
281
+ if (token.type !== 'punct' && token.token !== '=') throw "Expected = in Q: field";
282
+ token = tokens.shift();
283
+ if (token.type !== 'number') throw "Expected tempo in Q: field";
284
+ tempo.bpm = parseInt(token.token);
285
+ }
286
+ } else throw "Unknown value in Q: field";
287
+ if (tokens.length !== 0) {
288
+ token = tokens.shift();
289
+ if (token.type === 'quote') {
290
+ tempo.postString = token.token;
291
+ token = tokens.shift();
292
+ }
293
+ if (tokens.length !== 0) throw "Unexpected string at end of Q: field";
294
+ }
295
+ if (multilineVars.printTempo === false)
296
+ tempo.suppress = true;
297
+ return {type: delaySet?'delaySet':'immediate', tempo: tempo};
298
+ } catch (msg) {
299
+ warn(msg, line, start);
300
+ return {type: 'none'};
301
+ }
302
+ };
303
+
304
+ this.letter_to_inline_header = function(line, i)
305
+ {
306
+ var ws = tokenizer.eatWhiteSpace(line, i);
307
+ i +=ws;
308
+ if (line.length >= i+5 && line.charAt(i) === '[' && line.charAt(i+2) === ':') {
309
+ var e = line.indexOf(']', i);
310
+ switch(line.substring(i, i+3))
311
+ {
312
+ case "[I:":
313
+ var err = window.ABCJS.parse.parseDirective.addDirective(line.substring(i+3, e));
314
+ if (err) warn(err, line, i);
315
+ return [ e-i+1+ws ];
316
+ case "[M:":
317
+ var meter = this.setMeter(line.substring(i+3, e));
318
+ if (tune.hasBeginMusic() && meter)
319
+ tune.appendStartingElement('meter', -1, -1, meter);
320
+ else
321
+ multilineVars.meter = meter;
322
+ return [ e-i+1+ws ];
323
+ case "[K:":
324
+ var result = window.ABCJS.parse.parseKeyVoice.parseKey(line.substring(i+3, e));
325
+ if (result.foundClef && tune.hasBeginMusic())
326
+ tune.appendStartingElement('clef', -1, -1, multilineVars.clef);
327
+ if (result.foundKey && tune.hasBeginMusic())
328
+ tune.appendStartingElement('key', -1, -1, window.ABCJS.parse.parseKeyVoice.fixKey(multilineVars.clef, multilineVars.key));
329
+ return [ e-i+1+ws ];
330
+ case "[P:":
331
+ tune.appendElement('part', -1, -1, {title: line.substring(i+3, e)});
332
+ return [ e-i+1+ws ];
333
+ case "[L:":
334
+ this.setDefaultLength(line, i+3, e);
335
+ return [ e-i+1+ws ];
336
+ case "[Q:":
337
+ if (e > 0) {
338
+ var tempo = this.setTempo(line, i+3, e);
339
+ if (tempo.type === 'delaySet') tune.appendElement('tempo', -1, -1, this.calcTempo(tempo.tempo));
340
+ else if (tempo.type === 'immediate') tune.appendElement('tempo', -1, -1, tempo.tempo);
341
+ return [ e-i+1+ws, line.charAt(i+1), line.substring(i+3, e)];
342
+ }
343
+ break;
344
+ case "[V:":
345
+ if (e > 0) {
346
+ window.ABCJS.parse.parseKeyVoice.parseVoice(line, i+3, e);
347
+ //startNewLine();
348
+ return [ e-i+1+ws, line.charAt(i+1), line.substring(i+3, e)];
349
+ }
350
+ break;
351
+
352
+ default:
353
+ // TODO: complain about unhandled header
354
+ }
355
+ }
356
+ return [ 0 ];
357
+ };
358
+
359
+ this.letter_to_body_header = function(line, i)
360
+ {
361
+ if (line.length >= i+3) {
362
+ switch(line.substring(i, i+2))
363
+ {
364
+ case "I:":
365
+ var err = window.ABCJS.parse.parseDirective.addDirective(line.substring(i+2));
366
+ if (err) warn(err, line, i);
367
+ return [ line.length ];
368
+ case "M:":
369
+ var meter = this.setMeter(line.substring(i+2));
370
+ if (tune.hasBeginMusic() && meter)
371
+ tune.appendStartingElement('meter', -1, -1, meter);
372
+ return [ line.length ];
373
+ case "K:":
374
+ var result = window.ABCJS.parse.parseKeyVoice.parseKey(line.substring(i+2));
375
+ if (result.foundClef && tune.hasBeginMusic())
376
+ tune.appendStartingElement('clef', -1, -1, multilineVars.clef);
377
+ if (result.foundKey && tune.hasBeginMusic())
378
+ tune.appendStartingElement('key', -1, -1, window.ABCJS.parse.parseKeyVoice.fixKey(multilineVars.clef, multilineVars.key));
379
+ return [ line.length ];
380
+ case "P:":
381
+ if (tune.hasBeginMusic())
382
+ tune.appendElement('part', -1, -1, {title: line.substring(i+2)});
383
+ return [ line.length ];
384
+ case "L:":
385
+ this.setDefaultLength(line, i+2, line.length);
386
+ return [ line.length ];
387
+ case "Q:":
388
+ var e = line.indexOf('\x12', i+2);
389
+ if (e === -1) e = line.length;
390
+ var tempo = this.setTempo(line, i+2, e);
391
+ if (tempo.type === 'delaySet') tune.appendElement('tempo', -1, -1, this.calcTempo(tempo.tempo));
392
+ else if (tempo.type === 'immediate') tune.appendElement('tempo', -1, -1, tempo.tempo);
393
+ return [ e, line.charAt(i), window.ABCJS.parse.strip(line.substring(i+2))];
394
+ case "V:":
395
+ window.ABCJS.parse.parseKeyVoice.parseVoice(line, 2, line.length);
396
+ // startNewLine();
397
+ return [ line.length, line.charAt(i), window.ABCJS.parse(line.substring(i+2))];
398
+ default:
399
+ // TODO: complain about unhandled header
400
+ }
401
+ }
402
+ return [ 0 ];
403
+ };
404
+
405
+ var metaTextHeaders = {
406
+ A: 'author',
407
+ B: 'book',
408
+ C: 'composer',
409
+ D: 'discography',
410
+ F: 'url',
411
+ G: 'group',
412
+ I: 'instruction',
413
+ N: 'notes',
414
+ O: 'origin',
415
+ R: 'rhythm',
416
+ S: 'source',
417
+ W: 'unalignedWords',
418
+ Z: 'transcription'
419
+ };
420
+
421
+ this.parseHeader = function(line) {
422
+ if (window.ABCJS.parse.startsWith(line, '%%')) {
423
+ var err = window.ABCJS.parse.parseDirective.addDirective(line.substring(2));
424
+ if (err) warn(err, line, 2);
425
+ return {};
426
+ }
427
+ line = tokenizer.stripComment(line);
428
+ if (line.length === 0)
429
+ return {};
430
+
431
+ if (line.length >= 2) {
432
+ if (line.charAt(1) === ':') {
433
+ var nextLine = "";
434
+ if (line.indexOf('\x12') >= 0 && line.charAt(0) !== 'w') { // w: is the only header field that can have a continuation.
435
+ nextLine = line.substring(line.indexOf('\x12')+1);
436
+ line = line.substring(0, line.indexOf('\x12')); //This handles a continuation mark on a header field
437
+ }
438
+ var field = metaTextHeaders[line.charAt(0)];
439
+ if (field !== undefined) {
440
+ if (field === 'unalignedWords')
441
+ tune.addMetaTextArray(field, window.ABCJS.parse.parseDirective.parseFontChangeLine(tokenizer.translateString(tokenizer.stripComment(line.substring(2)))));
442
+ else
443
+ tune.addMetaText(field, tokenizer.translateString(tokenizer.stripComment(line.substring(2))));
444
+ return {};
445
+ } else {
446
+ switch(line.charAt(0))
447
+ {
448
+ case 'H':
449
+ tune.addMetaText("history", tokenizer.translateString(tokenizer.stripComment(line.substring(2))));
450
+ multilineVars.is_in_history = true;
451
+ break;
452
+ case 'K':
453
+ // since the key is the last thing that can happen in the header, we can resolve the tempo now
454
+ this.resolveTempo();
455
+ var result = window.ABCJS.parse.parseKeyVoice.parseKey(line.substring(2));
456
+ if (!multilineVars.is_in_header && tune.hasBeginMusic()) {
457
+ if (result.foundClef)
458
+ tune.appendStartingElement('clef', -1, -1, multilineVars.clef);
459
+ if (result.foundKey)
460
+ tune.appendStartingElement('key', -1, -1, window.ABCJS.parse.parseKeyVoice.fixKey(multilineVars.clef, multilineVars.key));
461
+ }
462
+ multilineVars.is_in_header = false; // The first key signifies the end of the header.
463
+ break;
464
+ case 'L':
465
+ this.setDefaultLength(line, 2, line.length);
466
+ break;
467
+ case 'M':
468
+ multilineVars.origMeter = multilineVars.meter = this.setMeter(line.substring(2));
469
+ break;
470
+ case 'P':
471
+ // TODO-PER: There is more to do with parts, but the writer doesn't care.
472
+ if (multilineVars.is_in_header)
473
+ tune.addMetaText("partOrder", tokenizer.translateString(tokenizer.stripComment(line.substring(2))));
474
+ else
475
+ multilineVars.partForNextLine = tokenizer.translateString(tokenizer.stripComment(line.substring(2)));
476
+ break;
477
+ case 'Q':
478
+ var tempo = this.setTempo(line, 2, line.length);
479
+ if (tempo.type === 'delaySet') multilineVars.tempo = tempo.tempo;
480
+ else if (tempo.type === 'immediate') tune.metaText.tempo = tempo.tempo;
481
+ break;
482
+ case 'T':
483
+ this.setTitle(line.substring(2));
484
+ break;
485
+ case 'U':
486
+ this.addUserDefinition(line, 2, line.length);
487
+ break;
488
+ case 'V':
489
+ window.ABCJS.parse.parseKeyVoice.parseVoice(line, 2, line.length);
490
+ if (!multilineVars.is_in_header)
491
+ return {newline: true};
492
+ break;
493
+ case 's':
494
+ return {symbols: true};
495
+ case 'w':
496
+ return {words: true};
497
+ case 'X':
498
+ break;
499
+ case 'E':
500
+ case 'm':
501
+ warn("Ignored header", line, 0);
502
+ break;
503
+ default:
504
+ // It wasn't a recognized header value, so parse it as music.
505
+ if (nextLine.length)
506
+ nextLine = "\x12" + nextLine;
507
+ //parseRegularMusicLine(line+nextLine);
508
+ //nextLine = "";
509
+ return {regular: true, str: line+nextLine};
510
+ }
511
+ }
512
+ if (nextLine.length > 0)
513
+ return {recurse: true, str: nextLine};
514
+ return {};
515
+ }
516
+ }
517
+
518
+ // If we got this far, we have a regular line of mulsic
519
+ return {regular: true, str: line};
520
+ };
521
+ };