From c992f0c53f237b3a6606d07b9f121f63ab7885a0 Mon Sep 17 00:00:00 2001 From: Ioan Lucut Date: Tue, 29 Sep 2026 20:29:37 +0300 Subject: [PATCH 1/2] fix(songbook): Fix the songbook's order, chords, repeats and front matter Slash chords now reach Leadsheets as written: it prints `D/F#` natively, and the old `/` to `-` rewrite made them look like the source's chord pairs such as `A4-A`. Songs sort field by field in Romanian order. Joining the fields with no separator put "Biruitor in pustiu" before "Biruitor". Romanian is the `polyglossia` language, which gives a Romanian `\today` and hyphenation; the Leadsheets labels therefore move from `English` to `Romanian` translations, or they fall back to "Chorus" and "composer". Chords use MonoLisa only where it is installed. It is a licensed font that CI lacks, so CI falls back to a bold sans serif; XeTeX reaches only the Regular instance of the variable font, so bold is synthesized. --- LaTeX/songbook/bes-songbook-config.tex | 32 ++++----- LaTeX/songbook/bes-songbook.template.txt | 34 +++++---- LaTeX/songbook/convertToSongbookTex.ts | 22 ++---- docs/leadsheets-and-songbook.md | 21 +++--- .../references/authoring-and-audit.md | 2 +- .../references/bes-to-leadsheets-mapping.md | 16 +++-- .../references/leadsheets-package-notes.md | 4 +- src/songToLeadsheetConverter.spec.ts | 70 +++++++++++++++++-- src/songToLeadsheetConverter.ts | 59 +++++++++------- 9 files changed, 160 insertions(+), 100 deletions(-) diff --git a/LaTeX/songbook/bes-songbook-config.tex b/LaTeX/songbook/bes-songbook-config.tex index 7223f3718..4b1910ab3 100644 --- a/LaTeX/songbook/bes-songbook-config.tex +++ b/LaTeX/songbook/bes-songbook-config.tex @@ -19,24 +19,24 @@ % Translations \newversetype{prechorus} -\DeclareTranslation{English}{leadsheets/major}{Major} -\DeclareTranslation{English}{leadsheets/minor}{Minor} -\DeclareTranslation{English}{leadsheets/composer}{Compozitor} -\DeclareTranslation{English}{leadsheets/lyrics}{Versuri} -\DeclareTranslation{English}{leadsheets/key}{Gama} -\DeclareTranslation{English}{leadsheets/capo}{Capo} -\DeclareTranslation{English}{leadsheets/fret}{Fret} -\DeclareTranslation{English}{leadsheets/interpret} +\DeclareTranslation{Romanian}{leadsheets/major}{Major} +\DeclareTranslation{Romanian}{leadsheets/minor}{Minor} +\DeclareTranslation{Romanian}{leadsheets/composer}{Compozitor} +\DeclareTranslation{Romanian}{leadsheets/lyrics}{Versuri} +\DeclareTranslation{Romanian}{leadsheets/key}{Gama} +\DeclareTranslation{Romanian}{leadsheets/capo}{Capo} +\DeclareTranslation{Romanian}{leadsheets/fret}{Fret} +\DeclareTranslation{Romanian}{leadsheets/interpret} {interpretat de \printsongpropertylist{interpret}{ \& }{, }{ \& }} -\DeclareTranslation{English}{leadsheets/intro}{Intro} -\DeclareTranslation{English}{leadsheets/interlude}{Interludiu} -\DeclareTranslation{English}{leadsheets/bridge}{Bridge} -\DeclareTranslation{English}{leadsheets/chorus}{Refren} +\DeclareTranslation{Romanian}{leadsheets/intro}{Intro} +\DeclareTranslation{Romanian}{leadsheets/interlude}{Interludiu} +\DeclareTranslation{Romanian}{leadsheets/bridge}{Bridge} +\DeclareTranslation{Romanian}{leadsheets/chorus}{Refren} % Hack - We use it for the ending -\DeclareTranslation{English}{leadsheets/outro}{End} -\DeclareTranslation{English}{leadsheets/prechorus}{Prerefren} -\DeclareTranslation{English}{leadsheets/solo}{Solo} -\DeclareTranslation{English}{leadsheets/verse}{Vers} +\DeclareTranslation{Romanian}{leadsheets/outro}{End} +\DeclareTranslation{Romanian}{leadsheets/prechorus}{Prerefren} +\DeclareTranslation{Romanian}{leadsheets/solo}{Solo} +\DeclareTranslation{Romanian}{leadsheets/verse}{Vers} \defineversetypetemplate{framed} {% diff --git a/LaTeX/songbook/bes-songbook.template.txt b/LaTeX/songbook/bes-songbook.template.txt index 6fe3d2424..357a9197d 100644 --- a/LaTeX/songbook/bes-songbook.template.txt +++ b/LaTeX/songbook/bes-songbook.template.txt @@ -9,7 +9,6 @@ \documentclass[headheight=29pt,footheight=29pt]{scrartcl} -\usepackage{datetime} % For custom date and time formatting \usepackage[full]{leadsheets} \usepackage{array} @@ -28,6 +27,17 @@ % https://tex.stackexchange.com/questions/352804/setmainfont-vs-fontspec \usepackage{fontspec} + +% Romanian hyphenation, and a Romanian `\today` in the footer and title page. +\usepackage{polyglossia} +\setdefaultlanguage{romanian} + +% Chords use MonoLisa where it is installed; it is a licensed font, so the CI +% build, which lacks it, falls back to a bold sans serif. XeTeX only reaches +% the Regular instance of MonoLisa's variable font, so bold is synthesized. +\IfFontExistsTF{MonoLisaCode} + {\newfontfamily\chordfont{MonoLisaCode}[AutoFakeBold=1.5]} + {\newcommand\chordfont{\sffamily}} \usepackage[12pt]{extsizes} \KOMAoptions{headheight=29pt,footheight=29pt} \recalctypearea @@ -64,6 +74,8 @@ info/format = \itshape, % chorus/format = \itshape, bar-shortcuts = true, + align-chords = l, + chords/format = \chordfont\bfseries, obey-lines = false, smash-chords = false, % after-song = AS, @@ -79,7 +91,7 @@ % Define headers with underline \ihead{\textit{Caiet de cântece BES.}\par\noindent\rule{\textwidth}{0.4pt}} % Inner (left) header with underline \ohead{\textbf{Pagina \thepage}\par\noindent\rule{\textwidth}{0.4pt}} % Outer (right) header with underline -\cfoot{\textit{\textcopyright\ 2025 BES. Toate drepturile rezervate. \\ \scriptsize{Versiunea \today.}}} % Centered footer +\cfoot{\textit{\textcopyright\ \the\year\ BES. Toate drepturile rezervate. \\ \scriptsize{Versiunea \today.}}} % Centered footer \input{bes-songbook-config.tex} @@ -93,7 +105,7 @@ % Title {\Huge \textbf{Caiet de Cântece}} \\ \vspace{0.5cm} - {\LARGE \textbf{BES - 2025}} \\ + {\LARGE \textbf{BES - \the\year}} \\ % Add a decorative horizontal line \vspace{1cm} @@ -119,21 +131,7 @@ \end{titlepage} % Table of content - % Set a custom title for the Table of Contents in another language - \renewcommand{\contentsname}{\textbf{Cuprins}} - - % Set a custom title for the Table of Contents - \renewcommand{\contentsname}{Conținut} % Suppress the default "Contents" title - - % Add the title to the Table of Contents without numbering - \addcontentsline{toc}{section}{\contentsname} - - % Display the Table of Contents - \begin{center} - \vspace*{1cm} - {\Huge \textbf{Cuprins}} % Center the title - \vspace*{0.5cm} - \end{center} + \renewcommand{\contentsname}{Cuprins} \tableofcontents diff --git a/LaTeX/songbook/convertToSongbookTex.ts b/LaTeX/songbook/convertToSongbookTex.ts index cf4eb9f9b..d000c8c4c 100644 --- a/LaTeX/songbook/convertToSongbookTex.ts +++ b/LaTeX/songbook/convertToSongbookTex.ts @@ -5,7 +5,6 @@ import path from 'path'; import { fileURLToPath } from 'url'; import '../../bin/env.js'; import { - EMPTY_STRING, LEADSHEETS_DIR, logFileWithLinkInConsole, logProcessingFile, @@ -13,12 +12,14 @@ import { padForTex, parse, readSongFiles, - SongAST, TEX_EXTENSION, TEX_MUSICAL_NOTATIONS, TXT_EXTENSION, } from '../../src/index.js'; -import { convertSongToLeadsheet } from '../../src/songToLeadsheetConverter.js'; +import { + compareSongbookOrder, + convertSongToLeadsheet, +} from '../../src/songToLeadsheetConverter.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -32,24 +33,11 @@ const readFiles = async (dir: string) => songAST: parse(songFile.content, { rejoinSubsections: true }), })); -const getSortKey = ({ - title, - alternative, - composer, - arranger, - band, - genre, - version, -}: SongAST) => - [title, alternative, composer, arranger, band, genre, version].join( - EMPTY_STRING, - ); - const runForDirs = async (songsDirs: string[]) => { const songs = (await Promise.all(songsDirs.map(readFiles))) .flat() .sort(({ songAST: songA }, { songAST: songB }) => - getSortKey(songA).localeCompare(getSortKey(songB)), + compareSongbookOrder(songA, songB), ); // Absolute paths, because `songbook:compile` runs from the repository root diff --git a/docs/leadsheets-and-songbook.md b/docs/leadsheets-and-songbook.md index 91ed101a9..086a2b986 100644 --- a/docs/leadsheets-and-songbook.md +++ b/docs/leadsheets-and-songbook.md @@ -39,10 +39,11 @@ Spaces inside the braces are rejected. The converter adapts the markup for the LaTeX [`leadsheets`](https://ctan.org/pkg/leadsheets) package: -- `/` becomes `-` (`^{D/F#}` → `^{D-F#}`); - when one word carries several chords, all but the last become `^*{…}` and the word is split, so the chords do not collide: `^{G4}th^{G}is` → - `^*{G4}th ^{G}is`. + `^*{G4}th ^{G}is`; +- the standalone repeat marks `/:` and `:/` become the repeat bars `|:` and + `:|`. AI agents that author or audit lead sheets follow [`skills/bes-song-leadsheets`](../skills/bes-song-leadsheets/SKILL.md), which @@ -53,13 +54,15 @@ also maps the complete `leadsheets` package. `npm run songbook:convert` reads `leadsheets/trupe_lauda_si_inchinare/`, the worship-band lead sheets, and writes one `.tex` file per song into `LaTeX/songbook/target-tex/`. It then fills `bes-songbook.template.txt` to -produce `bes-songbook.tex`. Songs are sorted by title and numbered, and each -starts on a new page. Verses, choruses, pre-choruses, bridges, endings and -recitals become `leadsheets` environments, and every section other than a verse -is framed. Each song's header shows its interpreter, composer, lyricist, genre, -tempo and key when they are set. The book opens with a title page and a table of -contents, and its labels are in Romanian (`Refren`, `Gama`, `Versuri`, -`Cuprins`). +produce `bes-songbook.tex`. Songs are sorted by title in Romanian alphabetical +order and numbered, and each starts on a new page. Verses, choruses, +pre-choruses, bridges, endings and recitals become `leadsheets` environments, +and every section other than a verse is framed. Each song's header shows its +interpreter, composer, lyricist, genre, tempo and key when they are set. The +book opens with a title page and a table of contents, and it is typeset in +Romanian: hyphenation, dates and labels (`Refren`, `Gama`, `Versuri`, +`Cuprins`). Chords are set in MonoLisa where that licensed font is installed, +and in a bold sans serif elsewhere, including CI. `npm run songbook:compile` builds the PDF with `latexmk` and XeLaTeX, and it fails on LaTeX errors. `npm run songbook:dist` runs both steps. Locally this diff --git a/skills/bes-song-leadsheets/references/authoring-and-audit.md b/skills/bes-song-leadsheets/references/authoring-and-audit.md index 713b701a4..b73f4c187 100644 --- a/skills/bes-song-leadsheets/references/authoring-and-audit.md +++ b/skills/bes-song-leadsheets/references/authoring-and-audit.md @@ -90,7 +90,7 @@ Direct-TeX rules: 1. `song` takes optional song options in `[...]` and mandatory properties in `{...}`. 2. `^{G}` is the in-song shortcut for `\chord{G}`. 3. The chord command consumes following text plus a mandatory trailing space; line endings need care. -4. Use direct slash-chord notation such as `^{D/F#}`. The BES converter's dash normalization is repository-specific and should not be copied into handwritten TeX. +4. Use direct slash-chord notation such as `^{D/F#}`, as the BES converter also emits. 5. Use `\writechord{G}` when printing a chord outside lyric placement, such as in a title template. 6. The `leadsheet` class supplies `prechorus`, `\instruction`, and `\choir` in addition to package song functionality. diff --git a/skills/bes-song-leadsheets/references/bes-to-leadsheets-mapping.md b/skills/bes-song-leadsheets/references/bes-to-leadsheets-mapping.md index 004212f2c..34e21a9a9 100644 --- a/skills/bes-song-leadsheets/references/bes-to-leadsheets-mapping.md +++ b/skills/bes-song-leadsheets/references/bes-to-leadsheets-mapping.md @@ -77,13 +77,16 @@ Validation/repair sequence: 2. Add missing caret before `{...}` when absent. 3. If multiple chord notations appear in one word, split them and convert all but the final chord marker to starred form (`^*{...}`). 4. Throw on a chord that `verify:leadsheets` would reject, and TeX-escape the lyrics around the chords. -5. Replace slash in chord-bass notation with dash (`^{D/F#}` -> `^{D-F#}`). +5. Rewrite the standalone repeat marks `/:` and `:/` as the Leadsheets repeat bars `|:` and `:|`. + +Slash chords stay as written (`^{D/F#}`), and a dash still chains chords in one marker (`^{A4-A}`). Examples: 1. `^{G4}th^{G}is` -> `^*{G4}th ^{G}is` 2. `{G4}th{G}is` -> `^*{G4}th ^{G}is` -3. `^{Db/Ab}invi^{Ab}at` -> `^*{Db-Ab}invi ^{Ab}at` +3. `^{Db/Ab}invi^{Ab}at` -> `^*{Db/Ab}invi ^{Ab}at` +4. `/: Da, eu ^{G}cred :/` -> `|: Da, eu ^{G}cred :|` ## Line Layout and Escaping @@ -92,7 +95,7 @@ Line handling: 1. Each newline in section content is rewritten to TeX line break form (` \\`). 2. Converter pads lines with spaces for readability but padding is cosmetic. -Escaping: lyrics outside chord markup and every song property except a chord `key` are printed, never interpreted. `\ % # $ _ & ~ ^ { }` become `\textbackslash{}`, `\%`, `\#`, `\$`, `\_`, `\&`, `\textasciitilde{}`, `\textasciicircum{}`, `\{` and `\}`. +Escaping: lyrics outside chord markup and every song property except a chord `key` are printed, never interpreted. `\ % # $ _ & ~ ^ { } |` become `\textbackslash{}`, `\%`, `\#`, `\$`, `\_`, `\&`, `\textasciitilde{}`, `\textasciicircum{}`, `\{`, `\}` and `\textbar{}`, so a lyric `|` never becomes a bar. TeX document shape: @@ -117,7 +120,6 @@ If explicit subsection rendering is required in TeX: ## Known Limitations 1. Canonical lyric edits must be applied to paired lead sheets; `npm run verify:leadsheets` detects but does not repair drift. -2. `/:` and `:/` repeat syntax is not transformed into left/right repeat commands by current converter. -3. `title` metadata parsing is simple brace splitting; malformed nested braces can break extraction. -4. Sequence parsing assumes comma-separated tokens without extra wrappers. -5. Converter ignores some available `leadsheets` properties even if present in BES metadata. +2. `title` metadata parsing is simple brace splitting; malformed nested braces can break extraction. +3. Sequence parsing assumes comma-separated tokens without extra wrappers. +4. Converter ignores some available `leadsheets` properties even if present in BES metadata. diff --git a/skills/bes-song-leadsheets/references/leadsheets-package-notes.md b/skills/bes-song-leadsheets/references/leadsheets-package-notes.md index eaad5d254..645e6e4c8 100644 --- a/skills/bes-song-leadsheets/references/leadsheets-package-notes.md +++ b/skills/bes-song-leadsheets/references/leadsheets-package-notes.md @@ -145,7 +145,7 @@ With `bar-shortcuts=true` inside `song`: - `||` → double bar - `|||` → final/stop bar -The BES converter currently preserves its source repeat markers unless explicitly changed. +The BES converter rewrites the standalone source repeat marks `/:` and `:/` as `|:` and `:|`, and escapes any other `|` in lyrics. ## Templates — Pages 33–44 @@ -168,7 +168,7 @@ Templates can implement arbitrary layout such as flush alignment or framed secti Use `\DeclareTranslation{language}{leadsheets/key}{translation}`. -Predefined concepts include major/minor, verse types, composer, lyrics, key, capo, fret, and interpretation. BES overrides relevant English translation entries with Romanian labels. +Predefined concepts include major/minor, verse types, composer, lyrics, key, capo, fret, and interpretation. The BES songbook sets Romanian as its `polyglossia` language and declares its labels as `Romanian` translations. ## Standalone Class and External Inclusion — Pages 46–51 diff --git a/src/songToLeadsheetConverter.spec.ts b/src/songToLeadsheetConverter.spec.ts index 2ccae1b57..a0e2c0aae 100644 --- a/src/songToLeadsheetConverter.spec.ts +++ b/src/songToLeadsheetConverter.spec.ts @@ -1,8 +1,10 @@ import { parse } from './songParser.js'; import { + compareSongbookOrder, convertSongToLeadsheet, getNormalizedContent, } from './songToLeadsheetConverter.js'; +import { SongAST } from './types.js'; describe('songToLeadsheetConverter', () => { it('should correctly convert to a leadsheet song', () => { @@ -207,17 +209,26 @@ Ori zece mii de ani și-n veșnicii.", ); }); - it('should normalise words with notations having bass (from / to -)', () => { - expect(getNormalizedContent('^{Ab/C}th')).toEqual('^{Ab-C}th'); + it('keeps slash chords as written', () => { + expect(getNormalizedContent('^{Ab/C}th')).toEqual('^{Ab/C}th'); expect(getNormalizedContent('^{D/F#}lumi^{G}nat')).toEqual( - '^*{D-F#}lumi ^{G}nat', + '^*{D/F#}lumi ^{G}nat', ); - expect(getNormalizedContent('/: th :/')).toEqual('/: th :/'); + }); + + it('prints standalone repeat marks as repeat bars', () => { + expect(getNormalizedContent('/: Da, eu ^{G}cred :/ (x2)')).toEqual( + '|: Da, eu ^{G}cred :| (x2)', + ); + }); + + it('keeps repeat marks inside a word as text', () => { + expect(getNormalizedContent('a/: b:/')).toEqual('a/: b:/'); }); it('should normalise words with multiple complex chords within a single word', () => { expect(getNormalizedContent('^{Db/Ab}invi^{Ab}at.')).toEqual( - '^*{Db-Ab}invi ^{Ab}at.', + '^*{Db/Ab}invi ^{Ab}at.', ); }); @@ -245,6 +256,7 @@ Ori zece mii de ani și-n veșnicii.", ['~', '\\textasciitilde{}'], ['^', '\\textasciicircum{}'], ['}', '\\}'], + ['|', '\\textbar{}'], ])('escapes "%s" in lyrics as "%s"', (texActiveChar, escapedChar) => { expect(getNormalizedContent(`a${texActiveChar}b`)).toEqual( `a${escapedChar}b`, @@ -253,7 +265,7 @@ Ori zece mii de ani și-n veșnicii.", it('escapes lyrics around chords without touching the chords', () => { expect(getNormalizedContent('^{D/F#}lumi_na^{G}t 100%')).toEqual( - '^*{D-F#}lumi\\_na ^{G}t 100\\%', + '^*{D/F#}lumi\\_na ^{G}t 100\\%', ); }); @@ -325,4 +337,50 @@ Text expect(tex).toContain('key={Re\\_major}'); }); }); + + describe('compareSongbookOrder', () => { + const toSong = (song: Partial & Pick) => + ({ + contentHash: 'abc', + id: 'abc', + sectionOrder: [], + sectionsMap: {}, + sequence: [], + ...song, + }) satisfies SongAST; + + const sortTitles = (songs: SongAST[]) => + songs.sort(compareSongbookOrder).map(({ title }) => title); + + it('sorts a title before a longer title it starts', () => { + expect( + sortTitles([ + toSong({ title: 'Biruitor in pustiu', composer: 'Grupul Eldad' }), + toSong({ title: 'Biruitor', composer: 'Dani Ardelean' }), + ]), + ).toEqual(['Biruitor', 'Biruitor in pustiu']); + }); + + it('sorts titles in Romanian alphabetical order', () => { + expect( + sortTitles([ + toSong({ title: 'Cântă, suflet' }), + toSong({ title: 'Aș vrea' }), + toSong({ title: 'Cântarea mea' }), + toSong({ title: 'Astăzi' }), + ]), + ).toEqual(['Astăzi', 'Aș vrea', 'Cântarea mea', 'Cântă, suflet']); + }); + + it('sorts songs sharing a title by composer', () => { + const songs = [ + toSong({ title: 'Ce mare ești', composer: 'Bethanias' }), + toSong({ title: 'Ce mare ești', composer: 'Anonim' }), + ]; + + expect( + songs.sort(compareSongbookOrder).map(({ composer }) => composer), + ).toEqual(['Anonim', 'Bethanias']); + }); + }); }); diff --git a/src/songToLeadsheetConverter.ts b/src/songToLeadsheetConverter.ts index c6b41c5ba..88877392a 100644 --- a/src/songToLeadsheetConverter.ts +++ b/src/songToLeadsheetConverter.ts @@ -69,7 +69,8 @@ const getChordNotationMatches = (singleWord: string) => Array.from(singleWord.matchAll(/\^\*?\{[^}]+\}/gim)); // Song text is printed, never interpreted: every character TeX treats as a -// command, argument, comment or chord marker is replaced by its printed form. +// command, argument, comment, chord marker or bar shortcut is replaced by its +// printed form. const TEX_ESCAPES: Record = { '\\': '\\textbackslash{}', '%': '\\%', @@ -81,11 +82,12 @@ const TEX_ESCAPES: Record = { '^': '\\textasciicircum{}', '{': '\\{', '}': '\\}', + '|': '\\textbar{}', }; const escapeTex = (text: string) => text.replaceAll( - /[\\%#$_&~^{}]/g, + /[\\%#$_&~^{}|]/g, (texActiveChar) => TEX_ESCAPES[texActiveChar], ); @@ -150,28 +152,15 @@ const rewriteWordWithRightMusicalNotationSyntaxIfNeeded = ( return trim(rewrittenWordWithSpaceBetween); }; -const rewriteNotationsWithDashForChordsWithBass = ( - singleWord: string, -): string => { - const maybeRegExpMatchArrays = getChordNotationMatches(singleWord).filter( - ([chordNotation]) => chordNotation.includes('/'), - ); - - if (isEmpty(maybeRegExpMatchArrays)) { - return singleWord; - } - - return singleWord.replace( - /\^(\*?)\{([^}]+)\}/gim, - (_match, emphasis, chord) => `^${emphasis}{${chord.replaceAll('/', '-')}}`, - ); +// BES marks a repeat with standalone `/:` and `:/`; Leadsheets' bar shortcuts +// print `|:` and `:|` as repeat bars. +const REPEAT_BARS: Record = { + '/:': '|:', + ':/': ':|', }; -// const rewriteLeftRightRepeat = (singleWord: string): string => { -// return singleWord -// .replaceAll(/\/:/gi, '\\leftrepeat') -// .replaceAll(/:\//gi, '\\rightrepeat'); -// }; +const rewriteRepeatMarkAsRepeatBar = (singleWord: string) => + REPEAT_BARS[singleWord] ?? singleWord; export const getNormalizedContent = (sectionAsContent: string) => { return sectionAsContent @@ -183,13 +172,35 @@ export const getNormalizedContent = (sectionAsContent: string) => { .map(warnIfIsNotProperlyFormatted) .map(rewriteWordWithRightMusicalNotationSyntaxIfNeeded) .map(escapeTexAroundChords) - .map(rewriteNotationsWithDashForChordsWithBass) - // .map(rewriteLeftRightRepeat) + .map(rewriteRepeatMarkAsRepeatBar) .join(SPACE_CHAR), ) .join(NEW_LINE); }; +const SONGBOOK_ORDER_FIELDS = [ + 'title', + 'alternative', + 'composer', + 'arranger', + 'band', + 'genre', + 'version', +] as const satisfies (keyof SongAST)[]; + +// Songs sort by title, and by each later field only among songs sharing the +// earlier ones, in Romanian alphabetical order (a < ă < â, s < ș, t < ț). +export const compareSongbookOrder = (songA: SongAST, songB: SongAST) => + SONGBOOK_ORDER_FIELDS.reduce( + (order, field) => + order || + (songA[field] ?? EMPTY_STRING).localeCompare( + songB[field] ?? EMPTY_STRING, + 'ro', + ), + 0, + ); + // Leadsheets reads `key` as a chord for transposition, so a valid chord stays // as written. const toKeyProperty = (key: string) => From a675a96b5146dc692d713be02ad72fa45a13ed81 Mon Sep 17 00:00:00 2001 From: Ioan Lucut Date: Wed, 30 Sep 2026 10:06:33 +0300 Subject: [PATCH 2/2] feat(songbook): Set the lyrics in MonoLisa Text where it is installed The installed MonoLisa already covers every Romanian letter, so the webfont download is not needed. Like the chords, the lyrics fall back when the font is missing, so CI keeps Latin Modern. --- LaTeX/songbook/bes-songbook.template.txt | 10 +++++++--- docs/leadsheets-and-songbook.md | 5 +++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/LaTeX/songbook/bes-songbook.template.txt b/LaTeX/songbook/bes-songbook.template.txt index 357a9197d..9cc1e44c5 100644 --- a/LaTeX/songbook/bes-songbook.template.txt +++ b/LaTeX/songbook/bes-songbook.template.txt @@ -32,9 +32,13 @@ \usepackage{polyglossia} \setdefaultlanguage{romanian} -% Chords use MonoLisa where it is installed; it is a licensed font, so the CI -% build, which lacks it, falls back to a bold sans serif. XeTeX only reaches -% the Regular instance of MonoLisa's variable font, so bold is synthesized. +% Lyrics and chords use MonoLisa where it is installed; it is a licensed font, +% so the CI build, which lacks it, falls back to Latin Modern and a bold sans +% serif. XeTeX only reaches the Regular instance of MonoLisa's variable font, +% so bold is synthesized. +\IfFontExistsTF{MonoLisaText} + {\setmainfont{MonoLisaText}[AutoFakeBold=1.5]} + {} \IfFontExistsTF{MonoLisaCode} {\newfontfamily\chordfont{MonoLisaCode}[AutoFakeBold=1.5]} {\newcommand\chordfont{\sffamily}} diff --git a/docs/leadsheets-and-songbook.md b/docs/leadsheets-and-songbook.md index 086a2b986..39f905fe7 100644 --- a/docs/leadsheets-and-songbook.md +++ b/docs/leadsheets-and-songbook.md @@ -61,8 +61,9 @@ and every section other than a verse is framed. Each song's header shows its interpreter, composer, lyricist, genre, tempo and key when they are set. The book opens with a title page and a table of contents, and it is typeset in Romanian: hyphenation, dates and labels (`Refren`, `Gama`, `Versuri`, -`Cuprins`). Chords are set in MonoLisa where that licensed font is installed, -and in a bold sans serif elsewhere, including CI. +`Cuprins`). Lyrics and chords are set in MonoLisa where that licensed font is +installed; elsewhere, including CI, lyrics use Latin Modern and chords a bold +sans serif. `npm run songbook:compile` builds the PDF with `latexmk` and XeLaTeX, and it fails on LaTeX errors. `npm run songbook:dist` runs both steps. Locally this