1/* 2 * iRealRenderer 3 * 4 * Render any iReal Pro song into an HTML container element. 5 */ 6 7class iRealRenderer { 8 constructor() { 9 this.transposeFlat = [ 10 "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B", 11 "C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B" 12 ]; 13 this.transposeSharp = [ 14 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", 15 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" 16 ]; 17 this.cells = []; 18 // This is set to true if the renderer is to render as a web component. 19 // It inhibits the creation of a <irr-chords>tag because the tag is 20 // already created as a web component. For now, just ignore the setting. 21 this.isComponent = false; 22 } 23 24 /** 25 * The parser cracks up the music string at song.music into several objects, 26 * one for each cell. iReal Pro works with rows of 16 cell each. The result 27 * is stored at song.cells. 28 * 29 * Each object has the following properties: 30 * 31 * chord: if non-null, a chord object with these properties: 32 * note - the base note (also blank, W = invisible root, p/x/r - pause/bar repeat/double-bar repeat, n - no chord) 33 * modifiers - the modifiers, like 7, + o etc (string) 34 * over - if non-null, another chord object for the under-note 35 * alternate - if non-null another chord object for the alternate chord 36 * annots: annotations, a string of: 37 * *x - section, like *v, *I, *A, *B etc 38 * Nx - repeat bots (N1, N2 etc) 39 * Q - coda 40 * S - segno 41 * Txx - measure (T44 = 4/4 etc, but T12 = 12/8) 42 * U - END 43 * f - fermata 44 * l - (letter l) normal notes 45 * s - small notes 46 * comments: an array of comment strings 47 * bars: bar specifiers, a string of: 48 * | - single vertical bar, left 49 * [ - double bar, left 50 * ] - double bar, right 51 * { - repeat bar, left 52 * } - repeat bar, right 53 * Z - end bar, right 54 * spacer - a number indicating the number of vertical spacers above this cell 55 * 56 * @param {Song} song 57 * @returns {undefined} 58 */ 59 parse(song) { 60 var text = song.music; 61 var arr = [], headers = [], comments = []; 62 var i; 63 text = text.trimRight(); 64 while(text) { 65 var found = false; 66 for (i = 0; i < iRealRenderer.regExps.length; i++) { 67 var match = iRealRenderer.regExps[i].exec(text); 68 if (match) { 69 found = true; 70 if (match.length <= 2) { 71 match = match[0]; 72 arr.push(match); 73 text = text.substr(match.length); 74 } 75 else { 76 // a chord 77 arr.push(match); 78 text = text.substr(match[0].length); 79 } 80 break; 81 } 82 } 83 if (!found) { 84 // ignore the comma separator 85 if (text[0] !== ',') 86 arr.push(text[0]); 87 text = text.substr(1); 88 } 89 } 90// console.log(arr); 91 // pass 2: extract prefixes, suffixes, annotations and comments 92 var out = []; 93 var obj = this.newToken(out); 94 var prevobj = null; 95 for (i = 0; i < arr.length; i++) { 96 var token = arr[i]; 97 if (token instanceof Array) { 98 obj.chord = this.parseChord(token); 99 token = " "; 100 } 101 switch (token[0]) { 102 case '{': // open repeat 103 case '[': // open double bar 104 obj.bars = token; token = null; break; 105 case '|': // single bar - close previous and open this 106 if (prevobj) { prevobj.bars += ')'; prevobj = null; } 107 obj.bars = '('; token = null; break; 108 case ']': // close double bar 109 case '}': // close repeat 110 case 'Z': // ending double bar 111 if (prevobj) { prevobj.bars += token; prevobj = null; } 112 token = null; break; 113 case 'n': // N.C. 114 obj.chord = new iRealChord(token[0]); 115 break; 116 case ',': token = null; break; // separator 117 case 'S': // segno 118 case 'T': // time measurement 119 case 'Q': // coda 120 case 'N': // repeat 121 case 'U': // END 122 case 's': // small 123 case 'l': // normal 124 case 'f': // fermata 125 case '*': obj.annots.push(token); token = null; break; 126 case 'Y': obj.spacer++; token = null; prevobj = null; break; 127 case 'r': 128 case 'x': 129 case 'W': 130 obj.chord = new iRealChord(token); 131 break; 132 case '<': 133 token = token.substr(1, token.length-2); 134 obj.comments.push(token);
135 token = null; break; 136 default: 137 } 138 if (token && i < arr.length-1) { 139 prevobj = obj; // so we can add any closing barline later 140 obj = this.newToken(out); 141 } 142 } 143// console.log(out); 144 song.cells = out; 145 } 146 147 parseChord(match) { 148 var note = match[1] || " "; 149 var modifiers = match[2] || ""; 150 var comment = match[3] || ""; 151 if (comment) 152 modifiers += comment.substr(1, comment.length-2); 153 var over = match[4] || ""; 154 if (over[0] === '/') 155 over = over.substr(1); 156 var alternate = match[5] || null; 157 if (alternate) { 158 match = iRealRenderer.chordRegex.exec(alternate.substr(1, alternate.length-2)); 159 if (!match) 160 alternate = null; 161 else 162 alternate = this.parseChord(match); 163 } 164 // empty cell? 165 if (note === " " && !alternate && !over) 166 return null; 167 if (over) { 168 var offset = (over[1] === '#' || over[1] === 'b') ? 2 : 1; 169 over = new iRealChord(over.substr(0, offset), over.substr(offset), null, null); 170 } 171 else 172 over = null; 173 modifiers = modifiers.replace(/b/g, "\u266d").replace(/#/g, "\u266f"); // convert to proper flat and sharp 174 return new iRealChord(note, modifiers, over, alternate); 175 } 176 177 newToken(arr) { 178 var obj = new iRealToken; 179 arr.push(obj); 180 return obj; 181 } 182 183 //////////////////////////////////////////////////////////////////////////// 184 185 /** 186 * Transpose a song. Use the following options: 187 * 188 * transpose: 189 * a value between -6 and 15 as halftones 190 * minor: 191 * small - convert Bb- to bb 192 * m - convert Bb- to Bbm 193 * useH: 194 * use H for B chords 195 * @param {type} song 196 * @param {type} options 197 * @returns {undefined} 198 */ 199 transpose(song, options) { 200 song = Object.assign({}, song); 201 if (song.cells) 202 song.cells = song.cells.slice(0); 203 var chord = { note: song.key, modifiers:"", over: null, alternate: null }; 204 if (chord.note.endsWith("-")) { 205 chord.note = song.key.substr(0, song.key.length-1); 206 chord.modifiers = "-"; 207 } 208 options.transpose += song.transpose; 209 // Pre-compute target key to determine sharp vs flat convention 210 var keyArr = this.transposeFlat; 211 var ki = keyArr.indexOf(chord.note); 212 if (ki < 0) { keyArr = this.transposeSharp; ki = keyArr.indexOf(chord.note); } 213 if (ki >= 0) { 214 var ti = ki + (options.transpose % 12); 215 if (ti < 0) ti += 12; 216 // Determine target key using flat array first, then check convention 217 var sharpKeys = ["C", "G", "D", "A", "E", "B", "F#", "C#"]; 218 var flatKey = this.transposeFlat[ti]; 219 var sharpKey = this.transposeSharp[ti]; 220 options._targetKey = sharpKeys.includes(sharpKey) ? sharpKey : flatKey; 221 } 222 this.transposeChord(chord, options); 223 song.key = chord.note + chord.modifiers; 224 if (song.cells) 225 song.cells = song.cells.map(el => { 226 if (el.chord) 227 this.transposeChord(el.chord, options); 228 return el; 229 }); 230 options.transpose -= song.transpose; 231 return song; 232 } 233 234 /** 235 * Transpose the given chord; use the given options. 236 * @param {Object} chord 237 * @param {Object} options 238 * @returns {String} 239 */ 240 transposeChord(chord, options) { 241 // Jazz-conventional enharmonic spellings for chord roots by target key. 242 // Enharmonic pairs: C#/Db (1), D#/Eb (3), F#/Gb (6), G#/Ab (8), A#/Bb (10) 243 // A#/Bb and D#/Eb: ALWAYS use flats (Bb, Eb) â A# and D# are never chord roots. 244 // F#/Gb: use F# in sharp keys (C,G,D,A,E,B,F#,C#), Gb in flat keys 245 // C#/Db: use C# in keys with 3+ sharps (A,E,B,F#,C#), Db otherwise 246 // G#/Ab: use G# in keys with 4+ sharps (E,B,F#,C#), Ab otherwise 247 var targetKey = options._targetKey || ""; 248 var fSharp = ["C","G","D","A","E","B","F#","C#"].includes(targetKey) ? "F#" : "Gb"; 249 var cSharp = ["A","E","B","F#","C#"].includes(targetKey) ? "C#" : "Db"; 250 var gSharp = ["E","B","F#","C#"].includes(targetKey) ? "G#" : "Ab"; 251 // C C#/Db D D#/Eb E F F#/Gb G G#/Ab A Bb B 252 var jazzSpellings = ["C", cSharp, "D", "Eb", "E", "F", fSharp, "G", gSharp, "A", "Bb", "B", 253 "C", cSharp, "D", "Eb", "E", "F", fSharp, "G", gSharp, "A", "Bb", "B"]; 254 255 // Find the note in either array to get its current semitone index 256 var arr = this.transposeFlat; 257 var i = arr.indexOf(chord.note); 258 if (i < 0) { 259 arr = this.transposeSharp; 260 i = arr.indexOf(chord.note); 261 } 262 if (i >= 0) { 263 i += (options.transpose % 12); 264 if (i < 0) 265 i += 12; 266 chord.note = jazzSpellings[i]; 267 if (options.useH && chord.note === "B") 268 chord.note = "H"; 269 } 270 if (chord.modifiers.includes("-")) { 271 switch (options.minor) { 272 case "small": 273 var note = chord.note[0].toLowerCase(); 274 if (chord.note[1]) 275 note += chord.note[1]; 276 chord.note = note; 277 chord.modifiers = chord.modifiers.replace("-", ""); 278 break; 279 case "m": 280 chord.modifiers = chord.modifiers.replace("-", "m"); 281 break; 282 } 283 } 284 if (chord.alternate) 285 this.transposeChord(chord.alternate, options); 286 if (chord.over) 287 this.transposeChord(chord.over, options); 288 } 289 290 //////////////////////////////////////////////////////////////////////////// 291 292 /** 293 * Render the parsed array.
294 * @param {Song} song - with attached cells property 295 * @param {Element} container - HTML container element to render into (appends) 296 * @param {Object} options - render annots, comments etc in red if hilite property is set 297 * @returns {undefined} 298 */ 299 render(song, container, options = {}) { 300 if (!song.cells) 301 return; 302 var hilite = options.hilite || false; 303 if (!this.isComponent) { 304 var table = document.createElement("irr-chords"); 305 if (hilite) 306 table.setAttribute("hilite", ""); 307 container.appendChild(table); 308 } 309 else 310 table = container; 311 this.cell = -1; 312 this.closebar = false; 313 this.small = false; 314 this.hilite = hilite; 315 316 for (var i = 0; i < song.cells.length; i++) { 317 var cell = song.cells[i]; 318 if (this.cell < 0 || this.cell === 15) 319 this.nextRow(table, cell.spacer); 320 else 321 this.cell++; 322 var html = ""; 323 if (cell.annots.length) 324 html += this.annotHtml(cell.annots); 325 if (cell.comments.length) 326 html += this.commentHtml(cell.comments); 327 html += this.cellHtml(cell); 328 var el = this.cells[this.cell]; 329 var cls = ""; 330 if (this.small) 331 cls += "small"; 332 if (cell.comments.length) 333 cls += " irr-comment"; 334 if (cls) 335 el.setAttribute("class", cls.trim()); 336 el.innerHTML = html; 337 this.closebar = cell.bars.indexOf(')') >= 0; 338 } 339 } 340 341 //////////////////////////////////////////////////////////////////////////// 342 343 // Private methods 344 345 cellHtml(data) { 346 let html = ""; 347 if (data.chord) html = this.chordHtml(data.chord); 348 for (var i = 0; i < data.bars.length; i++) { 349 let c = data.bars[i]; 350 let cls = iRealRenderer.classes[c]; 351 switch(c) { 352 case '(': 353 case '[': 354 case '{': 355 html = `<irr-lbar class="${cls}"></irr-lbar>` + html; break; 356 //case ')': // not handled here, only at end of line below 357 case ']': 358 case '}': 359 case 'Z': 360 html = `<irr-rbar class="${cls}"></irr-rbar>` + html; break; 361 } 362 } 363 if (!html) 364 return html; 365 return `<irr-chord>${html}</irr-chord>`; 366 } 367 368 chordHtml(chord) { 369 if (typeof chord === "string") { 370 chord = iRealRenderer.chordRegex.exec(chord); 371 if (!chord) 372 return; 373 } 374 var html = this.baseChordHtml(chord); 375 var { alternate, over } = chord; 376 if (over) 377 html += `<irr-over>${this.baseChordHtml(over)}</irr-over>`; 378 if (alternate) 379 html = `<irr-chord>${this.chordHtml(alternate)}</irr-chord>` + html; 380 return html; 381 } 382 383 baseChordHtml(chord) { 384 var { note, modifiers } = chord; 385 if (note === "W") 386 note = `<irr-char class="irr-root Root"></irr-char>`; 387 if (note === "p") 388 note = `<irr-char class="Repeated-Figure1"></irr-char>`; 389 if (["x", "r", "n"].includes(note)) { 390 // 1-bar repeat, 2-bar repeat, and no-chord 391 note = `<irr-char class="${iRealRenderer.classes[note]}"></irr-char>`; 392 } 393 var sup = ""; 394 switch(note[1]) { 395 case 'b': sup = "<sup class=\"irr-acc\">\u266d</sup>"; note = note[0]; break; 396 case '#': sup = "<sup class=\"irr-acc\">\u266f</sup>"; note = note[0]; break; 397 } 398 if (modifiers) { 399 modifiers = modifiers.replace("^", "\u25B3").replace("h", "\u00D8").replace("-", "\u2013"); 400 // Split into quality symbol (baseline) and numeric part (superscript) 401 var qualMatch = modifiers.match(/^([^\d\u266d\u266f]*)([\d\u266d\u266f].*)?$/); 402 if (qualMatch) { 403 var qual = qualMatch[1] || ""; 404 var num = qualMatch[2] || ""; 405 modifiers = (qual ? `<sub class="irr-qual">${qual}</sub>` : "") + (num ? `<sup>${num}</sup>` : ""); 406 } else { 407 modifiers = `<sub>${modifiers}</sub>`; 408 } 409 } 410 return `${note}${sup}${modifiers}`; 411 } 412 413 /** 414 * Render an annotation. 415 * @param {type} annots 416 * @returns {undefined} 417 */ 418 annotHtml(annots) { 419 var t = ""; 420 for (var i = 0; i < annots.length; i++) { 421 var annot = annots[i]; 422 var s; 423 switch(annot[0]) { 424 case '*': // section 425 s = annot[1]; 426 switch(s) { 427 case "i": s = "In"; break; 428 } 429 t += `<irr-section>${s}</irr-section>`; 430 break; 431 case 'N': // repeat bracket 432 t += `<irr-repeat>${annot[1]}</irr-repeat>`; break; 433 case 'f': // fermata 434 case 'Q': // coda 435 case 'S': // segno 436 t += `<irr-annot class="${iRealRenderer.classes[annot[0]]}"></irr-annot>`; break; 437 break; 438 case 'T': // measure: Txx, where T12 is 12/8 439 var m1 = annot.charCodeAt(1) - 48; 440 var m2 = annot.charCodeAt(2) - 48; 441 if (m1 === 1 && m2 === 2) 442 m1 = 12, m2 = 8; 443 s = `<irr-measure><span class="Measure-${m1}-Low"></span><br/><span class="Measure-${m2}-High"></span></irr-measure>`; 444 t = s + t; 445 break; 446 case 's': 447 this.small = true; break; 448 case 'l': 449 this.small = false; break; 450 } 451 } 452 return t; 453 } 454 455 commentHtml(comments) { 456 var cell = this.cells[this.cell]; 457 var style = getComputedStyle(cell); 458 var top = parseInt(style.height) + parseInt(style["margin-top"]); 459 var html = ""; 460 for (var i = 0; i < comments.length; i++) { 461 var c = comments[i]; 462 var offset = 0; 463 if (c[0] === '*') { 464 offset = (c.charCodeAt(1) - 48) * 10 + (c.charCodeAt(2) - 48); 465 c = c.substr(3); 466 } 467 // assume that 1 unit is = 1/20 em 468 offset /= 20; 469 html += `<irr-comment style="margin-top:${-offset}em">${c}</irr-comment>`; 470 } 471 return html; 472 } 473 474 nextRow(table, spacer) { 475 this.checkIfNeedsLastBar(); 476 // insert a spacer
477 if (spacer) { 478 var spc = document.createElement("irr-spacer"); 479 spc.setAttribute("style", `height:${spacer*10}px`); 480 table.appendChild(spc); 481 } 482 this.cells = []; 483 for (let i = 0; i < 16; i++) { 484 var cell = document.createElement("irr-cell"); 485 this.cells.push(cell); 486 table.appendChild(cell); 487 } 488 489 this.cell = 0; 490 } 491 492 /** 493 * Check if the current cell is the last cell of a row, and if it 494 * needs a closing bar. This is true if there has been an opening 495 * bar in the last 4 cells. 496 */ 497 checkIfNeedsLastBar() { 498 if (this.cell !== 15) 499 return; 500 if (!this.closebar) 501 return; 502 let curCell = this.cells[this.cell]; 503 var bar = document.createElement("irr-rbar"); 504 bar.classList.add("Single-Barline"); 505 curCell.insertBefore(bar, curCell.firstChild); // must insert, not append, for correct positioning 506 } 507} 508 509/** 510 * The RegExp for a complete chord. The match array contains: 511 * 1 - the base note 512 * 2 - the modifiers (+-ohd0123456789 and su for sus) 513 * 3 - any comments (may be e.g. add, sub, or private stuff) 514 * 4 - the "over" part starting with a slash 515 * 5 - the top chord as (chord) 516 * @type RegExp 517 */ 518iRealRenderer.chordRegex = /^([A-Gxnr][b#]?)((?:sus|alt|add|[\+\-\^\dhob#])*)(\*.+?\*)*(\/[A-G][#b]?)?(\(.*?\))?/; 519iRealRenderer.chordRegex2 = /^([ Wp])()()(\/[A-G][#b]?)?(\(.*?\))?/; // need the empty captures to match chordRegex 520 521iRealRenderer.regExps = [ 522 /^\*[a-zA-Z]/, // section 523 /^T\d\d/, // time measurement 524 /^N./, // repeat marker 525 /^<.*?>/, // comments 526 iRealRenderer.chordRegex, // chords 527 iRealRenderer.chordRegex2, // space, W and p (with optional alt chord) 528]; 529 530iRealRenderer.cssPrefix = ""; 531 532iRealRenderer.classes = { 533 '(': "Single-Barline", 534 ')': "Single-Barline", 535 '[': "Double-Barline", 536 ']': "Double-Barline", 537 "?Z0": "Final-Barline", 538 'Z': "Reverse-Final-Barline", 539 '{': "Left-Repeat-Sign", 540 '}': "Right-Repeat-Sign", 541 "?DS": "Dal-Segno", 542 "?DC": "Da-Capo", 543 n: "No-Chord", 544 f: "Fermata", 545 S: "Segno", 546 Q: "Coda", 547 "?Q": "Codetta", 548 W: "Root", 549 "?W": "Root-Filled", 550 p: "Repeated-Figure1", 551 x: "Repeated-Figure2", 552 r: "Repeated-Figure3", 553 "?r4": "Repeated-Figure4", 554 "0L": "Measure-0-Low", 555 "1L": "Measure-1-Low", 556 "2L": "Measure-2-Low", 557 "3L": "Measure-3-Low", 558 "4L": "Measure-4-Low", 559 "5L": "Measure-5-Low", 560 "6L": "Measure-6-Low", 561 "7L": "Measure-7-Low", 562 "8L": "Measure-8-Low", 563 "9L": "Measure-9-Low", 564 "10L": "Measure-10-Low", 565 "11L": "Measure-11-Low", 566 "12L": "Measure-12-Low", 567 "0H": "Measure-0-High",
568 "1H": "Measure-1-High", 569 "2H": "Measure-2-High", 570 "3H": "Measure-3-High", 571 "4H": "Measure-4-High", 572 "5H": "Measure-5-High", 573 "6H": "Measure-6-High", 574 "7H": "Measure-7-High", 575 "8H": "Measure-8-High", 576 "9H": "Measure-9-High", 577 "10H": "Measure-10-High", 578 "11H": "Measure-11-High", 579 "12H": "Measure-12-High" 580}; 581 582if (typeof module !== "undefined") 583 module.exports = { iRealRenderer, Playlist: require("./ireal-reader-tiny.js") }; 584 585 586class iRealToken { 587 constructor() { 588 this.annots = []; 589 this.comments = []; 590 this.bars = ""; 591 this.spacer = 0; 592 this.chord = null; 593 } 594} 595 596class iRealChord { 597 constructor(note, modifiers = "", over = null, alternate = null) { 598 this.note = note; 599 this.modifiers = modifiers; 600 this.over = over; 601 this.alternate = alternate; 602 } 603}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.