From c871bab2839b1a46ae809631f3e76a6d0f2c3519 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 01:34:04 +0000 Subject: [PATCH] Real-world charts render: tolerant chord lines + bare section labels (v2026.09.07.002) Owner sent a screenshot of a ZZ Top chart from their library with almost no coloured chords and no visible section markers. Two detection defects. 1. isChordLine required EVERY token on a line to be a chord, so a single mangled token dropped the whole line -- its good chords included -- to plain lyrics. "E G#dD#" lost its E. A song pasted with damaged chords showed almost none, which is what the screenshot was. A line now qualifies when every token is a chord, a separator, a cue, or at least chord-SHAPED (starts A-G, chord-legal characters only, no run of 3+ lowercase letters after the root -- so "G#dD#" and "C#d" pass while "Cause", "Blackis" and "Don't" do not); at least one token is a REAL chord; and real chords are at least half the musical tokens. Those last two rules are the lyric guard. Drop either and lines like "A big deal" become chord rows; with them, ten sampled lyric lines stay lyrics. An unrecognised token renders as a neutral dashed pill keeping its literal text, and is never transposed or pitch-coloured -- CHORD_RE would read "G#dD#" as root G# plus suffix "dD#" and shift only the root, quietly corrupting it. It stays visible so a bad import can be spotted and fixed. Inline cues ("E riff", "A (x4)") get a muted italic pill instead, since they are not broken chords. 2. Bare "Intro Riff:" / "Verse 1:" / "Chorus:" labels -- what chord sites and OnSong actually emit -- now become headings. Only {c:...} did before, so the shipped showcase song looked immaculate while the structure of every imported song was invisible. The colon must be the last character, which excludes metadata rows like "Key: G", and chord lines win the tie so "E:" stays a chord. Both fixes are at render time, so songs already in a library heal without being re-imported. Verified against the owner's chart transcribed from the screenshot: 14 assertions covering headings, recovered chords, neutral rendering of the mangled tokens, cue styling, and that transposing never alters an unrecognised token. Ten ordinary lyric lines still parse as lyrics. Existing suites green -- links 24, showcase 11, wake 11, UI audit 18 -- and the six seed songs render byte-identically. Still unexplained: what mangled "G#d" and joined words like "downthe" on the way in. That needs the raw stored text, which the owner is sending as a song link. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RFni8j63y7ynwzmNLMPMTB --- CLAUDE.md | 21 +++++++++++++- README.md | 2 +- setlist69.html | 78 ++++++++++++++++++++++++++++++++++++++++++++++---- sw.js | 2 +- 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9f30a6a..0232293 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -345,6 +345,17 @@ v2026.09.07.001 Visual pass — one icon language, real type hierarchy, readabl `install()` in the long-press IIFE now takes the opener as its 3rd argument. Played = a `✓` in the `.num` slot only. Version tag moved to `#aboutLine` in the ≡ sheet; see §9 for why `.brand small` must stay in the DOM. +v2026.09.07.002 Real-world charts render. `isChordLine` no longer requires every token to be a + chord — one mangled token used to drop the whole line (and its good chords) to + plain lyrics. New rule: every token is a chord, separator, cue, or **chord-shaped** + (`chordShaped()`: starts A-G, no run of 3+ lowercase after the root); ≥1 real + chord; and real chords ≥ half the musical tokens (that ratio is the lyric guard — + "A big deal" is 1-in-3). Unrecognised tokens render as `.chordpill.unk` (neutral, + dashed, literal text) and are **never transposed or pitch-coloured** — the root + parser would read "G#dD#" as G#. Cues (`CHORD_CUE`: riff/solo/(x4)/2x…) render as + `.chordpill.cue`. Also: bare `Intro Riff:` / `Verse 1:` / `Chorus:` labels become + headings (colon must be the LAST character, excluding `Key: G`; chord lines win + the tie). Both at RENDER time, so existing songs heal with no re-import. ``` > **Note:** the changelog comment at the top of `setlist69.html` is missing entries @@ -519,7 +530,15 @@ the used count — measure distinct `.sect` x-offsets to know what actually rend /^[A-G][#b]?(?:maj|min|m|M|dim|aug|sus|add|°|ø|\+|-|[#b]?(?:2|4|5|6|7|9|11|13)|\([^)]*\))*(?:\/[A-G][#b]?)?[!*]?$/ ``` -`isChordLine(line)` — all tokens must be chords OR pure separators (`/`, `-`, `|`); at least one chord required. This handles "Cm / Bb / Dm - D" correctly. +`isChordLine(line)` (rewritten v2026.09.07.002) — **tolerant of tokens it can't parse**, because real charts arrive with mangled chords and inline cues. A line qualifies when: + +1. every token is a real chord, a pure separator (`/ - |`), a cue (`CHORD_CUE`: `riff`, `solo`, `(x4)`, `2x`…), or at least **chord-shaped** — `chordShaped()`: starts on `A-G`, only chord-legal characters, and no run of 3+ lowercase letters after the root (so `G#dD#`, `C#d` pass; `Cause`, `Blackis`, `Don't` fail); +2. **at least one token is a real chord**; and +3. real chords are **at least half** the musical tokens. + +Rules 2 and 3 are the lyric guard — drop either and lines like "A big deal" (1 chord in 3) become chord rows. The old all-or-nothing rule meant one bad token dropped the line, its good chords included, to plain lyrics. + +**An unrecognised token must never be treated as a chord.** `renderLine` gives it `.chordpill.unk` (neutral, dashed, literal text), `retuneSheet` skips it, and `renderSheet` keeps it out of `sheetRawChords`. Transposing it would corrupt it: `CHORD_RE` reads `G#dD#` as root `G#` + suffix `dD#` and would shift only the root. ### Chordie auto-clean diff --git a/README.md b/README.md index 20011b8..f726fed 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@
[![CI](https://github.com/cdburgess75/SetList69/actions/workflows/check.yml/badge.svg)](https://github.com/cdburgess75/SetList69/actions/workflows/check.yml) -[![Version](https://img.shields.io/badge/version-2026.09.07.001-informational?style=flat-square)](#versioning) +[![Version](https://img.shields.io/badge/version-2026.09.07.002-informational?style=flat-square)](#versioning) [![PWA](https://img.shields.io/badge/PWA-installable-5A0FC8?style=flat-square)](#save-it-to-your-phone) [![Offline](https://img.shields.io/badge/offline-first-2ea043?style=flat-square)](#features) [![Runtime deps](https://img.shields.io/badge/runtime_dependencies-0-f5a623?style=flat-square)](#tech-stack) diff --git a/setlist69.html b/setlist69.html index 6dacb35..04990d5 100644 --- a/setlist69.html +++ b/setlist69.html @@ -274,6 +274,26 @@ the DOM but display:none because CI greps it and APP_VERSION reads it, and the 5-tap gesture-debug trigger moved onto #aboutLine with it. Sheet renderer verified byte-identical across all six seed songs. + v2026.09.07.002 Real-world charts render (owner screenshot: a ZZ Top chart with almost no + coloured chords and no visible sections). Two defects, both in detection. + (1) isChordLine required EVERY token to be a chord, so one mangled token took + the whole line down to plain lyrics — "E G#dD#" lost its E, and a song pasted + with damaged chords showed nearly none. Now a line qualifies when every token + is a chord, a separator, a cue, or at least chord-SHAPED (starts A-G, no + word-like run of 3+ lowercase after the root — so "G#dD#" and "C#d" pass while + "Cause" and "Blackis" do not); at least one is a REAL chord; and real chords + are at least half the musical tokens. That ratio is what keeps lyrics out — + "A big deal" is one chord in three. Unrecognised tokens render as a neutral + dashed pill (.unk) keeping their literal text, and are NEVER transposed or + colour-coded: the root parser would read "G#dD#" as G# and shift only that. + Inline cues ("E riff", "A (x4)") get a muted italic .cue pill instead. + (2) Bare "Intro Riff:" / "Verse 1:" / "Chorus:" labels — what chord sites and + OnSong actually emit — now become headings; only {c:...} did before, so the + shipped showcase looked immaculate while every imported song's structure was + invisible. The colon must be the LAST character, which excludes metadata + ("Key: G" carries a value) and ordinary lyrics; chord lines win the tie. + Both fixes are at RENDER time, so existing songs heal with no re-import. + Seed songs verified byte-identical. --> @@ -398,6 +418,12 @@ .chev{color:var(--faint);font-size:1.1rem;} .num{font-family:var(--mono);color:var(--faint);font-size:.85rem;min-width:1.3rem;text-align:right;} + /* chord-line token we can't parse: neutral, outlined, obviously not a real chord */ + .chordpill.unk{background:transparent;color:var(--faint);border:1px dashed var(--line); + box-shadow:none;font-weight:600;} + /* a performance cue on the chord line ("E riff") — muted, no border, clearly not a chord */ + .chordpill.cue{background:transparent;color:var(--muted);border:none;box-shadow:none; + font-family:var(--ui);font-style:italic;font-weight:600;letter-spacing:0;} .pill{font-family:var(--mono);font-size:.72rem;font-weight:700;padding:.2rem .5rem;border-radius:7px; color:#fff;letter-spacing:.3px;white-space:nowrap;box-shadow:inset 0 0 0 1px #ffffff22;} @@ -663,7 +689,7 @@
-
SetList69v2026.09.07.001
+
SetList69v2026.09.07.002
@@ -978,7 +1004,29 @@

Songs

// marker. Recognises Cmaj7, C7sus4, Cm7b5, Bbm7b5, D7#9, Cadd9, F°, Bø, slash chords, Gm! — while // still rejecting ordinary lyric words (used by isChordLine for chords-above detection + import). function looksChord(t){return /^[A-G][#b]?(?:maj|min|m|M|dim|aug|sus|add|°|ø|\+|-|[#b]?(?:2|4|5|6|7|9|11|13)|\([^)]*\))*(?:\/[A-G][#b]?)?[!*]?$/.test(t);} -function isChordLine(line){const toks=line.trim().split(/\s+/).filter(Boolean);if(!toks.length)return false;let chords=0;for(const t of toks){if(looksChord(t))chords++;else if(/^[\/\-|]+$/.test(t))continue;else return false;}return chords>0;} +/* Inline cues that sit on a chord line without being chords: "E riff", "A (x4)", "D 2x". */ +const CHORD_CUE=/^(\(.*\)|x\s*\d+|\d+\s*x|riff|solo|fill|break|tacet|hold|ring|let|only|repeat|out|end|tag|intro|outro|verse|chorus|bridge)$/i; +/* "Chord-shaped" = could plausibly BE a chord: starts on A-G, no punctuation a chord never has, + and no word-like run of 3+ lowercase letters after the root ("Cause" and "Blackis" are words, + "G#dD#" and "C#d" are mangled chords). */ +function chordShaped(t){return /^[A-G][#b]?[A-Za-z0-9#b°ø+\-\/()]*$/.test(t)&&!/[a-z]{3}/.test(t.slice(1));} +/* A single unrecognised token used to drop the WHOLE line to plain lyrics, taking its good chords + with it — a chart pasted with mangled chords ("E G#dD#") lost the E as well, and every chord in + the song vanished. Now a line is a chord line when every token is a chord, a separator, a cue, or + at least chord-shaped; at least one is a REAL chord; and real chords are at least half the + musical tokens. That last ratio is what keeps lyrics out: "A big deal" has one chord in three. */ +function isChordLine(line){ + const toks=line.trim().split(/\s+/).filter(Boolean);if(!toks.length)return false; + let real=0,musical=0; + for(const t of toks){ + if(/^[\/\-|]+$/.test(t))continue; + if(CHORD_CUE.test(t))continue; + musical++; + if(looksChord(t))real++; + else if(!chordShaped(t))return false; // an actual word — this is a lyric line + } + return real>0&&real*2>=musical; +} function pitchClass(key){const m=(key||"").match(CHORD_RE);return m&&m[1]in IDX?IDX[m[1]]:-1;} function keyColor(key){const pc=pitchClass(key);if(pc<0)return"#7a7160";const hue=(pc*30)%360;return"hsl("+hue+" 52% 42%)";} /* Nashville Number System: chord root -> scale degree relative to the key. Computed from the @@ -1041,6 +1089,15 @@

Songs

out.push({type:"comment",text:bareLbl[1].trim()+(bareLbl[2]?" "+bareLbl[2].trim():"")});continue;} if(/^[eEADGB]b?\s*\|[-0-9hpbrx\/\\~|\s]*\|?\s*$/.test(line.trim())){ out.push({type:"tab",text:line});continue;} + /* Bare section labels — "Intro Riff:", "Verse 1:", "Chorus:" — which is what chord sites and + OnSong actually emit. Only {c:...} used to become a heading, so the markers in real imported + songs stayed invisible while the shipped showcase looked immaculate. The colon must be the + LAST character: that excludes metadata rows ("Key: G" carries a value) and ordinary lyrics + (they carry sentence punctuation). Chord lines win the tie, so "E:" stays a chord. */ + const secLbl=line.trim(); + if(/^[A-Za-z][A-Za-z0-9 #'&.\/-]{0,38}:$/.test(secLbl)&&secLbl.split(/\s+/).length<=5 + &&!isChordLine(secLbl.slice(0,-1))){ + out.push({type:"comment",text:secLbl.slice(0,-1)});continue;} if(!hasInline){ const n=line.replace(/\b([A-G][#b]?)(\s+)slide(\s+)([A-G][#b]?)\b/g,"$1$2 $3$4") .replace(/\b([A-G][#b]?)s([A-G][#b]?)\b/g,"$1 $2"); @@ -1108,8 +1165,18 @@

Songs

const lineHasChord=groups.some(g=>g.some(c=>c.chord)); return groups.map(g=>{ const inner=g.map(cell=>{ + /* A token on the chord line that isn't a recognisable chord gets a neutral pill instead of a + pitch colour, and keeps its literal text: transposing or colouring it would be a lie (the + root parser would read "G#dD#" as G# and shift only that). It stays visible so a mangled + import can be spotted and fixed, rather than silently dropping the whole line to lyrics. */ + const known=cell.raw?looksChord(cell.raw):false; const crow=lineHasChord - ? ''+(cell.chord?''+((nashville&&nashKeyPc>=0)?nashPill(toNashville(cell.raw,nashKeyPc)):esc(cell.chord))+'':'')+'' + ? ''+(cell.chord?(known + ? ''+((nashville&&nashKeyPc>=0)?nashPill(toNashville(cell.raw,nashKeyPc)):esc(cell.chord))+'' + : (CHORD_CUE.test(cell.raw) + ? ''+esc(cell.raw)+'' + : ''+esc(cell.raw)+'')) + :'')+'' : ''; return ''+crow+''+(esc(cell.text)||" ")+''; }).join(""); @@ -1121,7 +1188,8 @@

Songs

function retuneSheet(){ const pills=document.querySelectorAll("#sheet .chordpill"); const nash=nashville&&nashKeyPc>=0; - for(const p of pills){const raw=p.dataset.ch||"",t=transposeChord(raw);p.style.background=keyColor(t); + for(const p of pills){if(p.classList.contains("unk")||p.classList.contains("cue"))continue; // literal text, never transposed + const raw=p.dataset.ch||"",t=transposeChord(raw);p.style.background=keyColor(t); if(nash)p.innerHTML=nashPill(toNashville(raw,nashKeyPc));else p.textContent=t;} renderChordChart(); updateScrollProg(); @@ -1170,7 +1238,7 @@

Songs

} closeSect(); sheetRawChords=[]; - for(const ln of r.lines)if(ln.type==="segs")for(const sg of ln.segs)if(sg.chord)sheetRawChords.push(sg.chord); + for(const ln of r.lines)if(ln.type==="segs")for(const sg of ln.segs)if(sg.chord&&looksChord(sg.chord))sheetRawChords.push(sg.chord); renderChordChart(); const sheet=document.getElementById("sheet");sheet.innerHTML=html;sheet.style.setProperty("--sheet-size",fontSize+"rem"); } diff --git a/sw.js b/sw.js index 2c99b54..a5aab65 100644 --- a/sw.js +++ b/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'setlist69-v2026.09.07.001'; +const CACHE = 'setlist69-v2026.09.07.002'; const PRECACHE = [ './', './index.html',