From a6e0318b5beb98ef7920a0a60426e429811445b0 Mon Sep 17 00:00:00 2001 From: Martin Pitt Date: Wed, 23 Sep 2026 22:30:13 +0200 Subject: [PATCH] Lay out chords and lyrics as segments 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 | 30 +++++++++++++++++++++--------- chordpro.css | 35 ++++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/chordpro-web.js b/chordpro-web.js index 5abd0d6..abbf404 100644 --- a/chordpro-web.js +++ b/chordpro-web.js @@ -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`${chord}${text}`; + 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`${part.slice(1, -1)}` - : (part ? html`${part}` : 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); } } diff --git a/chordpro.css b/chordpro.css index e2e75c4..ef73f9d 100644 --- a/chordpro.css +++ b/chordpro.css @@ -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; } -- 2.47.3