Chorus
I hate to sail on this rotten tub
Leave her Johnny leave her
No grog allowed and rotten grub
And it's time for us to leave her
Chorus
diff --git a/booklet/shanties.css b/booklet/shanties.css
index 028e77a..9ab8253 100644
--- a/booklet/shanties.css
+++ b/booklet/shanties.css
@@ -19,6 +19,15 @@
--rule: #333;
--display: "Primitive", "UnifrakturCook", "Blackletter", serif;
--body: "Lucida Bright", "Lucida Serif", "Georgia", "Times New Roman", serif;
+
+ /* Harmonic-function colors, keyed by scale degree (1-7), in rainbow order. */
+ --deg1: #d62828; /* tonic I - red */
+ --deg2: #e8720c; /* supertonic ii - orange */
+ --deg3: #d19a00; /* mediant III - yellow */
+ --deg4: #2a9d3a; /* subdominant IV - green */
+ --deg5: #2564d6; /* dominant V - blue */
+ --deg6: #544ee1; /* submediant vi - indigo */
+ --deg7: #8338ec; /* subtonic vii - violet */
}
/* Body, page numbers, chorus headers — everything but titles — in Lucida Bright. */
@@ -189,19 +198,43 @@
and the chord is absolutely positioned over the segment's first letter. */
.cl {
position: relative;
- padding-top: 1em;
- margin: 0 0 0.04in;
+ padding-top: 1.0em;
+ margin: 0 0 0.06in;
line-height: 1.4;
}
.cl .seg { position: relative; }
.cl .chd {
position: absolute;
- top: -1em;
+ bottom: 100%; /* sit fully above the lyric, never overlapping */
left: 0;
+ margin-bottom: 0.14em;
font-family: var(--body);
font-size: 0.8em;
font-weight: 700;
+ font-style: normal; /* stay upright even over italic refrain lyrics */
line-height: 1;
color: var(--chord);
white-space: nowrap;
}
+
+/* Key-aware chords (Option 3): a solid pill in the chord's harmonic-function
+ color, with its Roman numeral to the right in the same color. The Roman
+ numeral keeps the encoding legible when printed in grayscale. */
+.cl .chd.deg-1 { --fn: var(--deg1); }
+.cl .chd.deg-2 { --fn: var(--deg2); }
+.cl .chd.deg-3 { --fn: var(--deg3); }
+.cl .chd.deg-4 { --fn: var(--deg4); }
+.cl .chd.deg-5 { --fn: var(--deg5); }
+.cl .chd.deg-6 { --fn: var(--deg6); }
+.cl .chd.deg-7 { --fn: var(--deg7); }
+.cl .chd .chd-name {
+ background: var(--fn, var(--chord));
+ color: #fff;
+ border-radius: 0.28em;
+ padding: 0.05em 0.32em;
+}
+.cl .chd .chd-rn {
+ margin-left: 0.15em;
+ font-size: 0.7em;
+ color: var(--fn, var(--chord));
+}
diff --git a/patches/markdown-booklet+0.1.0.patch b/patches/markdown-booklet+0.1.0.patch
new file mode 100644
index 0000000..ca60a95
--- /dev/null
+++ b/patches/markdown-booklet+0.1.0.patch
@@ -0,0 +1,173 @@
+diff --git a/node_modules/markdown-booklet/src/render.js b/node_modules/markdown-booklet/src/render.js
+index 5da5418..727c059 100644
+--- a/node_modules/markdown-booklet/src/render.js
++++ b/node_modules/markdown-booklet/src/render.js
+@@ -80,12 +80,13 @@ function renderSource(source) {
+ case PageType.SONG: {
+ const id = `song-${++spreadCounter}`;
+ const parts = song.splitPages(source.content || '');
++ const songOpts = { key: meta.key };
+ if (meta.spread === false) {
+ // Single-page song: flows like a normal page (no forced spread).
+ return [
+ {
+ type: PageType.SONG,
+- html: song.parsePage(parts.join('\n---\n')),
++ html: song.parsePage(parts.join('\n---\n'), songOpts),
+ showPageNumber,
+ startOn,
+ },
+@@ -100,14 +101,14 @@ function renderSource(source) {
+ return [
+ {
+ type: PageType.SONG,
+- html: song.parsePage(parts[0]),
++ html: song.parsePage(parts[0], songOpts),
+ showPageNumber,
+ spreadId: id,
+ spreadRole: 'left',
+ },
+ {
+ type: PageType.SONG,
+- html: song.parsePage(parts.slice(1).join('\n---\n')),
++ html: song.parsePage(parts.slice(1).join('\n---\n'), songOpts),
+ showPageNumber,
+ spreadId: id,
+ spreadRole: 'right',
+diff --git a/node_modules/markdown-booklet/src/song.js b/node_modules/markdown-booklet/src/song.js
+index eba96d9..4f19223 100644
+--- a/node_modules/markdown-booklet/src/song.js
++++ b/node_modules/markdown-booklet/src/song.js
+@@ -24,8 +24,81 @@ function esc(text) {
+ .replace(/>/g, '>');
+ }
+
++// --- Key-aware chord analysis -------------------------------------------
++// Given a song key, each chord is tagged with its scale degree (1-7) and its
++// Roman numeral, so the stylesheet can color it by harmonic function. Purely
++// additive: chords still render verbatim, and without a key they stay plain.
++const NOTE_PC = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
++const SCALE_LETTERS = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
++const ROMAN = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII'];
++const MAJOR_STEPS = [0, 2, 4, 5, 7, 9, 11];
++const MINOR_STEPS = [0, 2, 3, 5, 7, 8, 10];
++
++function pitchClass(note) {
++ let pc = NOTE_PC[note[0]];
++ if (pc === undefined) return null;
++ for (let i = 1; i < note.length; i += 1) {
++ if (note[i] === '#') pc += 1;
++ else if (note[i] === 'b') pc -= 1;
++ }
++ return ((pc % 12) + 12) % 12;
++}
++
++function parseKey(key) {
++ const m = /^([A-G])([#b]?)\s*(m|min|minor)?$/.exec(String(key || '').trim());
++ if (!m) return null;
++ return { letter: m[1], pc: pitchClass(m[1] + m[2]), minor: !!m[3] };
++}
++
++/** Analyze a chord symbol against a key. Returns { degree, roman } or null. */
++function analyzeChord(symbol, key) {
++ const parsedKey = parseKey(key);
++ if (!parsedKey) return null;
++ const m = /^([A-G])([#b]?)(.*)$/.exec(symbol);
++ if (!m) return null;
++ const [, rootLetter, rootAcc, rest] = m;
++
++ let quality = 'maj';
++ if (/^(m(?!aj)|min)/.test(rest)) quality = 'min';
++ else if (/^(dim|\u00B0)/i.test(rest)) quality = 'dim';
++ else if (/^(aug|\+)/.test(rest)) quality = 'aug';
++
++ const steps = parsedKey.minor ? MINOR_STEPS : MAJOR_STEPS;
++ const tonicIdx = SCALE_LETTERS.indexOf(parsedKey.letter);
++ const rootIdx = SCALE_LETTERS.indexOf(rootLetter);
++ const degIdx = (((rootIdx - tonicIdx) % 7) + 7) % 7;
++ const diatonicPc = (parsedKey.pc + steps[degIdx]) % 12;
++ const acc = (((pitchClass(rootLetter + rootAcc) - diatonicPc + 6) % 12) + 12) % 12 - 6;
++ const accStr = acc > 0 ? '\u266F'.repeat(acc) : acc < 0 ? '\u266D'.repeat(-acc) : '';
++
++ let numeral = ROMAN[degIdx];
++ if (quality === 'min' || quality === 'dim') numeral = numeral.toLowerCase();
++ if (quality === 'dim') numeral += '\u00B0';
++ else if (quality === 'aug') numeral += '+';
++ const ext = (rest.match(/\d+/) || [''])[0];
++
++ return { degree: degIdx + 1, roman: accStr + numeral + ext };
++}
++
++/** Markup for one chord: a plain `.chd`, or a degree-tagged pill + numeral. */
++function chordMarkup(chord, opts) {
++ const key = opts && opts.key;
++ if (key) {
++ const info = analyzeChord(chord, key);
++ if (info) {
++ return (
++ `
` +
++ `${esc(chord)}` +
++ `${esc(info.roman)}` +
++ ``
++ );
++ }
++ }
++ return `
${esc(chord)}`;
++}
++
+ /** Convert inline [C] markers into chord-over-lyric segments. */
+-function chordHtml(text) {
++function chordHtml(text, opts) {
+ if (!text.includes('[')) return esc(text);
+ const tokens = text.split(/(\[[^\]]+\])/);
+ let html = '';
+@@ -35,7 +108,7 @@ function chordHtml(text) {
+ if (chord === null) {
+ if (buffer) html += `
${esc(buffer)}`;
+ } else {
+- html += `
${esc(chord)}${esc(buffer)}`;
++ html += `
${chordMarkup(chord, opts)}${esc(buffer)}`;
+ }
+ buffer = '';
+ };
+@@ -53,20 +126,20 @@ function chordHtml(text) {
+ }
+
+ /** Render one lyric line. Chorus lines are always `cl`; refrains are italic. */
+-function lyricLine(line, inChorus) {
++function lyricLine(line, inChorus, opts) {
+ const refrain = line.startsWith('>');
+ const text = (refrain ? line.replace(/^>\s?/, '') : line).trim();
+ const chorded = text.includes('[');
+ const classes = [];
+ if (inChorus || chorded) classes.push('cl');
+ if (refrain) classes.push('refrain');
+- const body = chorded || inChorus ? chordHtml(text) : esc(text);
++ const body = chorded || inChorus ? chordHtml(text, opts) : esc(text);
+ const cls = classes.length ? ` class="${classes.join(' ')}"` : '';
+ return `
${body}
`;
+ }
+
+ /** Parse one page of song text into HTML blocks. */
+-function parsePage(text) {
++function parsePage(text, opts) {
+ const lines = text.replace(/\s+$/, '').split(/\r?\n/);
+ const blocks = [];
+ let i = 0;
+@@ -87,14 +160,14 @@ function parsePage(text) {
+ i += 1;
+ const rows = [];
+ while (i < lines.length && lines[i].trim()) {
+- rows.push(lyricLine(lines[i].trim(), true));
++ rows.push(lyricLine(lines[i].trim(), true, opts));
+ i += 1;
+ }
+ blocks.push(`
`);
+ } else {
+ const rows = [];
+ while (i < lines.length && lines[i].trim() && !/^\{chorus\}$/i.test(lines[i].trim())) {
+- rows.push(lyricLine(lines[i].trim(), false));
++ rows.push(lyricLine(lines[i].trim(), false, opts));
+ i += 1;
+ }
+ blocks.push(`
${rows.join('')}
`);
From 1d040ea69e43ef29ba5b452e76045e3f613e1223 Mon Sep 17 00:00:00 2001
From: Chris <26607885+chrisglein@users.noreply.github.com>
Date: Tue, 7 Jul 2026 23:22:34 -0700
Subject: [PATCH 3/3] Use an outlined pill style for chords
---
booklet/sea-shanties-single.html | 8 ++++----
booklet/sea-shanties.html | 8 ++++----
booklet/shanties.css | 8 ++++----
3 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/booklet/sea-shanties-single.html b/booklet/sea-shanties-single.html
index ce7bc25..56fee8e 100644
--- a/booklet/sea-shanties-single.html
+++ b/booklet/sea-shanties-single.html
@@ -266,7 +266,7 @@
white-space: nowrap;
}
-/* Key-aware chords (Option 3): a solid pill in the chord's harmonic-function
+/* Key-aware chords (Option 6): an outlined pill in the chord's harmonic-function
color, with its Roman numeral to the right in the same color. The Roman
numeral keeps the encoding legible when printed in grayscale. */
.cl .chd.deg-1 { --fn: var(--deg1); }
@@ -277,10 +277,10 @@
.cl .chd.deg-6 { --fn: var(--deg6); }
.cl .chd.deg-7 { --fn: var(--deg7); }
.cl .chd .chd-name {
- background: var(--fn, var(--chord));
- color: #fff;
+ color: var(--fn, var(--chord));
+ border: 1px solid var(--fn, var(--chord));
border-radius: 0.28em;
- padding: 0.05em 0.32em;
+ padding: 0.02em 0.28em;
}
.cl .chd .chd-rn {
margin-left: 0.15em;
diff --git a/booklet/sea-shanties.html b/booklet/sea-shanties.html
index f00b9f6..d6471e8 100644
--- a/booklet/sea-shanties.html
+++ b/booklet/sea-shanties.html
@@ -268,7 +268,7 @@
white-space: nowrap;
}
-/* Key-aware chords (Option 3): a solid pill in the chord's harmonic-function
+/* Key-aware chords (Option 6): an outlined pill in the chord's harmonic-function
color, with its Roman numeral to the right in the same color. The Roman
numeral keeps the encoding legible when printed in grayscale. */
.cl .chd.deg-1 { --fn: var(--deg1); }
@@ -279,10 +279,10 @@
.cl .chd.deg-6 { --fn: var(--deg6); }
.cl .chd.deg-7 { --fn: var(--deg7); }
.cl .chd .chd-name {
- background: var(--fn, var(--chord));
- color: #fff;
+ color: var(--fn, var(--chord));
+ border: 1px solid var(--fn, var(--chord));
border-radius: 0.28em;
- padding: 0.05em 0.32em;
+ padding: 0.02em 0.28em;
}
.cl .chd .chd-rn {
margin-left: 0.15em;
diff --git a/booklet/shanties.css b/booklet/shanties.css
index 9ab8253..e5d6f7a 100644
--- a/booklet/shanties.css
+++ b/booklet/shanties.css
@@ -217,7 +217,7 @@
white-space: nowrap;
}
-/* Key-aware chords (Option 3): a solid pill in the chord's harmonic-function
+/* Key-aware chords (Option 6): an outlined pill in the chord's harmonic-function
color, with its Roman numeral to the right in the same color. The Roman
numeral keeps the encoding legible when printed in grayscale. */
.cl .chd.deg-1 { --fn: var(--deg1); }
@@ -228,10 +228,10 @@
.cl .chd.deg-6 { --fn: var(--deg6); }
.cl .chd.deg-7 { --fn: var(--deg7); }
.cl .chd .chd-name {
- background: var(--fn, var(--chord));
- color: #fff;
+ color: var(--fn, var(--chord));
+ border: 1px solid var(--fn, var(--chord));
border-radius: 0.28em;
- padding: 0.05em 0.32em;
+ padding: 0.02em 0.28em;
}
.cl .chd .chd-rn {
margin-left: 0.15em;