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..e5d6f7a 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 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); }
+.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 {
+ color: var(--fn, var(--chord));
+ border: 1px solid var(--fn, var(--chord));
+ border-radius: 0.28em;
+ padding: 0.02em 0.28em;
+}
+.cl .chd .chd-rn {
+ margin-left: 0.15em;
+ font-size: 0.7em;
+ color: var(--fn, var(--chord));
+}
diff --git a/booklet/skipper-jan-rebec.md b/booklet/skipper-jan-rebec.md
index e9c518b..a9f2fa9 100644
--- a/booklet/skipper-jan-rebec.md
+++ b/booklet/skipper-jan-rebec.md
@@ -1,3 +1,7 @@
+---
+key: Eb
+---
+
# Skipper Jan Rebec
[Eb]Who's the king of the [Ab]fighting [Eb]Dutch
diff --git a/booklet/south-australia.md b/booklet/south-australia.md
index a1626f8..7d399e4 100644
--- a/booklet/south-australia.md
+++ b/booklet/south-australia.md
@@ -1,3 +1,7 @@
+---
+key: E
+---
+
# South Australia
[E]In South Australia I was born
diff --git a/booklet/wellerman.md b/booklet/wellerman.md
index d4013cb..18112e9 100644
--- a/booklet/wellerman.md
+++ b/booklet/wellerman.md
@@ -1,8 +1,12 @@
+---
+key: Cm
+---
+
# Wellerman
There [Cm]once was a ship that put to sea,
the [Fm]name of the ship was the [Cm]Billy o Tea.
-The winds blew hard, her bow dipped down.
+The [Cm]winds blew hard, her bow dipped down.
Oh [G]blow my bully boys [Cm]blow.
{chorus}
diff --git a/booklet/whiskey-johnny.md b/booklet/whiskey-johnny.md
index 7ff0f3f..83534dc 100644
--- a/booklet/whiskey-johnny.md
+++ b/booklet/whiskey-johnny.md
@@ -1,3 +1,7 @@
+---
+key: A
+---
+
# Whiskey Johnny
[E]Whiskey is the life of man
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('')}
`);
diff --git a/songs/old-maui.txt b/songs/old-maui.txt
index 1fe3110..243e07a 100644
--- a/songs/old-maui.txt
+++ b/songs/old-maui.txt
@@ -1,5 +1,5 @@
T: Old Maui
-K: G
+K: Em
%%notelabels 1
P: Verse
BB | "Em"e2 e2 "B"^d2 BB | "G"g2 fe "D"f2 (ba) | "Em"(gf) e2 "B"f2 de | "Em"e4 BB |
diff --git a/songs/randy-dandy-o.txt b/songs/randy-dandy-o.txt
index 8797680..c695f81 100644
--- a/songs/randy-dandy-o.txt
+++ b/songs/randy-dandy-o.txt
@@ -2,7 +2,7 @@ T: Randy Dandy-O
%%vocalfont times 10
%%notecolors 1
%%notelabels 1
-K: C
+K: Am
M: 6/8
L: 1/16
Q: 1/4=140
diff --git a/songs/wellerman.txt b/songs/wellerman.txt
index b928aad..26dbe90 100644
--- a/songs/wellerman.txt
+++ b/songs/wellerman.txt
@@ -5,7 +5,7 @@ T: Wellerman
%%notelabels 1
M: 4/4
L: 1/8
-K: Eb
+K: Cm
G2 | "Cm"C2 CC C2 E2 | G2 G2 G3 G | "Fm"A2 F2 F2 AB | "Cm"c2 G2 G3 G |
w: There once was a ship that put to sea, the name of~the ship was the Billy o Tea. The
w: She had not ~ been two weeks from shore, when down on her a ~ right whale bore. The