PageSourceSearch

https://cdn.allsolos.com/js/ireal-renderer.js

js allsolos.com collected 2026-10-03 01:33:25 UTC 17,734 bytes, 603 lines download raw bytes

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.