]> piware.de Git - chordpro-web.git/commitdiff
Lay out chords and lyrics as segments
authorMartin Pitt <martin@piware.de>
Wed, 23 Sep 2026 20:30:13 +0000 (22:30 +0200)
committerMartin Pitt <martin@piware.de>
Wed, 23 Sep 2026 20:44:04 +0000 (22:44 +0200)
Zero-width chords overlapped whenever the text under them, or even
empty: adjacent chords (intros/bridges without text), chords separated by
a single space, or a long chord name over a short syllable. The rule from
dc37808 only widened the second chord of a run, so the first pair still
collided.

Group each chord with the lyrics up to the next chord into a segment with
two rows. Its width is the wider of the two, which fixes the overlap.

chordpro-web.js
chordpro.css

index 5abd0d6af5213684737783ca01e43b2af294f7c4..abbf4045fe1d4752dc09a19dbd69c30bf8f972d4 100644 (file)
@@ -5,6 +5,10 @@ const re_directive = /^{(\w+):\s*([^}]+)}$/;
 const el_song = document.getElementById('song');
 const song = [];
 
+// The chord span is emitted even when empty, so that lyrics before the first
+// 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><span class="text">${text}</span></span>`;
+
 const directive_handlers = {
     title: value => {
         document.getElementById('title').textContent = value;
@@ -42,15 +46,23 @@ function renderChordpro(text) {
             if (verseLines == null)
                 verseLines = [];
 
-            // split line into parts on chords
-            const parts = [];
-            line.split(/(\[[^\]]+\])/).forEach(part => parts.push(part[0] == '['
-                ? html`<span class="chord">${part.slice(1, -1)}</span>`
-                : (part ? html`<span class="text">${part}</span>` : null)
-            ));
-            verseHasChords ||= parts.length > 1;
-            parts.push('\n')
-            verseLines.push(parts);
+            // split line into segments of one chord plus the lyrics up to the
+            // next chord; the first element is the lyrics before the first chord
+            const [lead, ...rest] = line.split(/\[([^\]]+)\]/);
+            const segments = lead ? [segment('', lead)] : [];
+            for (let i = 0; i < rest.length; i += 2) {
+                let [chord, text] = [rest[i], rest[i + 1]];
+                // spacing between chords without lyrics belongs into the
+                // chord row, so that chord-only lines stay one row tall
+                if (!text.trim()) {
+                    chord += text;
+                    text = '';
+                }
+                segments.push(segment(chord, text));
+            }
+            verseHasChords ||= rest.length > 0;
+            segments.push('\n');
+            verseLines.push(segments);
         }
     }
 
index e2e75c408d800c54e5707f602d67dd1c279e381b..ef73f9d19869138156baea63c13975f1120ed09a 100644 (file)
@@ -15,22 +15,35 @@ h2 {
   margin-bottom: 0.3em;
 }
 
-.verse.chords {
-  line-height: 2.3em;
+/* A segment is one chord and the lyrics up to the next chord, stacked as two
+   rows. Its width is the wider of the two, so chords can never overlap each
+   other or the next segment. Line wrapping only happens between segments;
+   spaces in the lyrics are kept as written. */
+.verse.chords .seg {
+  display: inline-block;
+  white-space: pre;
+  /* A segment without lyrics has no baseline in its lyrics row, so baseline
+     alignment would put its chord into the lyrics row of its neighbours. All
+     chord rows have the same height, so aligning the tops lines them up. */
+  vertical-align: top;
+}
+
+.verse.chords .chord,
+.verse.chords .text {
+  display: block;
 }
 
-.chord + .chord {
-  width: unset;
-  margin-left: 0.3ex;
+.verse.chords .chord {
+  /* chord names have no descenders, so keep the row tight to the lyrics */
+  line-height: 1;
+  /* keep lyrics in the bottom row when there is no chord above them */
+  min-height: 1lh;
 }
 
 .chord {
-  position: relative;
-  top: -1em;
-  display: inline-block;
-  width: 0;
-  overflow: visible;
-  text-shadow: #CCC 1px 1px;
   color: #F33;
   font-weight: bold;
+  text-shadow: #CCC 1px 1px;
+  /* gap between adjacent chords */
+  padding-right: 0.3ex;
 }