]> piware.de Git - chordpro-web.git/commitdiff
Render chord diagrams for {define} directives
authorMartin Pitt <martin@piware.de>
Wed, 23 Sep 2026 21:21:50 +0000 (23:21 +0200)
committerMartin Pitt <martin@piware.de>
Wed, 23 Sep 2026 21:21:55 +0000 (23:21 +0200)
See https://www.chordpro.org/chordpro/directives-define/

chordpro-web.js
chordpro.css

index 5f5ba2f715ce9d67beec3285329a22715430a6be..93a859aa95b70550150e879a9de84c55eb2cacef 100644 (file)
@@ -9,7 +9,57 @@ const el_song = document.getElementById('song');
 // 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;
@@ -22,6 +72,11 @@ const directive_handlers = {
         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;
@@ -30,6 +85,7 @@ directive_handlers.c = directive_handlers.comment;
 
 function renderChordpro(text) {
     const song = [];
+    const chords = [];
     let verseLines = [];
     let verseHasChords = false;
 
@@ -52,7 +108,7 @@ function renderChordpro(text) {
             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 === '') {
@@ -78,6 +134,8 @@ function renderChordpro(text) {
         }
     }
     flushVerse();
+    if (chords.length > 0)
+        song.push(html`<div class="diagrams">${chords.map(chordDiagram)}</div>`);
 
     render(song, el_song);
 }
index 75ddada5a28d6c94cf55ea6aacbe77f372ef35bf..628b0fd9629c63e8a1c277c980298653bf568825 100644 (file)
@@ -14,6 +14,58 @@ h2, .comment {
   content: '𝄞 ';
 }
 
+/* Chord diagrams: strings are columns with a line through the middle, frets
+   are the row borders. The header row marks open (o) and muted (x) strings,
+   the header column shows the base fret. */
+.diagrams {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 1em 2em;
+  font-size: 0.8em;
+
+  figure {
+    margin: 0;
+    text-align: center;
+  }
+
+  figcaption {
+    font-weight: bold;
+  }
+
+  table {
+    border-collapse: collapse;
+  }
+
+  td, th {
+    width: 1.5em;
+    height: 1.5em;
+    padding: 0;
+    font-weight: normal;
+    text-align: center;
+  }
+
+  tbody td {
+    border-top: 1px solid;
+    border-bottom: 1px solid;
+    background: linear-gradient(currentColor, currentColor) center / 1px 100% no-repeat;
+  }
+
+  .nut tbody tr:first-child td {
+    border-top-width: 3px;
+  }
+
+  /* finger position, with the finger number if known */
+  .dot {
+    display: inline-block;
+    width: 1.2em;
+    height: 1.2em;
+    line-height: 1.2em;
+    border-radius: 50%;
+    background: CanvasText;
+    color: Canvas;
+  }
+}
+
 .verse {
   white-space: pre-line;
 }