diff --git a/CHANGELOG.md b/CHANGELOG.md index e7de07e4..408d7f97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ As of v3.0.0 this project adheres to [Semantic Versioning](http://semver.org/). - Fixed a bug that could cause problems with headers/footers by generalizing the fix for fancyhdr (see [#1610](https://github.com/gregorio-project/gregorio/pull/1610) to work with any headers/footers. In particular, it fixes a similar problem with the `memoir` class. See [#1753](https://github.com/gregorio-project/gregorio/pull/1753). - Fixed some typos that would cause a handful of spaces to be scaled incorrectly. See [PR #1746](https://github.com/gregorio-project/gregorio/pull/1746). - Fixed a few bugs related to horizontal spacing around bars and clef changes. See issues [#1191](https://github.com/gregorio-project/gregorio/issues/1191), case 3 of [#1724](https://github.com/gregorio-project/gregorio/issues/1724), [PR #1743](https://github.com/gregorio-project/gregorio/pull/1743), and [#1745](https://github.com/gregorio-project/gregorio/issues/1745). +- 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/doc/Command_Index_gregorio.tex b/doc/Command_Index_gregorio.tex index be17c035..7db18c10 100644 --- a/doc/Command_Index_gregorio.tex +++ b/doc/Command_Index_gregorio.tex @@ -1352,9 +1352,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 b54b7434..e0b85cea 100644 --- a/doc/Command_Index_internal.tex +++ b/doc/Command_Index_internal.tex @@ -136,14 +136,13 @@ \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} - \#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} @@ -248,16 +247,15 @@ \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} - \#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} @@ -1152,43 +1150,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. @@ -1884,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. @@ -2430,18 +2388,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 04372644..a41a1dd6 100644 --- a/tex/gregoriotex-main.tex +++ b/tex/gregoriotex-main.tex @@ -47,20 +47,13 @@ % 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} -% 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... -\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 @@ -68,6 +61,14 @@ % attribute for syllable tracking \newattribute\gre@attr@syllable@id +% attributes for horizontal spacing +% 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 + % attributes for soft alterations % Alterations are numbered consecutively starting from 1. If an @@ -220,7 +221,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% @@ -249,6 +254,17 @@ \hfill \fi \gre@penalty{-10001}% + % 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 + \ifgre@eolshiftsenabled + \kern \gre@dimen@eolshift + \fi \fi \fi % %% @@ -650,6 +666,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% @@ -681,7 +698,7 @@ \endgre@style@abovelinestext \hss}% \fi% - \unsetattribute{\gre@attr@part}% + }% restore \gre@attr@part \gre@trace@end% }% @@ -720,12 +737,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) @@ -1118,8 +1133,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 +1254,6 @@ \fi % \gre@computespaces% \gre@cancelpenalties % - \gre@attr@dash=0\relax % \gre@generatelines % \noindent% \gre@calculate@additionalspaces @@ -1289,7 +1301,6 @@ \setbox\gre@box@commentary=\box\voidb@x% \directlua{gregoriotex.at_score_end()}% \gre@unset@glyph@heights - \unsetattribute{\gre@attr@dash}% \xdef\gre@bolshiftcleftypelocal{\gre@bolshiftcleftypeglobal}% \ifnum\gre@count@lastline=0\relax \parfillskip=\gre@saved@parfillskip\relax% @@ -1301,6 +1312,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% @@ -1322,7 +1334,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-spaces.tex b/tex/gregoriotex-spaces.tex index 7205a295..d6d56226 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% @@ -911,24 +848,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 +903,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 % @@ -1238,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% }% @@ -1305,61 +1242,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 new file mode 100644 index 00000000..c5f7010a --- /dev/null +++ b/tex/gregoriotex-syllable.lua @@ -0,0 +1,523 @@ +--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 spacing of syllables. + +-- GREGORIO_VERSION 6.2.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 disc = node.id('disc') +local glyph = node.id('glyph') +local whatsit = node.id('whatsit') + +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_before_text = 2 +local skip_type_text_notes = 3 +local skip_type_after_notes = 4 +local skip_type_clearsyllable = 5 + +--- Possible values of syllables[sid].dash +local dash_maybedash = 1 +local dash_hasdash = 2 +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' + 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 + +--- 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 + 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 + +--- 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 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 + return {a[1]+b[1], a[2]+b[2], a[3]+b[3]} +end + +-- Miscellaneous helper functions + +--- 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, where + else + 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 + +--- 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 = {} +gregoriotex.syllables = syllables + +--- Return the data structure for the current syllable. +--- @return table 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 + syllables[sid].sid = sid + syllables[sid].type = type + syllables[sid].font = font.current() +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) + 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 + if syllables[sid] == nil then syllables[sid] = {} end + syllables[sid].raw_text = node.copy_list(cur) + 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 + local g = tex.skip['gre@skip@minTextDistance'] + syllables[sid].min_text_distance = {g.width, g.stretch, g.shrink} + g = tex.skip['gre@skip@minNotesDistance'] + 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) + syllables[sid] = nil + end +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) + 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 + 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 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 + end + syllables[sid].last_note = n + elseif skip_type == skip_type_before_test then + 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 + syllables[sid].after_notes_skip = n + elseif skip_type == skip_type_syllablefinal then + syllables[sid].syllablefinalskip = n + elseif skip_type == skip_type_clearsyllable then + syllables[sid].clearsyllable = n + end + end + end + end + end + 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, 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, 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)) + 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 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 + debugmessage('syllablespacing', ' syllable final skip = %s', glue_to_string(syllablefinalskip)) + 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) + -- 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) + add_to_raw_text(cur, 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 + 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 + + -- 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, then 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[cur.next_sid] + + -- 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 + + local needs_hyphen = false + -- If there is too much space between text, add a hyphen + 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) + + 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 + -- If hyphen was forced, add a hyphen + if cur.text ~= nil and cur.dash == 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 + end + end +end + +--- Clear all syllables that are marked for clearing. +local function syllable_clearing() + for sid, cur in pairs(syllables) do + 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, 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, 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 + debugmessage('clear', ' kern %fpt', kern/2^16) + cur.clearsyllable.kern = kern + end + 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 + + local start = 1 + 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. + -- 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 + else + local stop = start + while stop+1 <= num_syllables do + -- There are several conditions that prevent syllable rewriting: + -- if either text node is missing + 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 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 + 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. + if start < stop then + debugmessage('syllablerewriting', 'merge syllables %d-%d', start, stop) + for sid = start+1, stop do + -- Extend new text + local n = syllables[sid].raw_text + syllables[sid].raw_text = nil + add_to_raw_text(syllables[start], n, node.tail(n)) + end + 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 + syllables[sid].text.head = nil + node.flush_list(del) + local kern = node.new(kern, 'userkern') + kern.kern = syllables[sid].text.width + if sid == start then + syllables[sid].text.head = head + kern.kern = kern.kern - node.dimensions(head) + syllables[sid].text.head = node.insert_after(head, tail, kern) + else + syllables[sid].text.head = kern + syllables[sid].is_merged = true + end + end + end + start = stop + 1 + end + end +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 +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 0a47a784..c71d65ea 100644 --- a/tex/gregoriotex-syllable.tex +++ b/tex/gregoriotex-syllable.tex @@ -561,7 +561,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% @@ -601,9 +601,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% @@ -901,93 +898,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{% @@ -1013,27 +923,11 @@ %% 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}{}% - % 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('note')}% \gre@showhyphenafterthissyllablefalse% \ifcase#4\ifgre@forcehyphen% \gre@debugmsg{hyphen}{Forcing hyphen}% @@ -1042,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. @@ -1098,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 @@ -1114,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@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@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}}% @@ -1130,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% + \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% @@ -1147,130 +1041,50 @@ \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 % - \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@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@endsyllablepart#3{\GreHyph}\relax}{#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@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@endsyllablepart#3{\GreHyph}\relax}{#6}}}}% - \else - \setbox\gre@box@syllabletext=\box\voidb@x% - \fi - \fi% - \else % - \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@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@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@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@endsyllablepart#3{-}}{#6}}}}% - \else - \setbox\gre@box@syllabletext=\box\voidb@x% - \fi - \fi% - \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@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 - % 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@emit@syllabletext{\gre@pointandclick{\gre@firstsyllablepart\gre@middlesyllablepart\gre@emit@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=\box\voidb@x% - \fi% - \else % - \global\gre@possibleluahyphenafterthissyllablefalse % - \fi % - \fi% + \ifcase#4 + \global\gre@possibleluahyphenafterthissyllabletrue + \ifgre@showhyphenafterthissyllable + \directlua{gregoriotex.current_syllable().dash=5}% needs forced hyphen + \else + \directlua{gregoriotex.current_syllable().dash=1}% maybe needs hyphen + \fi + \else + \global\gre@possibleluahyphenafterthissyllablefalse + \ifgre@showhyphenafterthissyllable + \directlua{gregoriotex.current_syllable().dash=5}% needs forced hyphen + \else + \directlua{gregoriotex.current_syllable().dash=3}% end of word + \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}% + {\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 - \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 % \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 % - % here we need to unset \gre@attr@dash for the typesetting of notes - \gre@attr@dash=0\relax % + {\gre@attr@skip@type=3 + \kern\dimexpr-\wd\gre@box@syllabletext-\gre@dimen@begindifference}% + \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 @@ -1297,6 +1111,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% @@ -1306,7 +1121,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 % @@ -1398,17 +1212,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% @@ -1421,14 +1231,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 % @@ -1436,6 +1246,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}% @@ -1471,6 +1290,7 @@ \gre@debugmsg{general}{New bar syllable}% \gre@debugmsg{general}{}% \global\advance\gre@attr@syllable@id by 1\relax % + \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. @@ -1478,26 +1298,29 @@ % 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}% + \directlua{gregoriotex.current_syllable().dash=4}% + \gre@attr@part=4 \ifgre@showlyrics% \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}}% {}% 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% + \setbox\gre@box@syllabletext=\hbox{}% \fi \fi% + \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 % @@ -1519,7 +1342,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}% @@ -1541,18 +1364,19 @@ \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 + +\gre@space@skip@bar@rubber)}% the rubber component \GreNoBreak % % all that extra stuff (translations and the like) #8% \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 @@ -1560,9 +1384,11 @@ \gre@mustdotranslationcenterendfalse% \fi % %move back to the beginning of the bar line + {\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 + -\wd\gre@box@syllablenotes)}% back up from end of bar line to beginning \GreNoBreak% + \gre@attr@part=10 \ifgre@shownotes% \IfSubStr{\gre@debug}{,barspacing,}% % when debugging we add a zero-width line to mark the syllable bound @@ -1591,14 +1417,18 @@ \fi \fi \fi% + \unsetattribute{\gre@attr@part}% \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% @@ -1644,10 +1474,11 @@ \kern-\gre@skip@alterationshift % \GreNoBreak %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 % @@ -1713,12 +1544,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% @@ -1726,15 +1558,23 @@ \gre@dotranslationcenterend % \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% + \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 % \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% @@ -1757,7 +1597,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 @@ -1768,18 +1608,22 @@ \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% \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 + {\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 % @@ -1792,7 +1636,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 aea8f633..fbe4b461 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 @@ -62,6 +63,8 @@ end local hyphen = tex.defaulthyphenchar or 45 +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 @@ -73,9 +76,13 @@ local part_nabc = 7 local part_blnabc = 8 local part_annotation = 9 -local dash_attr = luatexbase.attributes['gre@attr@dash'] -local potentialdashvalue = 1 -local nopotentialdashvalue = 2 +local skip_type_attr = luatexbase.attributes['gre@attr@skip@type'] + +--- Possible values of syllables[sid].dash +local dash_maybedash = 1 +local dash_hasdash = 2 +local dash_endofword = 3 +local dash_forced = 5 local center_attr = luatexbase.attributes['gre@attr@center'] local startcenter = 1 @@ -88,8 +95,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 @@ -399,53 +404,50 @@ 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,skip=%s", + has_attribute(n, syllable_id_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}", 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 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 + 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 font=%d {%s}", charname, n.font, 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) 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 @@ -951,10 +953,65 @@ 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) + -- 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() + gregoriotex.syllable_rewriting() + --dump_nodes(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. + + -- Find the last syllable on the line. + local last_sid + for n in traverse_id(hlist, line.head) do + if has_attribute(n, part_attr, part_lyrics) then + last_sid = has_attribute(n, syllable_id_attr) + end + end + + 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 + 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 @@ -981,7 +1038,7 @@ local function post_linebreak(h, groupcode, glyphes) new_score_last_syllables[syl_id] = syl_id end end - + -- Line height adjustment. if tex.count['gre@variableheightexpansion'] == 0 then -- uniform local info @@ -1041,36 +1098,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 - -- 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 + add_eol_hyphen(line) end --dump_nodes(h) @@ -1152,16 +1180,20 @@ end --- Add GregorioTeX callbacks. local function add_callbacks() - debugmessage('callbacks', 'adding post_linebreak and hyphenate callbacks') + debugmessage('callbacks', 'adding 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 --- Remove GregorioTeX callbacks. local function remove_callbacks() - debugmessage('callbacks', 'removing post_linebreak and hyphenate callbacks') + debugmessage('callbacks', 'removing 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 --- Called when a page is full and is about to be shipped out. @@ -1216,6 +1248,7 @@ local function at_score_beginning(score_id) new_score_first_alterations = {} new_first_alterations[score_id] = new_score_first_alterations end + add_callbacks() luatexbase.add_to_callback('pre_output_filter', pre_output, 'gregoriotex.pre_output') luatexbase.add_to_callback('buildpage_filter', buildpage, 'gregoriotex.buildpage') @@ -1229,6 +1262,7 @@ local function at_score_end() luatexbase.remove_from_callback('buildpage_filter', 'gregoriotex.buildpage') per_line_dims = {} per_line_counts = {} + gregoriotex.free_syllables() end -- Inserted copy of https://github.com/ToxicFrog/luautil/blob/master/lfs.lua @@ -1923,6 +1957,14 @@ local function mode_part(part) end end +--- 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 + -- 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 @@ -1998,7 +2040,12 @@ gregoriotex.change_next_score_line_dim = change_next_score_line_dim 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.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')) dofile(kpse.find_file('gregoriotex-signs.lua', 'lua')) +dofile(kpse.find_file('gregoriotex-syllable.lua', 'lua')) dofile(kpse.find_file('gregoriotex-symbols.lua', 'lua'))