Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,17 @@ v2026.09.07.002 Real-world charts render. `isChordLine` no longer requires ever
`.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.
v2026.09.07.003 Library repair, from an audit of a real 50-song backup. Three render-time fixes;
298 -> 418 coloured chords across that library with **no song losing one**.
(1) `splitChordRun()` splits space-stripped runs (`DDCD`, `GmFDmCGmFDmCA`) — 8
songs. Returns null unless the whole string is consumed by real chords; ordinary
words can't pass because roots must be **uppercase** A-G. (2) A chord row followed
by a **whitespace-only** line then a lyric is paired across it (23 rows). **A
genuinely empty line must not trigger this** — "intro chords, blank, verse" is
deliberate; 17 such rows are correctly left alone. (3) `Bd` -> `Bb` (owner-
confirmed corrupted flat), **bare letter only** and length-preserving.
**Finding worth keeping:** ~half that library has no chords in the data at all —
lyrics-only imports. No parser change fixes that.
```

> **Note:** the changelog comment at the top of `setlist69.html` is missing entries
Expand Down Expand Up @@ -538,6 +549,10 @@ the used count — measure distinct `.sect` x-offsets to know what actually rend

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.

**Space-stripped runs** (v2026.09.07.003): `splitChordRun(t)` rebuilds a progression stored as one token (`CGBdA` → `C G Bb A`). It is greedy left-to-right and returns `null` unless the entire string is consumed by `looksChord` tokens, so a leftover character rejects the line rather than half-parsing it. **The safety comes from case**: a chord root must be an uppercase `A-G`, so "Cage", "Face", "Bag" fail on their second character. A bare letter followed by lowercase `d` is read as a flat (`Bd` → `Bb`) — roots are uppercase, so a lowercase `d` is unambiguous. Only applied when the line has no whitespace at all, is ≥4 chars, and yields ≥3 chords.

**Orphaned chord rows** (v2026.09.07.003): chord line → **whitespace-only** line → lyric gets paired across the junk line. **A genuinely empty (`""`) line must NOT trigger this** — "intro chords, blank, verse" is a real layout and pairing would drop the intro onto the first sung line.

**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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<br>

[![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.002-informational?style=flat-square)](#versioning)
[![Version](https://img.shields.io/badge/version-2026.09.07.003-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)
Expand Down
70 changes: 69 additions & 1 deletion setlist69.html
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,25 @@
("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.
v2026.09.07.003 Library repair, driven by an audit of a real 50-song backup rather than guesses.
Three render-time fixes; no song in that library loses a chord and the total
goes 298 -> 418 coloured chords. (1) SPACE-STRIPPED CHORD RUNS: a paste artifact
in 8 songs stores a whole progression as one token ("DDCD",
"GmFDmCGmFDmCA"). splitChordRun() greedily splits it back and returns null
unless the WHOLE string is consumed by real chords. Ordinary words cannot pass
because a chord root must be an UPPERCASE A-G — "Cage"/"Face"/"Bag" die on
their second character. Verified against the actual progressions: White Room
gives Gm F Dm C Gm F Dm C A. (2) ORPHANED CHORD ROWS: chord line, then a
WHITESPACE-ONLY line, then the lyric — 23 rows where the lyric's leading spaces
were split onto their own line, leaving the chord stranded. Now paired across
it. A GENUINELY EMPTY line must never trigger this: "intro chords, blank,
verse" is deliberate, and pairing there would drop intro chords onto the first
sung line (17 such rows correctly left alone). (3) "Bd" -> "Bb", a corrupted
flat the owner confirmed, 6 occurrences. BARE LETTER ONLY — "C#d"/"G#d" already
carry an accidental and would become "C-sharp-flat", so they stay flagged.
Length-preserving (2 chars -> 2) so column alignment is untouched. NOTE: about
half that library has no chords in the stored data at all — imported as lyrics
only. No parser change can conjure those; they need re-importing.
-->
<html lang="en">
<head>
Expand Down Expand Up @@ -689,7 +708,7 @@
<div class="app">
<header>
<button class="back" id="backBtn" style="display:none" aria-label="Back"><svg class="ico" aria-hidden="true"><use href="#i-back"/></svg><span id="backLabel">Back</span></button>
<div class="brand" id="brand"><span class="dot"></span>SetList69<small>v2026.09.07.002</small></div>
<div class="brand" id="brand"><span class="dot"></span>SetList69<small>v2026.09.07.003</small></div>
<div class="htitle" id="headerTitle" style="display:none"></div>
<div class="spacer"></div>
<button class="iconbtn" id="updateHdr" title="Update ready" aria-label="Update ready — refresh" hidden><svg class="ico" aria-hidden="true"><use href="#i-refresh"/></svg></button>
Expand Down Expand Up @@ -1009,6 +1028,29 @@ <h2>Songs</h2><span class="count" id="allCount"></span>
/* "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). */
/* Split a chord run whose spaces were stripped by a bad paste ("CGBdA", "GmFDmCGmFDmCA") back
into chords. Greedy, left to right; returns null unless the WHOLE string is consumed and every
piece is a real chord, so a leftover character rejects the line rather than half-parsing it.

Ordinary words cannot pass: a chord root must be an UPPERCASE A-G, so "Cage"/"Face"/"Bag" die on
their second character. Only genuine runs of capitalised roots (plus lowercase chord suffixes)
survive. A bare letter followed by lowercase "d" is a corrupted FLAT — the owner confirmed "Bd"
is B-flat, and since roots are uppercase a lowercase d is unambiguous. */
function splitChordRun(t){
const RE=/^[A-G][#b]?(?:maj|min|m|M|dim|aug|sus|add|[0-9]|\/[A-G][#b]?)*/;
const out=[];let i=0,guard=0;
while(i<t.length){
if(++guard>64)return null;
const m=RE.exec(t.slice(i));
if(!m||!m[0])return null;
let tok=m[0];
if(tok.length===1&&t[i+1]==="d"){tok+="b";i+=2;} // "Bd" -> "Bb"
else i+=m[0].length;
if(!looksChord(tok))return null;
out.push(tok);
}
return out.length?out:null;
}
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
Expand Down Expand Up @@ -1102,10 +1144,36 @@ <h2>Songs</h2><span class="count" id="allCount"></span>
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");
if(n!==line&&isChordLine(n))line=n;
/* "Bd" -> "Bb": a corrupted flat, owner-confirmed, seen 6x across two songs. BARE LETTER
ONLY — "C#d"/"G#d" already carry an accidental and would become "C-sharp-flat", so those
stay flagged as unknown. Same length (2 chars -> 2), so pairToSegs' column alignment is
untouched; a shorter replacement would slide every later chord onto the wrong syllable.
Guarded on isChordLine so a lyric like "Ed Sullivan" is never rewritten. */
const f=line.replace(/\b([A-G])d\b/g,"$1b");
if(f!==line&&isChordLine(f))line=f;
}
/* A chord run with every space stripped, e.g. "DDCD" or "GmFDmCGmFDmCA" — a paste artifact
found in 8 songs of the owner's library. Split it back apart so the chords are readable and
colourable. There is no column information left in such a line, so it renders as a bar row
with no lyric beneath, which is exactly what it is. */
if(!hasInline&&!/\s/.test(line.trim())&&line.trim().length>=4&&!isChordLine(line)){
const runParts=splitChordRun(line.trim());
if(runParts&&runParts.length>=3){
out.push({type:"segs",chorus:inChorus,
segs:runParts.map((c,ri)=>({chord:c,text:ri<runParts.length-1?" ":""}))});
continue;}
}
if(hasInline&&/\[[^\]]+\]/.test(line)){out.push({type:"segs",segs:inlineToSegs(line),chorus:inChorus});}
else if(!hasInline&&isChordLine(line)&&i+1<lines.length&&lines[i+1].trim()!==""&&!isChordLine(lines[i+1])){
out.push({type:"segs",segs:pairToSegs(line,lines[i+1]),chorus:inChorus});i++;}
/* Chord row, then a WHITESPACE-ONLY line, then the lyric — a paste artifact where the lyric's
leading spaces were split onto their own line (23 rows across the owner's library). The chord
used to render alone with the lyric bare underneath. Pair across the junk line.
A GENUINELY EMPTY line must never trigger this: "intro chords, blank, verse" is a deliberate
layout, and pairing there would drop the intro chords onto the first sung line. */
else if(!hasInline&&isChordLine(line)&&lines[i+1]!==undefined&&lines[i+1]!==""&&lines[i+1].trim()===""
&&lines[i+2]!==undefined&&lines[i+2].trim()!==""&&!isChordLine(lines[i+2])){
out.push({type:"segs",segs:pairToSegs(line,lines[i+2]),chorus:inChorus});i+=2;}
else if(!hasInline&&isChordLine(line)){out.push({type:"segs",segs:pairToSegs(line,""),chorus:inChorus});}
else{out.push({type:"segs",segs:[{chord:"",text:line}],chorus:inChorus});}
}
Expand Down
2 changes: 1 addition & 1 deletion sw.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const CACHE = 'setlist69-v2026.09.07.002';
const CACHE = 'setlist69-v2026.09.07.003';
const PRECACHE = [
'./',
'./index.html',
Expand Down
Loading