// chord of a line still end up in the lyrics row; see chordpro.css.
const segment = (chord, text) => html`<span class="seg"><span class="chord">${chord}</span>${text}</span>`;
-// Handlers return the song content to render for the directive, if any
+// {define: name [base-fret N] frets p… [fingers f…]}, see
+// https://www.chordpro.org/chordpro/directives-define/
+const define_keywords = ['base-fret', 'frets', 'fingers', 'keys', 'diagram', 'display', 'copy', 'copyall', 'format'];
+const define_supported = ['base-fret', 'frets', 'fingers'];
+
+function parseDefine(value) {
+ const [name, ...tokens] = value.split(/\s+/);
+ const props = {};
+ let key;
+ for (const token of tokens) {
+ if (define_keywords.includes(token))
+ props[key = token] = [];
+ else
+ props[key]?.push(token);
+ }
+ if (!props.frets) {
+ console.warn('chord definition without frets:', value);
+ return null;
+ }
+ for (const k of Object.keys(props))
+ if (!define_supported.includes(k))
+ console.warn(`ignoring ${k} in chord definition:`, value);
+
+ return {
+ name,
+ base: Number(props['base-fret']?.[0] ?? 1),
+ // null marks a muted string
+ frets: props.frets.map(p => /^(x|N|-1)$/.test(p) ? null : Number(p)),
+ fingers: props.fingers ?? [],
+ };
+}
+
+// Strings are table columns, frets are rows; see chordpro.css
+function chordDiagram({ name, base, frets, fingers }) {
+ const rows = Array.from({ length: Math.max(4, ...frets) }, (_, i) => i + 1);
+ const marker = f => f === null ? 'x' : f === 0 ? 'o' : '';
+ const dot = i => html`<span class="dot">${/^[1-9]$/.test(fingers[i]) ? fingers[i] : ''}</span>`;
+ return html`<figure>
+ <figcaption>${name}</figcaption>
+ <table class=${base === 1 ? 'nut' : ''}>
+ <thead><tr><th></th>${frets.map(f => html`<td>${marker(f)}</td>`)}</tr></thead>
+ <tbody>${rows.map(row => html`<tr>
+ <th>${row === 1 && base > 1 ? base : ''}</th>
+ ${frets.map((f, i) => html`<td>${f === row ? dot(i) : ''}</td>`)}
+ </tr>`)}</tbody>
+ </table>
+ </figure>`;
+}
+
+// Handlers get the directive argument and the list of chord definitions, and
+// return the song content to render for the directive, if any
const directive_handlers = {
title: value => {
document.getElementById('title').textContent = value;
document.getElementById('key').textContent = value;
},
comment: value => html`<p class="comment">${value}</p>`,
+ define: (value, chords) => {
+ const chord = parseDefine(value);
+ if (chord)
+ chords.push(chord);
+ },
};
// short forms
directive_handlers.t = directive_handlers.title;
function renderChordpro(text) {
const song = [];
+ const chords = [];
let verseLines = [];
let verseHasChords = false;
const [, directive, value] = m;
const handler = directive_handlers[directive];
if (handler)
- song.push(handler(value ?? ''));
+ song.push(handler(value ?? '', chords));
else
console.warn('unknown directive:', line);
} else if (line === '') {
}
}
flushVerse();
+ if (chords.length > 0)
+ song.push(html`<div class="diagrams">${chords.map(chordDiagram)}</div>`);
render(song, el_song);
}