abcjs-rails 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ };