Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ As of v3.0.0 this project adheres to [Semantic Versioning](http://semver.org/).
- Added horizontal spacing preservation for NABC neumes, preventing overlap. Solves [#1699](https://github.com/gregorio-project/gregorio/issues/1699).
- Added overtie/undertie special symbols (`<sp>ut</sp>` for `\greundertie`, `<sp>ot</sp>` for `\greovertie`, and `<sp>dt</sp>` for `\gredoubletie`), and a configurable lyric tying shorthand (`~` for `\GreLyricTie`).
- Added support for the C23 standard (the default in GCC 15). The included build scripts continue to default to GNU89 C.
- Added optional second argument to `\gresetnabcalignment` for horizontal alignment (`left` or `center`). In `center` mode the NABC neume is centered on the GABC note group; in `neume`+`center` mode both left-side and right-side significative letters are excluded from the centering computation. See [#1665](https://github.com/gregorio-project/gregorio/issues/1665).

### Fixed
- Fixed a bug that could cause a punctum mora that is supposed to be below the line (`.0`) to appear above the line. This bug was platform-dependent and was observed on a Windows system. See [#1642](https://github.com/gregorio-project/gregorio/issues/1642).
Expand Down
12 changes: 10 additions & 2 deletions doc/Command_Index_User.tex
Original file line number Diff line number Diff line change
Expand Up @@ -1294,13 +1294,21 @@ \subsubsection{Sign printing}

\textbf{Important:} NABC glyphs visibility is controlled independently from notes visibility (\verb=\gresetnotes=). This allows you to hide the main notes (i.e. using \verb=\gresetnotes{invisible}= or some \verb=\gresetnotes{?phantom}= variant) while keeping NABC glyphs visible, which is useful for producing NABC-only output with lyrics.

\macroname{\textbackslash gresetnabcalignment}{[\#1]\{\#2\}}{gregoriotex-nabc.tex}
Sets the horizontal alignment reference for nabc neumes relative to the gabc notes below. By default, nabc neumes are aligned to the left edge of the entire complex glyph descriptor (including significative letters), which can produce aesthetically suboptimal results when there are significative letters to the left of the neume. The optional voice parameter allows setting the alignment independently for each nabc voice.
\macroname{\textbackslash gresetnabcalignment}{[\#1]\{\#2\}\{\#3\}}{gregoriotex-nabc.tex}
Sets the alignment reference and horizontal alignment of nabc neumes relative to the gabc notes below.

The first mandatory argument (\#2) controls the \emph{alignment reference}: which part of the nabc complex glyph descriptor is used as anchor. By default, the entire complex glyph descriptor (including significative letters) is used; with \texttt{neume}, only the neume body (basic glyph descriptor + prepunctis) is considered, which avoids visual misalignment caused by significative letters.

The second argument (\#3) is \emph{optional}: if omitted, the current horizontal alignment is not changed. This allows calling \verb=\gresetnabcalignment{full}= to change only the alignment reference while preserving the horizontal setting. When provided, it controls the \emph{horizontal alignment} of the nabc neume relative to the gabc note group.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the #3 argument is meant to be optional, can we wrap it in square braces?


When \texttt{center} is combined with \texttt{neume} mode, both left-side and right-side significative letters are excluded from the centering calculation, so that the neume body is centered on the gabc glyph.

\begin{argtable}
\#1 & integer & (Optional) The nabc voice number: 1 = above staff, 2 = below staff. If omitted, sets the default for all voices and clears any per-voice overrides.\\
\#2 & \texttt{neume} & Align to the neume (basic glyph descriptor + prepunctis), ignoring significative letters.\\
& \texttt{full} & Align to the entire complex glyph descriptor (default).\\
\#3 & \texttt{left} & (Optional) Left-align the nabc neume with the gabc glyph (default).\\
& \texttt{center} & Center the nabc neume on the gabc glyph.\\
\end{argtable}

\macroname{\textbackslash gresetnabcskipalterations}{[\#1]\{\#2\}}{gregoriotex-nabc.tex}
Expand Down
53 changes: 49 additions & 4 deletions doc/Command_Index_internal.tex
Original file line number Diff line number Diff line change
Expand Up @@ -1479,11 +1479,23 @@ \section{Gregorio\TeX{} Controls}
\#2 & box register & Box register to save the rendered content to\\
\end{argtable}

\macroname{\textbackslash gre@nabc@halign@content}{\#1\#2\#3}{gregoriotex-main.tex}
Helper that applies horizontal alignment inside a zero-width NABC hbox. Used by \verb=\gre@nabc@place@voice@i= and \verb=\gre@nabc@place@voice@ii= to shift the NABC content according to the chosen horizontal alignment mode.

\begin{argtable}
\#1 & count register & The horizontal alignment register: 0 = left, 1 = center\\
\#2 & box register & The NABC box (must still contain its content)\\
\#3 & dimen & Per-voice right-side significative letter overflow (\verb=\gre@dimen@nabcrightoverflow@i= or \verb=\gre@dimen@nabcrightoverflow@ii=). In \texttt{neume} mode this width is added back to the centering formula so that right-side LS are excluded from centering, mirroring the left-side exclusion\\
\end{argtable}

The centering formula is:
\verb=\kern\dimexpr(\gre@dimen@lastglyphwidth - \wd#2 + #3) / 2\relax=

\macroname{\textbackslash gre@nabc@place@voice@i}{}{gregoriotex-main.tex}
Phase 2 of NABC two-phase processing for voice 1 (above staff): places the pre-rendered NABC voice 1 content from \textbackslash gre@box@nabc@voice@i at the correct vertical position above the staff.
Phase 2 of NABC two-phase processing for voice 1 (above staff): places the pre-rendered NABC voice 1 content from \verb=\gre@box@nabc@voice@i= at the correct vertical position above the staff. Passes \verb=\gre@count@nabc@halign@i= and \verb=\gre@dimen@nabcrightoverflow@i= to \verb=\gre@nabc@halign@content= for horizontal alignment.

\macroname{\textbackslash gre@nabc@place@voice@ii}{}{gregoriotex-main.tex}
Phase 2 of NABC two-phase processing for voice 2 (below staff): places the pre-rendered NABC voice 2 content from \textbackslash gre@box@nabc@voice@ii at the correct vertical position below the staff.
Phase 2 of NABC two-phase processing for voice 2 (below staff): places the pre-rendered NABC voice 2 content from \verb=\gre@box@nabc@voice@ii= at the correct vertical position below the staff. Passes \verb=\gre@count@nabc@halign@ii= and \verb=\gre@dimen@nabcrightoverflow@ii= to \verb=\gre@nabc@halign@content= for horizontal alignment.

\macroname{\textbackslash gre@nabc@pre@voice@i}{}{gregoriotex-main.tex}
Deferred macro for phase~1 processing of NABC voice~1 (above staff). Set by
Expand Down Expand Up @@ -1511,22 +1523,43 @@ \section{Gregorio\TeX{} Controls}
\verb=\gre@nabc@emit@voice@i= for voice~2, guarded by \verb=\ifgre@nabc@voice@ii@ready=.

\macroname{\textbackslash gre@setnabcalignment@global}{\#1}{gregoriotex-nabc.tex}
Sets the default NABC horizontal alignment mode for all voices and clears any per-voice overrides.
Sets the default NABC alignment reference mode for all voices and clears any per-voice overrides.

\begin{argtable}
\#1 & \texttt{neume} & Align to the neume (basic glyph + prepunctis), ignoring significative letters\\
& \texttt{full} & Align to the entire complex glyph descriptor (default)\\
\end{argtable}

\macroname{\textbackslash gre@setnabcalignment@voice}{[\#1]\{\#2\}}{gregoriotex-nabc.tex}
Sets the NABC horizontal alignment mode for a specific voice.
Sets the NABC alignment reference mode for a specific voice.

\begin{argtable}
\#1 & integer & The nabc voice number: 1 = above staff, 2 = below staff\\
\#2 & \texttt{neume} & Align to the neume (basic glyph + prepunctis), ignoring significative letters\\
& \texttt{full} & Align to the entire complex glyph descriptor (default)\\
\end{argtable}

\macroname{\textbackslash gre@setnabchalign@global}{\#1}{gregoriotex-nabc.tex}
Sets the default NABC horizontal alignment for all voices. Called by
\verb=\gresetnabcalignment= when an optional second argument is provided
without a voice specifier.

\begin{argtable}
\#1 & \texttt{left} & Left-align the NABC neume with the GABC glyph (default)\\
& \texttt{center} & Center the NABC neume on the GABC glyph\\
\end{argtable}

\macroname{\textbackslash gre@setnabchalign@voice}{[\#1]\{\#2\}}{gregoriotex-nabc.tex}
Sets the NABC horizontal alignment for a specific voice. Called by
\verb=\gresetnabcalignment= when both a voice specifier and a second argument
are provided.

\begin{argtable}
\#1 & integer & The nabc voice number: 1 = above staff, 2 = below staff\\
\#2 & \texttt{left} & Left-align the NABC neume with the GABC glyph (default)\\
& \texttt{center} & Center the NABC neume on the GABC glyph\\
\end{argtable}

\macroname{\textbackslash gre@setnabcskipalterations@global}{\#1}{gregoriotex-nabc.tex}
Sets the skip-alterations flag for all NABC voices simultaneously. Internal
implementation of \verb=\gresetnabcskipalterations= when called without the
Expand Down Expand Up @@ -2441,6 +2474,18 @@ \subsection{Distances}
\macroname{\textbackslash gre@dimen@nabcleftoverflow}{}{gregoriotex-nabc.tex}
Dimension for communicating NABC left overflow from Lua to \TeX. When NABC alignment mode is \texttt{neume}, significative letters on the left side of a neume extend to the left of the neume body. This dimension holds the overflow width (set by Lua code during NABC rendering) so that the \TeX\ layer can reserve space at the note level to prevent overlap with previous elements. Reset to 0pt after each syllable.

\macroname{\textbackslash gre@dimen@nabcrightoverflow@i}{}{gregoriotex-nabc.tex}
Per-voice dimension for communicating the width of right-side significative letters (positions 3, 6, 9) from Lua to \TeX\ for voice~1. In \texttt{neume} alignment mode, the Lua code sets this to the maximum right-side LS width scaled by the NABC font scale. In \texttt{full} mode (or when there are no right-side LS), it is set to 0pt. Used by \verb=\gre@nabc@halign@content= to exclude right-side LS from the centering computation, mirroring the left-side exclusion.

\macroname{\textbackslash gre@dimen@nabcrightoverflow@ii}{}{gregoriotex-nabc.tex}
Same as \verb=\gre@dimen@nabcrightoverflow@i= but for voice~2 (below staff).

\macroname{\textbackslash gre@count@nabc@halign@i}{}{gregoriotex-nabc.tex}
Count register holding the horizontal alignment mode for voice~1. Values: 0 = left (default), 1 = center. Set by \verb=\gre@setnabchalign@global= or \verb=\gre@setnabchalign@voice=.

\macroname{\textbackslash gre@count@nabc@halign@ii}{}{gregoriotex-nabc.tex}
Count register holding the horizontal alignment mode for voice~2. Values: 0 = left (default), 1 = center. Set by \verb=\gre@setnabchalign@global= or \verb=\gre@setnabchalign@voice=.

\macroname{\textbackslash gre@dimen@nabckernemitted}{}{gregoriotex-nabc.tex}
Tracks the total kern emitted at the note level across all NABC voices for the current syllable. Used to coordinate left overflow handling between voice 1 (above staff) and voice 2 (below staff), ensuring that only the maximum overflow is applied. Reset to 0pt after each syllable.

Expand Down
110 changes: 104 additions & 6 deletions tex/gregoriotex-main.tex
Original file line number Diff line number Diff line change
Expand Up @@ -718,21 +718,108 @@
\global\setbox#2=\hbox{\unhbox\gre@box@nabctemp}%
}%

% Phase 2: Place voice 1 (above staff)
\def\gre@nabc@place@voice@i{%
% Helper: apply horizontal alignment kern inside a zero-width NABC hbox.
% #1: halign count register (0=left, 1=center)
% #2: nabc box register (must still contain the box)
% #3: per-voice right-side LS overflow dimension (\gre@dimen@nabcrightoverflow@i/ii)
% The kern shifts the NABC content to the right inside the zero-width hbox
% so that the neume aligns with the GABC glyph according to the chosen mode.
% \gre@dimen@lastglyphwidth must be set to the current GABC glyph width.
\def\gre@nabc@halign@content#1#2#3{%
\ifcase#1\relax
% 0 = left: no offset (current default behavior)
\unhbox#2%
\or
% 1 = center: kern = (glyphwidth - (nabcwidth - rightoverflow)) / 2
% In neume mode, rightoverflow excludes right-side LS from centering.
\kern\dimexpr(\gre@dimen@lastglyphwidth - \wd#2 + #3) / 2\relax
\unhbox#2%
\fi
}%

% Immediate placement helper for voice 1 (always places, no deferral check).
\def\gre@nabc@place@voice@i@now{%
\gre@attr@part=7\relax%
\gre@dimen@temp@five=\dimexpr(\gre@dimen@staffheight %
+ \gre@space@dimen@spacebeneathtext %
+ \gre@space@dimen@spacelinestext)\relax%
\leavevmode\raise\gre@dimen@temp@five\hbox to 0pt{\unhbox\gre@box@nabc@voice@i\hss}%
\leavevmode\raise\gre@dimen@temp@five\hbox to 0pt{%
\gre@nabc@halign@content{\gre@count@nabc@halign@i}{\gre@box@nabc@voice@i}{\gre@dimen@nabcrightoverflow@i}%
\hss}%
\unsetattribute{\gre@attr@part}%
}%
% Phase 2: Place voice 1 (above staff) — with deferral for center alignment.
\def\gre@nabc@place@voice@i{%
\ifgre@boxing
\gre@nabc@place@voice@i@now
\else\ifnum\gre@count@nabc@halign@i=1\relax
\global\gre@nabc@halign@pending@itrue
\ifgre@nabc@halign@pending\else
\global\gre@nabc@halign@pendingtrue
\global\gre@dimen@nabc@elementadvance=0pt\relax
\fi
\global\gre@dimen@nabc@advance@start@i=\gre@dimen@nabc@elementadvance\relax
\else
\gre@nabc@place@voice@i@now
\fi\fi
}%

% Phase 2: Place voice 2 (below staff)
\def\gre@nabc@place@voice@ii{%
% Immediate placement helper for voice 2 (always places, no deferral check).
\def\gre@nabc@place@voice@ii@now{%
\gre@dimen@temp@five=\dimexpr(\gre@space@dimen@spacebeneathtext
+ \gre@space@dimen@spacelinestext)\relax
\leavevmode\raise\gre@dimen@temp@five\hbox attr \gre@attrid@part=8 to 0pt{\unhbox\gre@box@nabc@voice@ii\hss}%
\leavevmode\raise\gre@dimen@temp@five\hbox attr \gre@attrid@part=8 to 0pt{%
\gre@nabc@halign@content{\gre@count@nabc@halign@ii}{\gre@box@nabc@voice@ii}{\gre@dimen@nabcrightoverflow@ii}%
\hss}%
}%
% Phase 2: Place voice 2 (below staff) — with deferral for center alignment.
\def\gre@nabc@place@voice@ii{%
\ifgre@boxing
\gre@nabc@place@voice@ii@now
\else\ifnum\gre@count@nabc@halign@ii=1\relax
\global\gre@nabc@halign@pending@iitrue
\ifgre@nabc@halign@pending\else
\global\gre@nabc@halign@pendingtrue
\global\gre@dimen@nabc@elementadvance=0pt\relax
\fi
\global\gre@dimen@nabc@advance@start@ii=\gre@dimen@nabc@elementadvance\relax
\else
\gre@nabc@place@voice@ii@now
\fi\fi
}%

% Flush deferred center-aligned NABC placement for a single voice.
% #1 = voice suffix (i or ii).
% Kerns back from current position to the voice's start, centers over
% that voice's span, then kerns forward to the current position.
% N.B. We use \gre@dimen@temp@one for the span because the @now macros
% overwrite \gre@dimen@temp@five with the vertical raise amount.
\def\gre@nabc@flush@deferred@voice#1{%
\csname ifgre@nabc@halign@pending@#1\endcsname
\begingroup
\gre@dimen@temp@one=\dimexpr\gre@dimen@nabc@elementadvance
- \csname gre@dimen@nabc@advance@start@#1\endcsname\relax
\kern-\gre@dimen@temp@one\relax
\gre@dimen@lastglyphwidth=\gre@dimen@temp@one\relax
\csname gre@nabc@place@voice@#1@now\endcsname
\kern\gre@dimen@temp@one\relax
\endgroup
\global\csname gre@nabc@halign@pending@#1false\endcsname
% Clear global pending flag when no voice remains pending.
\ifgre@nabc@halign@pending@i\else
\ifgre@nabc@halign@pending@ii\else
\global\gre@nabc@halign@pendingfalse
\fi
\fi
\fi
}%

% Flush all deferred center-aligned NABC voices.
\def\gre@nabc@flush@deferred@halign{%
\ifgre@nabc@halign@pending
\gre@nabc@flush@deferred@voice{i}%
\gre@nabc@flush@deferred@voice{ii}%
\fi
}%

% Deferred macros for two-phase processing.
Expand Down Expand Up @@ -1468,6 +1555,9 @@
%% 2: unison (breakable according to the unisonbreakbehavior setting)
% #3 is the number of notes emitted in this syllable before this macro
\def\GreEndOfElement#1#2#3{%
% Flush all deferred NABC voices at element boundaries so that centering
% spans do not extend across elements.
\gre@nabc@flush@deferred@halign
\ifnum\gre@count@syllablenotes<\gre@count@unbreakabletotalnotes\relax %
\gre@unbreakableendofelementtrue %
\else %
Expand All @@ -1490,6 +1580,11 @@
\fi %
\fi %
\fi %
% While center-aligned NABC is deferred, keep elements unbreakable so
% pending NABC cannot be flushed on the next line detached from its glyph.
\ifgre@nabc@halign@pending
\gre@unbreakableendofelementtrue %
\fi
\ifgre@unbreakableendofelement %
\GreNoBreak %
\else %
Expand Down Expand Up @@ -1616,6 +1711,9 @@
\GreNoBreak %
\gre@get@spaceskip{#1}%
\gre@hskip\gre@skip@temp@four %
\ifgre@nabc@halign@pending
\global\advance\gre@dimen@nabc@elementadvance by \gre@skip@temp@four\relax
\fi
\GreNoBreak %
\relax%
}%
Expand Down
36 changes: 34 additions & 2 deletions tex/gregoriotex-nabc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,20 @@ local gregallparse_neumes = function(str, kind, scale, voice)
end
lscount = lscount + 1
end
-- Accumulate per-position LS widths from ALL original entries,
-- before font resolution may clear ls[i] for combined glyphs.
-- This ensures overflow widths (lwidths[10]/[12]) are correct
-- even when LS are baked into a combined font glyph.
local all_lwidths = { 0, 0, 0, 0, 0, 0, 0, 0, 0 }
for i = 0, lscount - 1 do
if ls[i] ~= '' then
local p = tonumber(ls[i]:sub(-1, -1))
local l = ls[i]:sub(1, -2)
if gregallmetrics[kind][l] then
all_lwidths[p] = all_lwidths[p] + gregallmetrics[kind][l].width
end
end
end
if base ~= "ERR" then
local l = {}
function l.try (kind, base, parts, pp, su, ls5, ls)
Expand Down Expand Up @@ -445,6 +459,10 @@ local gregallparse_neumes = function(str, kind, scale, voice)
lwidths[10] = math.max (lwidths[1], lwidths[4], lwidths[7])
lwidths[11] = math.max (lwidths[2], lwidths[8])
lwidths[12] = math.max (lwidths[3], lwidths[6], lwidths[9])
-- For alignment overflow purposes, use the pre-resolution widths
-- so that LS baked into combined glyphs are still accounted for.
local overflow_left = math.max(all_lwidths[1], all_lwidths[4], all_lwidths[7])
local overflow_right = math.max(all_lwidths[3], all_lwidths[6], all_lwidths[9])
local pre = ''
local post = ''
for i = 0, lscount - 1 do
Expand All @@ -462,11 +480,25 @@ local gregallparse_neumes = function(str, kind, scale, voice)
-- Also set \gre@dimen@nabcleftoverflow so the TeX layer can
-- reserve space at the note level and prevent overlap with the
-- previous element.
if get_nabc_alignment(voice) == 'neume' and lwidths[10] > 0 then
local overflow_sp = string.format("%.3f", lwidths[10] * scale)
local is_neume_mode = (get_nabc_alignment(voice) == 'neume')
if is_neume_mode and overflow_left > 0 then
local overflow_sp = string.format("%.3f", overflow_left * scale)
base = '\\global\\gre@dimen@nabcleftoverflow=' .. overflow_sp .. 'sp'
.. '\\kern -' .. overflow_sp .. 'sp' .. base
end
-- Communicate right-side significative letter width to TeX.
-- In 'neume' + 'center' mode, the centering computation ignores
-- this width so the neume glyph is centered on the GABC note
-- group, mirroring the left-side exclusion.
local rdim = '\\gre@dimen@nabcrightoverflow@'
.. (voice == 1 and 'i' or 'ii')
if is_neume_mode and overflow_right > 0 then
local rval = string.format("%.3f", overflow_right * scale)
base = base .. '\\global' .. rdim .. '='
.. rval .. 'sp'
else
base = base .. '\\global' .. rdim .. '=0sp'
end
end
end
ret = ret .. base
Expand Down
Loading