diff --git a/CHANGELOG.md b/CHANGELOG.md
index 363a8715..a7791827 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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 (`ut` for `\greundertie`, `ot` for `\greovertie`, and `dt` 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).
diff --git a/doc/Command_Index_User.tex b/doc/Command_Index_User.tex
index 259e226e..7f87b65f 100644
--- a/doc/Command_Index_User.tex
+++ b/doc/Command_Index_User.tex
@@ -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.
+
+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}
diff --git a/doc/Command_Index_internal.tex b/doc/Command_Index_internal.tex
index e92569b0..f8e85006 100644
--- a/doc/Command_Index_internal.tex
+++ b/doc/Command_Index_internal.tex
@@ -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
@@ -1511,7 +1523,7 @@ \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\\
@@ -1519,7 +1531,7 @@ \section{Gregorio\TeX{} Controls}
\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\\
@@ -1527,6 +1539,27 @@ \section{Gregorio\TeX{} Controls}
& \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
@@ -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.
diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex
index fc812825..0bf34de1 100644
--- a/tex/gregoriotex-main.tex
+++ b/tex/gregoriotex-main.tex
@@ -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.
@@ -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 %
@@ -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 %
@@ -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%
}%
diff --git a/tex/gregoriotex-nabc.lua b/tex/gregoriotex-nabc.lua
index b95f900f..7316eab6 100644
--- a/tex/gregoriotex-nabc.lua
+++ b/tex/gregoriotex-nabc.lua
@@ -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)
@@ -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
@@ -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
diff --git a/tex/gregoriotex-nabc.tex b/tex/gregoriotex-nabc.tex
index 63d49099..f8a8d29d 100644
--- a/tex/gregoriotex-nabc.tex
+++ b/tex/gregoriotex-nabc.tex
@@ -121,6 +121,45 @@
\newif\ifgre@nabc@suppress@leftoverflow
\gre@nabc@suppress@leftoverflowfalse
+% Horizontal alignment of NABC neumes relative to the GABC note group.
+% Values: 0 = left (default), 1 = center.
+% Per-voice registers; set via the optional second argument of
+% \gresetnabcalignment.
+\newcount\gre@count@nabc@halign@i
+\gre@count@nabc@halign@i=0\relax
+\newcount\gre@count@nabc@halign@ii
+\gre@count@nabc@halign@ii=0\relax
+
+% Per-voice dimensions for right-side significative letter overflow.
+% In 'neume' mode, the Lua code sets these to the width of right-side LS
+% (positions 3, 6, 9). The center alignment computation subtracts this
+% width so that centering is based on the neume glyph only, ignoring
+% right-side significative letters (mirroring the left-side exclusion).
+\newdimen\gre@dimen@nabcrightoverflow@i
+\gre@dimen@nabcrightoverflow@i=0pt\relax
+\newdimen\gre@dimen@nabcrightoverflow@ii
+\gre@dimen@nabcrightoverflow@ii=0pt\relax
+
+% Element-wide horizontal advance tracking for center alignment of
+% multi-glyph and multi-element neumes. When center alignment is active,
+% NABC placement is deferred to the end of the NABC scope so that
+% centering uses the full element advance, not just the first glyph width.
+\newdimen\gre@dimen@nabc@elementadvance
+\gre@dimen@nabc@elementadvance=0pt\relax
+\newif\ifgre@nabc@halign@pending
+\gre@nabc@halign@pendingfalse
+\newif\ifgre@nabc@halign@pending@i
+\gre@nabc@halign@pending@ifalse
+\newif\ifgre@nabc@halign@pending@ii
+\gre@nabc@halign@pending@iifalse
+% Per-voice advance start: the value of \gre@dimen@nabc@elementadvance
+% at the moment each voice entered pending state. This allows each voice
+% to span a different number of elements when one voice has empty NABC.
+\newdimen\gre@dimen@nabc@advance@start@i
+\gre@dimen@nabc@advance@start@i=0pt\relax
+\newdimen\gre@dimen@nabc@advance@start@ii
+\gre@dimen@nabc@advance@start@ii=0pt\relax
+
\def\gresetnabc#1#2{%
\gre@processvisibilitywithphantom{#2}%
{\csname gre@nabcvoice@\romannumeral#1@visibletrue\endcsname}%
@@ -130,6 +169,16 @@
}
% See Command_Index_User.tex for documentation.
+% \gresetnabcalignment{neume|full}
+% \gresetnabcalignment{neume|full}{left|center}
+% \gresetnabcalignment[voice]{neume|full}
+% \gresetnabcalignment[voice]{neume|full}{left|center}
+%
+% The first argument selects the vertical alignment mode (significative-letter
+% handling). The optional second argument selects the horizontal alignment
+% of the NABC neume relative to the GABC note group: 'left' (default) or
+% 'center'. When the second argument is omitted the horizontal alignment
+% is left unchanged.
\def\gresetnabcalignment{\@ifnextchar[{\gre@setnabcalignment@voice}{\gre@setnabcalignment@global}}%
\def\gre@setnabcalignment@global#1{%
\IfStrEqCase{#1}{%
@@ -140,6 +189,8 @@
}[% all other cases
\gre@error{Unrecognized option "#1" for \protect\gresetnabcalignment\MessageBreak Possible options are: 'neume' and 'full'}%
]%
+ % Check for optional second argument (horizontal alignment)
+ \@ifnextchar\bgroup{\gre@setnabchalign@global}{}%
}%
\def\gre@setnabcalignment@voice[#1]#2{%
\IfStrEqCase{#2}{%
@@ -150,7 +201,35 @@
}[% all other cases
\gre@error{Unrecognized option "#2" for \protect\gresetnabcalignment\MessageBreak Possible options are: 'neume' and 'full'}%
]%
-}
+ % Check for optional second argument (horizontal alignment)
+ \@ifnextchar\bgroup{\gre@setnabchalign@voice[#1]}{}%
+}%
+
+% Internal helpers for horizontal alignment (called from \gresetnabcalignment)
+\def\gre@setnabchalign@global#1{%
+ \IfStrEqCase{#1}{%
+ {left}{%
+ \global\gre@count@nabc@halign@i=0\relax
+ \global\gre@count@nabc@halign@ii=0\relax}%
+ {center}{%
+ \global\gre@count@nabc@halign@i=1\relax
+ \global\gre@count@nabc@halign@ii=1\relax}%
+ }[%
+ \gre@error{Unrecognized horizontal alignment "#1" for \protect\gresetnabcalignment\MessageBreak Possible options are: 'left' and 'center'}%
+ ]%
+}%
+\def\gre@setnabchalign@voice[#1]#2{%
+ \IfStrEqCase{#2}{%
+ {left}{%
+ \ifnum#1=1\relax\global\gre@count@nabc@halign@i=0\relax\fi
+ \ifnum#1=2\relax\global\gre@count@nabc@halign@ii=0\relax\fi}%
+ {center}{%
+ \ifnum#1=1\relax\global\gre@count@nabc@halign@i=1\relax\fi
+ \ifnum#1=2\relax\global\gre@count@nabc@halign@ii=1\relax\fi}%
+ }[%
+ \gre@error{Unrecognized horizontal alignment "#2" for \protect\gresetnabcalignment\MessageBreak Possible options are: 'left' and 'center'}%
+ ]%
+}%
% See Command_Index_User.tex for documentation.
\def\gresetnabcskipalterations{%
@@ -182,6 +261,13 @@
}
\def\GreNABCNeumes#1#2#3#4{%
+ % Flush only the voice being replaced, not all voices. This allows
+ % a voice whose NABC is empty on the next element to keep accumulating
+ % advance and center over the combined span.
+ \ifcase#1\relax
+ \or \gre@nabc@flush@deferred@voice{i}%
+ \or \gre@nabc@flush@deferred@voice{ii}%
+ \fi
\csname ifgre@nabcvoice@\romannumeral#1@visible\endcsname %
\ifcase#1\relax
\or % 1
@@ -202,6 +288,7 @@
\gre@nabc@pre@voice@ii
\gre@nabc@emit@voice@i
\gre@nabc@emit@voice@ii
+ \gre@nabc@flush@deferred@halign
}
\newif\ifgre@nabcfontloaded%
diff --git a/tex/gregoriotex-syllable.tex b/tex/gregoriotex-syllable.tex
index d1b67584..eb1b11a6 100644
--- a/tex/gregoriotex-syllable.tex
+++ b/tex/gregoriotex-syllable.tex
@@ -108,6 +108,10 @@
\fi%
\gre@nabc@emit@voice@i
\gre@nabc@emit@voice@ii
+ % Track element advance for deferred center alignment.
+ \ifgre@nabc@halign@pending
+ \global\advance\gre@dimen@nabc@elementadvance by \gre@dimen@lastglyphwidth\relax
+ \fi
% Reset NABC overflow accumulators after all voices have been processed.
% During boxing, keep values so the adjustment code can read them.
\ifgre@boxing\else%
@@ -171,10 +175,11 @@
% #6 are the signs to typeset after the glyph (almost all signs)
% #7 is the line:char:column for a textedit link
\def\GreGlyph#1#2#3#4#5#6#7{%
- \gre@newglyphcommon %
- % Create box containing the new glyph.
+ % Pre-measure the glyph so that \gre@dimen@lastglyphwidth is available
+ % during NABC placement (needed for center/right horizontal alignment).
\setbox\gre@box@temp@width=\hbox{\gre@pointandclick{\gre@font@music #1}{#7}}%
\global\gre@dimen@lastglyphwidth=\wd\gre@box@temp@width %
+ \gre@newglyphcommon %
% the three next lines are a trick to get the additional lines below the glyphs
\gre@skip@temp@one = \gre@dimen@lastglyphwidth\relax%
\kern\gre@skip@temp@one %
@@ -496,6 +501,9 @@
\gre@suppress@bars
\def\GreEndOfElement##1##2##3{}%
\def\GreEndOfGlyph##1{}%
+ % Disable deferred centering: no glyph widths to track in tight mode.
+ \let\gre@nabc@place@voice@i\gre@nabc@place@voice@i@now
+ \let\gre@nabc@place@voice@ii\gre@nabc@place@voice@ii@now
#1%
\endgroup
}%
@@ -1265,6 +1273,7 @@
{\raise 12pt\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}%
{}% do nothing if not debugging
#9% we do that instead of \unhbox\Syllablnotes, because it would not set the \localrightbox
+ \gre@nabc@flush@deferred@halign
% Apply per-syllable NABC kern computed in \gre@syllablenotes.
\ifdim\gre@dimen@nabc@extrakern>0pt\relax
\kern\gre@dimen@nabc@extrakern
@@ -1563,6 +1572,7 @@
{\raise 12pt\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}%
{}% do nothing if not debugging
#9%
+ \gre@nabc@flush@deferred@halign
\IfSubStr{\gre@debug}{,barspacing,}%
% when debugging we add a zero-width line to mark the syllable bound
{\raise 12pt\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}%