From 02ec95278623f615b5effda672bb2704bc8061aa Mon Sep 17 00:00:00 2001 From: David Chiang Date: Wed, 18 Feb 2026 20:10:26 -0500 Subject: [PATCH 01/17] Move syllable rewriting to Lua. The new algorithm works slightly differently from the old one, in the following ways: + The old algorithm worked by moving the last part of a syllable to the first part of the next syllable. It could therefore break ligatures between the middle and last part of a syllable. The new algorithm works by joining the text boxes together; it does not break ligatures. + If maximumspacewithoutdash > 0, the old algorithm could make the last part of a syllable "jump" across the space between syllables; the new one only rewrites syllables if they are actually touching. - The old algorithm computed syllable spacing after rewriting; the new algorithm computes them in the opposite order. This means that some syllable spacing will be slightly too big or small. This will be fixable when syllable spacing is also moved to Lua. --- doc/Command_Index_gregorio.tex | 3 - doc/Command_Index_internal.tex | 58 ++--------- tex/gregoriotex-main.tex | 20 ++-- tex/gregoriotex-spaces.tex | 39 +++---- tex/gregoriotex-syllable.lua | 169 ++++++++++++++++++++++++++++++ tex/gregoriotex-syllable.tex | 179 +++++++------------------------- tex/gregoriotex.lua | 184 ++++++++++++++++++++------------- 7 files changed, 361 insertions(+), 291 deletions(-) create mode 100644 tex/gregoriotex-syllable.lua diff --git a/doc/Command_Index_gregorio.tex b/doc/Command_Index_gregorio.tex index f0626717..8bafd338 100644 --- a/doc/Command_Index_gregorio.tex +++ b/doc/Command_Index_gregorio.tex @@ -1361,9 +1361,6 @@ \section{Gregorio Controls} \macroname{\textbackslash GreNoBreak}{}{gregoriotex-spaces.tex} Macro used to prevent a line break from occurring at a given position. -\macroname{\textbackslash GreScoreId}{}{gregoriotex-main.tex} -A Lua\TeX\ attribute which designates a unique identifier for each score. - \macroname{\textbackslash GreNABCNeumes}{\#1\#2\#3\#4}{gregoriotex-nabc.tex} Macro called by the generated \texttt{.gtex} file to typeset the neumes of one NABC voice for a GABC element. Dispatches to \verb=\GreSetNabcAboveLines= or diff --git a/doc/Command_Index_internal.tex b/doc/Command_Index_internal.tex index e92569b0..5c7405b3 100644 --- a/doc/Command_Index_internal.tex +++ b/doc/Command_Index_internal.tex @@ -140,10 +140,9 @@ \section{Gregorio\TeX{} Controls} Macro for calculating \verb=\gre@textaligncenter=. \begin{argtable} - \#1 & string & The carry-over letters from the previous syllable that should be moved to the current.\\ - \#2 & string & The first part of the syllable (any preceding consonants in Latin).\\ - \#3 & string & The middle part of the syllable (the vowel in Latin, the whole syllable in English).\\ - \#4 & \texttt{0} & Calculation is being performed for the current syllable.\\ + \#1 & string & The first part of the syllable (any preceding consonants in Latin).\\ + \#2 & string & The middle part of the syllable (the vowel in Latin, the whole syllable in English).\\ + \#3 & \texttt{0} & Calculation is being performed for the current syllable.\\ & \texttt{1} & Calculation is being performed for the next syllable.\\ \end{argtable} @@ -252,12 +251,11 @@ \section{Gregorio\TeX{} Controls} Macro to calculate \texttt{nextbegindifference}. \begin{argtable} - \#1 & string & the carry-over letters for the next syllable\\ - \#2 & string & the first letters of the next syllable\\ - \#3 & string & the middle letters of the next syllable (the vowel in Latin, the whole syllable in English)\\ - \#4 & string & the end letters of the next syllable\\ - \#5 & integer & the type of notes alignment. See \Nameref{notesalign}.\\ - \#6 & integer & the type of alteration. See \Nameref{alterationtype}.\\ + \#1 & string & the first letters of the next syllable\\ + \#2 & string & the middle letters of the next syllable (the vowel in Latin, the whole syllable in English)\\ + \#3 & string & the end letters of the next syllable\\ + \#4 & integer & the type of notes alignment. See \Nameref{notesalign}.\\ + \#5 & integer & the type of alteration. See \Nameref{alterationtype}.\\ \end{argtable} \macroname{\textbackslash gre@strip@pt}{\#1}{gregoriotex.sty \textup{and} gregoriotex.tex} @@ -1145,43 +1143,6 @@ \section{Gregorio\TeX{} Controls} \#1 & string & The syllable (usually built as \texttt{\small\pmac{gre@nextfirstsyllablepart}\linebreak[1]\pmac{gre@nextmiddlesyllablepart}\linebreak[1]\pmac{gre@nextendsyllablepart}})\\ \end{argtable} -\macroname{\textbackslash gre@if@rewritesyllable}{\#1\#2}{gregoriotex-syllable.tex} -Performs \#1 if the syllable should be rewritten, else \#2. - -\begin{argtable} - \#1 & \TeX\ code & Code to perform when rewriting the syllable\\ - \#2 & \TeX\ code & Code to perform when \emph{not} rewriting the syllable\\ -\end{argtable} - -\macroname{\textbackslash gre@push@endsyllable}{\#1}{gregoriotex-syllable.tex} -Sets the save aliases to push the end-syllable part of the current syllable to the next syllable if necessary. - -\begin{argtable} - \#1 & link target & line:char:column for the link to use for the pushed syllable part\\ -\end{argtable} - -\macroname{\textbackslash gre@emit@syllabletext}{\#1}{gregoriotex-syllable.tex} -Emits the text for the syllable, prepending the carry-over syllable part if necessary and consolidating the fixed text styles if possible. - -\begin{argtable} - \#1 & \TeX\ code & Code that emits the syllable text\\ -\end{argtable} - -\macroname{\textbackslash gre@emit@endsyllablepart}{}{gregoriotex-syllable.tex} -Emits the text for the end syllable part if it \emph{is not} to be moved to the next syllable. - -\macroname{\textbackslash gre@emit@endsyllablepartfornextsyllable}{}{gregoriotex-syllable.tex} -Emits the text for the end syllable part if it \emph{is} to be moved to the next syllable. This is used when projecting the next syllable text while processing some syllable. - -\macroname{\textbackslash gre@syllable@args}{}{gregoriotex-syllable.tex} -Saves the arguments to \verb=\GreSyllable=. Needed so that \verb=\GreSyllable= can look forward to see if the next token is \verb=\GreBarSyllable=. - -\macroname{\textbackslash gre@syllable@expand}{}{gregoriotex-syllable.tex} -Calls \verb=\gre@syllable@act=, passing the arguments saved in \verb=gre@syllable@args=. Needed so that \verb=\GreSyllable= can look forward to see if the next token is \verb=\GreBarSyllable=. - -\macroname{\textbackslash gre@syllable@act}{\#1\#2\#3\#4\#5\#6\#7\#8\#9}{gregoriotex-syllable.tex} -Typesets the syllable. Same arguments as \verb=\GreSyllable=. See the description of that macro for more information. Needed so that \verb=\GreSyllable= can look forward to see if the next token is \verb=\GreBarSyllable=. - \macroname{\textbackslash gre@gabcname}{}{gregoriotex-main.tex} Macro which holds the point-and-click file name. @@ -1871,6 +1832,9 @@ \subsection{Flags} \macroname{\textbackslash ifgre@endofscore}{}{gregoriotex-syllable.tex} Boolean to mark the last syllable of the score. +\macroname{\textbackslash gre@attr@score}{}{gregoriotex-main.tex} +A Lua\TeX\ attribute which indicates whether a line is part of a score. + \macroname{\textbackslash ifgre@firstglyph}{}{gregoriotex-syllable.tex} Boolean that tells us if the current glyph is the first glyph or not. diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index fc812825..7f0e58e8 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -44,6 +44,11 @@ %%%%%%%%%%%%%%% +% an attribute to mark everything in the score +% 1 = in score +% undefined = not in score +\newattribute\gre@attr@score + % an attribute to mark various parts of the score % 1 = commentary, 2 = stafflines, 3 = initial % 4 = lyrics, 5 = translation, 6 = alt, 7 = nabc, 8 = nabc below @@ -51,16 +56,15 @@ \edef\gre@attrid@part{\the\allocationnumber} % an attribute we put on the text nodes. -% if it is 1, it means that there may be a dash here if this syllable is at the end of a line -% if it is 2, it means that it's never useful to typeset a dash -% if it is 0, it just means that we are in a score... +% 1 = the syllable may need a dash added (in Lua) if at the end of a line +% 2 = the syllable doesn't need a dash because it already has one +% 3 = the syllable doesn't need a dash because it's word-final +% 4 = the syllable doesn't need a dash because it is a \GreBarSyllable \newattribute\gre@attr@dash % an attribute used for translation centering \newattribute\gre@attr@center -\newattribute\GreScoreId - % attributes for tracking glyph heights \newattribute\gre@attr@glyph@top \newattribute\gre@attr@glyph@bottom @@ -1118,8 +1122,6 @@ }% \gresetnoteadditionalspacelinestext{automatic}%default setting -% gre@attr@dash (see its definition in gregorio-syllable) is 0 when we are in a score, and unset when we are not - \newif\ifgre@beginningofscore% \newcount\gre@count@stafflines @@ -1241,7 +1243,7 @@ \fi % \gre@computespaces% \gre@cancelpenalties % - \gre@attr@dash=0\relax % + \gre@attr@score=1 \gre@generatelines % \noindent% \gre@calculate@additionalspaces @@ -1290,7 +1292,7 @@ \directlua{gregoriotex.at_score_end()}% \unsetattribute{\gre@attr@glyph@top}% \unsetattribute{\gre@attr@glyph@bottom}% - \unsetattribute{\gre@attr@dash}% + \unsetattribute{\gre@attr@score}% \xdef\gre@bolshiftcleftypelocal{\gre@bolshiftcleftypeglobal}% \ifnum\gre@count@lastline=0\relax \parfillskip=\gre@saved@parfillskip\relax% diff --git a/tex/gregoriotex-spaces.tex b/tex/gregoriotex-spaces.tex index 74838c40..bd2eddf8 100644 --- a/tex/gregoriotex-spaces.tex +++ b/tex/gregoriotex-spaces.tex @@ -911,24 +911,26 @@ }% %% macro that typesets the text of the syllable, and sets textaligncenter to the middle of the middle letters, it is needed because we align the note (often the middle of the note) with the middle of the middle letters -%% third argument is 0 if it's the current syllable, 1 if it's the alignment of the following one +%% #1: The first part of the syllable +%% #2: The middle part of the syllable +%% #3: 0 if it's the current syllable, 1 if it's the following one %% warning: gretextaligncenter is the width from the beginning of the letters to the middle of the middle letters %% warning: value is approximative when a ligature appears \newdimen\gre@dimen@textaligncenter\relax% -\def\gre@calculate@textaligncenter#1#2#3#4{% - \gre@trace{gre@calculate@textaligncenter{#1}{#2}{#3}{#4}}% - \ifnum#4=0\relax% - \gre@widthof{\gre@saved@syllable@fixedtextformat{#1}\gre@fixedtextformat{#2#3}}% +\def\gre@calculate@textaligncenter#1#2#3{% + \gre@trace{gre@calculate@textaligncenter{#1}{#2}{#3}}% + \ifnum#3=0\relax + \gre@widthof{\gre@fixedtextformat{#1#2}}% \else % - \gre@widthof{\gre@fixedtextformat{#1}\gre@fixednexttextformat{#2#3}}% + \gre@widthof{\gre@fixednexttextformat{#1#2}}% \fi % \global\gre@dimen@textaligncenter=\the\gre@dimen@temp@three % - \ifnum#4=0\relax% - \gre@widthof{\gre@fixedtextformat{#3}}% + \ifnum#3=0\relax + \gre@widthof{\gre@fixedtextformat{#2}}% \else % - \gre@widthof{\gre@fixednexttextformat{#3}}% + \gre@widthof{\gre@fixednexttextformat{#2}}% \fi % \divide\gre@dimen@temp@three by 2 % \global\advance\gre@dimen@textaligncenter by -\the\gre@dimen@temp@three% @@ -964,25 +966,24 @@ \newskip\gre@skip@nextbegindifference\relax% % macro to set nextbegindifference -%% 1 : the carry-over letters for the next syllable -%% 2 : the first letters of the next syllable -%% 3 : the middle letters of the next syllable -%% 4 : the end letters of the next syllable -%% 5 : the type of notes alignment -%% 6 : alteration type (see \gre@alteration) -\def\gre@calculate@nextbegindifference#1#2#3#4#5#6{% - \gre@trace{gre@calculate@nextbegindifference{#1}{#2}{#3}{#4}{#5}{#6}}% +%% 1 : the first letters of the next syllable +%% 2 : the middle letters of the next syllable +%% 3 : the end letters of the next syllable +%% 4 : the type of notes alignment +%% 5 : alteration type (see \gre@alteration) +\def\gre@calculate@nextbegindifference#1#2#3#4#5{% + \gre@trace{gre@calculate@nextbegindifference{#1}{#2}{#3}{#4}{#5}}% \ifnum\gre@lastoflinecount=1\relax % \global\gre@skip@nextbegindifference=0pt\relax% \else % %to prevent the pollution of the normal values, we stock them into a temp value \gre@dimen@temp@two=\gre@dimen@textaligncenter\relax% - \gre@calculate@textaligncenter{#1}{#2}{#3}{1}% + \gre@calculate@textaligncenter{#1}{#2}{1}% \gre@dimen@temp@four=\gre@dimen@notesaligncenter\relax% \global\gre@skip@nextbegindifference=-\gre@dimen@textaligncenter\relax% % caution: calculate@nextnotesaligncenter needs a properly set \gre@dimen@textaligncenter % (corresponding to the text align center of the next syllable) - \gre@calculate@nextnotesaligncenter{#5}{#6}% idem + \gre@calculate@nextnotesaligncenter{#4}{#5}% idem \global\advance\gre@skip@nextbegindifference by \the\gre@dimen@notesaligncenter\relax% \global\gre@dimen@textaligncenter=\gre@dimen@temp@two % \global\gre@dimen@notesaligncenter=\gre@dimen@temp@four % diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua new file mode 100644 index 00000000..5ae44b10 --- /dev/null +++ b/tex/gregoriotex-syllable.lua @@ -0,0 +1,169 @@ +--GregorioTeX Syllable Lua support file. +-- +--Copyright (C) 2015-2026 The Gregorio Project (see CONTRIBUTORS.md) +-- +--This file is part of Gregorio. +-- +--Gregorio is free software: you can redistribute it and/or modify +--it under the terms of the GNU General Public License as published by +--the Free Software Foundation, either version 3 of the License, or +--(at your option) any later version. +-- +--Gregorio is distributed in the hope that it will be useful, +--but WITHOUT ANY WARRANTY; without even the implied warranty of +--MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +--GNU General Public License for more details. +-- +--You should have received a copy of the GNU General Public License +--along with Gregorio. If not, see . + +-- this file contains lua functions to support signs used by GregorioTeX. + +-- GREGORIO_VERSION 6.1.0 + +local err = gregoriotex.module.err +local warn = gregoriotex.module.warn +local info = gregoriotex.module.info +local log = gregoriotex.module.log +local debugmessage = gregoriotex.module.debugmessage + +local has_attribute = node.has_attribute +local kern = node.id('kern') +local temp = node.id('temp') + +local syllable_id_attr = luatexbase.attributes['gre@attr@syllable@id'] + +local part_attr = luatexbase.attributes['gre@attr@part'] +local part_lyrics = 4 + +local dash_attr = luatexbase.attributes['gre@attr@dash'] +local dash_hasdash = 2 +local dash_barsyllable = 4 + +local saved_syllable_texts = {} +local function save_syllable_texts(head) + -- Save syllable texts before ligaturing and kerning happens. This + -- is needed later during syllable rewriting. + -- Because syllable_id_attr is set even for material not in the + -- syllable text, it's better to use dash_attr to detect whether + -- this box is really syllable text. + if tex.getattribute(dash_attr) > 0 then + local sid = tex.getattribute(syllable_id_attr) + local cur = head + while cur ~= nil and cur.id == temp do cur = cur.next end + saved_syllable_texts[sid] = node.copy_list(cur) + end +end + +local function free_saved_syllable_texts() + for sid, head in pairs(saved_syllable_texts) do + node.flush_list(head) + saved_syllable_texts[sid] = nil + end +end + +local function concat_list(head, tail, newhead, newtail) + if head == nil then + return newhead, newtail + elseif newhead == nil then + return head, tail + else + tail.next = newhead + newhead.prev = tail + return head, newtail + end +end + +local function shaping(head) + head = node.ligaturing(head) + head = node.kerning(head) + -- Under luaotfload, ligaturing and kerning are done inside the following + if nodes ~= nil and nodes.simple_font_handler ~= nil then + head = nodes.simple_font_handler(head) + end + return head +end + +local function syllable_rewriting(head) + --gregoriotex.dump_nodes(head) + if not gregoriotex.get_if('gre@rewritesyllables') then return head end + + local syllables = {} + local last_sid = nil + for n in node.traverse(head) do + -- This skips over discretionary nodes, which can't participate in syllable rewriting + local sid, part = has_attribute(n, syllable_id_attr), has_attribute(n, part_attr) + if sid ~= nil and part == part_lyrics then + if syllables[sid] ~= nil then + err('syllable %d has more than one text node', sid) + end + debugmessage('syllablerewriting', 'syllable %d has text node', sid) + syllables[sid] = {} + syllables[sid].text_node = n + last_sid = sid + end + end + + if last_sid == nil then return head end + + local start = 1 + while start <= last_sid do + -- Find longest run of syllables, starting from start, that have + -- zero distance between their text boxes. + if syllables[start] == nil or syllables[start].text_node == nil then + debugmessage('syllablerewriting', 'syllable %d has no text node', start) + start = start + 1 + else + local stop = start + while stop+1 <= last_sid do + -- There are several conditions that prevent syllable rewriting: + -- if either text node is missing + if syllables[stop+1] == nil or syllables[stop+1].text_node == nil then break end + -- don't rewrite across a line break + if gregoriotex.is_last_syllable_id_on_line(stop) then break end + -- don't rewrite across a hyphen + if has_attribute(syllables[stop].text_node, dash_attr, dash_hasdash) then break end + -- if either syllable is a \GreBarSyllable + if has_attribute(syllables[stop].text_node, dash_attr, dash_barsyllable) or + has_attribute(syllables[stop+1].text_node, dash_attr, dash_barsyllable) then break end + -- don't rewrite across a nonzero space + if node.dimensions(syllables[stop].text_node.next, syllables[stop+1].text_node) ~= 0 then break end + stop = stop + 1 + end + -- Concatenate syllable text boxes into one box. + if start < stop then + debugmessage('syllablerewriting', 'merge syllables %d-%d', start, stop) + local head, tail + for sid = start, stop do + -- Extend new text + local n = saved_syllable_texts[sid] + saved_syllable_texts[sid] = nil + head, tail = concat_list(head, tail, n, node.tail(n)) + end + head = shaping(head) + for sid = start, stop do + -- Rewrite text, inserting kerns to preserve widths + local del = syllables[sid].text_node.head + syllables[sid].text_node.head = nil + node.flush_list(del) + local kern = node.new(kern, 'userkern') + kern.kern = syllables[sid].text_node.width + if sid == start then + syllables[sid].text_node.head = head + kern.kern = kern.kern - node.dimensions(head) + concat_list(head, tail, kern) + else + syllables[sid].text_node.head = kern + end + end + end + start = stop + 1 + end + end + --gregoriotex.dump_nodes(head) + return head +end + +gregoriotex.save_syllable_texts = save_syllable_texts +gregoriotex.free_saved_syllable_texts = free_saved_syllable_texts +gregoriotex.syllable_rewriting = syllable_rewriting diff --git a/tex/gregoriotex-syllable.tex b/tex/gregoriotex-syllable.tex index d1b67584..0ceff383 100644 --- a/tex/gregoriotex-syllable.tex +++ b/tex/gregoriotex-syllable.tex @@ -595,9 +595,6 @@ % box that will contain the text of the syllable \newbox\gre@box@syllabletext% -% count that will be 0 if in the last text there was no dash (or if it is the beginning of a word, and 1 if there was -%\newcount\previousdash - % Flag to track if we are boxing the syllable notes or printing them \newif\ifgre@boxing% \gre@boxingfalse% @@ -895,93 +892,6 @@ ]% }% -% Performs #1 if the syllable should be rewitten, else #2 -\newif\ifgre@rewritethissyllable % -\def\gre@if@rewritesyllable#1#2{% - \gre@trace{gre@if@rewritesyllable{#1}{#2}}% - \gre@rewritethissyllablefalse % - \ifgre@rewritesyllables\relax % - \gre@debugmsg{syllablerewriting}{1}% - \ifnum\gre@insidediscretionary=0\relax % - \gre@debugmsg{syllablerewriting}{2}% - \ifgre@showhyphenafterthissyllable\else % - \gre@debugmsg{syllablerewriting}{3}% - \ifgre@possibleluahyphenafterthissyllable\relax % - \gre@debugmsg{syllablerewriting}{4}% - \ifx\gre@syllable@next\GreSyllable % - \gre@debugmsg{syllablerewriting}{5}% - \ifcase\directlua{gregoriotex.is_last_syllable_on_line()}\else % - \gre@debugmsg{syllablerewriting}{6}% - \gre@rewritethissyllabletrue % - \fi % - \fi % - \fi % - \fi % - \fi % - \fi % - \gre@debugmsg{syllablerewriting}{X}% - \ifgre@rewritethissyllable % - #1% - \else % - #2% - \fi % - \gre@trace@end% -}% - -\let\gre@saved@syllable@endsyllablepart\gre@nothing\relax % -\let\gre@saved@syllable@fixedtextformat\gre@textnormal\relax % -\let\gre@saved@syllable@pointandclick\gre@nothing\relax % -\def\gre@push@endsyllable#1{% - \gre@trace{gre@push@endsyllable{#1}}% - \let\gre@saved@syllable@endsyllablepart\gre@nothing\relax % - \let\gre@saved@syllable@fixedtextformat\gre@textnormal\relax % - \let\gre@saved@syllable@pointandclick\gre@nothing\relax % - \gre@if@rewritesyllable{% - \let\gre@saved@syllable@endsyllablepart\gre@endsyllablepart\relax % - \let\gre@saved@syllable@fixedtextformat\gre@fixedtextformat\relax % - \xdef\gre@saved@syllable@pointandclick{#1}% - }{}% - \relax % - \gre@trace@end% -}% - -\def\gre@emit@syllabletext#1{% - \gre@trace{gre@emit@syllabletext{#1}}% - \ifx\gre@saved@syllable@endsyllablepart\gre@nothing % - \gre@fixedtextformat{#1}% - \else % - \ifx\gre@saved@syllable@fixedtextformat\gre@fixedtextformat % - \gre@debugmsg{syllablerewriting}{merging format when prepending previous last syllable part}% - \gre@fixedtextformat{\gre@pointandclick{\gre@saved@syllable@endsyllablepart}{\gre@saved@syllable@pointandclick}#1}% - \else % - \gre@debugmsg{syllablerewriting}{prepending previous last syllable part}% - \gre@saved@syllable@fixedtextformat{\gre@pointandclick{\gre@saved@syllable@endsyllablepart}{\gre@saved@syllable@pointandclick}}% - \gre@fixedtextformat{#1}% - \fi % - \fi % - \relax % - \gre@trace@end% -}% - -\def\gre@emit@endsyllablepart{% - \gre@trace{gre@emit@endsyllablepart}% - \gre@if@rewritesyllable{}{% - \gre@debugmsg{syllablerewriting}{not rewriting syllable}% - \gre@endsyllablepart % - }% - \relax % - \gre@trace@end% -}% - -\def\gre@emit@endsyllablepartfornextsyllable{% - \gre@trace{gre@emit@endsyllablepartfornextsyllable}% - \gre@if@rewritesyllable{% - \gre@endsyllablepart % - }{}% - \relax % - \gre@trace@end% -}% - \newif\ifgre@textcleared% \def\GreClearSyllableText{% @@ -1007,16 +917,6 @@ %% at the end we wall \greendofword or \gre@endofsyllable with #7, to reduce the space in case of a flat or natural \def\GreSyllable#1#2#3#4#5#6#7#8#9{% \gre@textclearedfalse% - \def\gre@syllable@args{{#1}{#2}{#3}{#4}{#5}{#6}{#7}{#8}{#9}}% - \futurelet\gre@syllable@next\gre@syllable@expand% -}% -\def\gre@syllable@expand{% - \gre@trace{gre@syllable@expand}% - \expandafter\gre@syllable@act\gre@syllable@args% - \gre@trace@end% -}% -\def\gre@syllable@act#1#2#3#4#5#6#7#8#9{% - \gre@trace{gre@syllable@act{#1}{#2}{#3}{#4}{#5}{#6}{#7}{#8}{#9}}% \gre@debugmsg{general}{}% \gre@debugmsg{general}{New syllable: \expandafter\unexpanded{#1}}% \gre@debugmsg{general}{}% @@ -1036,7 +936,7 @@ #1% \gre@firstglyphtrue% \gre@dimen@bolextra = 0pt\relax% - \gre@calculate@textaligncenter{\gre@saved@syllable@endsyllablepart}{\gre@firstsyllablepart}{\gre@middlesyllablepart}{0}% we first get the width between the alignment point and the end of the syllable + \gre@calculate@textaligncenter{\gre@firstsyllablepart}{\gre@middlesyllablepart}{0}% we first get the width between the alignment point and the end of the syllable % Before measuring the notes, save the alteration id. We will % restore it later, so that the measured notes and the actual notes % have the same alteration ids, if any. @@ -1092,8 +992,6 @@ \hbox to 0pt{}% \GreNoBreak % \fi % - % by default, gre@attr@dash will be 2 - \gre@attr@dash=2\relax % #5% % if the next glyph has an alteration, check if it is suppressed; % if so, pretend there's no alteration @@ -1108,15 +1006,16 @@ \fi % now we can restore the alteration id to typeset the notes for real \global\gre@attr@alteration@id=\gre@saved@attr@alteration@id\relax % - \gre@calculate@nextbegindifference{\gre@emit@endsyllablepartfornextsyllable}{\gre@evaluatenextsyllable{\gre@nextfirstsyllablepart}}{\gre@evaluatenextsyllable{\gre@nextmiddlesyllablepart}}{\gre@evaluatenextsyllable{\gre@nextendsyllablepart}}{\gre@nextalignment}{\gre@nextalteration}% + \gre@calculate@nextbegindifference{\gre@evaluatenextsyllable{\gre@nextfirstsyllablepart}}{\gre@evaluatenextsyllable{\gre@nextmiddlesyllablepart}}{\gre@evaluatenextsyllable{\gre@nextendsyllablepart}}{\gre@nextalignment}{\gre@nextalteration}% \gre@unsetfixednexttextformat % + \gre@attr@dash=3 \ifgre@showlyrics% - \setbox\gre@box@syllabletext=\hbox{% + \setbox\gre@box@syllabletext=\hbox attr \gre@attrid@part=4 {% \IfSubStr{\gre@debug}{,notespacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% {}% do nothing if not debugging - \gre@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@endsyllablepart}{#6}}% + \gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart}{#6}}% \IfSubStr{\gre@debug}{,notespacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% @@ -1124,11 +1023,12 @@ }% \else% \ifnum\gre@lyrics@phantomwrapper>0\relax - \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@endsyllablepart}{#6}}}}% + \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart}{#6}}}}% \else \setbox\gre@box@syllabletext=\box\voidb@x% \fi \fi% + \unsetattribute{\gre@attr@dash}% \gre@calculate@enddifference{\wd\gre@box@syllablenotes}{\wd\gre@box@syllabletext}{\gre@dimen@textaligncenter}{\gre@dimen@notesaligncenter}{1}% % gre@count@temp@one holds 1 if next is a bar, 2 if an alteration, else 0 \gre@count@temp@one=0% @@ -1166,13 +1066,14 @@ \gre@debugmsg{hyphen}{Showing the hyphen}% % if it's the last syllable of line, the hyphen will be \GreHyph \ifnum\gre@lastoflinecount=1\relax % + \gre@attr@dash=2 \ifgre@showlyrics% - \setbox\gre@box@syllabletext=\hbox{% + \setbox\gre@box@syllabletext=\hbox attr \gre@attrid@part=4 {% \IfSubStr{\gre@debug}{,notespacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% {}% do nothing if not debugging - \gre@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@endsyllablepart#3{\GreHyph}\relax}{#6}}% + \gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart#3{\GreHyph}}{#6}}% \IfSubStr{\gre@debug}{,notespacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% @@ -1180,19 +1081,21 @@ }% \else% \ifnum\gre@lyrics@phantomwrapper>0\relax - \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@endsyllablepart#3{\GreHyph}\relax}{#6}}}}% + \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart#3{\GreHyph}\relax}{#6}}}}% \else \setbox\gre@box@syllabletext=\box\voidb@x% \fi \fi% + \unsetattribute{\gre@attr@dash}% \else % + \gre@attr@dash=2 \ifgre@showlyrics% - \setbox\gre@box@syllabletext=\hbox{% + \setbox\gre@box@syllabletext=\hbox attr \gre@attrid@part=4 {% \IfSubStr{\gre@debug}{,notespacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% {}% do nothing if not debugging - \gre@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@endsyllablepart#3{-}}{#6}}% + \gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart#3{-}}{#6}}% \IfSubStr{\gre@debug}{,notespacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% @@ -1200,29 +1103,30 @@ }% \else% \ifnum\gre@lyrics@phantomwrapper>0\relax - \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@endsyllablepart#3{-}}{#6}}}}% + \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart#3{-}}{#6}}}}% \else \setbox\gre@box@syllabletext=\box\voidb@x% \fi \fi% + \unsetattribute{\gre@attr@dash}% \fi % % recomputing end difference and final skip with the final hyphen - \gre@calculate@nextbegindifference{\gre@emit@endsyllablepartfornextsyllable}{\gre@evaluatenextsyllable{\gre@nextfirstsyllablepart}}{\gre@evaluatenextsyllable{\gre@nextmiddlesyllablepart}}{\gre@evaluatenextsyllable{\gre@nextendsyllablepart}}{\gre@nextalignment}{\gre@nextalteration}% + \gre@calculate@nextbegindifference{\gre@evaluatenextsyllable{\gre@nextfirstsyllablepart}}{\gre@evaluatenextsyllable{\gre@nextmiddlesyllablepart}}{\gre@evaluatenextsyllable{\gre@nextendsyllablepart}}{\gre@nextalignment}{\gre@nextalteration}% \gre@calculate@enddifference{\wd\gre@box@syllablenotes}{\wd\gre@box@syllabletext}{\gre@dimen@textaligncenter}{\gre@dimen@notesaligncenter}{0}% \gre@calculate@syllablefinalskip{#4}{\gre@count@temp@one}% \else % \ifcase#4 % \global\gre@possibleluahyphenafterthissyllabletrue % \gre@debugmsg{hyphen}{No hyphen}% - \gre@attr@dash=1\relax % in this particular case where it is not the end of a word and we haven't put a dash, we set potentital dash to 1 + \gre@attr@dash=1 % if not the end of a word and we haven't added a dash, we set potential dash to 1 % we rebuild this box, in order it to have the attribute \ifgre@showlyrics% - \setbox\gre@box@syllabletext=\hbox{% + \setbox\gre@box@syllabletext=\hbox attr \gre@attrid@part=4 {% \IfSubStr{\gre@debug}{,notespacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% {}% do nothing if not debugging - \gre@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@endsyllablepart}{#6}}% + \gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart}{#6}}% \IfSubStr{\gre@debug}{,notespacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% @@ -1231,6 +1135,7 @@ \else% \setbox\gre@box@syllabletext=\box\voidb@x% \fi% + \unsetattribute{\gre@attr@dash}% \else % \global\gre@possibleluahyphenafterthissyllablefalse % \fi % @@ -1246,7 +1151,7 @@ \fi% #8\relax % \raise\gre@space@dimen@spacebeneathtext - \hbox attr \gre@attrid@part=4 {\unhcopy\gre@box@syllabletext}% + \copy\gre@box@syllabletext \ifgre@mustdotranslationcenterend% % case of end of translation centering, we do it after the typesetting of the text \gre@dotranslationcenterend % @@ -1257,7 +1162,7 @@ \gre@skip@temp@one = -\gre@dimen@begindifference\relax% \kern\gre@skip@temp@one % % here we need to unset \gre@attr@dash for the typesetting of notes - \gre@attr@dash=0\relax % + \unsetattribute{\gre@attr@dash}% \GreNoBreak % no line breaks between text and notes \ifgre@shownotes% \IfSubStr{\gre@debug}{,notespacing,}% @@ -1300,7 +1205,6 @@ \fi% % we call end of syllable \gre@syllable@end{\gre@nextalignment}{\gre@nextalteration}{\gre@evaluatenextsyllable{\gre@nextfirstsyllablepart\gre@nextmiddlesyllablepart\gre@nextendsyllablepart}}{#4}% - \gre@push@endsyllable{#6}\relax % \global\gre@dimen@notesaligncenter=0pt\relax% very important, see flat and natural \gre@unsetfixedtextformat % \ifgre@blockeolcustos\ifnum\gre@insidediscretionary=0\relax % @@ -1392,17 +1296,13 @@ \gre@debugmsg{syllablespacing}{ set penalty \the\gre@space@count@nobreakpenalty}% \else % \gre@count@temp@one=0\relax % - \gre@if@rewritesyllable{% - \GreNoBreak% - }{% - \ifnum#2=1\relax % - \gre@penalty{\the\gre@space@count@endofwordpenalty}% - \gre@debugmsg{syllablespacing}{ set penalty \the\gre@space@count@endofwordpenalty}% - \else % - \gre@penalty{\the\gre@space@count@endofsyllablepenalty}% - \gre@debugmsg{syllablespacing}{ set penalty \the\gre@space@count@endofsyllablepenalty}% - \fi % - }% + \ifnum#2=1\relax + \gre@penalty{\the\gre@space@count@endofwordpenalty}% + \gre@debugmsg{syllablespacing}{ set penalty \the\gre@space@count@endofwordpenalty}% + \else + \gre@penalty{\the\gre@space@count@endofsyllablepenalty}% + \gre@debugmsg{syllablespacing}{ set penalty \the\gre@space@count@endofsyllablepenalty}% + \fi \gre@count@temp@one=0\relax % \fi % \ifgre@eolshiftsenabled% @@ -1472,26 +1372,28 @@ % there are two different cases that have almost nothing in common : the case where there is something written under the bar, and the case where there is nothing. % first of all we need to calculate previousenddifference, begindifference, enddifference and nextbegindifference. #1% - \gre@calculate@textaligncenter{\gre@saved@syllable@endsyllablepart}{\gre@firstsyllablepart}{\gre@middlesyllablepart}{0}% + \gre@calculate@textaligncenter{\gre@firstsyllablepart}{\gre@middlesyllablepart}{0}% + \gre@attr@dash=4 \ifgre@showlyrics% - \setbox\gre@box@syllabletext=\hbox{% + \setbox\gre@box@syllabletext=\hbox attr \gre@attrid@part=4 {% \IfSubStr{\gre@debug}{,barspacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% {}% do nothing if not debugging - \gre@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@endsyllablepart}{#6}}% + \gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart}{#6}}% \IfSubStr{\gre@debug}{,barspacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% {}% do nothing if not debugging - }% + }% \else% \ifnum\gre@lyrics@phantomwrapper>0\relax - \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@endsyllablepart}{#6}}}}% + \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart}{#6}}}}% \else \setbox\gre@box@syllabletext=\box\voidb@x% \fi \fi% + \unsetattribute{\gre@attr@dash}% \gre@debugmsg{barspacing}{Width of bar text: \the\wd\gre@box@syllabletext}% \global\let\gre@saved@prelinedelay@newlinecommon\gre@newlinecommon % \global\let\gre@newlinecommon\gre@newlinecommondelayed % @@ -1513,7 +1415,7 @@ \def\gre@nextalteration{0}% \fi \fi - \gre@calculate@nextbegindifference{\gre@emit@endsyllablepartfornextsyllable}{\gre@evaluatenextsyllable{\gre@nextfirstsyllablepart}}{\gre@evaluatenextsyllable{\gre@nextmiddlesyllablepart}}{\gre@evaluatenextsyllable{\gre@nextendsyllablepart}}{\gre@nextalignment}{\gre@nextalteration}% + \gre@calculate@nextbegindifference{\gre@evaluatenextsyllable{\gre@nextfirstsyllablepart}}{\gre@evaluatenextsyllable{\gre@nextmiddlesyllablepart}}{\gre@evaluatenextsyllable{\gre@nextendsyllablepart}}{\gre@nextalignment}{\gre@nextalteration}% \gre@unsetfixednexttextformat % \gre@debugmsg{barspacing}{previousenddifference: \the\gre@dimen@previousenddifference}% \gre@debugmsg{barspacing}{begindifference: \the\gre@dimen@begindifference}% @@ -1546,7 +1448,7 @@ \GreNoBreak % %print the text, the raise is in case of a translation \raise\gre@space@dimen@spacebeneathtext - \hbox attr \gre@attrid@part=4 {\unhcopy\gre@box@syllabletext}% + \copy\gre@box@syllabletext %and the code which handles translation centering \ifgre@mustdotranslationcenterend% % case of end of translation centering, we do it after the typesetting of the text @@ -1785,7 +1687,6 @@ %and that's it !! \fi % \fi% - \gre@push@endsyllable{#6}\relax % \global\gre@dimen@notesaligncenter= 0 pt\relax % very important, see flat and natural \gre@unsetfixedtextformat % \ifgre@blockeolcustos\ifnum\gre@insidediscretionary=0\relax % diff --git a/tex/gregoriotex.lua b/tex/gregoriotex.lua index 0b9ad938..66aa4468 100644 --- a/tex/gregoriotex.lua +++ b/tex/gregoriotex.lua @@ -52,6 +52,7 @@ local rule = node.id('rule') local whatsit = node.id('whatsit') local rule = node.id('rule') local disc = node.id('disc') +local temp = node.id('temp') local subtype_lineskip, subtype_baselineksip for i, t in ipairs(node.subtypes('glue')) do @@ -61,6 +62,9 @@ for i, t in ipairs(node.subtypes('glue')) do end local hyphen = tex.defaulthyphenchar or 45 +local dash_node = node.new(glyph, 0) +dash_node.font = 0 +dash_node.char = hyphen local part_attr = luatexbase.attributes['gre@attr@part'] local part_commentary = 1 @@ -73,9 +77,13 @@ local part_nabc = 7 local part_blnabc = 8 local part_annotation = 9 +local score_attr = luatexbase.attributes['gre@attr@score'] + local dash_attr = luatexbase.attributes['gre@attr@dash'] -local potentialdashvalue = 1 -local nopotentialdashvalue = 2 +local dash_maybedash = 1 +local dash_hasdash = 2 +local dash_endofword = 3 +local dash_barsyllable = 4 local center_attr = luatexbase.attributes['gre@attr@center'] local startcenter = 1 @@ -399,49 +407,40 @@ local function init(arg) new_first_alterations = {} end --- node factory -local tmpnode = node.new(glyph, 0) -tmpnode.font = 0 -tmpnode.char = hyphen -local function gethyphennode() - return copy(tmpnode) -end - -local function getdashnnode() - local hyphnode = gethyphennode() - local dashnode = hpack(hyphnode) - dashnode.shift = 0 - return dashnode,hyphnode -end - -- a simple (for now) function to dump nodes for debugging local function dump_nodes_helper(head, indent) local dots = string.rep('..', indent) for n in traverse(head) do - local ids = format("g=%s,%s,a=%s,%s", - has_attribute(n, glyph_top_attr), - has_attribute(n, glyph_bottom_attr), - has_attribute(n, alteration_pitch_attr), - has_attribute(n, alteration_id_attr)) + local type = node.type(n.id) + local subtype + if node.subtypes(n.id) ~= nil then + subtype = node.subtypes(n.id)[n.subtype] + end + local attrs = format("syllable=%s,part=%s,dash=%s", + has_attribute(n, syllable_id_attr), + has_attribute(n, part_attr), + has_attribute(n, dash_attr)) if n.id == hlist or n.id == vlist then - log(dots .. "%s [%s] width=%.2fpt height=%.2fpt depth=%.2fpt shift=%.2fpt {%s}", node.type(n.id), n.subtype, n.width/2^16, n.height/2^16, n.depth/2^16, n.shift/2^16, ids) + log(dots .. "%s [%s] width=%.2fpt height=%.2fpt depth=%.2fpt shift=%.2fpt {%s}", type, subtype, n.width/2^16, n.height/2^16, n.depth/2^16, n.shift/2^16, attrs) elseif n.id == rule then - log(dots .. "rule [%s] width=%.2fpt height=%.2fpt depth=%.2fpt", n.subtype, n.width/2^16, n.height/2^16, n.depth/2^16) - elseif n.id == whatsit and n.subtype == user_defined_subtype and n.user_id == marker_whatsit_id then + log(dots .. "rule [%s] width=%.2fpt height=%.2fpt depth=%.2fpt", subtype, n.width/2^16, n.height/2^16, n.depth/2^16) + elseif n.id == whatsit and subtype == user_defined_subtype and n.user_id == marker_whatsit_id then log(dots .. "marker-whatsit %s", n.value) elseif n.id == glue then - log(dots .. "glue [%s] width=%.2fpt", n.subtype, n.width/2^16) - elseif node.type(n.id) == 'penalty' then - log(dots .. "penalty %s {%s}", n.penalty, ids) + log(dots .. "glue [%s] width=%.2fpt", subtype, n.width/2^16) + elseif n.id == kern then + log(dots .. "kern [%s] kern=%.2fpt", subtype, n.kern/2^16) + elseif type == 'penalty' then + log(dots .. "penalty %s {%s}", n.penalty, attrs) elseif n.id == glyph then local f = font.fonts[n.font] local charname for k, v in pairs(f.resources.unicodes) do if v == n.char then charname = k end end - log(dots .. "glyph %s {%s}", charname, ids) + log(dots .. "glyph %s {%s}", charname, attrs) else - log(dots .. "node %s [%s] {%s}", node.type(n.id), n.subtype, ids) + log(dots .. "node %s [%s] {%s}", node.type(n.id), subtype, attrs) end if n.id == hlist or n.id == vlist then dump_nodes_helper(n.head, indent+1) @@ -951,19 +950,64 @@ local function adjust_additional_spaces(line, info, linenum) end end --- in each function we check if we really are inside a score, --- which we can see with the dash_attr being set or not +local function ligaturing(head) + gregoriotex.save_syllable_texts(head) + head = node.ligaturing(head) + return head +end + +local function pre_linebreak(head) + head = gregoriotex.syllable_rewriting(head) + return head +end + +local function add_dash(line) + -- Add an end-of-line dash to line, if necessary. + + local last_text, last_text_with_glyph, last_glyph + + -- Look for the last text node in the line that has + -- dash_attr. If it is dash_maybedash, then we may need to + -- append a dash. Due to syllable rewriting, the actual text + -- may be in a node further to the left. So, we also look for + -- the last text node that actually contains a glyph, and the + -- last glyph in that node. + + for n in traverse_id(hlist, line.head) do + if has_attribute(n, dash_attr) then + last_text = n + for g in node.traverse_id(glyph, n.head) do + last_text_with_glyph = n + last_glyph = g + end + end + end + + if last_text and + has_attribute(last_text, dash_attr, dash_maybedash) and + last_glyph and + -- don't add a dash if there already is one + not (last_glyph.char == hyphen or last_glyph.char == 45) then + + local g = copy(dash_node) + g.font = last_glyph.font + local h = hpack(g) + h.shift = 0 + + insert_after(last_text_with_glyph.head, last_glyph, h) + end +end + local function post_linebreak(h, groupcode, glyphes) --dump_nodes(h) -- TODO: to be changed according to the font - local lastseennode = nil local centerstartnode = nil local linenum = 0 local syl_id = nil -- we explore the lines for line in traverse(h) do - if line.id == hlist and has_attribute(line, dash_attr) then + if line.id == hlist and has_attribute(line, score_attr) then linenum = linenum + 1 debugmessage('linesglues', 'line %d: %s factor %.0f%%', linenum, glue_sign_name[line.glue_sign], line.glue_set*100) centerstartnode = nil @@ -986,7 +1030,7 @@ local function post_linebreak(h, groupcode, glyphes) end end end - + -- Line height adjustment. if tex.count['gre@variableheightexpansion'] == 0 then -- uniform local info @@ -1046,37 +1090,8 @@ local function post_linebreak(h, groupcode, glyphes) -- Look for words that are broken across lines and insert a hyphen for line in traverse_id(hlist, h) do - if has_attribute(line, dash_attr) then - -- Look for the last node that has dash_attr > 0 - local adddash=false - for n in traverse_id(hlist, line.head) do - -- If a syllable is not word-final, it may need a dash if it - -- ends up being line-final. - -- Note: This also loops over translations, but translations - -- come before lyrics, so they should never become lastseennode - if has_attribute(n, dash_attr, potentialdashvalue) then - adddash=true - lastseennode=n - -- if we encounter a text that doesn't need a dash, we acknowledge it - elseif has_attribute(n, dash_attr, nopotentialdashvalue) then - adddash=false - end - end - - -- If the last syllable needed a dash, add it - if adddash then - local lastglyph - -- we traverse the list, to detect the font to use, - -- and also not to add an hyphen if there is already one - for g in node.traverse_id(glyph, lastseennode.head) do - lastglyph = g - end - if not (lastglyph.char == hyphen or lastglyph.char == 45) then - local dashnode, hyphnode = getdashnnode() - hyphnode.font = lastglyph.font - insert_after(lastseennode.head, lastglyph, dashnode) - end - end + if has_attribute(line, score_attr) then + add_dash(line) end end @@ -1157,6 +1172,20 @@ local function get_score_font_unicode_pairs(name) return pairs(unicodes) end +local function add_callbacks() + luatexbase.add_to_callback('post_linebreak_filter', post_linebreak, 'gregoriotex.post_linebreak', 1) + luatexbase.add_to_callback('hyphenate', disable_hyphenation, 'gregoriotex.disable_hyphenation', 1) + luatexbase.add_to_callback('ligaturing', ligaturing, 'gregoriotex.ligaturing') + luatexbase.add_to_callback('pre_linebreak_filter', pre_linebreak, 'gregoriotex.pre_linebreak', 1) +end + +local function remove_callbacks() + luatexbase.remove_from_callback('post_linebreak_filter', 'gregoriotex.post_linebreak') + luatexbase.remove_from_callback('hyphenate', 'gregoriotex.disable_hyphenation') + luatexbase.remove_from_callback('ligaturing', 'gregoriotex.ligaturing') + luatexbase.remove_from_callback('pre_linebreak_filter', 'gregoriotex.pre_linebreak') +end + local inside_score = false --- Start a score -- Prepare all variables for processing a new score and add our callbacks @@ -1191,19 +1220,19 @@ local function at_score_beginning(score_id) new_score_first_alterations = {} new_first_alterations[score_id] = new_score_first_alterations end + saved_syllable_texts = {} - luatexbase.add_to_callback('post_linebreak_filter', post_linebreak, 'gregoriotex.post_linebreak', 1) - luatexbase.add_to_callback("hyphenate", disable_hyphenation, "gregoriotex.disable_hyphenation", 1) + add_callbacks() end --- Finish a score -- Reset variables to out of score state and remove our callbacks local function at_score_end() inside_score = false - luatexbase.remove_from_callback('post_linebreak_filter', 'gregoriotex.post_linebreak') - luatexbase.remove_from_callback("hyphenate", "gregoriotex.disable_hyphenation") + remove_callbacks() per_line_dims = {} per_line_counts = {} + gregoriotex.free_saved_syllable_texts() end --- Toggle the state of GregorioTeX callbacks. @@ -1213,11 +1242,9 @@ end local function fancyhdr_toggle_callbacks() if inside_score then if luatexbase.is_active_callback('post_linebreak_filter','gregoriotex.post_linebreak') then - luatexbase.remove_from_callback('post_linebreak_filter', 'gregoriotex.post_linebreak') - luatexbase.remove_from_callback("hyphenate", "gregoriotex.disable_hyphenation") + remove_callbacks() else - luatexbase.add_to_callback('post_linebreak_filter', post_linebreak, 'gregoriotex.post_linebreak', 1) - luatexbase.add_to_callback("hyphenate", disable_hyphenation, "gregoriotex.disable_hyphenation", 1) + add_callbacks() end end end @@ -1902,6 +1929,11 @@ local function mode_part(part) end end +-- this function is meant to be called from Lua +local function is_last_syllable_id_on_line(sid) + return not score_last_syllables or score_last_syllables[sid] +end + -- this function is meant to be used from \ifcase; prints 0 for true and 1 for false local function is_last_syllable_on_line() if score_last_syllables then @@ -1978,7 +2010,11 @@ gregoriotex.change_next_score_line_count = change_next_score_line_count gregoriotex.set_base_output_dir = set_base_output_dir gregoriotex.is_first_alteration = is_first_alteration gregoriotex.fancyhdr_toggle_callbacks = fancyhdr_toggle_callbacks +gregoriotex.get_if = get_if +gregoriotex.is_last_syllable_id_on_line = is_last_syllable_id_on_line +gregoriotex.dump_nodes = dump_nodes dofile(kpse.find_file('gregoriotex-nabc.lua', 'lua')) dofile(kpse.find_file('gregoriotex-signs.lua', 'lua')) +dofile(kpse.find_file('gregoriotex-syllable.lua', 'lua')) dofile(kpse.find_file('gregoriotex-symbols.lua', 'lua')) From 4fa508d2a27c519f07bc00ff0f5a756de970e140 Mon Sep 17 00:00:00 2001 From: David Chiang Date: Wed, 18 Feb 2026 20:10:26 -0500 Subject: [PATCH 02/17] Recalculate syllablefinalskip in Lua. The syllablefinalskip is still calculated in TeX for now, because it's needed in a couple of places, but \GreSyllable only generates a zero-width \hskip, and the Lua code recalculates it and resizes the \hskip accordingly. The Lua syllablefinalskip calculation uses the actual begindifference of the next syllable, not the predicted one (nextbegindifference). In the old code, sometimes this prediction was wrong (#1723). Therefore this commit breaks several tests, but I believe they are all actually bug fixes. --- tex/gregoriotex-main.tex | 15 ++- tex/gregoriotex-syllable.lua | 252 +++++++++++++++++++++++++++++------ tex/gregoriotex-syllable.tex | 59 ++++++-- tex/gregoriotex.lua | 29 ++-- 4 files changed, 287 insertions(+), 68 deletions(-) diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index 7f0e58e8..fc2f8680 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -52,6 +52,7 @@ % an attribute to mark various parts of the score % 1 = commentary, 2 = stafflines, 3 = initial % 4 = lyrics, 5 = translation, 6 = alt, 7 = nabc, 8 = nabc below +% 9 = annotation, 10 = notes \newattribute\gre@attr@part \edef\gre@attrid@part{\the\allocationnumber} @@ -72,6 +73,13 @@ % attribute for syllable tracking \newattribute\gre@attr@syllable@id +% attributes for horizontal spacing +% 1 = syllablefinalskip between \GreSyllables +% 2 = \GreBarSyllable from syllable left to text left +% 3 = \GreBarSyllable from text right to bar left +% 4 = \GreBarSyllable from bar right to syllable right (to do) +\newattribute\gre@attr@skip@type + % attributes for soft alterations % Alterations are numbered consecutively starting from 1. If an @@ -654,6 +662,7 @@ % #2: 0 = text, 1 = nabc \def\gre@typesettextabovelines#1#2{% \gre@trace{gre@typesettextabovelines{#1}{#2}}% + {% localize change to \gre@attr@part \ifnum#2=0\relax% \gre@attr@part=6\relax \else% @@ -685,7 +694,7 @@ \endgre@style@abovelinestext \hss}% \fi% - \unsetattribute{\gre@attr@part}% + }% restore \gre@attr@part \gre@trace@end% }% @@ -724,12 +733,10 @@ % Phase 2: Place voice 1 (above staff) \def\gre@nabc@place@voice@i{% - \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}% - \unsetattribute{\gre@attr@part}% + \leavevmode\raise\gre@dimen@temp@five\hbox attr \gre@attrid@part=7 to 0pt{\unhbox\gre@box@nabc@voice@i\hss}% }% % Phase 2: Place voice 2 (below staff) diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index 5ae44b10..2a67cf45 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -30,17 +30,80 @@ local debugmessage = gregoriotex.module.debugmessage local has_attribute = node.has_attribute local kern = node.id('kern') local temp = node.id('temp') +local disc = node.id('disc') local syllable_id_attr = luatexbase.attributes['gre@attr@syllable@id'] local part_attr = luatexbase.attributes['gre@attr@part'] local part_lyrics = 4 +local part_notes = 10 + +local skip_type_attr = luatexbase.attributes['gre@attr@skip@type'] +local skip_type_syllablefinal = 1 +local skip_type_barspacing1 = 2 local dash_attr = luatexbase.attributes['gre@attr@dash'] local dash_hasdash = 2 local dash_barsyllable = 4 -local saved_syllable_texts = {} +-- Functions for manipulating glue, which we just store as a 3-tuple +-- {width, stretch, shrink} in sp. + +local function glue_to_string(g) + if g == nil then + return 'nil' + elseif type(g) == 'number' then + return string.format('%.5fpt', g/2^16) + else + if type(g) == 'userdata' then -- glue or glue_spec node + g = {g.width, g.stretch, g.shrink} + end + local s = string.format('%.5fpt', g[1]/2^16) + if g[2] ~= 0 then s = s .. string.format(' plus %.5fpt', g[2]/2^16) end + if g[3] ~= 0 then s = s .. string.format(' minus %.5fpt', g[3]/2^16) end + return s + end +end + +local function string_to_glue(s) + local stretch = 0 + local shrink = 0 + local i, j + i, j = string.find(s, 'minus', 1, true) + if i ~= nil then + shrink = tex.sp(s:sub(j+1)) + s = s:sub(1, i-1) + end + i, j = string.find(s, 'plus', 1, true) + if i ~= nil then + stretch = tex.sp(s:sub(j+1)) + s = s:sub(1, i-1) + end + local width = tex.sp(s) + return {width, stretch, shrink} +end + +local function dimen_to_glue(dimen) + return {dimen, 0, 0} +end + +local function glue_max(a, b) + -- If the natural widths are equal, return a. + if type(a) == 'number' then a = dimen_to_glue(a) end + if type(b) == 'number' then b = dimen_to_glue(b) end + if a[1] > b[1] then return a else return b end +end + +local function glue_add(a, b) + if type(a) == 'number' then a = dimen_to_glue(a) end + if type(b) == 'number' then b = dimen_to_glue(b) end + return {a[1]+b[1], a[2]+b[2], a[3]+b[3]} +end + +-- Table for storing information about syllables that is impossible or +-- inconvenient to recover from node attributes. +local saved_syllables = {} + local function save_syllable_texts(head) -- Save syllable texts before ligaturing and kerning happens. This -- is needed later during syllable rewriting. @@ -51,14 +114,24 @@ local function save_syllable_texts(head) local sid = tex.getattribute(syllable_id_attr) local cur = head while cur ~= nil and cur.id == temp do cur = cur.next end - saved_syllable_texts[sid] = node.copy_list(cur) + if saved_syllables[sid] == nil then saved_syllables[sid] = {} end + saved_syllables[sid].text = node.copy_list(cur) end end -local function free_saved_syllable_texts() - for sid, head in pairs(saved_syllable_texts) do - node.flush_list(head) - saved_syllable_texts[sid] = nil +local function save_min_distances() + local sid = tex.getattribute(syllable_id_attr) + if saved_syllables[sid] == nil then saved_syllables[sid] = {} end + local g = tex.skip['gre@skip@minTextDistance'] + saved_syllables[sid].min_text_distance = {g.width, g.stretch, g.shrink} + g = tex.skip['gre@skip@minNotesDistance'] + saved_syllables[sid].min_notes_distance = {g.width, g.stretch, g.shrink} +end + +local function free_saved_syllables() + for sid, syl in pairs(saved_syllables) do + node.flush_list(syl.text) + saved_syllables[sid] = nil end end @@ -84,50 +157,148 @@ local function shaping(head) return head end -local function syllable_rewriting(head) - --gregoriotex.dump_nodes(head) - if not gregoriotex.get_if('gre@rewritesyllables') then return head end - +local function scan_syllables(head) + -- Find nodes corresponding to various parts of syllables and store them in a + -- data structure more convenient for downstream processing. local syllables = {} - local last_sid = nil - for n in node.traverse(head) do - -- This skips over discretionary nodes, which can't participate in syllable rewriting - local sid, part = has_attribute(n, syllable_id_attr), has_attribute(n, part_attr) - if sid ~= nil and part == part_lyrics then - if syllables[sid] ~= nil then - err('syllable %d has more than one text node', sid) + local prev_sid = 0 + local function visit(head) + for n in node.traverse(head) do + -- to do: The two syllables in a discretionary are numbered + -- differently, meaning that in the output, the syllables are + -- not necessarily numbered consecutively. + if n.id == disc then + visit(n.pre) + visit(n.post) + visit(n.replace) + else + local sid = has_attribute(n, syllable_id_attr) + local part = has_attribute(n, part_attr) + local skip_type = has_attribute(n, skip_type_attr) + if sid ~= nil then + while prev_sid < sid do + prev_sid = prev_sid+1 + syllables[prev_sid] = {} + if saved_syllables[prev_sid] == nil then saved_syllables[prev_sid] = {} end + end + if part == part_lyrics then + if syllables[sid].text ~= nil then + err(' syllable %d has more than one text node', sid) + end + syllables[sid].text = n + elseif part == part_notes then + if syllables[sid].first_note == nil then + syllables[sid].first_note = n + end + syllables[sid].last_note = n + elseif skip_type == skip_type_syllablefinal then + syllables[sid].syllablefinalskip = n + elseif skip_type == skip_type_barspacing1 then + syllables[sid].barspacing1 = n + end + end end - debugmessage('syllablerewriting', 'syllable %d has text node', sid) - syllables[sid] = {} - syllables[sid].text_node = n - last_sid = sid end end + visit(head) + return syllables +end + +local function syllable_spacing(syllables) - if last_sid == nil then return head end + -- Compute begin_difference and end_difference of each syllable (how + -- much the notes extend past the text to the left or right, + -- respectively) + for sid, cur in pairs(syllables) do + debugmessage('syllablespacing', 'after syllable %d', sid) + if cur.text and cur.first_note and cur.last_note then + -- The text comes first, then the notes + syllables[sid].begin_difference = -node.dimensions(cur.text, cur.first_note) + syllables[sid].end_difference = node.dimensions(cur.text.next, cur.last_note.next) + debugmessage('syllablespacing', 'begin difference = %s', glue_to_string(syllables[sid].begin_difference)) + debugmessage('syllablespacing', 'end difference = %s', glue_to_string(syllables[sid].end_difference)) + elseif cur.text then + -- Text, but no notes: arbitrarily place the empty "notes" at the left + -- edge of the text (it shouldn't matter) + syllables[sid].begin_difference = 0 + syllables[sid].end_difference = -cur.text.width + elseif cur.first_note and cur.last_note then + -- Notes, but no text (this normally shouldn't happen) + syllables[sid].begin_difference = 0 + syllables[sid].end_difference = -node.dimensions(cur.first_note, cur.last_note.next) + else + -- Neither notes nor text?! + syllables[sid].begin_difference = 0 + syllables[sid].end_difference = 0 + end + end + + for sid, cur in pairs(syllables) do + -- If the next syllable is a bar syllable, then this syllable + -- shouldn't have syllablefinalskip. But (due to a bug, #1724) + -- if the next syllable is a clef change, it is a bar syllable + -- and this syllable does have syllablefinalskip; we ignore it. + debugmessage('syllablespacing', 'after syllable %d', sid) + local next = syllables[sid+1] + if cur.syllablefinalskip and next ~= nil and not next.barspacing1 then + + local text_distance = math.max(0, cur.end_difference) + math.max(0, next.begin_difference) + debugmessage('syllablespacing', ' text distance = %s', glue_to_string(text_distance)) + local min_text_distance = saved_syllables[sid].min_text_distance + debugmessage('syllablespacing', ' min text distance = %s', glue_to_string(min_text_distance)) + local min_text_shift = glue_add(min_text_distance, -text_distance) + debugmessage('syllablespacing', ' min text shift = %s', glue_to_string(min_text_shift)) + + local notes_distance = math.max(0, -cur.end_difference) + math.max(0, -next.begin_difference) + debugmessage('syllablespacing', ' notes distance = %s', glue_to_string(notes_distance)) + local min_notes_distance = saved_syllables[sid].min_notes_distance + debugmessage('syllablespacing', ' min notes distance = %s', glue_to_string(min_notes_distance)) + local min_notes_shift = glue_add(min_notes_distance, -notes_distance) + debugmessage('syllablespacing', ' min notes shift = %s', glue_to_string(min_notes_shift)) + + local syllablefinalskip = {cur.syllablefinalskip.width, cur.syllablefinalskip.stretch, cur.syllablefinalskip.shrink} + -- Ensure that min text shift and min notes shift are satisfied. + syllablefinalskip = glue_add(syllablefinalskip, glue_max(min_text_shift, min_notes_shift)) + -- If this syllable has a hyphen, add some additional stretch. + -- Note: This happens even if there is no text (\gresetlyrics{invisible}). + if cur.text and has_attribute(cur.text, dash_attr, dash_hasdash) then + debugmessage('syllablespacing', ' adding stretch for hyphen') + syllablefinalskip = glue_add(syllablefinalskip, string_to_glue(token.get_macro('gre@space@skip@intersyllablespacestretchhyphen'))) + end + debugmessage('syllablespacing', ' syllable final skip = %s', glue_to_string(syllablefinalskip)) + node.setglue(cur.syllablefinalskip, table.unpack(syllablefinalskip)) + else + debugmessage('syllablespacing', ' no syllable final skip, not adjusting') + end + end +end + +local function syllable_rewriting(syllables) + if not gregoriotex.get_if('gre@rewritesyllables') then return end local start = 1 - while start <= last_sid do + local num_syllables = #syllables + while start <= num_syllables do -- Find longest run of syllables, starting from start, that have -- zero distance between their text boxes. - if syllables[start] == nil or syllables[start].text_node == nil then + if syllables[start].text == nil then debugmessage('syllablerewriting', 'syllable %d has no text node', start) start = start + 1 else local stop = start - while stop+1 <= last_sid do + while stop+1 <= num_syllables do -- There are several conditions that prevent syllable rewriting: -- if either text node is missing - if syllables[stop+1] == nil or syllables[stop+1].text_node == nil then break end + if syllables[stop+1].text == nil then break end -- don't rewrite across a line break if gregoriotex.is_last_syllable_id_on_line(stop) then break end -- don't rewrite across a hyphen - if has_attribute(syllables[stop].text_node, dash_attr, dash_hasdash) then break end + if has_attribute(syllables[stop].text, dash_attr, dash_hasdash) then break end -- if either syllable is a \GreBarSyllable - if has_attribute(syllables[stop].text_node, dash_attr, dash_barsyllable) or - has_attribute(syllables[stop+1].text_node, dash_attr, dash_barsyllable) then break end + if has_attribute(syllables[stop].text, dash_attr, dash_barsyllable) or + has_attribute(syllables[stop+1].text, dash_attr, dash_barsyllable) then break end -- don't rewrite across a nonzero space - if node.dimensions(syllables[stop].text_node.next, syllables[stop+1].text_node) ~= 0 then break end + if node.dimensions(syllables[stop].text.next, syllables[stop+1].text) ~= 0 then break end stop = stop + 1 end -- Concatenate syllable text boxes into one box. @@ -136,34 +307,35 @@ local function syllable_rewriting(head) local head, tail for sid = start, stop do -- Extend new text - local n = saved_syllable_texts[sid] - saved_syllable_texts[sid] = nil + local n = saved_syllables[sid].text + saved_syllables[sid].text = nil head, tail = concat_list(head, tail, n, node.tail(n)) end head = shaping(head) for sid = start, stop do -- Rewrite text, inserting kerns to preserve widths - local del = syllables[sid].text_node.head - syllables[sid].text_node.head = nil + local del = syllables[sid].text.head + syllables[sid].text.head = nil node.flush_list(del) local kern = node.new(kern, 'userkern') - kern.kern = syllables[sid].text_node.width + kern.kern = syllables[sid].text.width if sid == start then - syllables[sid].text_node.head = head + syllables[sid].text.head = head kern.kern = kern.kern - node.dimensions(head) concat_list(head, tail, kern) else - syllables[sid].text_node.head = kern + syllables[sid].text.head = kern end end end start = stop + 1 end end - --gregoriotex.dump_nodes(head) - return head end gregoriotex.save_syllable_texts = save_syllable_texts -gregoriotex.free_saved_syllable_texts = free_saved_syllable_texts +gregoriotex.save_min_distances = save_min_distances +gregoriotex.free_saved_syllables = free_saved_syllables +gregoriotex.scan_syllables = scan_syllables +gregoriotex.syllable_spacing = syllable_spacing gregoriotex.syllable_rewriting = syllable_rewriting diff --git a/tex/gregoriotex-syllable.tex b/tex/gregoriotex-syllable.tex index 0ceff383..5149f77e 100644 --- a/tex/gregoriotex-syllable.tex +++ b/tex/gregoriotex-syllable.tex @@ -555,7 +555,7 @@ \setbox\gre@box@syllablenotes=\hbox{\gre@notes@rendernabc@tight{#1}}% \gre@compute@nabc@extrakern \else - \setbox\gre@box@syllablenotes=\box\voidb@x% + \setbox\gre@box@syllablenotes=\hbox{}% \fi \fi \fi% @@ -1009,8 +1009,9 @@ \gre@calculate@nextbegindifference{\gre@evaluatenextsyllable{\gre@nextfirstsyllablepart}}{\gre@evaluatenextsyllable{\gre@nextmiddlesyllablepart}}{\gre@evaluatenextsyllable{\gre@nextendsyllablepart}}{\gre@nextalignment}{\gre@nextalteration}% \gre@unsetfixednexttextformat % \gre@attr@dash=3 + \gre@attr@part=4 \ifgre@showlyrics% - \setbox\gre@box@syllabletext=\hbox attr \gre@attrid@part=4 {% + \setbox\gre@box@syllabletext=\hbox{% \IfSubStr{\gre@debug}{,notespacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% @@ -1025,10 +1026,11 @@ \ifnum\gre@lyrics@phantomwrapper>0\relax \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart}{#6}}}}% \else - \setbox\gre@box@syllabletext=\box\voidb@x% + \setbox\gre@box@syllabletext=\hbox{}% \fi \fi% \unsetattribute{\gre@attr@dash}% + \unsetattribute{\gre@attr@part}% \gre@calculate@enddifference{\wd\gre@box@syllablenotes}{\wd\gre@box@syllabletext}{\gre@dimen@textaligncenter}{\gre@dimen@notesaligncenter}{1}% % gre@count@temp@one holds 1 if next is a bar, 2 if an alteration, else 0 \gre@count@temp@one=0% @@ -1067,8 +1069,9 @@ % if it's the last syllable of line, the hyphen will be \GreHyph \ifnum\gre@lastoflinecount=1\relax % \gre@attr@dash=2 + \gre@attr@part=4 \ifgre@showlyrics% - \setbox\gre@box@syllabletext=\hbox attr \gre@attrid@part=4 {% + \setbox\gre@box@syllabletext=\hbox{% \IfSubStr{\gre@debug}{,notespacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% @@ -1083,14 +1086,16 @@ \ifnum\gre@lyrics@phantomwrapper>0\relax \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart#3{\GreHyph}\relax}{#6}}}}% \else - \setbox\gre@box@syllabletext=\box\voidb@x% + \setbox\gre@box@syllabletext=\hbox{}% \fi \fi% \unsetattribute{\gre@attr@dash}% + \unsetattribute{\gre@attr@part}% \else % \gre@attr@dash=2 + \gre@attr@part=4 \ifgre@showlyrics% - \setbox\gre@box@syllabletext=\hbox attr \gre@attrid@part=4 {% + \setbox\gre@box@syllabletext=\hbox{% \IfSubStr{\gre@debug}{,notespacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% @@ -1105,10 +1110,11 @@ \ifnum\gre@lyrics@phantomwrapper>0\relax \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart#3{-}}{#6}}}}% \else - \setbox\gre@box@syllabletext=\box\voidb@x% + \setbox\gre@box@syllabletext=\hbox{}% \fi \fi% \unsetattribute{\gre@attr@dash}% + \unsetattribute{\gre@attr@part}% \fi % % recomputing end difference and final skip with the final hyphen \gre@calculate@nextbegindifference{\gre@evaluatenextsyllable{\gre@nextfirstsyllablepart}}{\gre@evaluatenextsyllable{\gre@nextmiddlesyllablepart}}{\gre@evaluatenextsyllable{\gre@nextendsyllablepart}}{\gre@nextalignment}{\gre@nextalteration}% @@ -1119,9 +1125,10 @@ \global\gre@possibleluahyphenafterthissyllabletrue % \gre@debugmsg{hyphen}{No hyphen}% \gre@attr@dash=1 % if not the end of a word and we haven't added a dash, we set potential dash to 1 + \gre@attr@part=4 % we rebuild this box, in order it to have the attribute \ifgre@showlyrics% - \setbox\gre@box@syllabletext=\hbox attr \gre@attrid@part=4 {% + \setbox\gre@box@syllabletext=\hbox{% \IfSubStr{\gre@debug}{,notespacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% @@ -1133,9 +1140,10 @@ {}% do nothing if not debugging }% \else% - \setbox\gre@box@syllabletext=\box\voidb@x% + \setbox\gre@box@syllabletext=\hbox{}% \fi% \unsetattribute{\gre@attr@dash}% + \unsetattribute{\gre@attr@part}% \else % \global\gre@possibleluahyphenafterthissyllablefalse % \fi % @@ -1163,13 +1171,14 @@ \kern\gre@skip@temp@one % % here we need to unset \gre@attr@dash for the typesetting of notes \unsetattribute{\gre@attr@dash}% + \gre@attr@part=10 \GreNoBreak % no line breaks between text and notes \ifgre@shownotes% \IfSubStr{\gre@debug}{,notespacing,}% % when debugging we add a zero-width line to mark the syllable bound {\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 + #9% we do that instead of \unhbox\gre@syllablenotes, because it would not set the \localrightbox % Apply per-syllable NABC kern computed in \gre@syllablenotes. \ifdim\gre@dimen@nabc@extrakern>0pt\relax \kern\gre@dimen@nabc@extrakern @@ -1196,6 +1205,7 @@ \fi \fi \fi% + \unsetattribute{\gre@attr@part}% \GreNoBreak % no line breaks between notes and end of syllable skips \gre@debugmsg{ifdim}{ enddifference < 0pt}% \ifdim\gre@dimen@enddifference <0pt\relax% @@ -1315,14 +1325,14 @@ %the new bar spacing algorithms take care of this for us when the next syllable is a bar \relax% \else% - \gre@hskip\gre@skip@syllablefinalskip\relax% + \gre@syllablefinalskip \fi% \ifnum#3=1\relax % \GreNoBreak % \else% \ifgre@newbarspacing% %the new bar spacing algorithm still needs the syllablefinalskip when the next syllable is not a bar - \gre@hskip\gre@skip@syllablefinalskip\relax% + \gre@syllablefinalskip \fi% \fi % \fi % @@ -1330,6 +1340,15 @@ \gre@trace@end% }% +\def\gre@syllablefinalskip{% + % Save the computed minNotesDistance and minTextDistance + \directlua{gregoriotex.save_min_distances()}% + % Emit a zero-width skip, which will be resized in the Lua pre-linebreak filter. + {\gre@attr@skip@type=1 + \gre@hskip0pt + }% +} + \def\gresetbarspacing#1{% \IfStrEqCase{#1}{% {new}% @@ -1374,8 +1393,9 @@ #1% \gre@calculate@textaligncenter{\gre@firstsyllablepart}{\gre@middlesyllablepart}{0}% \gre@attr@dash=4 + \gre@attr@part=4 \ifgre@showlyrics% - \setbox\gre@box@syllabletext=\hbox attr \gre@attrid@part=4 {% + \setbox\gre@box@syllabletext=\hbox{% \IfSubStr{\gre@debug}{,barspacing,}% % when debugging we add a zero-width line to mark the syllable bound {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% @@ -1390,10 +1410,11 @@ \ifnum\gre@lyrics@phantomwrapper>0\relax \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart}{#6}}}}% \else - \setbox\gre@box@syllabletext=\box\voidb@x% + \setbox\gre@box@syllabletext=\hbox{}% \fi \fi% \unsetattribute{\gre@attr@dash}% + \unsetattribute{\gre@attr@part}% \gre@debugmsg{barspacing}{Width of bar text: \the\wd\gre@box@syllabletext}% \global\let\gre@saved@prelinedelay@newlinecommon\gre@newlinecommon % \global\let\gre@newlinecommon\gre@newlinecommondelayed % @@ -1437,11 +1458,13 @@ \fi% \GreNoBreak % %move to the beginning of the text + \gre@attr@skip@type=2 \gre@hskip\glueexpr(\gre@skip@bar@allocation/2% right from end of previous notes to nominal middle of bar line +\gre@dimen@bar@shift% from nominal middle of bar line to actual middle -\wd\gre@box@syllablenotes/2% back up to beginning of bar line +\gre@dimen@begindifference% from beginning of bar line to beginning of text +\gre@space@skip@bar@rubber)\relax % the rubber component + \unsetattribute{\gre@attr@skip@type}% \GreNoBreak % % all that extra stuff (translations and the like) #8% @@ -1456,9 +1479,12 @@ \gre@mustdotranslationcenterendfalse% \fi % %move back to the beginning of the bar line + \gre@attr@skip@type=2 \kern\dimexpr(\gre@dimen@enddifference% move from end of text to end of bar line -\wd\gre@box@syllablenotes)\relax % back up from end of bar line to beginning + \unsetattribute{\gre@attr@skip@type}% \GreNoBreak% + \gre@attr@part=10 \ifgre@shownotes% \IfSubStr{\gre@debug}{,barspacing,}% % when debugging we add a zero-width line to mark the syllable bound @@ -1487,6 +1513,7 @@ \fi \fi \fi% + \unsetattribute{\gre@attr@part}% \global\let\gre@newlinecommon\gre@saved@prelinedelay@newlinecommon % \GreNoBreak% % get into position to place the penalty @@ -1621,9 +1648,11 @@ \gre@dotranslationcenterend % \gre@mustdotranslationcenterendfalse% \fi % + \gre@attr@part=10 \ifgre@shownotes% #9\relax % \fi% + \unsetattribute{\gre@attr@part}% \gre@penalty{\the\gre@space@count@endafterbaraltpenalty }% TODO: isn't it a bit buggy? % end of same code as syllable \ifnum\gre@lastoflinecount=1\relax % @@ -1667,9 +1696,11 @@ \kern\gre@skip@temp@one % \gre@skip@temp@one = -\gre@dimen@begindifference\relax% \kern\gre@skip@temp@one % + \gre@attr@part=10 \ifgre@shownotes% #9% \fi% + \unsetattribute{\gre@attr@part}% \gre@debugmsg{ifdim}{ enddifference < 0pt}% \ifdim\gre@dimen@enddifference <0pt\relax% %% important, else we are not really at the end of the syllable diff --git a/tex/gregoriotex.lua b/tex/gregoriotex.lua index 66aa4468..965e4b2f 100644 --- a/tex/gregoriotex.lua +++ b/tex/gregoriotex.lua @@ -66,6 +66,9 @@ local dash_node = node.new(glyph, 0) dash_node.font = 0 dash_node.char = hyphen +local score_attr = luatexbase.attributes['gre@attr@score'] +local syllable_id_attr = luatexbase.attributes['gre@attr@syllable@id'] + local part_attr = luatexbase.attributes['gre@attr@part'] local part_commentary = 1 local part_stafflines = 2 @@ -77,7 +80,7 @@ local part_nabc = 7 local part_blnabc = 8 local part_annotation = 9 -local score_attr = luatexbase.attributes['gre@attr@score'] +local skip_type_attr = luatexbase.attributes['gre@attr@skip@type'] local dash_attr = luatexbase.attributes['gre@attr@dash'] local dash_maybedash = 1 @@ -96,8 +99,6 @@ local alteration_type_attr = luatexbase.attributes['gre@attr@alteration@type'] local alteration_pitch_attr = luatexbase.attributes['gre@attr@alteration@pitch'] local alteration_id_attr = luatexbase.attributes['gre@attr@alteration@id'] -local syllable_id_attr = luatexbase.attributes['gre@attr@syllable@id'] - local cur_score_id = nil local score_inclusion = {} local saved_positions = nil @@ -416,10 +417,9 @@ local function dump_nodes_helper(head, indent) if node.subtypes(n.id) ~= nil then subtype = node.subtypes(n.id)[n.subtype] end - local attrs = format("syllable=%s,part=%s,dash=%s", + local attrs = format("syllable=%s,part=%s", has_attribute(n, syllable_id_attr), - has_attribute(n, part_attr), - has_attribute(n, dash_attr)) + has_attribute(n, part_attr)) if n.id == hlist or n.id == vlist then log(dots .. "%s [%s] width=%.2fpt height=%.2fpt depth=%.2fpt shift=%.2fpt {%s}", type, subtype, n.width/2^16, n.height/2^16, n.depth/2^16, n.shift/2^16, attrs) elseif n.id == rule then @@ -427,9 +427,9 @@ local function dump_nodes_helper(head, indent) elseif n.id == whatsit and subtype == user_defined_subtype and n.user_id == marker_whatsit_id then log(dots .. "marker-whatsit %s", n.value) elseif n.id == glue then - log(dots .. "glue [%s] width=%.2fpt", subtype, n.width/2^16) + log(dots .. "glue [%s] width=%.2fpt {%s}", subtype, n.width/2^16, attrs) elseif n.id == kern then - log(dots .. "kern [%s] kern=%.2fpt", subtype, n.kern/2^16) + log(dots .. "kern [%s] kern=%.2fpt {%s}", subtype, n.kern/2^16, attrs) elseif type == 'penalty' then log(dots .. "penalty %s {%s}", n.penalty, attrs) elseif n.id == glyph then @@ -445,6 +445,11 @@ local function dump_nodes_helper(head, indent) if n.id == hlist or n.id == vlist then dump_nodes_helper(n.head, indent+1) elseif n.id == disc then + log(dots .. 'pre') + dump_nodes_helper(n.pre, indent+1) + log(dots .. 'post') + dump_nodes_helper(n.post, indent+1) + log(dots .. 'replace') dump_nodes_helper(n.replace, indent+1) end end @@ -957,7 +962,11 @@ local function ligaturing(head) end local function pre_linebreak(head) - head = gregoriotex.syllable_rewriting(head) + --dump_nodes(head) + local syllables = gregoriotex.scan_syllables(head) + if #syllables == 0 then return head end + gregoriotex.syllable_spacing(syllables) + gregoriotex.syllable_rewriting(syllables) return head end @@ -1232,7 +1241,7 @@ local function at_score_end() remove_callbacks() per_line_dims = {} per_line_counts = {} - gregoriotex.free_saved_syllable_texts() + gregoriotex.free_saved_syllables() end --- Toggle the state of GregorioTeX callbacks. From ec4e22c9bb2d3b90b96b656f90c2b254eb283a10 Mon Sep 17 00:00:00 2001 From: David Chiang Date: Sun, 8 Mar 2026 23:15:44 -0400 Subject: [PATCH 03/17] Continue working on syllable spacing in Lua: - Move syllable clearing for \GreSyllables into Lua, which works by directly measuring the distances between syllables. This fixes issue #1725 but also breaks tests/gabc-output/glyphs/clear.gabc because it now inserts no extra space (and never removes space). - Simplify syllable spacing by directly measuring distances between syllables, similarly to above. --- tex/gregoriotex-main.tex | 13 ++++++++ tex/gregoriotex-spaces.tex | 42 ++++-------------------- tex/gregoriotex-syllable.lua | 63 ++++++++++++++++++------------------ tex/gregoriotex-syllable.tex | 3 +- tex/gregoriotex.lua | 13 +++++--- 5 files changed, 61 insertions(+), 73 deletions(-) diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index fc2f8680..f51b23d4 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -78,6 +78,7 @@ % 2 = \GreBarSyllable from syllable left to text left % 3 = \GreBarSyllable from text right to bar left % 4 = \GreBarSyllable from bar right to syllable right (to do) +% 5 = clearsyllable \newattribute\gre@attr@skip@type % attributes for soft alterations @@ -261,6 +262,18 @@ \hfill \fi \gre@penalty{-10001}% + % Above, we emitted kerns of -enddifference and -eolshift early, + % and they will be emitted again later in \GreSyllable. The + % second set of kerns has no effect because they are at the + % beginning of a line. But in order that Lua pre_linebreak can + % measure distances accurately, we emit a third set of kerns to + % cancel out the second one. + \ifdim\gre@dimen@enddifference<0pt + \kern \gre@dimen@enddifference + \fi + \ifgre@eolshiftsenabled + \kern \gre@dimen@eolshift + \fi \fi \fi % %% diff --git a/tex/gregoriotex-spaces.tex b/tex/gregoriotex-spaces.tex index bd2eddf8..6a0dd967 100644 --- a/tex/gregoriotex-spaces.tex +++ b/tex/gregoriotex-spaces.tex @@ -1306,61 +1306,33 @@ }% % Clearing a syllable so it doesn't overlap with the previous one +% Only used for \GreBarSyllables \def\gre@clearsyllable#1{% \gre@trace{gre@clearsyllable{#1}}% - % because the way mora shifts are implemented is different for bars and - % notes, we have to use a different set of dimensions depending on which - % kind of syllable we're dealing with. We know this by looking at the - % argument of this function, which should be 'bar' for a bar syllable and 'note' - % for a note syllable. - \IfStrEq{#1}{bar}% - {% - \gre@debugmsg{clear}{adjustedpreviousenddifference = \the\gre@dimen@adjustedpreviousenddifference}% - \gre@dimen@temp@one=\gre@dimen@adjustedpreviousenddifference% - }% - {% - \gre@debugmsg{clear}{previousenddifference = \the\gre@dimen@previousenddifference}% - \gre@dimen@temp@one=\gre@dimen@previousenddifference% - }% + \gre@debugmsg{clear}{adjustedpreviousenddifference = \the\gre@dimen@adjustedpreviousenddifference}% + \gre@dimen@temp@one=\gre@dimen@adjustedpreviousenddifference% \gre@debugmsg{clear}{begindifference = \the\gre@dimen@begindifference}% - \gre@debugmsg{clear}{syllablefinalskip = \the\gre@skip@syllablefinalskip}% \ifdim\gre@dimen@temp@one > 0pt\relax% \ifdim\gre@dimen@begindifference < 0pt\relax% \ifdim\gre@dimen@temp@one > -\gre@dimen@begindifference\relax% - \gre@debugmsg{clear}{Case 1}% + \gre@debugmsg{clear}{Case 1: kern \the\dimexpr-\gre@dimen@begindifference\relax}% \kern -\gre@dimen@begindifference\relax% \else% - \gre@debugmsg{clear}{Case 2}% + \gre@debugmsg{clear}{Case 2: kern \the\gre@dimen@temp@one}% \kern \gre@dimen@temp@one\relax% \fi% - \IfStrEq{#1}{note}% - {% when dealing with notes we may have already skipped - % forward some, in which case we need to account for that - \ifdim\gre@skip@syllablefinalskip > 0pt\relax% - \gre@debugmsg{clear}{undo syllablefinalskip}% - \kern -\gre@skip@syllablefinalskip\relax% - \fi% - }{}% \else% \gre@debugmsg{clear}{Syllable already clear}% \fi% \else% \ifdim\gre@dimen@begindifference > 0pt\relax% \ifdim-\gre@dimen@temp@one > \gre@dimen@begindifference\relax% - \gre@debugmsg{clear}{Case 3}% + \gre@debugmsg{clear}{Case 3: kern \the\gre@dimen@begindifference}% \kern \gre@dimen@begindifference\relax% \else% - \gre@debugmsg{clear}{Case 4}% + \gre@debugmsg{clear}{Case 4: kern \the\dimexpr-\gre@dimen@temp@one\relax}% \kern -\gre@dimen@temp@one\relax% \fi% - \IfStrEq{#1}{note}% - {% when dealing with notes we may have already skipped - % forward some, in which case we need to account for that - \ifdim\gre@skip@syllablefinalskip > 0pt\relax% - \gre@debugmsg{clear}{undo syllablefinalskip}% - \kern -\gre@skip@syllablefinalskip\relax% - \fi% - }{}% \else% \gre@debugmsg{clear}{Syllable already clear}% \fi% diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index 2a67cf45..6862495f 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -41,6 +41,7 @@ local part_notes = 10 local skip_type_attr = luatexbase.attributes['gre@attr@skip@type'] local skip_type_syllablefinal = 1 local skip_type_barspacing1 = 2 +local skip_type_clearsyllable = 5 local dash_attr = luatexbase.attributes['gre@attr@dash'] local dash_hasdash = 2 @@ -195,6 +196,8 @@ local function scan_syllables(head) syllables[sid].syllablefinalskip = n elseif skip_type == skip_type_barspacing1 then syllables[sid].barspacing1 = n + elseif skip_type == skip_type_clearsyllable then + syllables[sid].clearsyllable = n end end end @@ -206,33 +209,6 @@ end local function syllable_spacing(syllables) - -- Compute begin_difference and end_difference of each syllable (how - -- much the notes extend past the text to the left or right, - -- respectively) - for sid, cur in pairs(syllables) do - debugmessage('syllablespacing', 'after syllable %d', sid) - if cur.text and cur.first_note and cur.last_note then - -- The text comes first, then the notes - syllables[sid].begin_difference = -node.dimensions(cur.text, cur.first_note) - syllables[sid].end_difference = node.dimensions(cur.text.next, cur.last_note.next) - debugmessage('syllablespacing', 'begin difference = %s', glue_to_string(syllables[sid].begin_difference)) - debugmessage('syllablespacing', 'end difference = %s', glue_to_string(syllables[sid].end_difference)) - elseif cur.text then - -- Text, but no notes: arbitrarily place the empty "notes" at the left - -- edge of the text (it shouldn't matter) - syllables[sid].begin_difference = 0 - syllables[sid].end_difference = -cur.text.width - elseif cur.first_note and cur.last_note then - -- Notes, but no text (this normally shouldn't happen) - syllables[sid].begin_difference = 0 - syllables[sid].end_difference = -node.dimensions(cur.first_note, cur.last_note.next) - else - -- Neither notes nor text?! - syllables[sid].begin_difference = 0 - syllables[sid].end_difference = 0 - end - end - for sid, cur in pairs(syllables) do -- If the next syllable is a bar syllable, then this syllable -- shouldn't have syllablefinalskip. But (due to a bug, #1724) @@ -241,16 +217,14 @@ local function syllable_spacing(syllables) debugmessage('syllablespacing', 'after syllable %d', sid) local next = syllables[sid+1] if cur.syllablefinalskip and next ~= nil and not next.barspacing1 then - - local text_distance = math.max(0, cur.end_difference) + math.max(0, next.begin_difference) - debugmessage('syllablespacing', ' text distance = %s', glue_to_string(text_distance)) + local text_distance = node.dimensions(cur.text.next, next.text) + debugmessage('syllablespacing', ' text distance = %s', glue_to_string(new_text_distance)) local min_text_distance = saved_syllables[sid].min_text_distance debugmessage('syllablespacing', ' min text distance = %s', glue_to_string(min_text_distance)) local min_text_shift = glue_add(min_text_distance, -text_distance) debugmessage('syllablespacing', ' min text shift = %s', glue_to_string(min_text_shift)) - local notes_distance = math.max(0, -cur.end_difference) + math.max(0, -next.begin_difference) - debugmessage('syllablespacing', ' notes distance = %s', glue_to_string(notes_distance)) + local notes_distance = node.dimensions(cur.last_note.next, next.first_note) local min_notes_distance = saved_syllables[sid].min_notes_distance debugmessage('syllablespacing', ' min notes distance = %s', glue_to_string(min_notes_distance)) local min_notes_shift = glue_add(min_notes_distance, -notes_distance) @@ -273,6 +247,30 @@ local function syllable_spacing(syllables) end end +local function syllable_clearing(syllables) + for sid, cur in pairs(syllables) do + local prev = syllables[sid-1] + if cur.clearsyllable and prev then + debugmessage('clear', 'syllable %d', sid) + local kern = 0 + -- current text must begin at or after prev notes' end + if prev.last_note and cur.text then + local overlap = -node.dimensions(prev.last_note.next, cur.text) + debugmessage('clear', ' text-note overlap %fpt', overlap/2^16) + kern = math.max(kern, overlap) + end + -- current notes must begin at or after prev text's end + if prev.text and cur.first_note then + local overlap = -node.dimensions(prev.text.next, cur.first_note) + debugmessage('clear', ' note-text overlap %fpt', overlap/2^16) + kern = math.max(kern, overlap) + end + debugmessage('clear', ' kern %fpt', kern/2^16) + cur.clearsyllable.kern = kern + end + end +end + local function syllable_rewriting(syllables) if not gregoriotex.get_if('gre@rewritesyllables') then return end @@ -338,4 +336,5 @@ gregoriotex.save_min_distances = save_min_distances gregoriotex.free_saved_syllables = free_saved_syllables gregoriotex.scan_syllables = scan_syllables gregoriotex.syllable_spacing = syllable_spacing +gregoriotex.syllable_clearing = syllable_clearing gregoriotex.syllable_rewriting = syllable_rewriting diff --git a/tex/gregoriotex-syllable.tex b/tex/gregoriotex-syllable.tex index 5149f77e..e97abcbc 100644 --- a/tex/gregoriotex-syllable.tex +++ b/tex/gregoriotex-syllable.tex @@ -1149,7 +1149,8 @@ \fi % \fi% \ifgre@textcleared% - \gre@clearsyllable{note}% + % Insert a zero kern that will be adjusted in the Lua function syllable_clearing. + {\gre@attr@skip@type=5 \kern0pt}% \fi% % then we reuse temp, we assign to it the \gre@dimen@begindifference, but only if it is positive, else it is 0 \gre@debugmsg{ifdim}{ begindifference > 0pt}% diff --git a/tex/gregoriotex.lua b/tex/gregoriotex.lua index 965e4b2f..04ed85cd 100644 --- a/tex/gregoriotex.lua +++ b/tex/gregoriotex.lua @@ -417,9 +417,11 @@ local function dump_nodes_helper(head, indent) if node.subtypes(n.id) ~= nil then subtype = node.subtypes(n.id)[n.subtype] end - local attrs = format("syllable=%s,part=%s", + local attrs = format("syllable=%s,part=%s,skip_type=%s", has_attribute(n, syllable_id_attr), - has_attribute(n, part_attr)) + has_attribute(n, part_attr), + has_attribute(n, skip_type_attr) + ) if n.id == hlist or n.id == vlist then log(dots .. "%s [%s] width=%.2fpt height=%.2fpt depth=%.2fpt shift=%.2fpt {%s}", type, subtype, n.width/2^16, n.height/2^16, n.depth/2^16, n.shift/2^16, attrs) elseif n.id == rule then @@ -427,7 +429,7 @@ local function dump_nodes_helper(head, indent) elseif n.id == whatsit and subtype == user_defined_subtype and n.user_id == marker_whatsit_id then log(dots .. "marker-whatsit %s", n.value) elseif n.id == glue then - log(dots .. "glue [%s] width=%.2fpt {%s}", subtype, n.width/2^16, attrs) + log(dots .. "glue [%s] width=%.2fpt stretch=%d shrink=%d {%s}", subtype, n.width/2^16, n.stretch, n.shrink, attrs) elseif n.id == kern then log(dots .. "kern [%s] kern=%.2fpt {%s}", subtype, n.kern/2^16, attrs) elseif type == 'penalty' then @@ -962,10 +964,11 @@ local function ligaturing(head) end local function pre_linebreak(head) - --dump_nodes(head) + dump_nodes(head) local syllables = gregoriotex.scan_syllables(head) if #syllables == 0 then return head end gregoriotex.syllable_spacing(syllables) + gregoriotex.syllable_clearing(syllables) gregoriotex.syllable_rewriting(syllables) return head end @@ -1008,7 +1011,7 @@ local function add_dash(line) end local function post_linebreak(h, groupcode, glyphes) - --dump_nodes(h) + dump_nodes(h) -- TODO: to be changed according to the font local centerstartnode = nil local linenum = 0 From a48c861348f42654c17ce27d0b0a6cbca281282d Mon Sep 17 00:00:00 2001 From: David Chiang Date: Tue, 10 Mar 2026 19:56:46 -0400 Subject: [PATCH 04/17] Intra-syllable hyphenation in Lua. If there is sufficient space between a syllable and the next, a hyphen is inserted. This used to be done in \GreSyllable, but now is done in the Lua pre_linebreak filter. - Added some missing cases to \gre@calculate@eolshift - Remove TeX code computing syllablefinalskip and code that depends on it * It is still used inside the new bar spacing algorithm, but for a different purpose * There is still a macro \gre@calculate@syllablefinalskip, but it actually computes minNotesDistance and minTextDistance - Intra-line and end-of-line hyphens are now generated by the same code * One very minor side effect is that end-of-line hyphens should now be correctly kerned. --- doc/Command_Index_internal.tex | 12 --- tex/gregoriotex-main.tex | 1 + tex/gregoriotex-spaces.tex | 112 +++++--------------- tex/gregoriotex-syllable.lua | 181 +++++++++++++++++++++++---------- tex/gregoriotex-syllable.tex | 137 ++++--------------------- tex/gregoriotex.lua | 49 ++++----- 6 files changed, 195 insertions(+), 297 deletions(-) diff --git a/doc/Command_Index_internal.tex b/doc/Command_Index_internal.tex index 5c7405b3..e487d1c1 100644 --- a/doc/Command_Index_internal.tex +++ b/doc/Command_Index_internal.tex @@ -2384,18 +2384,6 @@ \subsection{Distances} \macroname{\textbackslash gre@skip@minNotesDistance}{}{gregoriotex-spaces.tex} Minimum distance between notes. -\macroname{\textbackslash gre@dimen@curTextDistance}{}{gregoriotex-spaces.tex} -Current distance between text. - -\macroname{\textbackslash gre@dimen@curNotesDistance}{}{gregoriotex-spaces.tex} -Current distance between notes. - -\macroname{\textbackslash gre@skip@minShiftText}{}{gregoriotex-spaces.tex} -Minimum shift required for the text. - -\macroname{\textbackslash gre@skip@minShiftNotes}{}{gregoriotex-spaces.tex} -Minimum shift required for the notes. - \macroname{\textbackslash gre@scaledist}{}{gregoriotex-spaces.tex} Working alias for \verb=\gre@skip@temp@one= or \verb=\gre@dimen@temp@one=, as appropriate, used when rescaling a distance due to a change in \verb=\gre@factor=. diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index f51b23d4..23c46a46 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -61,6 +61,7 @@ % 2 = the syllable doesn't need a dash because it already has one % 3 = the syllable doesn't need a dash because it's word-final % 4 = the syllable doesn't need a dash because it is a \GreBarSyllable +% 5 = the syllable needs a forced dash \newattribute\gre@attr@dash % an attribute used for translation centering diff --git a/tex/gregoriotex-spaces.tex b/tex/gregoriotex-spaces.tex index 6a0dd967..aa3b73e9 100644 --- a/tex/gregoriotex-spaces.tex +++ b/tex/gregoriotex-spaces.tex @@ -256,12 +256,8 @@ \newskip\gre@skip@syllablefinalskip \newskip\gre@skip@minTextDistance% \newskip\gre@skip@minNotesDistance% -\newdimen\gre@dimen@curTextDistance% -\newdimen\gre@dimen@curNotesDistance% -\newskip\gre@skip@minShiftText% -\newskip\gre@skip@minShiftNotes% -%% @desc Macro computing the skip at the end of the syllable +%% @desc Macro computing minimum distances between notes/text of this syllable and next %% @arg#1 0 if end of syllable, 1 if end of word %% @arg#2 0 if next syllable is normal, 1 if it's a bar, 2 if it starts with %% an alteration @@ -277,19 +273,6 @@ %% min_notes_dist = space_between_notes %% if (barres sur cur ou barres sur next): %% min_notes_dist = space_between_bars -%% % space between end of syllable and current point for previous note -%% cur_dist_notes = cur_dist_text = 0 -%% if (cur_end_diff < 0): -%% cur_dist_text += -cur_end_diff -%% else: -%% cur_dist_notes += cur_end_diff -%% if (next_begin_diff < 0): -%% cur_dist_notes += -next_begin_diff -%% else: -%% cur_dist_text += next_begin_diff -%% min_shift_text = min_dist_text - cur_dist_text -%% min_shift_notes = min_dist_notes - cur_dist_notes -%% shift = max(min_shift_text, min_shift_notes) \def\gre@calculate@syllablefinalskip#1#2{% \gre@trace{gre@calculate@syllablefinalskip{#1}{#2}}% %% min_text_dist = prev_cur_word ? 0 : space_inter_words @@ -352,51 +335,6 @@ \fi % \fi % \gre@debugmsg{syllablespacing}{ minNotesDistance = \the\gre@skip@minNotesDistance}% - % determining current distance between notes and - % next notes, and current distance between text and next text - \gre@dimen@curTextDistance=0pt\relax% - \gre@dimen@curNotesDistance=0pt\relax% -%% cur_dist_notes = cur_dist_text = 0 -%% if (cur_end_diff < 0): -%% cur_dist_notes += -cur_end_diff -%% else: -%% cur_dist_text += cur_end_diff -%% if (next_begin_diff < 0): -%% cur_dist_notes += -next_begin_diff -%% else: -%% cur_dist_text += next_begin_diff - \gre@debugmsg{syllablespacing}{ enddifference = \the\gre@dimen@enddifference}% - \gre@debugmsg{syllablespacing}{ nextbegindifference = \the\gre@skip@nextbegindifference}% - \ifdim\gre@dimen@enddifference < 0 pt\relax% - \gre@dimen@curNotesDistance = -\gre@dimen@enddifference\relax% - \else % - \gre@dimen@curTextDistance = \gre@dimen@enddifference\relax% - \fi % - \ifdim\gre@skip@nextbegindifference < 0 pt\relax% - \advance\gre@dimen@curNotesDistance by -\gre@skip@nextbegindifference\relax% - \else % - \advance\gre@dimen@curTextDistance by \gre@skip@nextbegindifference\relax% - \fi % - \gre@debugmsg{syllablespacing}{ curNotesDistance = \the\gre@dimen@curNotesDistance}% - \gre@debugmsg{syllablespacing}{ curTextDistance = \the\gre@dimen@curTextDistance}% -%% min_shift_text = min_dist_text - cur_dist_text -%% min_shift_notes = min_dist_notes - cur_dist_notes -%% shift = max(min_shift_text, min_shift_notes) - \gre@skip@minShiftText = \glueexpr(\gre@skip@minTextDistance - \gre@dimen@curTextDistance)\relax % - \gre@skip@minShiftNotes = \glueexpr(\gre@skip@minNotesDistance - \gre@dimen@curNotesDistance)\relax % - \gre@debugmsg{syllablespacing}{ minShiftNotes = \the\gre@skip@minShiftNotes}% - \gre@debugmsg{syllablespacing}{ minShiftText = \the\gre@skip@minShiftText}% - \ifdim\gre@skip@minShiftNotes < \gre@skip@minShiftText % - \global\gre@skip@syllablefinalskip = \gre@skip@minShiftText % - \else % - \global\gre@skip@syllablefinalskip = \gre@skip@minShiftNotes % - \fi % - \ifgre@showhyphenafterthissyllable % - \gre@debugmsg{syllablespacing}{ add intersyllablespacestretchhyphen (\gre@space@skip@intersyllablespacestretchhyphen)}% - \advance\gre@skip@syllablefinalskip by \gre@space@skip@intersyllablespacestretchhyphen\relax% - \fi % - \gre@debugmsg{syllablespacing}{ syllablefinalskip = \the\gre@skip@syllablefinalskip}% - \relax % \gre@trace@end% } @@ -581,48 +519,47 @@ \def\gre@calculate@eolshift#1{% \gre@trace{gre@calculate@eolshift{#1}}% \gre@skip@temp@two=0pt\relax% - % we only need a shift if the lyrics are longer than the notes \gre@debugmsg{eolshift}{eolshift called with enddifference: \the #1}% % dimen@temp@three is the length of the hyphen at the end of the syllable + % which is added afterwards in Lua and we need to make some room for \gre@dimen@temp@three=0pt\relax % - % if there is a possible hyphen (added afterwards in lua), we keep some room for it \ifgre@possibleluahyphenafterthissyllable % \setbox\gre@box@temp@width=\hbox{\GreHyph}% \gre@dimen@temp@three=\dimexpr(\wd\gre@box@temp@width-\gre@protrusionfactor@eolhyphen\wd\gre@box@temp@width)\relax% - \gre@debugmsg{eolshift}{widthof the potential hyphen: \the\gre@dimen@temp@three}% + \gre@debugmsg{eolshift}{width of the potential hyphen: \the\gre@dimen@temp@three}% \fi % % The basic value for the eol shift is -enddifference + width of the hyphen \gre@skip@temp@two=\glueexpr(\gre@dimen@temp@three-\the #1)\relax% - \gre@debugmsg{eolshift}{adjusted enddifference: \the\gre@skip@temp@two}% - % if tex+hyphen goes further than the notes: - \ifdim\gre@skip@temp@two>0pt\relax% - \gre@skip@temp@three = 0pt% - \ifgre@blockeolcustos\else% + \gre@debugmsg{eolshift}{hyphen-enddifference: \the\gre@skip@temp@two}% + \gre@skip@temp@three = 0pt + \ifgre@shownotes + \ifgre@blockeolcustos\else % The maximum value is wd(custos) + spacebeforeeolcustos % Were the eolshift larger than this the lyrics would stick out % into the margin \setbox\gre@box@temp@width=\hbox{\gre@pickcustos{\gre@pitch@g}{0}}% \gre@skip@temp@three = \glueexpr(\wd\gre@box@temp@width+\gre@space@skip@spacebeforeeolcustos)\relax% - \fi % - \gre@debugmsg{eolshift}{custos + space before custos = \the\gre@skip@temp@three}% + \fi + \fi + \gre@debugmsg{eolshift}{custos + space before custos = \the\gre@skip@temp@three}% + \ifdim#1<0pt % pick the smaller of the two values calculated above \ifdim\gre@skip@temp@two>\gre@skip@temp@three% - \gre@debugmsg{eolshift}{imposing limit}% + \gre@debugmsg{eolshift}{text longer than notes, hyphen past custos}% \global\gre@dimen@eolshift = \glueexpr(\gre@skip@temp@three-\gre@dimen@temp@three)\relax % \else% - \ifdim\gre@skip@temp@two<\gre@dimen@temp@three % - \global\gre@dimen@eolshift = \gre@skip@temp@two\relax % - \else % - \global\gre@dimen@eolshift = \glueexpr(\gre@skip@temp@two-\gre@dimen@temp@three)\relax % - \fi % + \gre@debugmsg{eolshift}{text longer than notes, hyphen not past custos}% + \global\gre@dimen@eolshift = \glueexpr(\gre@skip@temp@two-\gre@dimen@temp@three)\relax \fi% - \else% - \global\gre@dimen@eolshift=0pt\relax% + \else % enddifference > 0 + \ifdim\gre@skip@temp@two>\gre@skip@temp@three + \gre@debugmsg{eolshift}{text shorter than notes, hyphen past custos}% + \global\gre@dimen@eolshift = \glueexpr(\gre@skip@temp@three-\gre@skip@temp@two)\relax + \else + \gre@debugmsg{eolshift}{text shorter than notes, hyphen not past custos}% + \global\gre@dimen@eolshift=0pt + \fi \fi % - % if the notes are not visible, then there's no shifting allowed. - \ifgre@shownotes\else% - \global\gre@dimen@eolshift=0pt\relax% - \fi% \gre@debugmsg{eolshift}{eolshift: \the\gre@dimen@eolshift}% \relax % \gre@trace@end% @@ -1239,10 +1176,9 @@ -\gre@dimen@begindifference% go from beginning of text to beginning of notes (opposite sense of begindifference so lead with negative) +\wd\gre@box@syllablenotes\relax% go from beginning of notes to end of notes (width is always positive so lead with positive to go right \gre@debugmsg{barspacing}{enddifference: \the\gre@dimen@enddifference}% - %we also need to correct syllablefinalskip to make sure it’s accurate because it depends on enddifference - \gre@debugmsg{barspacing}{Correcting syllablefinalskip}% + %we also need to correct minNotesDistance and minTextDistance because they depend on enddifference + \gre@debugmsg{barspacing}{Correcting minimum text and notes distances}% \gre@calculate@syllablefinalskip{#1}{\gre@count@temp@one}% - \gre@debugmsg{barspacing}{syllablefinalskip: \the\gre@skip@syllablefinalskip}% \gre@trace@end% }% diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index 6862495f..9d13efe6 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -17,7 +17,7 @@ --You should have received a copy of the GNU General Public License --along with Gregorio. If not, see . --- this file contains lua functions to support signs used by GregorioTeX. +-- This file contains Lua functions to support spacing of syllables. -- GREGORIO_VERSION 6.1.0 @@ -31,6 +31,7 @@ local has_attribute = node.has_attribute local kern = node.id('kern') local temp = node.id('temp') local disc = node.id('disc') +local glyph = node.id('glyph') local syllable_id_attr = luatexbase.attributes['gre@attr@syllable@id'] @@ -44,8 +45,10 @@ local skip_type_barspacing1 = 2 local skip_type_clearsyllable = 5 local dash_attr = luatexbase.attributes['gre@attr@dash'] +local dash_maybedash = 1 local dash_hasdash = 2 local dash_barsyllable = 4 +local dash_forced = 5 -- Functions for manipulating glue, which we just store as a 3-tuple -- {width, stretch, shrink} in sp. @@ -103,7 +106,16 @@ end -- Table for storing information about syllables that is impossible or -- inconvenient to recover from node attributes. -local saved_syllables = {} +local syllables = {} +gregoriotex.syllables = syllables + +local function save_syllable_info() + local sid = tex.getattribute(syllable_id_attr) + if syllables[sid] == nil then syllables[sid] = {} end + syllables[sid].sid = sid + syllables[sid].font = font.current() + log('saving font for syllable %s', sid) +end local function save_syllable_texts(head) -- Save syllable texts before ligaturing and kerning happens. This @@ -115,24 +127,24 @@ local function save_syllable_texts(head) local sid = tex.getattribute(syllable_id_attr) local cur = head while cur ~= nil and cur.id == temp do cur = cur.next end - if saved_syllables[sid] == nil then saved_syllables[sid] = {} end - saved_syllables[sid].text = node.copy_list(cur) + if syllables[sid] == nil then syllables[sid] = {} end + syllables[sid].raw_text = node.copy_list(cur) end end local function save_min_distances() local sid = tex.getattribute(syllable_id_attr) - if saved_syllables[sid] == nil then saved_syllables[sid] = {} end + if syllables[sid] == nil then syllables[sid] = {} end local g = tex.skip['gre@skip@minTextDistance'] - saved_syllables[sid].min_text_distance = {g.width, g.stretch, g.shrink} + syllables[sid].min_text_distance = {g.width, g.stretch, g.shrink} g = tex.skip['gre@skip@minNotesDistance'] - saved_syllables[sid].min_notes_distance = {g.width, g.stretch, g.shrink} + syllables[sid].min_notes_distance = {g.width, g.stretch, g.shrink} end -local function free_saved_syllables() - for sid, syl in pairs(saved_syllables) do - node.flush_list(syl.text) - saved_syllables[sid] = nil +local function free_syllables() + for sid, syl in pairs(syllables) do + node.flush_list(syl.raw_text) + syllables[sid] = nil end end @@ -161,8 +173,9 @@ end local function scan_syllables(head) -- Find nodes corresponding to various parts of syllables and store them in a -- data structure more convenient for downstream processing. - local syllables = {} - local prev_sid = 0 + for _, cur in pairs(syllables) do + cur.first_note = nil + end local function visit(head) for n in node.traverse(head) do -- to do: The two syllables in a discretionary are numbered @@ -177,11 +190,7 @@ local function scan_syllables(head) local part = has_attribute(n, part_attr) local skip_type = has_attribute(n, skip_type_attr) if sid ~= nil then - while prev_sid < sid do - prev_sid = prev_sid+1 - syllables[prev_sid] = {} - if saved_syllables[prev_sid] == nil then saved_syllables[prev_sid] = {} end - end + if syllables[sid] == nil then syllables[sid] = {} end if part == part_lyrics then if syllables[sid].text ~= nil then err(' syllable %d has more than one text node', sid) @@ -204,50 +213,118 @@ local function scan_syllables(head) end end visit(head) - return syllables end -local function syllable_spacing(syllables) +local function adjust_syllablefinalskip(cur, next) + local text_distance = node.dimensions(cur.text.next, next.text) + debugmessage('syllablespacing', ' text distance = %s', glue_to_string(text_distance)) + local min_text_distance = cur.min_text_distance + debugmessage('syllablespacing', ' min text distance = %s', glue_to_string(min_text_distance)) + local min_text_shift = glue_add(min_text_distance, -text_distance) + debugmessage('syllablespacing', ' min text shift = %s', glue_to_string(min_text_shift)) + + local notes_distance = node.dimensions(cur.last_note.next, next.first_note) + debugmessage('syllablespacing', ' notes distance = %s', glue_to_string(notes_distance)) + local min_notes_distance = cur.min_notes_distance + debugmessage('syllablespacing', ' min notes distance = %s', glue_to_string(min_notes_distance)) + local min_notes_shift = glue_add(min_notes_distance, -notes_distance) + debugmessage('syllablespacing', ' min notes shift = %s', glue_to_string(min_notes_shift)) + + local syllablefinalskip = {cur.syllablefinalskip.width, cur.syllablefinalskip.stretch, cur.syllablefinalskip.shrink} + -- Ensure that min text shift and min notes shift are satisfied. + syllablefinalskip = glue_add(syllablefinalskip, glue_max(min_text_shift, min_notes_shift)) + -- If this syllable has a hyphen, add some additional stretch. + -- Note: This happens even if there is no text (\gresetlyrics{invisible}). + if cur.text and has_attribute(cur.text, dash_attr, dash_hasdash) then + debugmessage('syllablespacing', ' adding stretch for hyphen') + syllablefinalskip = glue_add(syllablefinalskip, string_to_glue(token.get_macro('gre@space@skip@intersyllablespacestretchhyphen'))) + end + debugmessage('syllablespacing', ' syllable final skip = %s', glue_to_string(syllablefinalskip)) + node.setglue(cur.syllablefinalskip, table.unpack(syllablefinalskip)) +end + +local function add_hyphen(cur) + -- Append hyphen to saved syllable text (needed if the syllable gets rewritten) + local g = node.new(glyph) + g.font = cur.font + g.char = gregoriotex.hyphen + -- Find last glyph (because the last node may be a marker) + local last = node.tail(cur.raw_text) + while last ~= nil and last.id ~= glyph do last = last.prev end + cur.raw_text = node.insert_after(cur.raw_text, last, g) + + -- Replace actual syllable text + local old_width = cur.text.width + node.flush_list(cur.text.head) + cur.text.head = shaping(node.copy_list(cur.raw_text)) + local new_width = node.rangedimensions(cur.text, cur.text.head) + cur.text.width = new_width + local width_change = new_width - old_width + + -- Mark text as having a hyphen + node.set_attribute(cur.text, dash_attr, dash_hasdash) + -- The text node is immediately followed by a kern whose size + -- is the text width. To keep the text and notes aligned, we + -- need to update this kern. + local k = cur.text.next + if k.id ~= kern then err('expected kern to follow syllable text') end + k.kern = k.kern - width_change + + -- We also need to adjust the kern after the notes that moves + -- to the right edge of the syllable. If this syllable ends up + -- as the last of the line, \gre@calculateeolshift has already + -- allocated space for the hyphen, and this adjustment is not + -- necessary. So we want the adjustment to go after the + -- endofsyllablepenalty, where it will disappear in case of a + -- line break. But syllablefinalskip goes after the + -- endofsyllablepenalty, so we can just let + -- adjust_syllablefinalskip do all the work. + + -- Bug: if this syllable gets a hyphen and the next syllable is a + -- bar (presumably rare in practice, but occurs in the tests), then + -- the bar will have the wrong previousenddifference. +end + +local function syllable_spacing() for sid, cur in pairs(syllables) do + debugmessage('syllablespacing', 'after syllable %d', sid) + local next = syllables[sid+1] + -- If the next syllable is a bar syllable, then this syllable -- shouldn't have syllablefinalskip. But (due to a bug, #1724) -- if the next syllable is a clef change, it is a bar syllable -- and this syllable does have syllablefinalskip; we ignore it. - debugmessage('syllablespacing', 'after syllable %d', sid) - local next = syllables[sid+1] if cur.syllablefinalskip and next ~= nil and not next.barspacing1 then + adjust_syllablefinalskip(cur, next) + end + + local needs_hyphen = false + -- If there is too much space between text, add a hyphen + if (cur.text ~= nil and has_attribute(cur.text, dash_attr, dash_maybedash) and + next ~= nil and next.text ~= nil) then local text_distance = node.dimensions(cur.text.next, next.text) - debugmessage('syllablespacing', ' text distance = %s', glue_to_string(new_text_distance)) - local min_text_distance = saved_syllables[sid].min_text_distance - debugmessage('syllablespacing', ' min text distance = %s', glue_to_string(min_text_distance)) - local min_text_shift = glue_add(min_text_distance, -text_distance) - debugmessage('syllablespacing', ' min text shift = %s', glue_to_string(min_text_shift)) - - local notes_distance = node.dimensions(cur.last_note.next, next.first_note) - local min_notes_distance = saved_syllables[sid].min_notes_distance - debugmessage('syllablespacing', ' min notes distance = %s', glue_to_string(min_notes_distance)) - local min_notes_shift = glue_add(min_notes_distance, -notes_distance) - debugmessage('syllablespacing', ' min notes shift = %s', glue_to_string(min_notes_shift)) - - local syllablefinalskip = {cur.syllablefinalskip.width, cur.syllablefinalskip.stretch, cur.syllablefinalskip.shrink} - -- Ensure that min text shift and min notes shift are satisfied. - syllablefinalskip = glue_add(syllablefinalskip, glue_max(min_text_shift, min_notes_shift)) - -- If this syllable has a hyphen, add some additional stretch. - -- Note: This happens even if there is no text (\gresetlyrics{invisible}). - if cur.text and has_attribute(cur.text, dash_attr, dash_hasdash) then - debugmessage('syllablespacing', ' adding stretch for hyphen') - syllablefinalskip = glue_add(syllablefinalskip, string_to_glue(token.get_macro('gre@space@skip@intersyllablespacestretchhyphen'))) + local max_distance = tex.sp(token.get_macro('gre@space@dimen@maximumspacewithoutdash')) + if text_distance > max_distance then needs_hyphen = true end + end + -- If hyphen was forced, add a hyphen + if cur.text ~= nil and has_attribute(cur.text, dash_attr, dash_forced) then + needs_hyphen = true + end + -- If lyrics are disabled, don't add a hyphen + if not gregoriotex.get_if('gre@showlyrics') then needs_hyphen = false end + + if needs_hyphen then + add_hyphen(cur) + -- Since adding the hyphen made cur wider, recompute syllablefinalskip + if cur.syllablefinalskip and next ~= nil and not next.barspacing1 then + adjust_syllablefinalskip(cur, next) end - debugmessage('syllablespacing', ' syllable final skip = %s', glue_to_string(syllablefinalskip)) - node.setglue(cur.syllablefinalskip, table.unpack(syllablefinalskip)) - else - debugmessage('syllablespacing', ' no syllable final skip, not adjusting') end end end -local function syllable_clearing(syllables) +local function syllable_clearing() for sid, cur in pairs(syllables) do local prev = syllables[sid-1] if cur.clearsyllable and prev then @@ -271,7 +348,7 @@ local function syllable_clearing(syllables) end end -local function syllable_rewriting(syllables) +local function syllable_rewriting() if not gregoriotex.get_if('gre@rewritesyllables') then return end local start = 1 @@ -305,8 +382,8 @@ local function syllable_rewriting(syllables) local head, tail for sid = start, stop do -- Extend new text - local n = saved_syllables[sid].text - saved_syllables[sid].text = nil + local n = syllables[sid].raw_text + syllables[sid].raw_text = nil head, tail = concat_list(head, tail, n, node.tail(n)) end head = shaping(head) @@ -331,10 +408,12 @@ local function syllable_rewriting(syllables) end end +gregoriotex.save_syllable_info = save_syllable_info gregoriotex.save_syllable_texts = save_syllable_texts gregoriotex.save_min_distances = save_min_distances -gregoriotex.free_saved_syllables = free_saved_syllables +gregoriotex.free_syllables = free_syllables gregoriotex.scan_syllables = scan_syllables gregoriotex.syllable_spacing = syllable_spacing gregoriotex.syllable_clearing = syllable_clearing gregoriotex.syllable_rewriting = syllable_rewriting +gregoriotex.add_hyphen = add_hyphen diff --git a/tex/gregoriotex-syllable.tex b/tex/gregoriotex-syllable.tex index e97abcbc..de167bdd 100644 --- a/tex/gregoriotex-syllable.tex +++ b/tex/gregoriotex-syllable.tex @@ -920,14 +920,8 @@ \gre@debugmsg{general}{}% \gre@debugmsg{general}{New syllable: \expandafter\unexpanded{#1}}% \gre@debugmsg{general}{}% - % This needs to be calculated early for syllable rewriting, the value - % will be refined later on - \ifcase#4 % - \global\gre@possibleluahyphenafterthissyllabletrue % - \else % - \global\gre@possibleluahyphenafterthissyllablefalse % - \fi % \global\advance\gre@attr@syllable@id by 1\relax % + \directlua{gregoriotex.save_syllable_info()}% \gre@showhyphenafterthissyllablefalse% \ifcase#4\ifgre@forcehyphen% \gre@debugmsg{hyphen}{Forcing hyphen}% @@ -1008,8 +1002,6 @@ \global\gre@attr@alteration@id=\gre@saved@attr@alteration@id\relax % \gre@calculate@nextbegindifference{\gre@evaluatenextsyllable{\gre@nextfirstsyllablepart}}{\gre@evaluatenextsyllable{\gre@nextmiddlesyllablepart}}{\gre@evaluatenextsyllable{\gre@nextendsyllablepart}}{\gre@nextalignment}{\gre@nextalteration}% \gre@unsetfixednexttextformat % - \gre@attr@dash=3 - \gre@attr@part=4 \ifgre@showlyrics% \setbox\gre@box@syllabletext=\hbox{% \IfSubStr{\gre@debug}{,notespacing,}% @@ -1029,8 +1021,6 @@ \setbox\gre@box@syllabletext=\hbox{}% \fi \fi% - \unsetattribute{\gre@attr@dash}% - \unsetattribute{\gre@attr@part}% \gre@calculate@enddifference{\wd\gre@box@syllablenotes}{\wd\gre@box@syllabletext}{\gre@dimen@textaligncenter}{\gre@dimen@notesaligncenter}{1}% % gre@count@temp@one holds 1 if next is a bar, 2 if an alteration, else 0 \gre@count@temp@one=0% @@ -1043,111 +1033,25 @@ \fi % \gre@debugmsg{spacing}{ gre@count@temp@one = \the\gre@count@temp@one}% \gre@calculate@syllablefinalskip{#4}{\gre@count@temp@one}% - \ifcase#4 % - % we enter here if the end of word is 0, so we must determine if we need to type a dash here - \gre@skip@temp@one = \gre@skip@syllablefinalskip\relax% - \gre@debugmsg{ifdim}{ enddifference > 0pt}% - \ifdim\gre@dimen@enddifference >0pt\relax% - \advance\gre@skip@temp@one by \gre@dimen@enddifference\relax% - \fi % - \gre@debugmsg{ifdim}{ nextbegindifference > 0pt}% - \ifdim\gre@skip@nextbegindifference >0pt\relax% - \advance\gre@skip@temp@one by \gre@skip@nextbegindifference\relax% - \fi % - % - % then we compare it with \gre@space@dimen@maximumspacewithoutdash, if it is larger, we add a dash - % - \gre@debugmsg{ifdim}{ temp@skip@one > maximumspacewithoutdash}% - \ifdim\gre@skip@temp@one > \gre@space@dimen@maximumspacewithoutdash\relax% - \gre@debugmsg{hyphen}{spacing requires hyphen}% - \gre@showhyphenafterthissyllabletrue% - \fi % - \fi% ficase#4 - \ifgre@showhyphenafterthissyllable\relax% - \global\gre@possibleluahyphenafterthissyllablefalse % - \gre@debugmsg{hyphen}{Showing the hyphen}% - % if it's the last syllable of line, the hyphen will be \GreHyph - \ifnum\gre@lastoflinecount=1\relax % - \gre@attr@dash=2 - \gre@attr@part=4 - \ifgre@showlyrics% - \setbox\gre@box@syllabletext=\hbox{% - \IfSubStr{\gre@debug}{,notespacing,}% - % when debugging we add a zero-width line to mark the syllable bound - {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% - {}% do nothing if not debugging - \gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart#3{\GreHyph}}{#6}}% - \IfSubStr{\gre@debug}{,notespacing,}% - % when debugging we add a zero-width line to mark the syllable bound - {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% - {}% do nothing if not debugging - }% - \else% - \ifnum\gre@lyrics@phantomwrapper>0\relax - \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart#3{\GreHyph}\relax}{#6}}}}% - \else - \setbox\gre@box@syllabletext=\hbox{}% - \fi - \fi% - \unsetattribute{\gre@attr@dash}% - \unsetattribute{\gre@attr@part}% - \else % - \gre@attr@dash=2 - \gre@attr@part=4 - \ifgre@showlyrics% - \setbox\gre@box@syllabletext=\hbox{% - \IfSubStr{\gre@debug}{,notespacing,}% - % when debugging we add a zero-width line to mark the syllable bound - {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% - {}% do nothing if not debugging - \gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart#3{-}}{#6}}% - \IfSubStr{\gre@debug}{,notespacing,}% - % when debugging we add a zero-width line to mark the syllable bound - {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% - {}% do nothing if not debugging - }% - \else% - \ifnum\gre@lyrics@phantomwrapper>0\relax - \setbox\gre@box@syllabletext=\hbox{\gre@applyphantomwrapper{\gre@lyrics@phantomwrapper}{\gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart#3{-}}{#6}}}}% - \else - \setbox\gre@box@syllabletext=\hbox{}% - \fi - \fi% - \unsetattribute{\gre@attr@dash}% - \unsetattribute{\gre@attr@part}% - \fi % - % recomputing end difference and final skip with the final hyphen - \gre@calculate@nextbegindifference{\gre@evaluatenextsyllable{\gre@nextfirstsyllablepart}}{\gre@evaluatenextsyllable{\gre@nextmiddlesyllablepart}}{\gre@evaluatenextsyllable{\gre@nextendsyllablepart}}{\gre@nextalignment}{\gre@nextalteration}% - \gre@calculate@enddifference{\wd\gre@box@syllablenotes}{\wd\gre@box@syllabletext}{\gre@dimen@textaligncenter}{\gre@dimen@notesaligncenter}{0}% - \gre@calculate@syllablefinalskip{#4}{\gre@count@temp@one}% - \else % - \ifcase#4 % - \global\gre@possibleluahyphenafterthissyllabletrue % - \gre@debugmsg{hyphen}{No hyphen}% - \gre@attr@dash=1 % if not the end of a word and we haven't added a dash, we set potential dash to 1 - \gre@attr@part=4 - % we rebuild this box, in order it to have the attribute - \ifgre@showlyrics% - \setbox\gre@box@syllabletext=\hbox{% - \IfSubStr{\gre@debug}{,notespacing,}% - % when debugging we add a zero-width line to mark the syllable bound - {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% - {}% do nothing if not debugging - \gre@fixedtextformat{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@endsyllablepart}{#6}}% - \IfSubStr{\gre@debug}{,notespacing,}% - % when debugging we add a zero-width line to mark the syllable bound - {\hbox to 0pt{\rule{0.4pt}{12pt}\hss}}% - {}% do nothing if not debugging - }% - \else% - \setbox\gre@box@syllabletext=\hbox{}% - \fi% - \unsetattribute{\gre@attr@dash}% - \unsetattribute{\gre@attr@part}% - \else % - \global\gre@possibleluahyphenafterthissyllablefalse % - \fi % - \fi% + \gre@attr@part=4 % text + \ifcase#4 + \global\gre@possibleluahyphenafterthissyllabletrue + \ifgre@showhyphenafterthissyllable + \gre@attr@dash=5 % needs forced hyphen + \else + \gre@attr@dash=1 % maybe needs hyphen + \fi + \else + \global\gre@possibleluahyphenafterthissyllablefalse + \ifgre@showhyphenafterthissyllable + \gre@attr@dash=5 % needs forced hyphen + \else + \gre@attr@dash=3 % end of word + \fi + \fi + \setbox\gre@box@syllabletext=\hbox{\unhbox\gre@box@syllabletext}% + \unsetattribute{\gre@attr@dash}% + \unsetattribute{\gre@attr@part}% \ifgre@textcleared% % Insert a zero kern that will be adjusted in the Lua function syllable_clearing. {\gre@attr@skip@type=5 \kern0pt}% @@ -1385,6 +1289,7 @@ \gre@debugmsg{general}{New bar syllable}% \gre@debugmsg{general}{}% \global\advance\gre@attr@syllable@id by 1\relax % + \directlua{gregoriotex.save_syllable_info()}% \gre@possibleluahyphenafterthissyllablefalse % \gre@showhyphenafterthissyllablefalse % % the algorithm of this function is *extremely* complex, and has been much painful to write... good luck to understand. diff --git a/tex/gregoriotex.lua b/tex/gregoriotex.lua index 04ed85cd..9275d3ea 100644 --- a/tex/gregoriotex.lua +++ b/tex/gregoriotex.lua @@ -62,9 +62,6 @@ for i, t in ipairs(node.subtypes('glue')) do end local hyphen = tex.defaulthyphenchar or 45 -local dash_node = node.new(glyph, 0) -dash_node.font = 0 -dash_node.char = hyphen local score_attr = luatexbase.attributes['gre@attr@score'] local syllable_id_attr = luatexbase.attributes['gre@attr@syllable@id'] @@ -87,6 +84,7 @@ local dash_maybedash = 1 local dash_hasdash = 2 local dash_endofword = 3 local dash_barsyllable = 4 +local dash_forced = 5 local center_attr = luatexbase.attributes['gre@attr@center'] local startcenter = 1 @@ -417,10 +415,10 @@ local function dump_nodes_helper(head, indent) if node.subtypes(n.id) ~= nil then subtype = node.subtypes(n.id)[n.subtype] end - local attrs = format("syllable=%s,part=%s,skip_type=%s", + local attrs = format("syllable=%s,part=%s,dash=%s", has_attribute(n, syllable_id_attr), has_attribute(n, part_attr), - has_attribute(n, skip_type_attr) + has_attribute(n, dash_attr) ) if n.id == hlist or n.id == vlist then log(dots .. "%s [%s] width=%.2fpt height=%.2fpt depth=%.2fpt shift=%.2fpt {%s}", type, subtype, n.width/2^16, n.height/2^16, n.depth/2^16, n.shift/2^16, attrs) @@ -440,7 +438,7 @@ local function dump_nodes_helper(head, indent) for k, v in pairs(f.resources.unicodes) do if v == n.char then charname = k end end - log(dots .. "glyph %s {%s}", charname, attrs) + log(dots .. "glyph %s font=%d {%s}", charname, n.font, attrs) else log(dots .. "node %s [%s] {%s}", node.type(n.id), subtype, attrs) end @@ -964,19 +962,19 @@ local function ligaturing(head) end local function pre_linebreak(head) - dump_nodes(head) - local syllables = gregoriotex.scan_syllables(head) - if #syllables == 0 then return head end - gregoriotex.syllable_spacing(syllables) - gregoriotex.syllable_clearing(syllables) - gregoriotex.syllable_rewriting(syllables) + --dump_nodes(head) + gregoriotex.scan_syllables(head) + gregoriotex.syllable_spacing() + gregoriotex.syllable_clearing() + gregoriotex.syllable_rewriting() + --dump_nodes(head) return head end -local function add_dash(line) +local function add_eol_hyphen(line) -- Add an end-of-line dash to line, if necessary. - local last_text, last_text_with_glyph, last_glyph + local last_text, last_text_with_glyph -- Look for the last text node in the line that has -- dash_attr. If it is dash_maybedash, then we may need to @@ -990,28 +988,19 @@ local function add_dash(line) last_text = n for g in node.traverse_id(glyph, n.head) do last_text_with_glyph = n - last_glyph = g end end end if last_text and - has_attribute(last_text, dash_attr, dash_maybedash) and - last_glyph and - -- don't add a dash if there already is one - not (last_glyph.char == hyphen or last_glyph.char == 45) then - - local g = copy(dash_node) - g.font = last_glyph.font - local h = hpack(g) - h.shift = 0 - - insert_after(last_text_with_glyph.head, last_glyph, h) + (has_attribute(last_text, dash_attr, dash_maybedash) or has_attribute(last_text, dash_attr, dash_forced)) then + local sid = has_attribute(last_text, syllable_id_attr) + gregoriotex.add_hyphen(gregoriotex.syllables[sid]) end end local function post_linebreak(h, groupcode, glyphes) - dump_nodes(h) + --dump_nodes(h) -- TODO: to be changed according to the font local centerstartnode = nil local linenum = 0 @@ -1103,7 +1092,7 @@ local function post_linebreak(h, groupcode, glyphes) -- Look for words that are broken across lines and insert a hyphen for line in traverse_id(hlist, h) do if has_attribute(line, score_attr) then - add_dash(line) + add_eol_hyphen(line) end end @@ -1232,7 +1221,6 @@ local function at_score_beginning(score_id) new_score_first_alterations = {} new_first_alterations[score_id] = new_score_first_alterations end - saved_syllable_texts = {} add_callbacks() end @@ -1244,7 +1232,7 @@ local function at_score_end() remove_callbacks() per_line_dims = {} per_line_counts = {} - gregoriotex.free_saved_syllables() + gregoriotex.free_syllables() end --- Toggle the state of GregorioTeX callbacks. @@ -2024,6 +2012,7 @@ gregoriotex.is_first_alteration = is_first_alteration gregoriotex.fancyhdr_toggle_callbacks = fancyhdr_toggle_callbacks gregoriotex.get_if = get_if gregoriotex.is_last_syllable_id_on_line = is_last_syllable_id_on_line +gregoriotex.hyphen = hyphen gregoriotex.dump_nodes = dump_nodes dofile(kpse.find_file('gregoriotex-nabc.lua', 'lua')) From 1c11d5cf5b2dbd059c18f92c22d7fc3b271179d8 Mon Sep 17 00:00:00 2001 From: David Chiang Date: Sun, 19 Apr 2026 22:39:02 -0400 Subject: [PATCH 05/17] Correct and expand documentation --- doc/Command_Index_internal.tex | 4 +-- tex/gregoriotex-syllable.lua | 57 ++++++++++++++++++++++++++++++---- tex/gregoriotex.lua | 15 ++++++++- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/doc/Command_Index_internal.tex b/doc/Command_Index_internal.tex index adce8cfd..1d0612c4 100644 --- a/doc/Command_Index_internal.tex +++ b/doc/Command_Index_internal.tex @@ -136,7 +136,7 @@ \section{Gregorio\TeX{} Controls} \macroname{\textbackslash gre@calculate@additionalspaces}{}{gregoriotex-spaces.tex} Macro which initializes various dimensions and counts used for variable line height computation. -\macroname{\textbackslash gre@calculate@textaligncenter}{\#1\#2\#3\#4}{gregoriotex-spaces.tex} +\macroname{\textbackslash gre@calculate@textaligncenter}{\#1\#2\#3}{gregoriotex-spaces.tex} Macro for calculating \verb=\gre@textaligncenter=. \begin{argtable} @@ -247,7 +247,7 @@ \section{Gregorio\TeX{} Controls} \#2 & integer & the factor the distances are to be put into\\ \end{argtable} -\macroname{\textbackslash gre@calculate@nextbegindifference}{\#1\#2\#3\#4\#5\#6}{gregoriotex-spaces.tex} +\macroname{\textbackslash gre@calculate@nextbegindifference}{\#1\#2\#3\#4\#5}{gregoriotex-spaces.tex} Macro to calculate \texttt{nextbegindifference}. \begin{argtable} diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index 9d13efe6..d3c7b4dd 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -53,6 +53,9 @@ local dash_forced = 5 -- Functions for manipulating glue, which we just store as a 3-tuple -- {width, stretch, shrink} in sp. +--- Convert glue to a string. +--- @param g table The glue to be converted +--- @return string Human-readable string representation of g. local function glue_to_string(g) if g == nil then return 'nil' @@ -69,6 +72,9 @@ local function glue_to_string(g) end end +--- Convert a string to glue. +--- @param s string The string to be converted, e.g., "1pt plus 2pt minus 3pt" +--- @return table The glue represented by s. local function string_to_glue(s) local stretch = 0 local shrink = 0 @@ -87,17 +93,27 @@ local function string_to_glue(s) return {width, stretch, shrink} end +--- Convert a dimen to glue. +--- @param dimen number A dimension, in sp. +--- @return table The glue equivalent to dimen, with no stretch or shrink. local function dimen_to_glue(dimen) return {dimen, 0, 0} end +--- Find the maximum of two glues. +--- @param a table A glue. +--- @param b table Another glue. +--- @return table The greater of a and b. If the natural widths are equal, return a. local function glue_max(a, b) - -- If the natural widths are equal, return a. if type(a) == 'number' then a = dimen_to_glue(a) end if type(b) == 'number' then b = dimen_to_glue(b) end if a[1] > b[1] then return a else return b end end +--- Find the sum of two glues. +--- @param a table A glue. +--- @param b table Another glue. +--- @return table The sum of a and b. local function glue_add(a, b) if type(a) == 'number' then a = dimen_to_glue(a) end if type(b) == 'number' then b = dimen_to_glue(b) end @@ -109,6 +125,8 @@ end local syllables = {} gregoriotex.syllables = syllables +--- Save information about syllables that is impossible or +--- inconvenient to recover from node attributes. local function save_syllable_info() local sid = tex.getattribute(syllable_id_attr) if syllables[sid] == nil then syllables[sid] = {} end @@ -117,9 +135,10 @@ local function save_syllable_info() log('saving font for syllable %s', sid) end +--- Save syllable text before ligaturing and kerning happens. This +--- is needed later during syllable rewriting. +--- @param head node The syllable text. local function save_syllable_texts(head) - -- Save syllable texts before ligaturing and kerning happens. This - -- is needed later during syllable rewriting. -- Because syllable_id_attr is set even for material not in the -- syllable text, it's better to use dash_attr to detect whether -- this box is really syllable text. @@ -132,6 +151,9 @@ local function save_syllable_texts(head) end end +--- Save the minimum distance between text/notes of a \GreSyllable and +--- the following syllable, or before and after the text/notes of a +--- \GreBarSyllable. local function save_min_distances() local sid = tex.getattribute(syllable_id_attr) if syllables[sid] == nil then syllables[sid] = {} end @@ -141,6 +163,7 @@ local function save_min_distances() syllables[sid].min_notes_distance = {g.width, g.stretch, g.shrink} end +--- Free all information saved about syllables. local function free_syllables() for sid, syl in pairs(syllables) do node.flush_list(syl.raw_text) @@ -148,6 +171,13 @@ local function free_syllables() end end +--- Concatenate two node lists. +--- @param head node The head of the first list. +--- @param tail node The tail of the first list. +--- @param newhead node The head of the second list. +--- @param newtail node The tail of the second list. +--- @return node The head of the concatenated list. +--- @return node The tail of the concatenated list. local function concat_list(head, tail, newhead, newtail) if head == nil then return newhead, newtail @@ -160,6 +190,9 @@ local function concat_list(head, tail, newhead, newtail) end end +--- Apply ligaturing and kerning to a node list. +--- @param head node The head of the list to be processed. +--- @return node The head of the processed list. local function shaping(head) head = node.ligaturing(head) head = node.kerning(head) @@ -170,9 +203,11 @@ local function shaping(head) return head end +--- Find nodes corresponding to various parts of syllables and store them in a +--- data structure more convenient for downstream processing. +--- @param head node The head of the list to be processed. +--- @return node The head of the processed list. local function scan_syllables(head) - -- Find nodes corresponding to various parts of syllables and store them in a - -- data structure more convenient for downstream processing. for _, cur in pairs(syllables) do cur.first_note = nil end @@ -215,6 +250,10 @@ local function scan_syllables(head) visit(head) end +--- Determine the width of a syllable's syllable-final skip, which is +--- the last skip before the start of the next syllable. +--- @param cur table The current syllable. +--- @param next table The next syllable. local function adjust_syllablefinalskip(cur, next) local text_distance = node.dimensions(cur.text.next, next.text) debugmessage('syllablespacing', ' text distance = %s', glue_to_string(text_distance)) @@ -243,6 +282,8 @@ local function adjust_syllablefinalskip(cur, next) node.setglue(cur.syllablefinalskip, table.unpack(syllablefinalskip)) end +--- Add a hyphen to the end of a syllable's text. +--- @param cur table The current syllable. local function add_hyphen(cur) -- Append hyphen to saved syllable text (needed if the syllable gets rewritten) local g = node.new(glyph) @@ -286,10 +327,11 @@ local function add_hyphen(cur) -- the bar will have the wrong previousenddifference. end +--- Determine the width of all syllables' horizontal spacing. local function syllable_spacing() for sid, cur in pairs(syllables) do debugmessage('syllablespacing', 'after syllable %d', sid) - local next = syllables[sid+1] + local next = syllables[sid+1] -- to do: correctly handle discretionaries -- If the next syllable is a bar syllable, then this syllable -- shouldn't have syllablefinalskip. But (due to a bug, #1724) @@ -324,6 +366,7 @@ local function syllable_spacing() end end +--- Clear all syllables that are marked for clearing. local function syllable_clearing() for sid, cur in pairs(syllables) do local prev = syllables[sid-1] @@ -348,6 +391,8 @@ local function syllable_clearing() end end +--- Rewrite all syllable texts that have no space in between them, so that +--- ligaturing and kerning can take place. local function syllable_rewriting() if not gregoriotex.get_if('gre@rewritesyllables') then return end diff --git a/tex/gregoriotex.lua b/tex/gregoriotex.lua index 15d3d2c7..f04916e8 100644 --- a/tex/gregoriotex.lua +++ b/tex/gregoriotex.lua @@ -955,12 +955,18 @@ local function adjust_additional_spaces(line, info, linenum) end end +--- Callback for processing before ligaturing or kerning takes place. +--- @param head node The list of nodes to be processed. +--- @return node The processed list of nodes. local function ligaturing(head) gregoriotex.save_syllable_texts(head) head = node.ligaturing(head) return head end +--- Callback for processing after a paragraph is built but before line-breaking takes place. +--- @param head node The list of nodes to be processed. +--- @return node The processed list of nodes. local function pre_linebreak(head) --dump_nodes(head) gregoriotex.scan_syllables(head) @@ -971,6 +977,8 @@ local function pre_linebreak(head) return head end +--- Add a hyphen to the end of a line. +--- @param line node The list of nodes for the line. local function add_eol_hyphen(line) -- Add an end-of-line dash to line, if necessary. @@ -1173,6 +1181,7 @@ local function get_score_font_unicode_pairs(name) return pairs(unicodes) end +--- Add GregorioTeX callbacks. local function add_callbacks() luatexbase.add_to_callback('post_linebreak_filter', post_linebreak, 'gregoriotex.post_linebreak', 1) luatexbase.add_to_callback('hyphenate', disable_hyphenation, 'gregoriotex.disable_hyphenation', 1) @@ -1180,6 +1189,7 @@ local function add_callbacks() luatexbase.add_to_callback('pre_linebreak_filter', pre_linebreak, 'gregoriotex.pre_linebreak', 1) end +--- Remove GregorioTeX callbacks. local function remove_callbacks() luatexbase.remove_from_callback('post_linebreak_filter', 'gregoriotex.post_linebreak') luatexbase.remove_from_callback('hyphenate', 'gregoriotex.disable_hyphenation') @@ -1942,7 +1952,10 @@ local function mode_part(part) end end --- this function is meant to be called from Lua +--- Test whether a syllable is the last syllable in its line. +--- Similar to is_last_syllable_on_line but meant to be called from Lua. +--- @param sid number The id of the syllable to check. +--- @return boolean Whether it is the last syllable in its line. local function is_last_syllable_id_on_line(sid) return not score_last_syllables or score_last_syllables[sid] end From 58b85fb8c58a8a1d34cd3cccab5b788b1ce8d0e0 Mon Sep 17 00:00:00 2001 From: David Chiang Date: Tue, 28 Apr 2026 16:31:05 -0400 Subject: [PATCH 06/17] fix bug in rewriting plus eol hyphen --- tex/gregoriotex-syllable.lua | 1 + tex/gregoriotex.lua | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index d3c7b4dd..aa7b9909 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -431,6 +431,7 @@ local function syllable_rewriting() syllables[sid].raw_text = nil head, tail = concat_list(head, tail, n, node.tail(n)) end + syllables[start].raw_text = node.copy_list(head) -- in case it needs a hyphen head = shaping(head) for sid = start, stop do -- Rewrite text, inserting kerns to preserve widths diff --git a/tex/gregoriotex.lua b/tex/gregoriotex.lua index f04916e8..9f3a5f3a 100644 --- a/tex/gregoriotex.lua +++ b/tex/gregoriotex.lua @@ -1000,9 +1000,9 @@ local function add_eol_hyphen(line) end end - if last_text and + if last_text and last_text_with_glyph and (has_attribute(last_text, dash_attr, dash_maybedash) or has_attribute(last_text, dash_attr, dash_forced)) then - local sid = has_attribute(last_text, syllable_id_attr) + local sid = has_attribute(last_text_with_glyph, syllable_id_attr) gregoriotex.add_hyphen(gregoriotex.syllables[sid]) end end From 775a4cd9c98850a463662c29936affa3248716f9 Mon Sep 17 00:00:00 2001 From: David Chiang Date: Tue, 28 Apr 2026 21:01:12 -0400 Subject: [PATCH 07/17] Minor reorganization --- tex/gregoriotex-syllable.lua | 66 +++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index aa7b9909..93a6294f 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -120,6 +120,40 @@ local function glue_add(a, b) return {a[1]+b[1], a[2]+b[2], a[3]+b[3]} end +-- Miscellaneous helper functions + +--- Concatenate two node lists. +--- @param head node The head of the first list. +--- @param tail node The tail of the first list. +--- @param newhead node The head of the second list. +--- @param newtail node The tail of the second list. +--- @return node The head of the concatenated list. +--- @return node The tail of the concatenated list. +local function concat_list(head, tail, newhead, newtail) + if head == nil then + return newhead, newtail + elseif newhead == nil then + return head, tail + else + tail.next = newhead + newhead.prev = tail + return head, newtail + end +end + +--- Apply ligaturing and kerning to a node list. +--- @param head node The head of the list to be processed. +--- @return node The head of the processed list. +local function shaping(head) + head = node.ligaturing(head) + head = node.kerning(head) + -- Under luaotfload, ligaturing and kerning are done inside the following + if nodes ~= nil and nodes.simple_font_handler ~= nil then + head = nodes.simple_font_handler(head) + end + return head +end + -- Table for storing information about syllables that is impossible or -- inconvenient to recover from node attributes. local syllables = {} @@ -171,38 +205,6 @@ local function free_syllables() end end ---- Concatenate two node lists. ---- @param head node The head of the first list. ---- @param tail node The tail of the first list. ---- @param newhead node The head of the second list. ---- @param newtail node The tail of the second list. ---- @return node The head of the concatenated list. ---- @return node The tail of the concatenated list. -local function concat_list(head, tail, newhead, newtail) - if head == nil then - return newhead, newtail - elseif newhead == nil then - return head, tail - else - tail.next = newhead - newhead.prev = tail - return head, newtail - end -end - ---- Apply ligaturing and kerning to a node list. ---- @param head node The head of the list to be processed. ---- @return node The head of the processed list. -local function shaping(head) - head = node.ligaturing(head) - head = node.kerning(head) - -- Under luaotfload, ligaturing and kerning are done inside the following - if nodes ~= nil and nodes.simple_font_handler ~= nil then - head = nodes.simple_font_handler(head) - end - return head -end - --- Find nodes corresponding to various parts of syllables and store them in a --- data structure more convenient for downstream processing. --- @param head node The head of the list to be processed. From 57707651221d83478a2a7db591b9976af55aba34 Mon Sep 17 00:00:00 2001 From: David Chiang Date: Wed, 29 Apr 2026 09:17:05 -0400 Subject: [PATCH 08/17] Unify attributes on skips in note and bar syllables --- tex/gregoriotex-main.tex | 8 ++++---- tex/gregoriotex-syllable.lua | 27 +++++++++++++++------------ tex/gregoriotex-syllable.tex | 30 +++++++++++++++--------------- tex/gregoriotex.lua | 4 ++-- 4 files changed, 36 insertions(+), 33 deletions(-) diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index b29c09b7..0c760cfc 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -75,10 +75,10 @@ \newattribute\gre@attr@syllable@id % attributes for horizontal spacing -% 1 = syllablefinalskip between \GreSyllables -% 2 = \GreBarSyllable from syllable left to text left -% 3 = \GreBarSyllable from text right to bar left -% 4 = \GreBarSyllable from bar right to syllable right (to do) +% 1 = syllablefinalskip after syllable and penalty +% 2 = from syllable left to text left +% 3 = from text right to notes left +% 4 = from notes right to syllable right % 5 = clearsyllable \newattribute\gre@attr@skip@type diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index 93a6294f..23650b50 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -41,7 +41,9 @@ local part_notes = 10 local skip_type_attr = luatexbase.attributes['gre@attr@skip@type'] local skip_type_syllablefinal = 1 -local skip_type_barspacing1 = 2 +local skip_type_before_text = 2 +local skip_type_text_notes = 3 +local skip_type_after_notes = 4 local skip_type_clearsyllable = 5 local dash_attr = luatexbase.attributes['gre@attr@dash'] @@ -161,12 +163,12 @@ gregoriotex.syllables = syllables --- Save information about syllables that is impossible or --- inconvenient to recover from node attributes. -local function save_syllable_info() +local function save_syllable_info(type) local sid = tex.getattribute(syllable_id_attr) if syllables[sid] == nil then syllables[sid] = {} end syllables[sid].sid = sid + syllables[sid].type = type syllables[sid].font = font.current() - log('saving font for syllable %s', sid) end --- Save syllable text before ligaturing and kerning happens. This @@ -238,10 +240,14 @@ local function scan_syllables(head) syllables[sid].first_note = n end syllables[sid].last_note = n + elseif skip_type == skip_type_before_test then + syllables[sid].before_test_skip = n + elseif skip_type == skip_type_text_notes then + syllables[sid].text_notes_skip = n + elseif skip_type == skip_type_after_notes then + syllables[sid].after_notes_skip = n elseif skip_type == skip_type_syllablefinal then syllables[sid].syllablefinalskip = n - elseif skip_type == skip_type_barspacing1 then - syllables[sid].barspacing1 = n elseif skip_type == skip_type_clearsyllable then syllables[sid].clearsyllable = n end @@ -307,12 +313,8 @@ local function add_hyphen(cur) -- Mark text as having a hyphen node.set_attribute(cur.text, dash_attr, dash_hasdash) - -- The text node is immediately followed by a kern whose size - -- is the text width. To keep the text and notes aligned, we - -- need to update this kern. - local k = cur.text.next - if k.id ~= kern then err('expected kern to follow syllable text') end - k.kern = k.kern - width_change + -- To keep the text and notes aligned, update the kern between text and notes. + cur.text_notes_skip.kern = cur.text_notes_skip.kern - width_change -- We also need to adjust the kern after the notes that moves -- to the right edge of the syllable. If this syllable ends up @@ -339,7 +341,8 @@ local function syllable_spacing() -- shouldn't have syllablefinalskip. But (due to a bug, #1724) -- if the next syllable is a clef change, it is a bar syllable -- and this syllable does have syllablefinalskip; we ignore it. - if cur.syllablefinalskip and next ~= nil and not next.barspacing1 then + if (cur.type == 'note' and cur.syllablefinalskip ~= nil and + next ~= nil and not (next.type == 'bar' and gregoriotex.get_if('gre@newbarspacing'))) then adjust_syllablefinalskip(cur, next) end diff --git a/tex/gregoriotex-syllable.tex b/tex/gregoriotex-syllable.tex index 664a0bdb..290c12e1 100644 --- a/tex/gregoriotex-syllable.tex +++ b/tex/gregoriotex-syllable.tex @@ -926,7 +926,7 @@ \gre@debugmsg{general}{New syllable: \expandafter\unexpanded{#1}}% \gre@debugmsg{general}{}% \global\advance\gre@attr@syllable@id by 1\relax % - \directlua{gregoriotex.save_syllable_info()}% + \directlua{gregoriotex.save_syllable_info('note')}% \gre@showhyphenafterthissyllablefalse% \ifcase#4\ifgre@forcehyphen% \gre@debugmsg{hyphen}{Forcing hyphen}% @@ -1075,10 +1075,8 @@ \gre@dotranslationcenterend % \gre@mustdotranslationcenterendfalse% \fi % - \gre@skip@temp@one = -\wd\gre@box@syllabletext % - \kern\gre@skip@temp@one% - \gre@skip@temp@one = -\gre@dimen@begindifference\relax% - \kern\gre@skip@temp@one % + {\gre@attr@skip@type=3 + \kern\dimexpr-\wd\gre@box@syllabletext-\gre@dimen@begindifference}% % here we need to unset \gre@attr@dash for the typesetting of notes \unsetattribute{\gre@attr@dash}% \gre@attr@part=10 @@ -1294,7 +1292,7 @@ \gre@debugmsg{general}{New bar syllable}% \gre@debugmsg{general}{}% \global\advance\gre@attr@syllable@id by 1\relax % - \directlua{gregoriotex.save_syllable_info()}% + \directlua{gregoriotex.save_syllable_info('bar')}% \gre@possibleluahyphenafterthissyllablefalse % \gre@showhyphenafterthissyllablefalse % % the algorithm of this function is *extremely* complex, and has been much painful to write... good luck to understand. @@ -1369,13 +1367,12 @@ \fi% \GreNoBreak % %move to the beginning of the text - \gre@attr@skip@type=2 + {\gre@attr@skip@type=2 \gre@hskip\glueexpr(\gre@skip@bar@allocation/2% right from end of previous notes to nominal middle of bar line +\gre@dimen@bar@shift% from nominal middle of bar line to actual middle -\wd\gre@box@syllablenotes/2% back up to beginning of bar line +\gre@dimen@begindifference% from beginning of bar line to beginning of text - +\gre@space@skip@bar@rubber)\relax % the rubber component - \unsetattribute{\gre@attr@skip@type}% + +\gre@space@skip@bar@rubber)}% the rubber component \GreNoBreak % % all that extra stuff (translations and the like) #8% @@ -1390,10 +1387,9 @@ \gre@mustdotranslationcenterendfalse% \fi % %move back to the beginning of the bar line - \gre@attr@skip@type=2 + {\gre@attr@skip@type=3 \kern\dimexpr(\gre@dimen@enddifference% move from end of text to end of bar line - -\wd\gre@box@syllablenotes)\relax % back up from end of bar line to beginning - \unsetattribute{\gre@attr@skip@type}% + -\wd\gre@box@syllablenotes)}% back up from end of bar line to beginning \GreNoBreak% \gre@attr@part=10 \ifgre@shownotes% @@ -1428,11 +1424,14 @@ \global\let\gre@newlinecommon\gre@saved@prelinedelay@newlinecommon % \GreNoBreak% % get into position to place the penalty + {\gre@attr@skip@type=4 \ifdim\gre@dimen@enddifference < 0pt\relax% % the text extends past the notes, so we need to get to the end of the text \kern-\gre@dimen@enddifference% - \GreNoBreak% - \fi% + \else + \kern0pt + \fi}% + \GreNoBreak \ifgre@eolshiftsenabled% \ifgre@endofscore% \kern\glueexpr(-\gre@skip@bar@lastskip)\relax% @@ -1477,10 +1476,11 @@ % adjustment for alterations: \kern-\gre@skip@alterationshift % %move to the beginning of the notes of the next syllable + {\gre@attr@skip@type=1 \gre@hskip\glueexpr(-\wd\gre@box@syllablenotes/2% back up to middle of notes -\gre@dimen@bar@shift% go back from actual middle to nominal middle of bar line +\gre@skip@bar@allocation/2% go from nominal middle to the start of the next notes - +\gre@space@skip@bar@rubber)\relax % the rubber component + +\gre@space@skip@bar@rubber)}% the rubber component \ifdim\gre@skip@nextbegindifference < 0pt\relax% %we need to move back to where the text for the next syllable should start \GreNoBreak % diff --git a/tex/gregoriotex.lua b/tex/gregoriotex.lua index 9f3a5f3a..2344ba74 100644 --- a/tex/gregoriotex.lua +++ b/tex/gregoriotex.lua @@ -415,10 +415,10 @@ local function dump_nodes_helper(head, indent) if node.subtypes(n.id) ~= nil then subtype = node.subtypes(n.id)[n.subtype] end - local attrs = format("syllable=%s,part=%s,dash=%s", + local attrs = format("syllable=%s,part=%s,skip=%s", has_attribute(n, syllable_id_attr), has_attribute(n, part_attr), - has_attribute(n, dash_attr) + has_attribute(n, skip_type_attr) ) if n.id == hlist or n.id == vlist then log(dots .. "%s [%s] width=%.2fpt height=%.2fpt depth=%.2fpt shift=%.2fpt {%s}", type, subtype, n.width/2^16, n.height/2^16, n.depth/2^16, n.shift/2^16, attrs) From 1fd30bbc03e27f13fccb7dadae437eb595b8862f Mon Sep 17 00:00:00 2001 From: David Chiang Date: Wed, 29 Apr 2026 11:40:50 -0400 Subject: [PATCH 09/17] - Mark skips with skip type attribute more completely - Handle discretionaries (which disrupt syllable numbering) more correctly --- tex/gregoriotex-main.tex | 3 ++- tex/gregoriotex-syllable.lua | 50 +++++++++++++++++++++++++++--------- tex/gregoriotex-syllable.tex | 20 +++++++++++---- tex/gregoriotex.lua | 6 ++++- 4 files changed, 60 insertions(+), 19 deletions(-) diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index 0c760cfc..be462fbf 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -1324,6 +1324,7 @@ \def\gre@endafterbar#1{% \gre@trace{gre@endafterbar{#1}}% \gre@penalty{\the\gre@space@count@endafterbarpenalty }\relax % + {\gre@attr@skip@type=1 \ifnum#1=1\relax % \gre@debugmsg{ifdim}{ enddifference > 0pt}% \ifdim\gre@dimen@enddifference > 0 pt\relax% @@ -1345,7 +1346,7 @@ \gre@hskip\gre@skip@temp@four % \fi % \fi % - \fi % + \fi}% %\gre@penalty{\the\gre@space@count@endafterbarpenalty }\relax %\global\gre@dimen@enddifference=0pt \relax % diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index 23650b50..b669cd20 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -215,26 +215,38 @@ local function scan_syllables(head) for _, cur in pairs(syllables) do cur.first_note = nil end + local prev_sid local function visit(head) for n in node.traverse(head) do - -- to do: The two syllables in a discretionary are numbered - -- differently, meaning that in the output, the syllables are - -- not necessarily numbered consecutively. if n.id == disc then + -- Recurse into all three parts of a discretionary node. + local save_prev_sid = prev_sid visit(n.pre) visit(n.post) + prev_sid = save_prev_sid visit(n.replace) else local sid = has_attribute(n, syllable_id_attr) local part = has_attribute(n, part_attr) local skip_type = has_attribute(n, skip_type_attr) - if sid ~= nil then - if syllables[sid] == nil then syllables[sid] = {} end + if sid ~= nil and syllables[sid] ~= nil then + -- Record first and last node + if part ~= nil or skip_type ~= nil then + if syllables[sid].first == nil then + syllables[sid].first = n + end + end + syllables[sid].last = n if part == part_lyrics then if syllables[sid].text ~= nil then err(' syllable %d has more than one text node', sid) end syllables[sid].text = n + -- Since every syllable is guaranteed to have exactly one text node, + -- do some other bookkeeping here + syllables[sid].prev_sid = prev_sid + if prev_sid ~= nil then syllables[prev_sid].next_sid = sid end + prev_sid = sid elseif part == part_notes then if syllables[sid].first_note == nil then syllables[sid].first_note = n @@ -263,14 +275,20 @@ end --- @param cur table The current syllable. --- @param next table The next syllable. local function adjust_syllablefinalskip(cur, next) - local text_distance = node.dimensions(cur.text.next, next.text) + local text_distance = ( + node.dimensions(cur.text.next, cur.last.next) + + node.dimensions(next.first, next.text) + ) debugmessage('syllablespacing', ' text distance = %s', glue_to_string(text_distance)) local min_text_distance = cur.min_text_distance debugmessage('syllablespacing', ' min text distance = %s', glue_to_string(min_text_distance)) local min_text_shift = glue_add(min_text_distance, -text_distance) debugmessage('syllablespacing', ' min text shift = %s', glue_to_string(min_text_shift)) - local notes_distance = node.dimensions(cur.last_note.next, next.first_note) + local notes_distance = ( + node.dimensions(cur.last_note.next, cur.last.next) + + node.dimensions(next.first, next.first_note) + ) debugmessage('syllablespacing', ' notes distance = %s', glue_to_string(notes_distance)) local min_notes_distance = cur.min_notes_distance debugmessage('syllablespacing', ' min notes distance = %s', glue_to_string(min_notes_distance)) @@ -335,7 +353,7 @@ end local function syllable_spacing() for sid, cur in pairs(syllables) do debugmessage('syllablespacing', 'after syllable %d', sid) - local next = syllables[sid+1] -- to do: correctly handle discretionaries + local next = syllables[cur.next_sid] -- If the next syllable is a bar syllable, then this syllable -- shouldn't have syllablefinalskip. But (due to a bug, #1724) @@ -350,7 +368,10 @@ local function syllable_spacing() -- If there is too much space between text, add a hyphen if (cur.text ~= nil and has_attribute(cur.text, dash_attr, dash_maybedash) and next ~= nil and next.text ~= nil) then - local text_distance = node.dimensions(cur.text.next, next.text) + local text_distance = ( + node.dimensions(cur.text.next, cur.last.next) + + node.dimensions(next.first, next.text) + ) local max_distance = tex.sp(token.get_macro('gre@space@dimen@maximumspacewithoutdash')) if text_distance > max_distance then needs_hyphen = true end end @@ -374,19 +395,21 @@ end --- Clear all syllables that are marked for clearing. local function syllable_clearing() for sid, cur in pairs(syllables) do - local prev = syllables[sid-1] + local prev = syllables[cur.prev_sid] if cur.clearsyllable and prev then debugmessage('clear', 'syllable %d', sid) local kern = 0 -- current text must begin at or after prev notes' end if prev.last_note and cur.text then - local overlap = -node.dimensions(prev.last_note.next, cur.text) + local overlap = -(node.dimensions(prev.last_note.next, prev.last.next) + + node.dimensions(cur.first, cur.text)) debugmessage('clear', ' text-note overlap %fpt', overlap/2^16) kern = math.max(kern, overlap) end -- current notes must begin at or after prev text's end if prev.text and cur.first_note then - local overlap = -node.dimensions(prev.text.next, cur.first_note) + local overlap = -(node.dimensions(prev.text.next, prev.last.next) + + node.dimensions(cur.first, cur.first_note)) debugmessage('clear', ' note-text overlap %fpt', overlap/2^16) kern = math.max(kern, overlap) end @@ -406,6 +429,9 @@ local function syllable_rewriting() while start <= num_syllables do -- Find longest run of syllables, starting from start, that have -- zero distance between their text boxes. + -- Note: It's safe to assume that consecutive syllables are numbered consecutively, + -- because we don't rewrite into or out of discretionaries. If this changes, then + -- the code below must be updated accordingly. if syllables[start].text == nil then debugmessage('syllablerewriting', 'syllable %d has no text node', start) start = start + 1 diff --git a/tex/gregoriotex-syllable.tex b/tex/gregoriotex-syllable.tex index 290c12e1..e26cdbdd 100644 --- a/tex/gregoriotex-syllable.tex +++ b/tex/gregoriotex-syllable.tex @@ -1063,10 +1063,11 @@ \fi% % then we reuse temp, we assign to it the \gre@dimen@begindifference, but only if it is positive, else it is 0 \gre@debugmsg{ifdim}{ begindifference > 0pt}% + {\gre@attr@skip@type=2 \ifdim\gre@dimen@begindifference > 0 pt\relax% \gre@skip@temp@one = \gre@dimen@begindifference\relax% \kern\gre@skip@temp@one % - \fi% + \fi}% #8\relax % \raise\gre@space@dimen@spacebeneathtext \copy\gre@box@syllabletext @@ -1546,12 +1547,13 @@ \fi % \GreNoBreak % \gre@debugmsg{ifdim}{ temp@skip@two > -wd(gre@box@syllablenotes)}% + {\gre@attr@skip@type=2 \ifdim\gre@skip@temp@two > -\wd\gre@box@syllablenotes % \kern\gre@skip@temp@two % \else % \gre@skip@temp@one = -\wd\gre@box@syllablenotes % \kern\gre@skip@temp@one% - \fi % + \fi}% \GreNoBreak % #8\relax % \ifgre@mustdotranslationcenterend% @@ -1560,6 +1562,11 @@ \gre@mustdotranslationcenterendfalse% \fi % \gre@attr@part=10 + % In case there are no notes, ensure that there is at least one + % box with the attribute. + \ifdim\wd\gre@box@syllablenotes=0pt + \hbox{}% + \fi \ifgre@shownotes% #9\relax % \fi% @@ -1570,6 +1577,7 @@ \global\gre@lastoflinecount=2\relax % \else % \gre@debugmsg{ifdim}{ temp@skip@two < -wd(gre@box@syllablenotes)}% + {\gre@attr@skip@type=1 \ifdim\gre@skip@temp@two < -\wd\gre@box@syllablenotes % \gre@debugmsg{ifdim}{ nextbegindifference > 0pt}% \ifdim\gre@skip@nextbegindifference > 0 pt\relax% @@ -1592,7 +1600,7 @@ \gre@skip@temp@one = -\wd\gre@box@syllablenotes % \gre@hskip\gre@skip@temp@one % \fi % - \fi % + \fi}% \fi % % then the most simple : the case where there is something to write under the bar. We just need to adjust the spaces. \else %ifdim\wd\gre@box@syllabletext = 0 pt @@ -1603,10 +1611,11 @@ \gre@dotranslationcenterend % \gre@mustdotranslationcenterendfalse% \fi % + {\gre@attr@skip@type=3 \gre@skip@temp@one = -\wd\gre@box@syllabletext % \kern\gre@skip@temp@one % \gre@skip@temp@one = -\gre@dimen@begindifference\relax% - \kern\gre@skip@temp@one % + \kern\gre@skip@temp@one}% \gre@attr@part=10 \ifgre@shownotes% #9% @@ -1615,8 +1624,9 @@ \gre@debugmsg{ifdim}{ enddifference < 0pt}% \ifdim\gre@dimen@enddifference <0pt\relax% %% important, else we are not really at the end of the syllable + {\gre@attr@skip@type=4 \gre@skip@temp@one = -\gre@dimen@enddifference\relax% - \kern\gre@skip@temp@one % + \kern\gre@skip@temp@one}% \fi% % end of same code as syllable \ifnum\gre@lastoflinecount=1\relax % diff --git a/tex/gregoriotex.lua b/tex/gregoriotex.lua index 2344ba74..bbc0d27b 100644 --- a/tex/gregoriotex.lua +++ b/tex/gregoriotex.lua @@ -968,7 +968,11 @@ end --- @param head node The list of nodes to be processed. --- @return node The processed list of nodes. local function pre_linebreak(head) - --dump_nodes(head) + -- There are some lists that are not scores (e.g., braces) that we + -- don't want to process. The current heuristic is to skip the list + -- if it has zero width. + if node.dimensions(head) == 0 then return head end + dump_nodes(head) gregoriotex.scan_syllables(head) gregoriotex.syllable_spacing() gregoriotex.syllable_clearing() From 4caf9a9d146ea05d27a25cd5d3144cf2280aa1e5 Mon Sep 17 00:00:00 2001 From: David Chiang Date: Wed, 29 Apr 2026 11:57:33 -0400 Subject: [PATCH 10/17] Improve spacing before clef changes without bars --- tex/gregoriotex-syllable.lua | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index b669cd20..7a89597a 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -354,13 +354,11 @@ local function syllable_spacing() for sid, cur in pairs(syllables) do debugmessage('syllablespacing', 'after syllable %d', sid) local next = syllables[cur.next_sid] - - -- If the next syllable is a bar syllable, then this syllable - -- shouldn't have syllablefinalskip. But (due to a bug, #1724) - -- if the next syllable is a clef change, it is a bar syllable - -- and this syllable does have syllablefinalskip; we ignore it. - if (cur.type == 'note' and cur.syllablefinalskip ~= nil and - next ~= nil and not (next.type == 'bar' and gregoriotex.get_if('gre@newbarspacing'))) then + + -- If the next syllable is a clef change without a bar, there is still a + -- syllablefinalskip in between. As far as the new bar spacing algorithm is concerned, + -- this skip is part of both the text and notes of the current syllable (issue #1724). + if (cur.type == 'note' and cur.syllablefinalskip ~= nil and next ~= nil) then adjust_syllablefinalskip(cur, next) end From 9fbf3323a10c81242dd4c0706137eedd71cc7c71 Mon Sep 17 00:00:00 2001 From: David Chiang Date: Thu, 30 Apr 2026 17:31:09 -0400 Subject: [PATCH 11/17] Fix typos --- tex/gregoriotex-syllable.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index 7a89597a..0b9b875f 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -253,7 +253,7 @@ local function scan_syllables(head) end syllables[sid].last_note = n elseif skip_type == skip_type_before_test then - syllables[sid].before_test_skip = n + syllables[sid].before_text_skip = n elseif skip_type == skip_type_text_notes then syllables[sid].text_notes_skip = n elseif skip_type == skip_type_after_notes then From f0fe40d0c9869c85137804ea5db20c6d45e91651 Mon Sep 17 00:00:00 2001 From: David Chiang Date: Fri, 1 May 2026 13:24:56 -0400 Subject: [PATCH 12/17] Remove dash attribute 4 (which had little to do with dashes) --- tex/gregoriotex-main.tex | 1 - tex/gregoriotex-syllable.lua | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index be462fbf..2c84d074 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -60,7 +60,6 @@ % 1 = the syllable may need a dash added (in Lua) if at the end of a line % 2 = the syllable doesn't need a dash because it already has one % 3 = the syllable doesn't need a dash because it's word-final -% 4 = the syllable doesn't need a dash because it is a \GreBarSyllable % 5 = the syllable needs a forced dash \newattribute\gre@attr@dash diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index 0b9b875f..e980c61f 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -49,7 +49,6 @@ local skip_type_clearsyllable = 5 local dash_attr = luatexbase.attributes['gre@attr@dash'] local dash_maybedash = 1 local dash_hasdash = 2 -local dash_barsyllable = 4 local dash_forced = 5 -- Functions for manipulating glue, which we just store as a 3-tuple @@ -444,8 +443,7 @@ local function syllable_rewriting() -- don't rewrite across a hyphen if has_attribute(syllables[stop].text, dash_attr, dash_hasdash) then break end -- if either syllable is a \GreBarSyllable - if has_attribute(syllables[stop].text, dash_attr, dash_barsyllable) or - has_attribute(syllables[stop+1].text, dash_attr, dash_barsyllable) then break end + if not (syllables[stop].type == 'note' and syllables[stop+1].type == 'note') then break end -- don't rewrite across a nonzero space if node.dimensions(syllables[stop].text.next, syllables[stop+1].text) ~= 0 then break end stop = stop + 1 From be39ebb125a9129e5e1969416c48c46b0350af54 Mon Sep 17 00:00:00 2001 From: David Chiang Date: Sun, 3 May 2026 17:47:31 -0400 Subject: [PATCH 13/17] CHANGELOG and version --- CHANGELOG.md | 2 +- VersionManager.py | 2 ++ tex/gregoriotex-syllable.lua | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f032454b..f55ba37c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file. As of v3.0.0 this project adheres to [Semantic Versioning](http://semver.org/). It follows [some conventions](http://keepachangelog.com/). ## [Unreleased][develop] - +- Code for several features related to horizontal spacing (spacing between non-bar syllables, syllable rewriting, clearing, and hyphenation) was moved into Lua. This results in some changes in horizontal spacing, which, if perceptible, should be improvements. See [#1720](https://github.com/gregorio-project/gregorio/issues/1720). ## [Unreleased][CTAN] *Note:* 6.2.0 was not released to CTAN and is not compatible with 6.1.0 which is on CTAN. Please make all changes against develop until this is resolved. diff --git a/VersionManager.py b/VersionManager.py index 4fb61d25..f0f748a8 100755 --- a/VersionManager.py +++ b/VersionManager.py @@ -57,6 +57,7 @@ "tex/gregoriotex-symbols.tex", "tex/gregoriotex-symbols.lua", "tex/gregoriotex-syllable.tex", + "tex/gregoriotex-syllable.lua", "tex/gregoriotex-main.tex", "tex/gregoriotex-nabc.tex", "tex/gregoriotex-nabc.lua", @@ -78,6 +79,7 @@ "tex/Makefile.am", "tex/gregoriotex-common.tex", "tex/gregoriotex-syllable.tex", + "tex/gregoriotex-syllable.lua", "tex/gregoriotex.lua", "tex/gregoriotex.sty", "tex/gregoriosyms.sty", diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index e980c61f..55480f5a 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -19,7 +19,7 @@ -- This file contains Lua functions to support spacing of syllables. --- GREGORIO_VERSION 6.1.0 +-- GREGORIO_VERSION 6.2.0 local err = gregoriotex.module.err local warn = gregoriotex.module.warn From ea045b9d2ae49f07ca65c45d50eafb684d081bfc Mon Sep 17 00:00:00 2001 From: David Chiang Date: Mon, 4 May 2026 22:22:50 -0400 Subject: [PATCH 14/17] In \gre@newlinecommon, try to explain better why so many kerns are needed --- tex/gregoriotex-main.tex | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index 48a1cb18..8048426c 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -228,7 +228,11 @@ \ifgre@boxing\else% \global\gre@lastoflinecount=2\relax % \ifnum#2=0\relax % - % we have to repeat the end of syllable shifts here because the manual line breaks will occur before we get to the regular shifting code in \GreSyllable + % Place the penalty similarly to how the end-of-syllable penalty + % is placed. However, if the line break occurs mid-syllable, + % these kerns as well as the end-of-syllable kerns will be + % incorrect because they use the enddifference of the whole + % syllable (issue #1738). \ifdim\gre@dimen@enddifference <0pt\relax% %% important, else we are not really at the end of the syllable \kern -\gre@dimen@enddifference\relax% @@ -257,12 +261,11 @@ \hfill \fi \gre@penalty{-10001}% - % Above, we emitted kerns of -enddifference and -eolshift early, - % and they will be emitted again later in \GreSyllable. The - % second set of kerns has no effect because they are at the - % beginning of a line. But in order that Lua pre_linebreak can - % measure distances accurately, we emit a third set of kerns to - % cancel out the second one. + % Having placed the penalty, we now return to where we were + % before. Because the penalty is guaranteed to cause a line + % break, these kerns will be discarded because they are at the + % beginning of a line. But we do this so that Lua pre_linebreak + % can measure distances accurately. \ifdim\gre@dimen@enddifference<0pt \kern \gre@dimen@enddifference \fi From 6b2c63706a807a50dab15c98d7afdd4dc2943871 Mon Sep 17 00:00:00 2001 From: David Chiang Date: Tue, 5 May 2026 09:20:15 -0400 Subject: [PATCH 15/17] Remove \gre@attr@score from docs --- doc/Command_Index_internal.tex | 3 --- 1 file changed, 3 deletions(-) diff --git a/doc/Command_Index_internal.tex b/doc/Command_Index_internal.tex index 1d0612c4..b707b7c3 100644 --- a/doc/Command_Index_internal.tex +++ b/doc/Command_Index_internal.tex @@ -1839,9 +1839,6 @@ \subsection{Flags} \macroname{\textbackslash ifgre@endofscore}{}{gregoriotex-syllable.tex} Boolean to mark the last syllable of the score. -\macroname{\textbackslash gre@attr@score}{}{gregoriotex-main.tex} -A Lua\TeX\ attribute which indicates whether a line is part of a score. - \macroname{\textbackslash ifgre@firstglyph}{}{gregoriotex-syllable.tex} Boolean that tells us if the current glyph is the first glyph or not. From f256ea3afed9fc3605495e3628f3d96b04fa197b Mon Sep 17 00:00:00 2001 From: David Chiang Date: Wed, 6 May 2026 21:34:45 -0400 Subject: [PATCH 16/17] Fix syllable rewriting, which was broken by commit a48c861348f42654c17ce27d0b0a6cbca281282d --- doc/Command_Index_internal.tex | 3 --- tex/gregoriotex-main.tex | 7 ------ tex/gregoriotex-syllable.lua | 28 ++++++++++++++--------- tex/gregoriotex-syllable.tex | 19 ++++++---------- tex/gregoriotex.lua | 41 +++++++++++++++++----------------- 5 files changed, 45 insertions(+), 53 deletions(-) diff --git a/doc/Command_Index_internal.tex b/doc/Command_Index_internal.tex index b707b7c3..e0b85cea 100644 --- a/doc/Command_Index_internal.tex +++ b/doc/Command_Index_internal.tex @@ -1845,9 +1845,6 @@ \subsection{Flags} \macroname{\textbackslash ifgre@rewritesyllables}{}{gregoriotex-syllable.tex} Boolean that enables moving the last part of a syllable to the next if there is no hyphen. -\macroname{\textbackslash gre@attr@dash}{}{gregoriotex-main.tex} -A Lua\TeX\ attribute which indicates whether a syllable takes a dash if it ends a line. - \macroname{\textbackslash gre@attr@center}{}{gregoriotex-main.tex} A Lua\TeX\ attribute which indicates the type of translation centering. diff --git a/tex/gregoriotex-main.tex b/tex/gregoriotex-main.tex index 8048426c..a41a1dd6 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -51,13 +51,6 @@ \newattribute\gre@attr@part \edef\gre@attrid@part{\the\allocationnumber} -% an attribute we put on the text nodes. -% 1 = the syllable may need a dash added (in Lua) if at the end of a line -% 2 = the syllable doesn't need a dash because it already has one -% 3 = the syllable doesn't need a dash because it's word-final -% 5 = the syllable needs a forced dash -\newattribute\gre@attr@dash - % an attribute used for translation centering \newattribute\gre@attr@center diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index d7864f9b..12d6819c 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -46,7 +46,7 @@ local skip_type_text_notes = 3 local skip_type_after_notes = 4 local skip_type_clearsyllable = 5 -local dash_attr = luatexbase.attributes['gre@attr@dash'] +--- Possible values of syllables[sid].dash local dash_maybedash = 1 local dash_hasdash = 2 local dash_forced = 5 @@ -160,8 +160,17 @@ end local syllables = {} gregoriotex.syllables = syllables +--- Return the data structure for the current syllable. +--- @return node The syllable. +local function current_syllable() + local sid = tex.getattribute(syllable_id_attr) + if syllables[sid] == nil then syllables[sid] = {} end + return syllables[sid] +end + --- Save information about syllables that is impossible or --- inconvenient to recover from node attributes. +--- @param type string Type of syllable ('bar' or 'note') local function save_syllable_info(type) local sid = tex.getattribute(syllable_id_attr) if syllables[sid] == nil then syllables[sid] = {} end @@ -174,10 +183,7 @@ end --- is needed later during syllable rewriting. --- @param head node The syllable text. local function save_syllable_texts(head) - -- Because syllable_id_attr is set even for material not in the - -- syllable text, it's better to use dash_attr to detect whether - -- this box is really syllable text. - if tex.getattribute(dash_attr) > 0 then + if tex.getattribute(part_attr) == part_lyrics then local sid = tex.getattribute(syllable_id_attr) local cur = head while cur ~= nil and cur.id == temp do cur = cur.next end @@ -299,7 +305,7 @@ local function adjust_syllablefinalskip(cur, next) syllablefinalskip = glue_add(syllablefinalskip, glue_max(min_text_shift, min_notes_shift)) -- If this syllable has a hyphen, add some additional stretch. -- Note: This happens even if there is no text (\gresetlyrics{invisible}). - if cur.text and has_attribute(cur.text, dash_attr, dash_hasdash) then + if cur.text and cur.dash == dash_hasdash then debugmessage('syllablespacing', ' adding stretch for hyphen') syllablefinalskip = glue_add(syllablefinalskip, string_to_glue(token.get_macro('gre@space@skip@intersyllablespacestretchhyphen'))) end @@ -328,7 +334,7 @@ local function add_hyphen(cur) local width_change = new_width - old_width -- Mark text as having a hyphen - node.set_attribute(cur.text, dash_attr, dash_hasdash) + cur.dash = dash_hasdash -- To keep the text and notes aligned, update the kern between text and notes. cur.text_notes_skip.kern = cur.text_notes_skip.kern - width_change @@ -362,7 +368,7 @@ local function syllable_spacing() local needs_hyphen = false -- If there is too much space between text, add a hyphen - if (cur.text ~= nil and has_attribute(cur.text, dash_attr, dash_maybedash) and + if (cur.text ~= nil and cur.dash == dash_maybedash and next ~= nil and next.text ~= nil) then local text_distance = ( node.dimensions(cur.text.next, cur.last.next) + @@ -372,7 +378,7 @@ local function syllable_spacing() if text_distance > max_distance then needs_hyphen = true end end -- If hyphen was forced, add a hyphen - if cur.text ~= nil and has_attribute(cur.text, dash_attr, dash_forced) then + if cur.text ~= nil and cur.dash == dash_forced then needs_hyphen = true end -- If lyrics are disabled, don't add a hyphen @@ -440,7 +446,7 @@ local function syllable_rewriting() -- don't rewrite across a line break if gregoriotex.is_last_syllable_id_on_line(stop) then break end -- don't rewrite across a hyphen - if has_attribute(syllables[stop].text, dash_attr, dash_hasdash) then break end + if syllables[stop].dash == dash_hasdash then break end -- if either syllable is a \GreBarSyllable if not (syllables[stop].type == 'note' and syllables[stop+1].type == 'note') then break end -- don't rewrite across a nonzero space @@ -472,6 +478,7 @@ local function syllable_rewriting() concat_list(head, tail, kern) else syllables[sid].text.head = kern + syllables[sid].is_merged = true end end end @@ -483,6 +490,7 @@ end gregoriotex.save_syllable_info = save_syllable_info gregoriotex.save_syllable_texts = save_syllable_texts gregoriotex.save_min_distances = save_min_distances +gregoriotex.current_syllable = current_syllable gregoriotex.free_syllables = free_syllables gregoriotex.scan_syllables = scan_syllables gregoriotex.syllable_spacing = syllable_spacing diff --git a/tex/gregoriotex-syllable.tex b/tex/gregoriotex-syllable.tex index d2ef94f2..c71d65ea 100644 --- a/tex/gregoriotex-syllable.tex +++ b/tex/gregoriotex-syllable.tex @@ -1008,6 +1008,7 @@ \global\gre@attr@alteration@id=\gre@saved@attr@alteration@id\relax % \gre@calculate@nextbegindifference{\gre@evaluatenextsyllable{\gre@nextfirstsyllablepart}}{\gre@evaluatenextsyllable{\gre@nextmiddlesyllablepart}}{\gre@evaluatenextsyllable{\gre@nextendsyllablepart}}{\gre@nextalignment}{\gre@nextalteration}% \gre@unsetfixednexttextformat % + \gre@attr@part=4 \ifgre@showlyrics% \setbox\gre@box@syllabletext=\hbox{% \IfSubStr{\gre@debug}{,notespacing,}% @@ -1027,6 +1028,7 @@ \setbox\gre@box@syllabletext=\hbox{}% \fi \fi% + \unsetattribute{\gre@attr@part}% \gre@calculate@enddifference{\wd\gre@box@syllablenotes}{\wd\gre@box@syllabletext}{\gre@dimen@textaligncenter}{\gre@dimen@notesaligncenter}{1}% % gre@count@temp@one holds 1 if next is a bar, 2 if an alteration, else 0 \gre@count@temp@one=0% @@ -1039,25 +1041,21 @@ \fi % \gre@debugmsg{spacing}{ gre@count@temp@one = \the\gre@count@temp@one}% \gre@calculate@syllablefinalskip{#4}{\gre@count@temp@one}% - \gre@attr@part=4 % text \ifcase#4 \global\gre@possibleluahyphenafterthissyllabletrue \ifgre@showhyphenafterthissyllable - \gre@attr@dash=5 % needs forced hyphen + \directlua{gregoriotex.current_syllable().dash=5}% needs forced hyphen \else - \gre@attr@dash=1 % maybe needs hyphen + \directlua{gregoriotex.current_syllable().dash=1}% maybe needs hyphen \fi \else \global\gre@possibleluahyphenafterthissyllablefalse \ifgre@showhyphenafterthissyllable - \gre@attr@dash=5 % needs forced hyphen + \directlua{gregoriotex.current_syllable().dash=5}% needs forced hyphen \else - \gre@attr@dash=3 % end of word + \directlua{gregoriotex.current_syllable().dash=3}% end of word \fi \fi - \setbox\gre@box@syllabletext=\hbox{\unhbox\gre@box@syllabletext}% - \unsetattribute{\gre@attr@dash}% - \unsetattribute{\gre@attr@part}% \ifgre@textcleared% % Insert a zero kern that will be adjusted in the Lua function syllable_clearing. {\gre@attr@skip@type=5 \kern0pt}% @@ -1079,8 +1077,6 @@ \fi % {\gre@attr@skip@type=3 \kern\dimexpr-\wd\gre@box@syllabletext-\gre@dimen@begindifference}% - % here we need to unset \gre@attr@dash for the typesetting of notes - \unsetattribute{\gre@attr@dash}% \gre@attr@part=10 \GreNoBreak % no line breaks between text and notes \ifgre@shownotes% @@ -1303,7 +1299,7 @@ % first of all we need to calculate previousenddifference, begindifference, enddifference and nextbegindifference. #1% \gre@calculate@textaligncenter{\gre@firstsyllablepart}{\gre@middlesyllablepart}{0}% - \gre@attr@dash=4 + \directlua{gregoriotex.current_syllable().dash=4}% \gre@attr@part=4 \ifgre@showlyrics% \setbox\gre@box@syllabletext=\hbox{% @@ -1324,7 +1320,6 @@ \setbox\gre@box@syllabletext=\hbox{}% \fi \fi% - \unsetattribute{\gre@attr@dash}% \unsetattribute{\gre@attr@part}% \gre@debugmsg{barspacing}{Width of bar text: \the\wd\gre@box@syllabletext}% \global\let\gre@saved@prelinedelay@newlinecommon\gre@newlinecommon % diff --git a/tex/gregoriotex.lua b/tex/gregoriotex.lua index da685ce0..fbe4b461 100644 --- a/tex/gregoriotex.lua +++ b/tex/gregoriotex.lua @@ -78,11 +78,10 @@ local part_annotation = 9 local skip_type_attr = luatexbase.attributes['gre@attr@skip@type'] -local dash_attr = luatexbase.attributes['gre@attr@dash'] +--- Possible values of syllables[sid].dash local dash_maybedash = 1 local dash_hasdash = 2 local dash_endofword = 3 -local dash_barsyllable = 4 local dash_forced = 5 local center_attr = luatexbase.attributes['gre@attr@center'] @@ -971,7 +970,7 @@ local function pre_linebreak(head) -- don't want to process. The current heuristic is to skip the list -- if it has zero width. if node.dimensions(head) == 0 then return head end - dump_nodes(head) + --dump_nodes(head) gregoriotex.scan_syllables(head) gregoriotex.syllable_spacing() gregoriotex.syllable_clearing() @@ -985,28 +984,28 @@ end local function add_eol_hyphen(line) -- Add an end-of-line dash to line, if necessary. - local last_text, last_text_with_glyph - - -- Look for the last text node in the line that has - -- dash_attr. If it is dash_maybedash, then we may need to - -- append a dash. Due to syllable rewriting, the actual text - -- may be in a node further to the left. So, we also look for - -- the last text node that actually contains a glyph, and the - -- last glyph in that node. - + -- Find the last syllable on the line. + local last_sid for n in traverse_id(hlist, line.head) do - if has_attribute(n, dash_attr) then - last_text = n - for g in node.traverse_id(glyph, n.head) do - last_text_with_glyph = n - end + if has_attribute(n, part_attr, part_lyrics) then + last_sid = has_attribute(n, syllable_id_attr) end end - if last_text and last_text_with_glyph and - (has_attribute(last_text, dash_attr, dash_maybedash) or has_attribute(last_text, dash_attr, dash_forced)) then - local sid = has_attribute(last_text_with_glyph, syllable_id_attr) - gregoriotex.add_hyphen(gregoriotex.syllables[sid]) + if last_sid ~= nil then + debugmessage('hyphenation', 'last syllable on line: %d', last_sid) + -- Check if the last syllable needs a hyphen + if (gregoriotex.syllables[last_sid].dash == dash_maybedash or + gregoriotex.syllables[last_sid].dash == dash_forced) then + debugmessage('hyphenation', 'syllable %d needs hyphen', last_sid) + -- Due to syllable rewriting, the actual text may be in a syllable further to the left. + while last_sid ~= nil and gregoriotex.syllables[last_sid].is_merged do + debugmessage('hyphenation', 'syllable %d has been merged', last_sid) + last_sid = gregoriotex.syllables[last_sid].prev_sid + end + debugmessage('hyphenation', 'adding hyphen to syllable %d', last_sid) + gregoriotex.add_hyphen(gregoriotex.syllables[last_sid]) + end end end From e28d6514bf4b77783328ce90e70fce71ef69675e Mon Sep 17 00:00:00 2001 From: David Chiang Date: Thu, 7 May 2026 15:52:43 -0400 Subject: [PATCH 17/17] Previously, point-and-click inserted markers that were blocking kerning/ligaturing after syllable rewriting. This fixes the problem. When two syllables are merged, the link for the first syllable spans both syllables, and the original link for the second syllable is lost. --- tex/gregoriotex-syllable.lua | 66 ++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/tex/gregoriotex-syllable.lua b/tex/gregoriotex-syllable.lua index 12d6819c..c5f7010a 100644 --- a/tex/gregoriotex-syllable.lua +++ b/tex/gregoriotex-syllable.lua @@ -32,6 +32,7 @@ local kern = node.id('kern') local temp = node.id('temp') local disc = node.id('disc') local glyph = node.id('glyph') +local whatsit = node.id('whatsit') local syllable_id_attr = luatexbase.attributes['gre@attr@syllable@id'] @@ -123,21 +124,26 @@ end -- Miscellaneous helper functions ---- Concatenate two node lists. ---- @param head node The head of the first list. ---- @param tail node The tail of the first list. ---- @param newhead node The head of the second list. ---- @param newtail node The tail of the second list. ---- @return node The head of the concatenated list. ---- @return node The tail of the concatenated list. -local function concat_list(head, tail, newhead, newtail) +--- Insert one node list into another. +--- @param head node The head of the list to insert into. +--- @param where node The node after which newhead will be inserted. +--- @param newhead node The head of the list to insert. +--- @param newtail node The tail of the list to insert. +--- @return node The head of the new list. +--- @return node The new insertion point. +local function insert_list_after(head, where, newhead, newtail) if head == nil then return newhead, newtail elseif newhead == nil then - return head, tail + return head, where else - tail.next = newhead - newhead.prev = tail + local rest = where.next + where.next = newhead + newhead.prev = where + if rest ~= nil then + newtail.next = rest + rest.prev = newtail + end return head, newtail end end @@ -161,7 +167,7 @@ local syllables = {} gregoriotex.syllables = syllables --- Return the data structure for the current syllable. ---- @return node The syllable. +--- @return table The syllable. local function current_syllable() local sid = tex.getattribute(syllable_id_attr) if syllables[sid] == nil then syllables[sid] = {} end @@ -313,6 +319,27 @@ local function adjust_syllablefinalskip(cur, next) node.setglue(cur.syllablefinalskip, table.unpack(syllablefinalskip)) end +--- Append material to the end of a syllable's raw_text. +--- @param cur table The current syllable. +--- @param head node The material to append. +local function add_to_raw_text(cur, head) + -- Both cur.raw_text and head may be surrounded by markers (for + -- point-and-click links). To allow ligaturing and kerning to + -- occur, we need to discard head's markers and insert before + -- cur.raw_text's closing marker. + + local last = cur.raw_text and node.tail(cur.raw_text) + local tail = head and node.tail(head) + if last ~= nil and last.id == whatsit then last = last.prev end + if head ~= nil and head.id == whatsit then head = node.free(head) end + if tail ~= nil and tail.id == whatsit then + local del = tail + tail = tail.prev + node.free(del) + end + cur.raw_text = insert_list_after(cur.raw_text, last, head, tail) +end + --- Add a hyphen to the end of a syllable's text. --- @param cur table The current syllable. local function add_hyphen(cur) @@ -320,10 +347,9 @@ local function add_hyphen(cur) local g = node.new(glyph) g.font = cur.font g.char = gregoriotex.hyphen + -- Find last glyph (because the last node may be a marker) - local last = node.tail(cur.raw_text) - while last ~= nil and last.id ~= glyph do last = last.prev end - cur.raw_text = node.insert_after(cur.raw_text, last, g) + add_to_raw_text(cur, g) -- Replace actual syllable text local old_width = cur.text.width @@ -456,15 +482,13 @@ local function syllable_rewriting() -- Concatenate syllable text boxes into one box. if start < stop then debugmessage('syllablerewriting', 'merge syllables %d-%d', start, stop) - local head, tail - for sid = start, stop do + for sid = start+1, stop do -- Extend new text local n = syllables[sid].raw_text syllables[sid].raw_text = nil - head, tail = concat_list(head, tail, n, node.tail(n)) + add_to_raw_text(syllables[start], n, node.tail(n)) end - syllables[start].raw_text = node.copy_list(head) -- in case it needs a hyphen - head = shaping(head) + local head = shaping(node.copy_list(syllables[start].raw_text)) for sid = start, stop do -- Rewrite text, inserting kerns to preserve widths local del = syllables[sid].text.head @@ -475,7 +499,7 @@ local function syllable_rewriting() if sid == start then syllables[sid].text.head = head kern.kern = kern.kern - node.dimensions(head) - concat_list(head, tail, kern) + syllables[sid].text.head = node.insert_after(head, tail, kern) else syllables[sid].text.head = kern syllables[sid].is_merged = true