texlive[63413] Master/texmf-dist: lua-widow-control (27may22)

commits+karl at tug.org commits+karl at tug.org
Fri May 27 22:41:39 CEST 2022


Revision: 63413
          http://tug.org/svn/texlive?view=revision&revision=63413
Author:   karl
Date:     2022-05-27 22:41:39 +0200 (Fri, 27 May 2022)
Log Message:
-----------
lua-widow-control (27may22)

Modified Paths:
--------------
    trunk/Master/texmf-dist/doc/luatex/lua-widow-control/README.md
    trunk/Master/texmf-dist/doc/luatex/lua-widow-control/lua-widow-control.pdf
    trunk/Master/texmf-dist/doc/luatex/lua-widow-control/tb133chernoff-widows.pdf
    trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-manual.mkxl
    trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-manual.tex
    trunk/Master/texmf-dist/tex/context/third/lua-widow-control/t-lua-widow-control.mkxl
    trunk/Master/texmf-dist/tex/lualatex/lua-widow-control/lua-widow-control-2022-02-22.sty
    trunk/Master/texmf-dist/tex/lualatex/lua-widow-control/lua-widow-control.sty
    trunk/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.lua
    trunk/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.tex
    trunk/Master/texmf-dist/tex/optex/lua-widow-control/lua-widow-control.opm

Added Paths:
-----------
    trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-manual-samples.tex

Removed Paths:
-------------
    trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-sample.tex

Modified: trunk/Master/texmf-dist/doc/luatex/lua-widow-control/README.md
===================================================================
--- trunk/Master/texmf-dist/doc/luatex/lua-widow-control/README.md	2022-05-27 20:41:15 UTC (rev 63412)
+++ trunk/Master/texmf-dist/doc/luatex/lua-widow-control/README.md	2022-05-27 20:41:39 UTC (rev 63413)
@@ -9,7 +9,7 @@
 
 Lua-widow-control is a Plain TeX/LaTeX/ConTeXt/OpTeX package that removes widows and orphans without any user intervention. Using the power of LuaTeX, it does so _without_ stretching any glue or shortening any pages or columns. Instead, lua-widow-control automatically lengthens a paragraph on a page or column where a widow or orphan would otherwise occur.
 
-Please see the [**package documentation**](https://github.com/gucci-on-fleek/lua-widow-control/releases/latest/download/lua-widow-control.pdf) for usage details or the [***TUGboat* article**](https://github.com/gucci-on-fleek/lua-widow-control/releases/latest/download/tb133chernoff-widows.pdf) for background information and discussion.
+Please see the [**package manual**](https://github.com/gucci-on-fleek/lua-widow-control/releases/latest/download/lua-widow-control.pdf) for usage details or the [***TUGboat* article**](https://github.com/gucci-on-fleek/lua-widow-control/releases/latest/download/tb133chernoff-widows.pdf) for background information and discussion.
 
 Usage
 -----
@@ -48,4 +48,4 @@
 Please note that a compiled document is absolutely **not** considered to be an "Executable Form" as defined by the MPL. The use of lua-widow-control in a document does not place **any** obligations on the document's author or distributors. The MPL and CC-BY-SA licenses **only** apply to you if you distribute the lua-widow-control source code or documentation.
 
 ---
-_v2.1.1 (2022-05-20)_ <!--%%version %%dashdate-->
+_v2.1.2 (2022-05-26)_ <!--%%version %%dashdate-->

Modified: trunk/Master/texmf-dist/doc/luatex/lua-widow-control/lua-widow-control.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/luatex/lua-widow-control/tb133chernoff-widows.pdf
===================================================================
(Binary files differ)

Added: trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-manual-samples.tex
===================================================================
--- trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-manual-samples.tex	                        (rev 0)
+++ trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-manual-samples.tex	2022-05-27 20:41:39 UTC (rev 63413)
@@ -0,0 +1,178 @@
+% lua-widow-control
+% https://github.com/gucci-on-fleek/lua-widow-control
+% SPDX-License-Identifier: MPL-2.0+ OR CC-BY-SA-4.0+
+% SPDX-FileCopyrightText: 2022 Max Chernoff
+
+\startbuffer[demo-text]
+    \definepapersize[smallpaper][
+        width=6cm,
+        height=8.3cm
+    ]\setuppapersize[smallpaper]
+
+    \setuplayout[
+        topspace=0.1cm,
+        backspace=0.1cm,
+        width=middle,
+        height=middle,
+        header=0pt,
+        footer=0pt,
+    ]
+
+    \def\lwc/{\sans{lua-\allowbreak widow-\allowbreak control}}
+    \def\Lwc/{\sans{Lua-\allowbreak widow-\allowbreak control}}
+
+    \setupbodyfont[9pt]
+    \setupindenting[yes, 2em]
+
+    \definepalet[layout][grid=middlegray]
+    \showgrid[nonumber, none, lines]
+
+    \definefontfeature[default][default][expansion=quality,protrusion=quality]
+
+    \usetypescript[modern-base]
+    \setupbodyfont[reset,modern]
+
+    \setupalign[hz,hanging,tolerant]
+
+    \setuplanguage[en][spacing=packed]
+
+    \starttext
+        \Lwc/ can remove most widows and orphans from a document, \emph{without} stretching any glue or shortening any pages.
+
+        It does so by automatically lengthening a paragraph on a page where a widow or orphan would otherwise occur. While \TeX{} breaks paragraphs into their natural length, \lwc/ is breaking the paragraph 1~line longer than its natural length. \TeX{}'s paragraph is output to the page, but \lwc/'s paragraph is just stored for later. When a widow or orphan occurs, \lwc/ can take over. It selects the previously-saved paragraph with the least badness; then, it replaces \TeX{}'s paragraph with its saved paragraph. This increases the text block height of the page by 1~line.
+
+        Now, the last line of the current page can be pushed to the top of the next page. This removes the widow or the orphan without creating any additional work.
+    \stoptext
+\stopbuffer
+\savebuffer[list=demo-text]
+
+\startbuffer[shorten]
+    \parskip=0pt
+    \input lwc-manual-demo-text.tmp
+\stopbuffer
+
+\startbuffer[shorten-code]
+    \parskip=0pt
+
+    \clubpenalty=10000
+    \widowpenalty=10000
+\stopbuffer
+
+\startbuffer[stretch]
+    \parskip=0pt plus 1fill
+    \input lwc-manual-demo-text.tmp
+\stopbuffer
+
+\startbuffer[stretch-code]
+    \parskip=0pt plus 1fill
+
+    \clubpenalty=10000
+    \widowpenalty=10000
+\stopbuffer
+
+\startbuffer[ignore]
+    \startsetups[*default]
+        \clubpenalty=0
+        \widowpenalty=0
+        \displaywidowpenalty=0
+        \interlinepenalty=0
+        \brokenpenalty=0
+    \stopsetups
+
+    \setups[*default]
+
+    \input lwc-manual-demo-text.tmp
+\stopbuffer
+
+\startbuffer[ignore-code]
+    \parskip=0pt
+
+    \clubpenalty=0
+    \widowpenalty=0
+\stopbuffer
+
+\startbuffer[lwc]
+    \usemodule[lua-widow-control]
+    \input lwc-manual-demo-text.tmp
+\stopbuffer
+
+\startbuffer[lwc-code]
+    \usepackage{lua-widow-control}
+\stopbuffer
+
+\startbuffer[widow-orphan]
+    % TODO This is all really quite hacky
+    \define[1]\rulewords{\dorecurse{#1}{\blackrule[height=1.5ex, width=1em] \blackrule[height=1.5ex, width=2em] \blackrule[height=1.5ex, width=1.5em] \blackrule[height=1.5ex, width=3em] }}
+
+    \define[2]\fakestart{\framed[width=broad, align=normal, frame=off]{\parfillskip=0pt\spaceskip=0.2em plus 1fill\hskip 5em\rulewords{#1}\blackrule[height=1.5ex, width=#2]}}
+
+    \define[2]\fakeend{\framed[width=broad, align=normal, frame=off]{\parfillskip=5em\spaceskip=0.2em plus 1fill\rulewords{#1}\blackrule[height=1.5ex, width=#2]}}
+
+    \setupTABLE[width=broad, frame=off]
+    \setupTABLE[row][1][align=middle, style=\ssbf]
+    \setupTABLE[row][2][align=low, frame=on]
+    \setupTABLE[row][3][align=high, frame=on]
+    \setupTABLE[column][2][frame=off, width=1em]
+    \startTABLE
+        \NC Widow \NC\NC Orphan \NC\NR
+        \NC\fakestart{5}{1.5em}\NC\NC\fakestart{1}{2em}\NC\NR
+        \NC\fakeend{1}{2em}\NC\NC\fakeend{5}{1.5em}\NC\NR
+    \stopTABLE
+\stopbuffer
+
+\startbuffer[nobreak]
+    % This is also really hacky and terrible
+    \parfillskip=0pt
+    \define\lineone{%
+        \hbox to 0.3\textwidth{\blackrule[height=1.5ex, width=1em]\hfill%
+        \blackrule[height=1.5ex, width=3em]\hfill%
+        \blackrule[height=1.5ex, width=1em]\hfill%
+        \blackrule[height=1.5ex, width=3em]\hfill%
+        \blackrule[height=1.5ex, width=1em]}%
+    }
+
+    \define\linetwo{%
+        \hbox to 0.3\textwidth{\blackrule[height=1.5ex, width=2em]\hfill%
+        \blackrule[height=1.5ex, width=1em]\hfill%
+        \blackrule[height=1.5ex, width=2.5em]\hfill%
+        \blackrule[height=1.5ex, width=1em]\hfill%
+        \blackrule[height=1.5ex, width=2.5em]}%
+    }
+
+    \define\linethree{%
+        \hbox to 0.3\textwidth{\blackrule[height=1.5ex, width=1em]\hfill%
+        \blackrule[height=1.5ex, width=3em]\hfill%
+        \blackrule[height=1.5ex, width=1em]\hfill%
+        \blackrule[height=1.5ex, width=3em]\hfill%
+        \hskip1em\relax}%
+    }
+
+    \define\heading{\bold{Heading}}
+
+    \setupTABLE[
+        width=broad,
+        topframe=off,
+        bottomframe=off,
+        leftframe=on,
+        rightframe=on,
+        height=1.2\baselineskip
+    ]
+    \setupTABLE[row][1][
+        align=middle,
+        style=\ttbf,
+        bottomframe=on,
+        leftframe=off,
+        rightframe=off
+    ]
+    \setupTABLE[row][3][bottomframe=on]
+    \setupTABLE[row][5][bottomframe=on]
+    \setupTABLE[column][2][bottomframe=off, width=1em]
+    \setupTABLE[column][4][bottomframe=off, width=1em]
+    \startTABLE
+        \NC keep     \NC\NC split    \NC\NC warn     \NC\NR
+        \NC          \NC\NC          \NC\NC \heading \NC\NR
+        \NC          \NC\NC \heading \NC\NC \lineone    \NC\NR
+        \NC \heading \NC\NC \lineone \NC\NC \linetwo    \NC\NR
+        \NC \lineone \NC\NC \linetwo \NC\NC \linethree  \NC\NR
+    \stopTABLE
+\stopbuffer


Property changes on: trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-manual-samples.tex
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Modified: trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-manual.mkxl
===================================================================
--- trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-manual.mkxl	2022-05-27 20:41:15 UTC (rev 63412)
+++ trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-manual.mkxl	2022-05-27 20:41:39 UTC (rev 63413)
@@ -93,7 +93,7 @@
 
 % Acronym styling
 \definecharacterkerning[acronymkerning][factor=0.05]
-\definealternativestyle[acronymstyle][{\feature[+][allsmall]\switchtobodyfont[1.1em]\setcharacterkerning[acronymkerning]}][]
+\definealternativestyle[acronymstyle][{\feature[+][allsmall]\switchtobodyfont[1.2em]\setcharacterkerning[acronymkerning]}][]
 \definehighlight[acronym][style=acronymstyle]
 
 \startsetups[commandtable]

Modified: trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-manual.tex
===================================================================
--- trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-manual.tex	2022-05-27 20:41:15 UTC (rev 63412)
+++ trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-manual.tex	2022-05-27 20:41:39 UTC (rev 63413)
@@ -37,12 +37,12 @@
 \definetype[inlineTEX][option=tex, escape={$,$}, lines=hyphenated]
 \definetype[inlineLUA][option=lua, escape={$,$}, lines=hyphenated]
 
-\input lwc-sample
+\input lwc-manual-samples
 
 \startdocument[
     title=lua-widow-control,
     author=Max Chernoff,
-    version=2.1.1, %%version
+    version=2.1.2, %%version
     github=https://github.com/gucci-on-fleek/lua-widow-control,
     ctan=https://www.ctan.org/pkg/lua-widow-control,
 ]
@@ -621,7 +621,7 @@
 as it does with \waos/ between pages.
 
 \Lwc/ is known to work with the \LaTeX{} class option \type{twocolumn}
-and the two-column output routine from Chapter~23 of \cite[texbook].
+and the two-column output routine from Chapter~23 of \titlecite[texbook].
 
 \subsection{Performance}
 
@@ -648,11 +648,19 @@
 exactly as it would a \woo/. So while these commands won't break \lwc/, they
 are likely to lead to some unexpected behaviour.
 
+\subsection{Grids}
+
+\Lwc/ is fully compatible with the grid snapping features of \ConTeXt{} \mkiv/ and \mkxl/.
+
+\subsection{Footnotes}
+
+If there are footnotes (or any other type of inline \tex{insert}) present in the moved line, \lwc/ will move both the \q{footnote mark} and the \q{footnote text} such that both are on the same page. However, this may lead to an odd blank space at the bottom of the page since \lwc/ needs to move both the line and its footnotes. Footnotes cause the same page-breaking issues in unmodified Plain~\TeX{} and \LaTeX{}, so this is mostly unavoidable.
+
 \section{Stability}
 
 The documented interfaces of \lwc/ can be considered stable: I'm not planning on removing or modifying any existing options or commands in any way that would break documents.
 
-However, \lwc/'s page breaking \emph{is} subject to change. I will attempt to keep page breaks the same wherever reasonable; however, I will rarely make modifications to the algorithm when I can improve the output quality.
+However, \lwc/'s page breaking \emph{is} subject to change. I will attempt to keep page breaks the same whenever reasonable; however, I will \emph{rarely} make modifications to the algorithm when I can improve the output quality. Any such changes will be clearly noted in the release notes.
 
 \section{Short last lines}
 
@@ -681,6 +689,8 @@
 
     \item \Lwc/ only attempts to expand paragraphs; it never attempts to shrink them. See the \emph{TUGboat}~article\cite[article] §15.3 for further discussion. \githubissue{33}
 
+    \item \Lwc/ can only expand paragraphs that fit completely on a page. This is unavoidable due to the one-page-at-a-time model: you can't modify the bottom half of a paragraph since its top half has already shipped out, and you can't expand the top half of a paragraph since that can't remove orphans. This only causes issues if your document has paragraphs so long that a page only has two half-paragraphs and zero whole paragraphs.
+
     \item Sometimes a \woo/ cannot be eliminated because no paragraph has
     enough stretch. Sometimes this can be remediated by
     increasing \lwc/'s \estretch/; however, some pages just don't have
@@ -701,7 +711,7 @@
     impossible to solve in a single pass. Very rarely would such a
     system remove widows or orphans that \lwc/ cannot.
 
-    \item If there is a footnote on the last line of the page with a \woo/, \lwc/ will sometimes move the \q{footnote mark} but not the \q{footnote text}, thus breaking up a footnote. \githubissue{26}
+    \item \Lwc/ does not move footnotes in \mkxl/ due to limitations with the \LuaMetaTeX{} engine.
 \stopitemize
 
 \section{Contributions}
@@ -726,6 +736,12 @@
 ]
 \section[sec:implementation]{Implementation}
 
+From here and until the end of this manual is the raw source code of \lwc/. This is primarily of interest to developers; most users need not read further.
+
+This code vaguely resembles the typical \LaTeX{} literate programming style, although I use extensive inline comments instead of arcane \sans{docstrip} macros. Hopefully this is  useful as a reference for advanced \lwc/ users as well as anyone doing extensive node manipulation in \LuaTeX{}.
+
+If want to offer any improvements to the code below, please open an issue or a \acronym{PR} on \goto{GitHub}[url(projecturl)].
+
 \usemodule[scite]
 \setupbodyfont[10pt]
 \setuphead[subsection][

Deleted: trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-sample.tex
===================================================================
--- trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-sample.tex	2022-05-27 20:41:15 UTC (rev 63412)
+++ trunk/Master/texmf-dist/source/luatex/lua-widow-control/lwc-sample.tex	2022-05-27 20:41:39 UTC (rev 63413)
@@ -1,178 +0,0 @@
-% lua-widow-control
-% https://github.com/gucci-on-fleek/lua-widow-control
-% SPDX-License-Identifier: MPL-2.0+ OR CC-BY-SA-4.0+
-% SPDX-FileCopyrightText: 2022 Max Chernoff
-
-\startbuffer[demo-text]
-    \definepapersize[smallpaper][
-        width=6cm,
-        height=8.3cm
-    ]\setuppapersize[smallpaper]
-
-    \setuplayout[
-        topspace=0.1cm,
-        backspace=0.1cm,
-        width=middle,
-        height=middle,
-        header=0pt,
-        footer=0pt,
-    ]
-
-    \def\lwc/{\sans{lua-\allowbreak widow-\allowbreak control}}
-    \def\Lwc/{\sans{Lua-\allowbreak widow-\allowbreak control}}
-
-    \setupbodyfont[9pt]
-    \setupindenting[yes, 2em]
-
-    \definepalet[layout][grid=middlegray]
-    \showgrid[nonumber, none, lines]
-
-    \definefontfeature[default][default][expansion=quality,protrusion=quality]
-
-    \usetypescript[modern-base]
-    \setupbodyfont[reset,modern]
-
-    \setupalign[hz,hanging,tolerant]
-
-    \setuplanguage[en][spacing=packed]
-
-    \starttext
-        \Lwc/ can remove most widows and orphans from a document, \emph{without} stretching any glue or shortening any pages.
-
-        It does so by automatically lengthening a paragraph on a page where a widow or orphan would otherwise occur. While \TeX{} breaks paragraphs into their natural length, \lwc/ is breaking the paragraph 1~line longer than its natural length. \TeX{}'s paragraph is output to the page, but \lwc/'s paragraph is just stored for later. When a widow or orphan occurs, \lwc/ can take over. It selects the previously-saved paragraph with the least badness; then, it replaces \TeX{}'s paragraph with its saved paragraph. This increases the text block height of the page by 1~line.
-
-        Now, the last line of the current page can be pushed to the top of the next page. This removes the widow or the orphan without creating any additional work.
-    \stoptext
-\stopbuffer
-\savebuffer[list=demo-text]
-
-\startbuffer[shorten]
-    \parskip=0pt
-    \input lwc-manual-demo-text.tmp
-\stopbuffer
-
-\startbuffer[shorten-code]
-    \parskip=0pt
-
-    \clubpenalty=10000
-    \widowpenalty=10000
-\stopbuffer
-
-\startbuffer[stretch]
-    \parskip=0pt plus 1fill
-    \input lwc-manual-demo-text.tmp
-\stopbuffer
-
-\startbuffer[stretch-code]
-    \parskip=0pt plus 1fill
-
-    \clubpenalty=10000
-    \widowpenalty=10000
-\stopbuffer
-
-\startbuffer[ignore]
-    \startsetups[*default]
-        \clubpenalty=0
-        \widowpenalty=0
-        \displaywidowpenalty=0
-        \interlinepenalty=0
-        \brokenpenalty=0
-    \stopsetups
-
-    \setups[*default]
-
-    \input lwc-manual-demo-text.tmp
-\stopbuffer
-
-\startbuffer[ignore-code]
-    \parskip=0pt
-
-    \clubpenalty=0
-    \widowpenalty=0
-\stopbuffer
-
-\startbuffer[lwc]
-    \usemodule[lua-widow-control]
-    \input lwc-manual-demo-text.tmp
-\stopbuffer
-
-\startbuffer[lwc-code]
-    \usepackage{lua-widow-control}
-\stopbuffer
-
-\startbuffer[widow-orphan]
-    % TODO This is all really quite hacky
-    \define[1]\rulewords{\dorecurse{#1}{\blackrule[height=1.5ex, width=1em] \blackrule[height=1.5ex, width=2em] \blackrule[height=1.5ex, width=1.5em] \blackrule[height=1.5ex, width=3em] }}
-
-    \define[2]\fakestart{\framed[width=broad, align=normal, frame=off]{\parfillskip=0pt\spaceskip=0.2em plus 1fill\hskip 5em\rulewords{#1}\blackrule[height=1.5ex, width=#2]}}
-
-    \define[2]\fakeend{\framed[width=broad, align=normal, frame=off]{\parfillskip=5em\spaceskip=0.2em plus 1fill\rulewords{#1}\blackrule[height=1.5ex, width=#2]}}
-
-    \setupTABLE[width=broad, frame=off]
-    \setupTABLE[row][1][align=middle, style=\ssbf]
-    \setupTABLE[row][2][align=low, frame=on]
-    \setupTABLE[row][3][align=high, frame=on]
-    \setupTABLE[column][2][frame=off, width=1em]
-    \startTABLE
-        \NC Widow \NC\NC Orphan \NC\NR
-        \NC\fakestart{5}{1.5em}\NC\NC\fakestart{1}{2em}\NC\NR
-        \NC\fakeend{1}{2em}\NC\NC\fakeend{5}{1.5em}\NC\NR
-    \stopTABLE
-\stopbuffer
-
-\startbuffer[nobreak]
-    % This is also really hacky and terrible
-    \parfillskip=0pt
-    \define\lineone{%
-        \hbox to 0.3\textwidth{\blackrule[height=1.5ex, width=1em]\hfill%
-        \blackrule[height=1.5ex, width=3em]\hfill%
-        \blackrule[height=1.5ex, width=1em]\hfill%
-        \blackrule[height=1.5ex, width=3em]\hfill%
-        \blackrule[height=1.5ex, width=1em]}%
-    }
-
-    \define\linetwo{%
-        \hbox to 0.3\textwidth{\blackrule[height=1.5ex, width=2em]\hfill%
-        \blackrule[height=1.5ex, width=1em]\hfill%
-        \blackrule[height=1.5ex, width=2.5em]\hfill%
-        \blackrule[height=1.5ex, width=1em]\hfill%
-        \blackrule[height=1.5ex, width=2.5em]}%
-    }
-
-    \define\linethree{%
-        \hbox to 0.3\textwidth{\blackrule[height=1.5ex, width=1em]\hfill%
-        \blackrule[height=1.5ex, width=3em]\hfill%
-        \blackrule[height=1.5ex, width=1em]\hfill%
-        \blackrule[height=1.5ex, width=3em]\hfill%
-        \hskip1em\relax}%
-    }
-
-    \define\heading{\bold{Heading}}
-
-    \setupTABLE[
-        width=broad,
-        topframe=off,
-        bottomframe=off,
-        leftframe=on,
-        rightframe=on,
-        height=1.2\baselineskip
-    ]
-    \setupTABLE[row][1][
-        align=middle,
-        style=\ttbf,
-        bottomframe=on,
-        leftframe=off,
-        rightframe=off
-    ]
-    \setupTABLE[row][3][bottomframe=on]
-    \setupTABLE[row][5][bottomframe=on]
-    \setupTABLE[column][2][bottomframe=off, width=1em]
-    \setupTABLE[column][4][bottomframe=off, width=1em]
-    \startTABLE
-        \NC keep     \NC\NC split    \NC\NC warn     \NC\NR
-        \NC          \NC\NC          \NC\NC \heading \NC\NR
-        \NC          \NC\NC \heading \NC\NC \lineone    \NC\NR
-        \NC \heading \NC\NC \lineone \NC\NC \linetwo    \NC\NR
-        \NC \lineone \NC\NC \linetwo \NC\NC \linethree  \NC\NR
-    \stopTABLE
-\stopbuffer

Modified: trunk/Master/texmf-dist/tex/context/third/lua-widow-control/t-lua-widow-control.mkxl
===================================================================
--- trunk/Master/texmf-dist/tex/context/third/lua-widow-control/t-lua-widow-control.mkxl	2022-05-27 20:41:15 UTC (rev 63412)
+++ trunk/Master/texmf-dist/tex/context/third/lua-widow-control/t-lua-widow-control.mkxl	2022-05-27 20:41:39 UTC (rev 63413)
@@ -1,10 +1,10 @@
 %D \module
 %D   [     file=t-lua-widow-control,
-%D      version=2.1.1, %%version
+%D      version=2.1.2, %%version
 %D        title=lua-widow-control,
 %D     subtitle=\ConTeXt module for lua-widow-control,
 %D       author=Max Chernoff,
-%D         date=2022-05-20, %%dashdate
+%D         date=2022-05-26, %%dashdate
 %D    copyright=Max Chernoff,
 %D      license=MPL-2.0+,
 %D          url=https://github.com/gucci-on-fleek/lua-widow-control]

Modified: trunk/Master/texmf-dist/tex/lualatex/lua-widow-control/lua-widow-control-2022-02-22.sty
===================================================================
--- trunk/Master/texmf-dist/tex/lualatex/lua-widow-control/lua-widow-control-2022-02-22.sty	2022-05-27 20:41:15 UTC (rev 63412)
+++ trunk/Master/texmf-dist/tex/lualatex/lua-widow-control/lua-widow-control-2022-02-22.sty	2022-05-27 20:41:39 UTC (rev 63413)
@@ -12,8 +12,8 @@
 % report a real version number here for debugging.
 \PackageInfo{lua-widow-control}{%
     Real version:
-    2022/05/20 %%dashdate
-    v2.1.1 %%version
+    2022/05/26 %%slashdate
+    v2.1.2 %%version
 }
 
 \PackageWarning{lua-widow-control}{%

Modified: trunk/Master/texmf-dist/tex/lualatex/lua-widow-control/lua-widow-control.sty
===================================================================
--- trunk/Master/texmf-dist/tex/lualatex/lua-widow-control/lua-widow-control.sty	2022-05-27 20:41:15 UTC (rev 63412)
+++ trunk/Master/texmf-dist/tex/lualatex/lua-widow-control/lua-widow-control.sty	2022-05-27 20:41:39 UTC (rev 63413)
@@ -13,7 +13,7 @@
 
 \DeclareRelease{}{0000-00-00}{lua-widow-control-2022-02-22.sty}
 \DeclareRelease{v1.1.6}{2022-02-22}{lua-widow-control-2022-02-22.sty}
-\DeclareCurrentRelease{v2.1.1}{2022-05-20} %%version %%dashdate
+\DeclareCurrentRelease{v2.1.2}{2022-05-26} %%version %%dashdate
 
 % If this version of LaTeX doesn't support command hooks, then we load
 % the last v1.1.X version of the package.
@@ -23,8 +23,8 @@
 
 \ProvidesExplPackage
     {lua-widow-control}
-    {2022/05/14} %%dashdate
-    {v2.1.1} %%version
+    {2022/05/26} %%slashdate
+    {v2.1.2} %%version
     {Use Lua to remove widows and orphans}
 
 % Unconditional Package Loads

Modified: trunk/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.lua
===================================================================
--- trunk/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.lua	2022-05-27 20:41:15 UTC (rev 63412)
+++ trunk/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.lua	2022-05-27 20:41:39 UTC (rev 63413)
@@ -7,22 +7,24 @@
 
 --- Tell the linter about node attributes
 --- @class node
+--- @field depth integer
+--- @field height integer
+--- @field id integer
+--- @field list node
+--- @field next node
+--- @field penalty integer
 --- @field prev node
---- @field next node
---- @field id integer
 --- @field subtype integer
---- @field penalty integer
---- @field height integer
---- @field depth integer
 
--- Set some default variables
+-- Initial setup
 lwc = lwc or {}
 lwc.name = "lua-widow-control"
-lwc.nobreak_behaviour = "keep"
 
 -- Locals for `debug_print`
+local debug_lib = debug
+local string_rep = string.rep
 local write_nl = texio.write_nl
-local string_rep = string.rep
+
 local write_log
 if status.luatex_engine == "luametatex" then
     write_log = "logfile"
@@ -31,10 +33,11 @@
 end
 
 --- Prints debugging messages to the log, only if `debug` is set to `true`.
+---
 --- @param title string The "title" to use
 --- @param text string? The "content" to print
 --- @return nil
-local function debug_print(title, text)
+local function debug(title, text)
     if not lwc.debug then return end
 
     -- The number of spaces we need
@@ -47,6 +50,7 @@
     end
 end
 
+
 --[[
     \lwc/ is intended to be format-agonistic. It only runs on Lua\TeX{},
     but there are still some slight differences between formats. Here, we
@@ -70,33 +74,42 @@
 
 --[[
     Save some local copies of the node library to reduce table lookups.
-    This is probably a useless micro-optimization, but it can't hurt.
+    This is probably a useless micro-optimization, but it is done in all of the
+    ConTeXt and expl3 Lua code, so I should probably do it here too.
   ]]
 -- Node ID's
+-- (We need to hardcode the subid's sadly)
+local id_from_name = node.id
 local baselineskip_subid = 2
-local glue_id = node.id("glue")
-local glyph_id = node.id("glyph")
-local hlist_id = node.id("hlist")
+local glue_id = id_from_name("glue")
+local glyph_id = id_from_name("glyph")
+local hlist_id = id_from_name("hlist")
+local insert_id = id_from_name("insert") or id_from_name("ins")
 local line_subid = 1
 local linebreakpenalty_subid = 1
-local par_id = node.id("par") or node.id("local_par")
-local penalty_id = node.id("penalty")
+local par_id = id_from_name("par") or id_from_name("local_par")
+local penalty_id = id_from_name("penalty")
 
 -- Local versions of globals
 local abs = math.abs
-local copy = node.copy_list or node.copylist
+local copy = node.copy
+local copy_list = node.copy_list or node.copylist
 local find_attribute = node.find_attribute or node.findattribute
-local flush_list = node.flush_list or node.flushlist
 local free = node.free
-local getattribute = node.get_attribute or node.getattribute
+local free_list = node.flush_list or node.flushlist
+local get_attribute = node.get_attribute or node.getattribute
 local insert_token = token.put_next or token.putnext
 local last = node.slide
+local linebreak = tex.linebreak
 local new_node = node.new
-local node_id = node.is_node or node.isnode
 local set_attribute = node.set_attribute or node.setattribute
 local string_char = string.char
+local tex_box = tex.box
+local tex_count = tex.count
+local tex_dimen = tex.dimen
+local tex_lists = tex.lists
 local traverse = node.traverse
-local traverseid = node.traverse_id or node.traverseid
+local traverse_id = node.traverse_id or node.traverseid
 local vpack = node.vpack
 
 -- Misc. Constants
@@ -103,25 +116,31 @@
 local iffalse = token.create("iffalse")
 local iftrue = token.create("iftrue")
 local INFINITY = 10000
+local INSERT_CLASS_MULTIPLE = 1000 * 1000
+local INSERT_FIRST_MULTIPLE = 1000
 local min_col_width = tex.sp("250pt")
+local PAGE_MULTIPLE = 100
 local SINGLE_LINE = 50
-local PAGE_MULTIPLE = 100
 
---[[
-    Package/module initialization
+--[[ Package/module initialization.
+
+     Here, we replace any format/engine-specific variables/functions with some
+     generic equivalents. This way, we can write the rest of the module without
+     worrying about any format/engine differences.
   ]]
-local attribute,
-      contrib_head,
+local contrib_head,
       emergencystretch,
       info,
+      insert_attribute,
       max_cost,
       pagenum,
+      paragraph_attribute,
       stretch_order,
       warning
 
 if lmtx then
     -- LMTX has removed underscores from most of the Lua parts
-    debug_print("LMTX")
+    debug("LMTX")
     contrib_head = "contributehead"
     stretch_order = "stretchorder"
 else
@@ -130,23 +149,27 @@
 end
 
 if context then
-    debug_print("ConTeXt")
+    debug("ConTeXt")
 
     warning = logs.reporter(lwc.name, "warning")
     local _info = logs.reporter(lwc.name, "info")
+    --[[ We don't want the info messages on the terminal, but ConTeXt doesn't
+         provide any logfile-only reporters, so we need this hack.
+      ]]
     info = function (text)
         logs.pushtarget("logfile")
         _info(text)
         logs.poptarget()
     end
-    attribute = attributes.public(lwc.name)
-    pagenum = function() return tex.count["realpageno"] end
+    paragraph_attribute = attributes.public(lwc.name .. "_paragraph")
+    insert_attribute = attributes.public(lwc.name .. "_insert")
+    pagenum = function() return tex_count["realpageno"] end
 
     -- Dimen names
     emergencystretch = "lwc_emergency_stretch"
     max_cost = "lwc_max_cost"
 elseif plain or latex or optex then
-    pagenum = function() return tex.count[0] end
+    pagenum = function() return tex_count[0] end
 
     -- Dimen names
     if tex.isdimen("g__lwc_emergencystretch_dim") then
@@ -158,11 +181,11 @@
     end
 
     if plain or latex then
-        debug_print("Plain/LaTeX")
+        debug("Plain/LaTeX")
         luatexbase.provides_module {
             name = lwc.name,
-            date = "2022/05/14", --%%dashdate
-            version = "2.1.1", --%%version
+            date = "2022/05/26", --%%slashdate
+            version = "2.1.2", --%%version
             description = [[
 
 This module provides a LuaTeX-based solution to prevent
@@ -172,13 +195,15 @@
         }
         warning = function(str) luatexbase.module_warning(lwc.name, str) end
         info = function(str) luatexbase.module_info(lwc.name, str) end
-        attribute = luatexbase.new_attribute(lwc.name)
+        paragraph_attribute = luatexbase.new_attribute(lwc.name .. "_paragraph")
+        insert_attribute = luatexbase.new_attribute(lwc.name .. "_insert")
     elseif optex then
-        debug_print("OpTeX")
+        debug("OpTeX")
 
         warning = function(str) write_nl(lwc.name .. " Warning: " .. str) end
         info = function(str) write_nl("log", lwc.name .. " Info: " .. str) end
-        attribute = alloc.new_attribute(lwc.name)
+        paragraph_attribute = alloc.new_attribute(lwc.name .. "_paragraph")
+        insert_attribute = alloc.new_attribute(lwc.name .. "_insert")
     end
 else -- This shouldn't ever happen
     error [[Unsupported format.
@@ -186,8 +211,18 @@
 Please use LaTeX, Plain TeX, ConTeXt or OpTeX.]]
 end
 
-local paragraphs = {} -- List to hold the alternate paragraph versions
+--[[ Table to hold the alternate paragraph versions.
 
+     This is global(ish) mutable state, which isn't ideal, but any other way of
+     passing this data around would be even worse.
+  ]]
+local paragraphs = {}
+local inserts = {}
+
+--[[
+    Function definitions
+  ]]
+
 --- Gets the current paragraph and page locations
 --- @return string
 local function get_location()
@@ -194,11 +229,9 @@
     return "At " .. pagenum() .. "/" .. #paragraphs
 end
 
---[[
-    Function definitions
-  ]]
 
---- Prints the initial glyphs and glue of an hlist
+--- Prints the starting glyphs and glue of an `hlist`
+---
 --- @param head node
 --- @return nil
 local function get_chars(head)
@@ -220,10 +253,12 @@
         end
     end
 
-    debug_print(get_location(), chars)
+    debug(get_location(), chars)
 end
 
---- The "cost function" to use. See the manual.
+
+--- The "cost function" to use. Users can redefine this if they wish.
+---
 --- @param demerits number The demerits of the broken paragraph
 --- @param lines number The number of lines in the broken paragraph
 --- @return number The cost of the broken paragraph
@@ -231,6 +266,7 @@
     return demerits / math.sqrt(lines)
 end
 
+
 --- Checks if the ConTeXt "grid snapping" is active
 --- @return boolean
 local function grid_mode_enabled()
@@ -238,7 +274,9 @@
     return token.create("ifgridsnapping").mode == iftrue.mode
 end
 
---- Gets the next node of a type/subtype in a node list
+
+--- Gets the next node of a specified type/subtype in a node list
+---
 --- @param head node The head of the node list
 --- @param id number The node type
 --- @param args table?
@@ -247,13 +285,17 @@
 --- @return node
 local function next_of_type(head, id, args)
     args = args or {}
+
     if lmtx or not args.reverse then
-        for n, subtype in traverseid(id, head, args.reverse) do
+        for n, subtype in traverse_id(id, head, args.reverse) do
             if (subtype == args.subtype) or (args.subtype == nil) then
                 return n
             end
         end
-    else -- Only LMTX has the built-in backwards traverser
+    else
+        --[[ Only LMTX has the built-in backwards traverser, so we need to do it
+             ourselves here.
+          ]]
         while head do
             if head.id == id and
                (head.subtype == args.subtype or args.subtype == nil)
@@ -265,39 +307,24 @@
     end
 end
 
---- Saves each paragraph, but lengthened by 1 line
+
+--- Breaks a paragraph one line longer than natural
 ---
---- Called by the `pre_linebreak_filter` callback
----
---- @param head node
---- @return node
-function lwc.save_paragraphs(head)
-    if (head.id ~= par_id and context) or -- Ensure that we were actually given a par
-        status.output_active -- Don't run during the output routine
-    then
-        return head
-    end
+--- @param head node The unbroken paragraph
+--- @return node long_node The broken paragraph
+--- @return table long_info An info table about the broken paragraph
+local function long_paragraph(head)
+    -- We can't modify the original paragraph
+    head = copy_list(head)
 
-    -- Prevent the "underfull hbox" warnings when we store a potential paragraph
-    local renable_box_warnings
-    if (context or optex) or
-       #luatexbase.callback_descriptions("hpack_quality") == 0
-    then -- See #18 and michal-h21/linebreaker#3
-        renable_box_warnings = true
-        lwc.callbacks.disable_box_warnings.enable()
-    end
-
-    -- We need to return the unmodified head at the end, so we make a copy here
-    local new_head = copy(head)
-
     -- Prevent ultra-short last lines (\TeX{}Book p. 104), except with narrow columns
     -- Equivalent to \\parfillskip=0pt plus 0.8\\hsize
     local parfillskip
-    if lmtx or last(new_head).id ~= glue_id then
+    if lmtx or last(head).id ~= glue_id then
         -- LMTX does not automatically add the \\parfillskip glue
         parfillskip = new_node("glue", "parfillskip")
     else
-        parfillskip = last(new_head)
+        parfillskip = last(head)
     end
 
     if tex.hsize > min_col_width then
@@ -305,35 +332,78 @@
         parfillskip.stretch = 0.8 * tex.hsize -- Last line must be at least 20% long
     end
 
-    if lmtx or last(new_head).id ~= glue_id then
-        last(new_head).next = parfillskip
+    if lmtx or last(head).id ~= glue_id then
+        last(head).next = parfillskip
     end
 
     -- Break the paragraph 1 line longer than natural
-    local long_node, long_info = tex.linebreak(new_head, {
+    return linebreak(head, {
         looseness = 1,
-        emergencystretch = tex.getdimen(emergencystretch),
+        emergencystretch = tex_dimen[emergencystretch],
     })
+end
 
-    -- Break the natural paragraph so we know how long it was
-    nat_head = copy(head)
 
+--- Breaks a paragraph at its natural length
+---
+--- @param head node The unbroken paragraph
+--- @return table natural_info An info table about the broken paragraph
+local function natural_paragraph(head)
+    -- We can't modify the original paragraph
+    head = copy_list(head)
+
+    --[[ Contrary to the documentation, LMTX does not automatically add
+         the \\parfillskip glue before `pre_linebreak_filter`, so we need
+         to add it here so that our \\prevgraf comparisons are correct.
+      ]]
     if lmtx then
         parfillskip = new_node("glue", "parfillskip")
         parfillskip[stretch_order] = 1
         parfillskip.stretch = 1 -- 0pt plus 1fil
-        last(nat_head).next = parfillskip
+        last(head).next = parfillskip
     end
 
-    local natural_node, natural_info = tex.linebreak(nat_head)
-    flush_list(natural_node)
+    -- Break the paragraph naturally to get \\prevgraf
+    local natural_node, natural_info = linebreak(head)
+    free_list(natural_node)
 
+    return natural_info
+end
+
+
+--- Saves each paragraph, but lengthened by 1 line
+---
+--- Called by the `pre_linebreak_filter` callback
+---
+--- @param head node
+--- @return node
+function lwc.save_paragraphs(head)
+    if (head.id ~= par_id and context) or -- Ensure that we were actually given a par
+        status.output_active or -- Don't run during the output routine
+        tex.nest.ptr > 1 -- Don't run inside boxes
+    then
+        return head
+    end
+
+    -- Prevent the "underfull hbox" warnings when we store a potential paragraph
+    local renable_box_warnings
+    if (context or optex) or
+       #luatexbase.callback_descriptions("hpack_quality") == 0
+    then -- See #18 and michal-h21/linebreaker#3
+        renable_box_warnings = true
+        lwc.callbacks.disable_box_warnings.enable()
+    end
+
+    long_node, long_info = long_paragraph(head)
+
+    natural_info = natural_paragraph(head)
+
     if renable_box_warnings then
         lwc.callbacks.disable_box_warnings.disable()
     end
 
     if not grid_mode_enabled() then
-        -- Offset the accumulated \\prevdepth
+        -- Offset the \\prevdepth differences between natural and long
         local prevdepth = new_node("glue")
         prevdepth.width = natural_info.prevdepth - long_info.prevdepth
         last(long_node).next = prevdepth
@@ -352,14 +422,14 @@
 
     -- Print some debugging information
     get_chars(head)
-    debug_print(get_location(), "nat  lines    " .. natural_info.prevgraf)
-    debug_print(
+    debug(get_location(), "nat  lines    " .. natural_info.prevgraf)
+    debug(
         get_location(),
         "nat  cost " ..
         lwc.paragraph_cost(natural_info.demerits, natural_info.prevgraf)
     )
-    debug_print(get_location(), "long lines    " .. long_info.prevgraf)
-    debug_print(
+    debug(get_location(), "long lines    " .. long_info.prevgraf)
+    debug(
         get_location(),
         "long cost " ..
         lwc.paragraph_cost(long_info.demerits, long_info.prevgraf)
@@ -369,84 +439,117 @@
     return head
 end
 
+
 --- Tags the beginning and the end of each paragraph as it is added to the page.
 ---
 --- We add an attribute to the first and last node of each paragraph. The ID is
 --- some arbitrary number for \lwc/, and the value corresponds to the
---- paragraphs index, which is negated for the end of the paragraph. Called by the
---- `post_linebreak_filter` callback.
+--- paragraphs index, which is negated for the end of the paragraph.
 ---
 --- @param head node
---- @return node
-function lwc.mark_paragraphs(head)
+--- @return nil
+local function mark_paragraphs(head)
+    -- Tag the paragraphs
     if not status.output_active then -- Don't run during the output routine
         -- Get the start and end of the paragraph
         local top_para = next_of_type(head, hlist_id, { subtype = line_subid })
         local bottom_para = last(head)
 
+        while bottom_para.id == insert_id do
+            bottom_para = bottom_para.prev
+        end
+
         if top_para ~= bottom_para then
             set_attribute(
                 top_para,
-                attribute,
+                paragraph_attribute,
                 #paragraphs + (PAGE_MULTIPLE * pagenum())
             )
             set_attribute(
                 bottom_para,
-                attribute,
+                paragraph_attribute,
                 -1 * (#paragraphs + (PAGE_MULTIPLE * pagenum()))
             )
         else
             -- We need a special tag for a 1-line paragraph since the node can only
-            -- have one attribute value
+            -- have a single attribute value
             set_attribute(
                 top_para,
-                attribute,
+                paragraph_attribute,
                 #paragraphs + (PAGE_MULTIPLE * pagenum()) + SINGLE_LINE
             )
         end
     end
-
-    return head
 end
 
---- A "safe" version of the last/slide function.
+
+--- Tags the each line with the indices of any corresponding inserts.
 ---
---- Sometimes the node list can form a loop. Since there is no last element
---- of a looped linked-list, the `last()` function will never terminate. This
---- function provides a "safe" version of the `last()` function that will break
---- the loop at the end if the list is circular. Called by the `pre_output_filter`
---- callback.
+--- We need to tag the first element of the hlist before the any insert nodes
+--- since the insert nodes are removed before `pre_output_filter` gets called.
 ---
---- @param head node The start of a node list
---- @return node The last node in a list
-local function safe_last(head)
-    local ids = {}
-    local prev
+--- @param head node
+--- @return nil
+local function mark_inserts(head)
+    local insert_indices = {}
+    for insert in traverse_id(insert_id, head) do
+        -- Save the found insert nodes for later
+        inserts[#inserts+1] = copy(insert)
 
-    while head.next do
-        local id = node_id(head)
+        -- Tag the insert's content so that we can find it later
+        set_attribute(insert.list, insert_attribute, #inserts)
 
-        if ids[id] then
-            warning [[Circular node list detected!
-This should never happen. I'll try and
-recover, but your output may be corrupted.
-(Internal Error)]]
-            prev.next = nil
-            debug_print("safe_last", node.type(head.id) .. " " .. node.type(prev.id))
+        for n in traverse(insert.list.next) do
+            set_attribute(n, insert_attribute, -1 * #inserts)
+        end
 
-            return prev
+        --[[ Each hlist/line can have multiple inserts, but so we can't just tag
+             the hlist as we go. Instead, we need save up all of their indices,
+             then tag the hlist with the first and last indices.
+          ]]
+        insert_indices[#insert_indices+1] = #inserts
+
+        if not insert.next or
+           insert.next.id ~= insert_id
+        then
+            local hlist_before = next_of_type(insert, hlist_id, { reverse = true} )
+
+            --[[ We tag the first element of the hlist/line with an integer
+                 that holds the insert class and the first and last indices
+                 of the inserts contained in the line. This won't work if
+                 the line has multiple classes of inserts, but I don't think
+                 that happens in real-world documents.
+              ]]
+            set_attribute(
+                hlist_before.list,
+                insert_attribute,
+                insert.subtype    * INSERT_CLASS_MULTIPLE +
+                insert_indices[1] * INSERT_FIRST_MULTIPLE +
+                insert_indices[#insert_indices]
+            )
+
+            -- Clear the indices to prepare for the next line
+            insert_indices = {}
         end
-
-        ids[id] = true
-        head.prev = prev
-        prev = head
-        head = head.next
     end
+end
 
+
+--- Saves the inserts and tags a typeset paragraph. Called by the
+--- `post_linebreak_filter` callback.
+---
+--- @param head node
+--- @return node
+function lwc.mark_paragraphs(head)
+    mark_paragraphs(head)
+    mark_inserts(head)
+
     return head
 end
 
+
 --- Checks to see if a penalty matches the widow/orphan/broken penalties
+---
 --- @param penalty number
 --- @return boolean
 function is_matching_penalty(penalty)
@@ -455,15 +558,9 @@
     local displaywidowpenalty = tex.displaywidowpenalty
     local brokenpenalty = tex.brokenpenalty
 
-    --[[
-        We only need to process pages that have orphans or widows. If `paragraphs`
-        is empty, then there is nothing that we can do.
-
-        The list of penalties is from:
-        https://tug.org/TUGboat/tb39-3/tb123mitt-widows-code.pdf#subsection.0.2.1
-      ]]
     penalty = penalty - tex.interlinepenalty
 
+    -- https://tug.org/TUGboat/tb39-3/tb123mitt-widows-code.pdf#subsection.0.2.1
     return penalty ~= 0 and
            penalty <  INFINITY and (
                penalty == widowpenalty or
@@ -480,143 +577,233 @@
            )
 end
 
---- Remove the widows and orphans from the page, just after the output routine.
+
+--- Reset any state saved between pages
 ---
---- This function holds the "meat" of the module. It is called just after the
---- end of the output routine, before the page is shipped out. If the output
---- penalty indicates that the page was broken at a widow or an orphan, we
---- replace one paragraph with the same paragraph, but lengthened by one line.
---- Then, we can push the bottom line of the page to the next page.
----
---- @param head node
---- @return node
-function lwc.remove_widows(head)
-    local head_save = head -- Save the start of the `head` linked-list
+--- @return nil
+local function reset_state()
+    paragraphs = {}
 
-    debug_print("outputpenalty", tex.outputpenalty .. " " .. #paragraphs)
-
-    if not is_matching_penalty(tex.outputpenalty) or
-       #paragraphs == 0
-    then
-        paragraphs = {}
-        return head_save
+    for _, insert in ipairs(inserts) do
+        free(insert)
     end
 
-    info("Widow/orphan/broken hyphen detected. Attempting to remove")
+    inserts = {}
+end
 
-    local vsize = tex.dimen.vsize
-    local orig_height_diff = vpack(head).height - vsize
 
-    --[[
-        Find the paragraph on the page with the least cost.
-      ]]
-    local paragraph_index = 1
-    local best_cost = paragraphs[paragraph_index].cost
+--- When we are unable to remove a widow/orphan, print a warning
+---
+--- @return nil
+local function remove_widows_fail()
+    warning("Widow/Orphan/broken hyphen NOT removed on page " .. pagenum())
+    reset_state()
+end
 
-    local last_paragraph
-    local head_last = last(head)
+
+--- Finds the first and last paragraphs present on a page
+---
+--- @param head node The node representing the start of the page
+--- @return number first_index The index of the first paragraph on the page in
+---                            the `paragraphs` table
+--- @return number last_index The index of the last paragraph on the page in the
+---                           `paragraphs` table
+local function first_last_paragraphs(head)
+    local first_index, last_index
+
     -- Find the last paragraph on the page, starting at the end, heading in reverse
-    while head_last do
-        local value = getattribute(head_last, attribute)
+    local n = last(head)
+    while n do
+        local value = get_attribute(n, paragraph_attribute)
         if value then
-            last_paragraph = value % PAGE_MULTIPLE
+            last_index = value % PAGE_MULTIPLE
             break
         end
 
-        head_last = head_last.prev
+        n = n.prev
     end
 
-    local first_paragraph
     -- Find the first paragraph on the page, from the top
-    local first_attribute_val, first_attribute_head = find_attribute(head, attribute)
-    if first_attribute_val // 100 == pagenum() - 1 then
-        -- If the first complete paragraph on the page was initially broken on the
-        -- previous page, then we can't expand it here so we need to skip it.
-        first_paragraph = find_attribute(
-            first_attribute_head.next,
-            attribute
+    local first_val, first_head = find_attribute(head, paragraph_attribute)
+    if first_val // PAGE_MULTIPLE == pagenum() - 1 then
+        --[[ If the first complete paragraph on the page was initially broken on the
+             previous page, then we can't expand it here so we need to skip it.
+          ]]
+        first_index = find_attribute(
+            first_head.next,
+            paragraph_attribute
         ) % PAGE_MULTIPLE
     else
-        first_paragraph = first_attribute_val % PAGE_MULTIPLE
+        first_index = first_val % PAGE_MULTIPLE
     end
 
+    return first_index, last_index
+end
+
+
+--- Selects the "best" paragraph on the page to expand
+---
+--- @param head node The node representing the start of the page
+--- @return number? best_index The index of the paragraph to expand in the
+---                           `paragraphs` table
+local function best_paragraph(head)
+    local first_paragraph_index, last_paragraph_index = first_last_paragraphs(head)
+
+    -- Find the paragraph on the page with the least cost.
+    local best_index = 1
+    local best_cost = paragraphs[best_index].cost
+
     -- We find the current "best" replacement, then free the unused ones
-    for i, paragraph in pairs(paragraphs) do
+    for index, paragraph in pairs(paragraphs) do
         if paragraph.cost < best_cost and
-           i <  last_paragraph and
-           i >= first_paragraph
+           index <  last_paragraph_index and
+           index >= first_paragraph_index
         then
-            -- Clear the old best paragraph
-            flush_list(paragraphs[paragraph_index].node)
-            paragraphs[paragraph_index].node = nil
+            -- Free the old best paragraph
+            free_list(paragraphs[best_index].node)
+            paragraphs[best_index].node = nil
             -- Set the new best paragraph
-            paragraph_index, best_cost = i, paragraph.cost
-        elseif i > 1 then
+            best_index, best_cost = index, paragraph.cost
+        elseif index > 1 then
             -- Not sure why `i > 1` is required?
-            flush_list(paragraph.node)
+            free_list(paragraph.node)
             paragraph.node = nil
         end
     end
 
-    debug_print(
+    debug(
         "selected para",
-        pagenum() ..
-        "/" ..
-        paragraph_index ..
-        " (" ..
-        best_cost ..
-        ")"
+        pagenum() .. "/" .. best_index .. " (" .. best_cost .. ")"
     )
 
-    if best_cost > tex.getcount(max_cost) or
-       paragraph_index == last_paragraph
+    if best_cost  >  tex_count[max_cost] or
+       best_index == last_paragraph_index
     then
-        -- If the best replacement is too bad, we can't do anything
-        warning("Widow/Orphan/broken hyphen NOT removed on page " .. pagenum())
-        paragraphs = {}
-        return head_save
+        return nil
+    else
+        return best_index
     end
+end
 
-    local target_node = paragraphs[paragraph_index].node
 
+--- Gets any inserts present in the moved line
+---
+--- @param last_line node The moved last line
+--- @return table<node> inserts A list of the present inserts
+local function get_inserts(last_line)
+    local selected_inserts = {}
+
+    local n = last_line.list
+    while n do -- Iterate through the last line
+        local line_value
+        line_value, n = find_attribute(n, insert_attribute)
+
+        if not n then
+            break
+        end
+
+        --[[ With LuaMetaTeX, the subtype of `insert` nodes is always zero,
+             so we cannot detect their class therefore we can't fix any moved
+             footnotes.
+          ]]
+        if lmtx then
+            warning("!!!Incorrect footnotes on page " .. pagenum() .. "!!!")
+            return {}
+        end
+
+        -- Demux the insert values
+        local class = line_value // INSERT_CLASS_MULTIPLE
+        local first_index = (line_value % INSERT_CLASS_MULTIPLE) // INSERT_FIRST_MULTIPLE
+        local last_index = line_value % INSERT_FIRST_MULTIPLE
+
+        -- Get the output box containing the insert boxes
+        local insert_box = tex_box[class]
+
+        local m = insert_box.list
+        while m do -- Iterate through the insert box
+            local box_value
+            box_value, m = find_attribute(m, insert_attribute)
+
+            if not m then
+                break
+            end
+
+            if abs(box_value) >= first_index and
+               abs(box_value) <= last_index
+            then
+                -- Remove the respective contents from the insert box
+                insert_box.list = node.remove(insert_box.list, m)
+
+                if box_value > 0 then
+                    selected_inserts[#selected_inserts + 1] = copy(inserts[box_value])
+                end
+            end
+
+            m = m.next
+        end
+
+        if not insert_box.list then
+            tex_box[class] = nil
+        end
+
+        n = n.next
+    end
+
+    if #selected_inserts ~= 0 then
+        info("Moving footnotes on page " .. pagenum())
+    end
+
+    return selected_inserts
+end
+
+
+lwc.nobreak_behaviour = "keep"
+--- Moves the last line of the page onto the following page.
+---
+--- This is the most complicated function of the module since it needs to
+--- look back to see if there is a heading preceding the last line, then it does
+--- some low-level node shuffling.
+---
+--- @param head node The node representing the start of the page
+--- @return boolean success
+local function move_last_line(head)
     -- Start of final paragraph
-    debug_print("remove_widows", "moving last line")
+    debug("remove_widows", "moving last line")
 
     -- Here we check to see if the widow/orphan was preceded by a large penalty
-    head = last(head_save).prev
     local big_penalty_found, last_line, hlist_head
-    while head do
-        if head.id == glue_id then
+    local n = last(head).prev
+    while n do
+        if n.id == glue_id then
             -- Ignore any glue nodes
-        elseif head.id == penalty_id and head.penalty >= INFINITY then
+        elseif n.id == penalty_id and n.penalty >= INFINITY then
             -- Infinite break penalty
             big_penalty_found = true
-        elseif big_penalty_found and head.id == hlist_id then
+        elseif big_penalty_found and n.id == hlist_id then
             -- Line before the penalty
             if lwc.nobreak_behaviour == "keep" then
-                hlist_head = head
+                hlist_head = n
                 big_penalty_found = false
             elseif lwc.nobreak_behaviour == "split" then
-                head = last(head_save)
+                n = last(head)
                 break
             elseif lwc.nobreak_behaviour == "warn" then
-                warning("Widow/Orphan/broken hyphen NOT removed on page " .. pagenum())
-                paragraphs = {}
-                return head_save
+                debug("last line", "heading found")
+                return false
             end
         else
             -- Not found
             if hlist_head then
-                head = hlist_head
+                n = hlist_head
             else
-                head = last(head_save)
+                n = last(head)
             end
             break
         end
-        head = head.prev
+        n = n.prev
     end
 
-    local potential_penalty = head.prev.prev
+    local potential_penalty = n.prev.prev
 
     if potential_penalty and
        potential_penalty.id      == penalty_id and
@@ -626,43 +813,68 @@
         warning("Making a new widow/orphan/broken hyphen on page " .. pagenum())
     end
 
+    last_line = copy_list(n)
 
-    last_line = copy(head)
-    last(last_line).next = copy(tex.lists[contrib_head])
+    -- Reinsert any inserts originally present in this moved line
+    local selected_inserts = get_inserts(last_line)
+    for _, insert in ipairs(selected_inserts) do
+        last(last_line).next = insert
+    end
 
-    head.prev.prev.next = nil
-    -- Move the last line to the next page
-    tex.lists[contrib_head] = last_line
+    -- Add back in the content from the next page
+    last(last_line).next = copy_list(tex_lists[contrib_head])
 
+    n.prev.prev.next = nil
+
+    -- Set the content of the next page
+    last(last_line)
+    tex_lists[contrib_head] = last_line
+
+    return true
+end
+
+
+--- Replace the chosen paragraph with its expanded version.
+---
+--- This is the "core function" of the module since it is what ultimately causes
+--- the expansion to occur.
+---
+--- @param head node
+--- @param paragraph_index number
+local function replace_paragraph(head, paragraph_index)
+    local target_node = paragraphs[paragraph_index].node
     local free_next_nodes = false
 
+    local start_found = false
+    local end_found = false
+
     -- Loop through all of the nodes on the page with the lwc attribute
-    head = head_save
-    while head do
+    local n = head
+    while n do
         local value
-        value, head = find_attribute(head, attribute)
+        value, n = find_attribute(n, paragraph_attribute)
 
-        if not head then
+        if not n then
             break
         end
 
-        debug_print("remove_widows", "found " .. value)
+        debug("remove_widows", "found " .. value)
 
         -- Insert the start of the replacement paragraph
         if value == paragraph_index + (PAGE_MULTIPLE * pagenum()) or
            value == paragraph_index + (PAGE_MULTIPLE * pagenum()) + SINGLE_LINE
         then
-            debug_print("remove_widows", "replacement start")
-            safe_last(target_node) -- Remove any loops
+            debug("remove_widows", "replacement start")
+            start_found = true
 
             -- Fix the `\\baselineskip` glue between paragraphs
             height_difference = (
-                next_of_type(head, hlist_id, { subtype = line_subid }).height -
+                next_of_type(n, hlist_id, { subtype = line_subid }).height -
                 next_of_type(target_node, hlist_id, { subtype = line_subid }).height
             )
 
             local prev_bls = next_of_type(
-                head,
+                n,
                 glue_id,
                 { subtype = baselineskip_subid, reverse = true }
             )
@@ -671,7 +883,7 @@
                 prev_bls.width = prev_bls.width + height_difference
             end
 
-            head.prev.next = target_node
+            n.prev.next = target_node
             free_next_nodes = true
         end
 
@@ -679,18 +891,20 @@
         if value == -1 * (paragraph_index + (PAGE_MULTIPLE * pagenum())) or
            value ==       paragraph_index + (PAGE_MULTIPLE * pagenum()) + SINGLE_LINE
         then
-            debug_print("remove_widows", "replacement end")
-            local target_node_last = safe_last(target_node)
+            debug("remove_widows", "replacement end")
+            end_found = true
 
+            local target_node_last = last(target_node)
+
             if grid_mode_enabled() then
                 -- Account for the difference in depth
                 local after_glue = new_node("glue")
-                after_glue.width = head.depth - target_node_last.depth
+                after_glue.width = n.depth - target_node_last.depth
                 target_node_last.next = after_glue
 
-                after_glue.next = head.next
+                after_glue.next = n.next
             else
-                target_node_last.next = head.next
+                target_node_last.next = n.next
             end
 
             break
@@ -697,20 +911,74 @@
         end
 
         if free_next_nodes then
-            head = free(head)
+            n = free(n)
         else
-            head = head.next
+            n = n.next
         end
     end
 
-    local new_height_diff = vpack(head_save).height - vsize
-    -- We need the original height discrepancy in case there are \\vfill's
-    local net_height_diff = orig_height_diff - new_height_diff
+    if not (start_found and end_found) then
+        warning("Paragraph NOT expanded on page " .. pagenum())
+    end
+end
 
+
+--- Remove the widows and orphans from the page, just after the output routine.
+---
+--- This is called just after the end of the output routine, before the page is
+--- shipped out. If the output penalty indicates that the page was broken at a
+--- widow or an orphan, we replace one paragraph with the same paragraph, but
+--- lengthened by one line. Then, we can push the bottom line of the page to the
+--- next page.
+---
+--- @param head node
+--- @return node
+function lwc.remove_widows(head)
+    debug("outputpenalty", tex.outputpenalty .. " " .. #paragraphs)
+
+    -- See if there is a widow/orphan for us to remove
+    if not is_matching_penalty(tex.outputpenalty) then
+        reset_state()
+        return head
+    end
+
+    info("Widow/orphan/broken hyphen detected. Attempting to remove")
+
+    -- Nothing that we can do if there aren't any paragraphs available to expand
+    if #paragraphs == 0 then
+        remove_widows_fail()
+        return head
+    end
+
+    -- Check the original height of \\box255
+    local vsize = tex_dimen.vsize
+    local orig_height_diff = vpack(head).height - vsize
+
+    -- Find the paragraph to expand
+    local paragraph_index = best_paragraph(head)
+
+    if not paragraph_index then
+        remove_widows_fail()
+        return head
+    end
+
+    -- Move the last line of the page to the next page
+    if not move_last_line(head) then
+        remove_widows_fail()
+        return head
+    end
+
+    -- Replace the chosen paragraph with its expanded version
+    replace_paragraph(head, paragraph_index)
+
     --[[ The final \\box255 needs to be exactly \\vsize tall to avoid
          over/underfull box warnings, so we correct any discrepancies
          here.
       ]]
+    local new_height_diff = vpack(head).height - vsize
+    -- We need the original height discrepancy in case there are \\vfill's
+    local net_height_diff = orig_height_diff - new_height_diff
+
     if abs(net_height_diff) > 0 and
        -- A difference larger than 0.25\\baselineskip is probably not from \lwc/
        abs(net_height_diff) < tex.skip.baselineskip.width / 4
@@ -717,7 +985,7 @@
     then
         local bottom_glue = new_node("glue")
         bottom_glue.width = net_height_diff
-        last(head_save).next = bottom_glue
+        last(head).next = bottom_glue
     end
 
     info(
@@ -727,12 +995,14 @@
         .. pagenum()
     )
 
-    paragraphs = {} -- Clear paragraphs array at the end of the page
+    reset_state()
 
-    return head_save
+    return head
 end
 
+
 --- Create a table of functions to enable or disable a given callback
+---
 --- @param t table Parameters of the callback to create
 ---     callback: string = The \LuaTeX{} callback name
 ---     func: function = The function to call
@@ -756,8 +1026,9 @@
         }
     elseif context and not t.lowlevel then
         return {
-            -- Register the callback when the table is created,
-            -- but activate it when `enable()` is called.
+            --[[ Register the callback when the table is created,
+                 but activate it when `enable()` is called.
+              ]]
             enable = nodes.tasks.appendaction(t.category, t.position, "lwc." .. t.name)
                 or function()
                     nodes.tasks.enableaction(t.category, "lwc." .. t.name)
@@ -767,13 +1038,12 @@
             end,
         }
     elseif context and t.lowlevel then
-        --[[
-            Some of the callbacks in \ConTeXt{} have no associated "actions". Unlike
-            with \LuaTeX{}base, \ConTeXt{} leaves some \LuaTeX{} callbacks unregistered
-            and unfrozen. Because of this, we need to register some callbacks at the
-            engine level. This is fragile though, because a future \ConTeXt{} update
-            may decide to register one of these functions, in which case
-            \lwc/ will crash with a cryptic error message.
+        --[[ Some of the callbacks in \ConTeXt{} have no associated "actions". Unlike
+             with \LuaTeX{}base, \ConTeXt{} leaves some \LuaTeX{} callbacks unregistered
+             and unfrozen. Because of this, we need to register some callbacks at the
+             engine level. This is fragile though, because a future \ConTeXt{} update
+             may decide to register one of these functions, in which case
+             \lwc/ will crash with a cryptic error message.
           ]]
         return {
             enable = function() callback.register(t.callback, t.func) end,
@@ -791,6 +1061,7 @@
     end
 end
 
+
 -- Add all of the callbacks
 lwc.callbacks = {
     disable_box_warnings = register_callback({
@@ -822,33 +1093,33 @@
 }
 
 
-local enabled = false
---- Enable the paragraph callbacks
+local lwc_enabled = false
+--- Enables the paragraph callbacks
 function lwc.enable_callbacks()
-    debug_print("callbacks", "enabling")
-    if not enabled then
+    debug("callbacks", "enabling")
+    if not lwc_enabled then
         lwc.callbacks.save_paragraphs.enable()
         lwc.callbacks.mark_paragraphs.enable()
 
-        enabled = true
+        lwc_enabled = true
     else
         info("Already enabled")
     end
 end
 
---- Disable the paragraph callbacks
+
+--- Disables the paragraph callbacks
 function lwc.disable_callbacks()
-    debug_print("callbacks", "disabling")
-    if enabled then
+    debug("callbacks", "disabling")
+    if lwc_enabled then
         lwc.callbacks.save_paragraphs.disable()
         lwc.callbacks.mark_paragraphs.disable()
-        --[[
-            We do \emph{not} disable `remove_widows` callback, since we still want
-            to expand any of the previously-saved paragraphs if we hit an orphan
-            or a widow.
+        --[[ We do \emph{not} disable `remove_widows` callback, since we still want
+             to expand any of the previously-saved paragraphs if we hit an orphan
+             or a widow.
           ]]
 
-        enabled = false
+        lwc_enabled = false
     else
         info("Already disabled")
     end
@@ -855,8 +1126,8 @@
 end
 
 function lwc.if_lwc_enabled()
-    debug_print("iflwc")
-    if enabled then
+    debug("iflwc")
+    if lwc_enabled then
         insert_token(iftrue)
     else
         insert_token(iffalse)
@@ -863,10 +1134,12 @@
     end
 end
 
+
 --- Mangles a macro name so that it's suitable for a specific format
+---
 --- @param name string The plain name
 --- @param args table<string> The TeX types of the function arguments
---- @return string The mangled name
+--- @return string name The mangled name
 local function mangle_name(name, args)
     if plain then
         return "lwc@" .. name:gsub("_", "@")
@@ -879,7 +1152,9 @@
     end
 end
 
+
 --- Creates a TeX command that evaluates a Lua function
+---
 --- @param name string The name of the csname to define
 --- @param func function
 --- @param args table<string> The TeX types of the function arguments
@@ -921,6 +1196,7 @@
     end
 end
 
+
 register_tex_cmd("if_enabled", lwc.if_lwc_enabled, {})
 register_tex_cmd("enable", lwc.enable_callbacks, {})
 register_tex_cmd("disable", lwc.disable_callbacks, {})
@@ -954,13 +1230,15 @@
 --- This uses the Lua `debug` library to internally modify the log upvalue in the
 --- `add_to_callback` function. This is almost certainly a terrible idea, but I don't
 --- know of a better way.
+---
+--- @return nil
 local function silence_luatexbase()
-    local nups = debug.getinfo(luatexbase.add_to_callback).nups
+    local nups = debug_lib.getinfo(luatexbase.add_to_callback).nups
 
     for i = 1, nups do
-        local name, func = debug.getupvalue(luatexbase.add_to_callback, i)
+        local name, func = debug_lib.getupvalue(luatexbase.add_to_callback, i)
         if name == "luatexbase_log" then
-            debug.setupvalue(
+            debug_lib.setupvalue(
                 luatexbase.add_to_callback,
                 i,
                 function(text)
@@ -976,11 +1254,18 @@
     end
 end
 
--- Activate \lwc/
-if plain or latex then
+
+--[[ Call `silence_luatexbase` in Plain and LaTeX, unless the undocmented global
+     `LWC_NO_DEBUG` is set. We provide this opt-out in case something goes awry
+     with the `debug` library calls.
+  ]]
+if (plain or latex) and
+   not LWC_NO_DEBUG --- @diagnostic disable-line
+then
     silence_luatexbase()
 end
 
+-- Activate \lwc/
 lwc.callbacks.remove_widows.enable()
 
 return lwc

Modified: trunk/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.tex
===================================================================
--- trunk/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.tex	2022-05-27 20:41:15 UTC (rev 63412)
+++ trunk/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.tex	2022-05-27 20:41:39 UTC (rev 63413)
@@ -3,7 +3,7 @@
 % SPDX-License-Identifier: MPL-2.0+
 % SPDX-FileCopyrightText: 2022 Max Chernoff
 
-\wlog{lua-widow-control v2.1.1} %%version
+\wlog{lua-widow-control v2.1.2} %%version
 
 \ifx\directlua\undefined
     \errmessage{%

Modified: trunk/Master/texmf-dist/tex/optex/lua-widow-control/lua-widow-control.opm
===================================================================
--- trunk/Master/texmf-dist/tex/optex/lua-widow-control/lua-widow-control.opm	2022-05-27 20:41:15 UTC (rev 63412)
+++ trunk/Master/texmf-dist/tex/optex/lua-widow-control/lua-widow-control.opm	2022-05-27 20:41:39 UTC (rev 63413)
@@ -3,7 +3,7 @@
 % SPDX-License-Identifier: MPL-2.0+
 % SPDX-FileCopyrightText: 2022 Max Chernoff
 
-\_codedecl\lwcenable{lua-widow-control <v2.1.1>} %%version
+\_codedecl\lwcenable{lua-widow-control <v2.1.2>} %%version
 \_namespace{lwc}
 
 \_clubpenalty=1



More information about the tex-live-commits mailing list.