Skip to content

Support stacked (multi-level) lyric lines in GABC syllables - #1765

Open
lbssousa wants to merge 19 commits into
gregorio-project:developfrom
lbssousa:feat-multilevel-lyrics
Open

Support stacked (multi-level) lyric lines in GABC syllables#1765
lbssousa wants to merge 19 commits into
gregorio-project:developfrom
lbssousa:feat-multilevel-lyrics

Conversation

@lbssousa

Copy link
Copy Markdown
Contributor

Closes #374.

Adds support for multiple stacked lines of lyrics under a single line of
notes, written by separating the lines with | inside a syllable's text:

(c4) <sp>V/</sp>. Sanc|Sanc|Sanc(h)|ta|ta(h)ta| De| Vir(g) Ma|i|go(h)rí| Gé| Vír(ixir1)|ni|gi(gr)a,|trix,|num,(g.) (:) <sp>R/</sp>. o(g)ra(f) pro(g) no(h)bis.(h) (::)

The text before the first | is the main lyric line. Each subsequent |
starts a new line further below the staff. This is meant for cases like litanies,
psalm tones, or any score where several verses/invocations share the same
melody and are traditionally printed stacked under one staff instead of
repeating the notes for each one.

This proposed syntax is heavily based on the one introduced in
Source & Summit online GABC editor.

Syntax rules

  • Each line of the stack is centered under the notes independently,
    following the normal centering rules (including forced centering with
    {}, usable separately per line).
  • Word boundaries and hyphenation are computed independently per line,
    following the same rule as the main line: a leading space starts a new
    word. Without one, two consecutive syllables' lines are the same word,
    even where the main line's word already ended. A word only ends where
    the following syllable's stack doesn't reach that deep at all (a plain
    syllable, or a shallower stack). A line that's present but
    deliberately left empty (ca||ci(h), or a trailing empty line at a
    stack's end) does not end the word, it continues on the next syllable
    that carries text there.
  • | is now reserved in syllable text; a literal pipe must be escaped as
    $|. GABC files written by Gregorio itself already escape |, so
    regenerated files are unaffected.

lbssousa added 8 commits July 30, 2026 12:36
Recognize | as a new lyric-line separator (LYRIC_CUT), and accumulate
what follows each one into a gregorio_lyric_line list (extra_lyrics)
attached to the syllable, alongside the existing level-1 text.
A leading space on a line marks the start of a new word there.
Give each extra lyric line its own running word-boundary state, closed
whenever a syllable's stack doesn't reach that level (it's shallower
there, or has no stack at all — even a syllable with no text on any
level, such as a bare divisio) or when a line explicitly starts a new
word with a leading space. A level that is present but left empty does
not close its word: the word just continues on the next syllable that
carries text there.

close_stacked_word_at_level marks the end on word_end_line, the most
recent line with actual text at that level, since the syllable that
detects the level as unreached may be several syllables past the one
that last carried text on it.
Mirror the main line's post-parse rebuild — forced-center detection,
character rebuilding, and first-word styling — for each extra lyric
line, so every level of a stack gets its own centering independent of
the others.
Drop the extra_lyrics parameter from gregorio_add_syllable and assign
it directly on the new syllable afterward, the same way the parser
already sets translation centering. Keeps the constructor's parameter
list from growing a third character-ish parameter next to
first_character and first_translation_character, which would be easy
to transpose by accident.
Write each extra lyric line back as |text, emitting a leading space
only where the previous syllable actually carried that same line.
That's the one case where omitting the space would make the round
trip read as a word continuing instead of a new one starting. A
syllable with no text at all already closes every stacked word on its
own, so there's no need to look further back than the immediately
preceding syllable.
Write each extra lyric line as a \GreWriteLyricLine call inside the
syllable's eighth argument, alongside the translation. Extracted
write_text_pair, the "text or empty braces" pattern write_text already
had, so the two share it instead of duplicating it.

Styles are always written in full for extra lines: the fixed-style
optimization that write_text otherwise applies only concerns the main
lyric line.
@lbssousa lbssousa changed the title Support stacked (multi-level) lyric lines in gabc syllablesFeat multilevel lyrics Support stacked (multi-level) lyric lines in gabc syllables Jul 30, 2026
@lbssousa lbssousa changed the title Support stacked (multi-level) lyric lines in gabc syllables Support stacked (multi-level) lyric lines in GABC syllables Jul 30, 2026
lbssousa added 11 commits July 30, 2026 16:11
Give each additional lyric line (level 2+) its own entry in
syllables[sid].levels, populated by scan_syllables from the box
\GreWriteLyricLine emits and by set_lyric_line_dash (called from that
same macro) with whether the line ends a word here. level_edges finds
a level's text within its zero-width outer box, for the spacing and
hyphenation logic that consumes this data.
Each additional lyric line has its own word position, independent of
the main line, so give it its own minimum inter-syllable distance in
adjust_syllablefinalskip (interwordspacetext where the line ends a word
here, none otherwise).

add_level_hyphen mirrors add_hyphen for a stacked line's zero-width
outer box. syllable_spacing hyphenates every line the same way it
already hyphenates the main one, iterating to a fixed point since
widening one line can open a gap elsewhere that then needs its own
hyphen.
Declare the vertical separation between two stacked lyric lines as a
scalable dimension, following the font-relative style already used by
spacelinestext, and register it as a valid target for
\grechangenextscorelinedim alongside the other per-line distances.
This is the entry point GregorioTeX emits for each additional lyric
line (level 2+) of a syllable, invoked from the same argument of
\GreSyllable that writes the main line. It mirrors the main line's
centering logic (syllable/vowel/first-letter, or a forced center),
then raises the result by lyricstackseparation times the level's
depth and tags the box with \gre@attrid@part so gregoriotex-syllable.lua
can find it via scan_syllables.
… end

compute_line_statistics now tracks max_lyric_level, the deepest
additional lyric line present anywhere in a system, using the same
part_lyric_line_base offset gregoriotex-syllable.lua already tags those
boxes with.

adjust_additional_spaces turns that into lyric_stack_extra, the depth
the stack needs beyond the main lyrics line, and lowers the translation
(and shifts each stacked line's own box) accordingly. everything_raise
deliberately excludes lyric_stack_extra: the deepest line is already
positioned by its own \GreWriteLyricLine raise, so counting that
distance again there would shift the whole system instead of just
growing its depth.

add_eol_hyphen gives each stacked line's last syllable the same
end-of-line hyphen treatment as the main line, since a word can
continue past the line break independently on any level.
adjust_glue's baselineskip/lineskip choice is driven by the previous
line's depth, but that depth may include lyric_stack_extra — room the
stacked lyrics genuinely need, not tall notation baselineskip already
budgets for. Counting it in the lineskiplimit check could tip an
otherwise-normal transition into \lineskip and collapse the gap.

forgive_depth carves that portion out of the check while still adding
it back into the glue's width, so only the stack's own extra depth is
reserved. adjust_additional_spaces now returns its lyric_stack_extra,
and post_linebreak threads it through as prev_stack_extra for the next
line's adjust_glue call.
@lbssousa
lbssousa marked this pull request as ready for review July 31, 2026 11:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Multiple lines of text

1 participant