summaryrefslogtreecommitdiff
path: root/macros/luatex/generic
diff options
context:
space:
mode:
Diffstat (limited to 'macros/luatex/generic')
-rw-r--r--macros/luatex/generic/lua-widow-control/README.md4
-rw-r--r--macros/luatex/generic/lua-widow-control/lua-widow-control-2022-02-22.sty4
-rw-r--r--macros/luatex/generic/lua-widow-control/lua-widow-control.lua815
-rw-r--r--macros/luatex/generic/lua-widow-control/lua-widow-control.opm2
-rw-r--r--macros/luatex/generic/lua-widow-control/lua-widow-control.pdfbin434335 -> 459643 bytes
-rw-r--r--macros/luatex/generic/lua-widow-control/lua-widow-control.sty6
-rw-r--r--macros/luatex/generic/lua-widow-control/lua-widow-control.tex2
-rw-r--r--macros/luatex/generic/lua-widow-control/lwc-manual-samples.tex (renamed from macros/luatex/generic/lua-widow-control/lwc-sample.tex)0
-rw-r--r--macros/luatex/generic/lua-widow-control/lwc-manual.mkxl2
-rw-r--r--macros/luatex/generic/lua-widow-control/lwc-manual.tex26
-rw-r--r--macros/luatex/generic/lua-widow-control/t-lua-widow-control.mkxl4
-rw-r--r--macros/luatex/generic/lua-widow-control/tb133chernoff-widows.pdfbin333164 -> 333173 bytes
12 files changed, 583 insertions, 282 deletions
diff --git a/macros/luatex/generic/lua-widow-control/README.md b/macros/luatex/generic/lua-widow-control/README.md
index 025a8088a1..c1ce5258f0 100644
--- a/macros/luatex/generic/lua-widow-control/README.md
+++ b/macros/luatex/generic/lua-widow-control/README.md
@@ -9,7 +9,7 @@ lua-widow-control
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 @@ Lua-widow-control is licensed under the [_Mozilla Public License_, version 2.0](
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-->
diff --git a/macros/luatex/generic/lua-widow-control/lua-widow-control-2022-02-22.sty b/macros/luatex/generic/lua-widow-control/lua-widow-control-2022-02-22.sty
index 16f01966ee..b7b2ab44c0 100644
--- a/macros/luatex/generic/lua-widow-control/lua-widow-control-2022-02-22.sty
+++ b/macros/luatex/generic/lua-widow-control/lua-widow-control-2022-02-22.sty
@@ -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}{%
diff --git a/macros/luatex/generic/lua-widow-control/lua-widow-control.lua b/macros/luatex/generic/lua-widow-control/lua-widow-control.lua
index 2af5575799..4bd109e483 100644
--- a/macros/luatex/generic/lua-widow-control/lua-widow-control.lua
+++ b/macros/luatex/generic/lua-widow-control/lua-widow-control.lua
@@ -7,22 +7,24 @@
--- Tell the linter about node attributes
--- @class node
---- @field prev node
---- @field next node
+--- @field depth integer
+--- @field height integer
--- @field id integer
---- @field subtype integer
+--- @field list node
+--- @field next node
--- @field penalty integer
---- @field height integer
---- @field depth integer
+--- @field prev node
+--- @field subtype integer
--- Set some default variables
+-- Initial setup
lwc = lwc or {}
lwc.name = "lua-widow-control"
-lwc.nobreak_behaviour = "keep"
-- Locals for `debug_print`
-local write_nl = texio.write_nl
+local debug_lib = debug
local string_rep = string.rep
+local write_nl = texio.write_nl
+
local write_log
if status.luatex_engine == "luametatex" then
write_log = "logfile"
@@ -31,10 +33,11 @@ else
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 @@ local function debug_print(title, text)
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,58 +74,73 @@ end
--[[
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
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 SINGLE_LINE = 50
local PAGE_MULTIPLE = 100
+local SINGLE_LINE = 50
---[[
- 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 @@ else
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 @@ elseif plain or latex or optex then
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 @@ paragraphs.]],
}
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,7 +211,17 @@ else -- This shouldn't ever happen
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
@@ -194,11 +229,9 @@ local function get_location()
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 @@ local function get_chars(head)
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 @@ function lwc.paragraph_cost(demerits, lines)
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 @@ local function grid_mode_enabled()
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 @@ end
--- @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 @@ local function next_of_type(head, id, args)
end
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 -- Don't run during the output routine
- 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
-
- -- We need to return the unmodified head at the end, so we make a copy here
- local new_head = copy(head)
+--- Breaks a paragraph one line longer than natural
+---
+--- @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 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 @@ function lwc.save_paragraphs(head)
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 @@ function lwc.save_paragraphs(head)
-- 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 @@ function lwc.save_paragraphs(head)
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
-
- while head.next do
- local id = node_id(head)
+--- @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)
- 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))
+ -- Tag the insert's content so that we can find it later
+ set_attribute(insert.list, insert_attribute, #inserts)
- return prev
+ for n in traverse(insert.list.next) do
+ set_attribute(n, insert_attribute, -1 * #inserts)
end
- ids[id] = true
- head.prev = prev
- prev = head
- head = head.next
+ --[[ 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
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 @@ function is_matching_penalty(penalty)
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 @@ function is_matching_penalty(penalty)
)
end
---- Remove the widows and orphans from the page, just after the output routine.
----
---- 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
- debug_print("outputpenalty", tex.outputpenalty .. " " .. #paragraphs)
+--- Reset any state saved between pages
+---
+--- @return nil
+local function reset_state()
+ 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
+
+
+--- 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
- local last_paragraph
- local head_last = last(head)
-- 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 @@ function lwc.remove_widows(head)
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
+
+ -- 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
- head.prev.prev.next = nil
- -- Move the last line to the next page
- tex.lists[contrib_head] = last_line
+--- 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 @@ function lwc.remove_widows(head)
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,45 +891,101 @@ function lwc.remove_widows(head)
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
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
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 @@ function lwc.remove_widows(head)
.. 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 @@ local function register_callback(t)
}
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 @@ local function register_callback(t)
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 @@ local function register_callback(t)
end
end
+
-- Add all of the callbacks
lwc.callbacks = {
disable_box_warnings = register_callback({
@@ -822,51 +1093,53 @@ lwc.callbacks = {
}
-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
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)
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 @@ local function mangle_name(name, args)
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 @@ local function register_tex_cmd(name, func, args)
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 @@ register_tex_cmd(
--- 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 @@ local function silence_luatexbase()
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
diff --git a/macros/luatex/generic/lua-widow-control/lua-widow-control.opm b/macros/luatex/generic/lua-widow-control/lua-widow-control.opm
index a9edffc77c..f1261e6c24 100644
--- a/macros/luatex/generic/lua-widow-control/lua-widow-control.opm
+++ b/macros/luatex/generic/lua-widow-control/lua-widow-control.opm
@@ -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
diff --git a/macros/luatex/generic/lua-widow-control/lua-widow-control.pdf b/macros/luatex/generic/lua-widow-control/lua-widow-control.pdf
index cb803ce68f..f2d8337ee8 100644
--- a/macros/luatex/generic/lua-widow-control/lua-widow-control.pdf
+++ b/macros/luatex/generic/lua-widow-control/lua-widow-control.pdf
Binary files differ
diff --git a/macros/luatex/generic/lua-widow-control/lua-widow-control.sty b/macros/luatex/generic/lua-widow-control/lua-widow-control.sty
index 0e3462d22b..89f417a457 100644
--- a/macros/luatex/generic/lua-widow-control/lua-widow-control.sty
+++ b/macros/luatex/generic/lua-widow-control/lua-widow-control.sty
@@ -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
diff --git a/macros/luatex/generic/lua-widow-control/lua-widow-control.tex b/macros/luatex/generic/lua-widow-control/lua-widow-control.tex
index 5c2c93662f..b8ae064c04 100644
--- a/macros/luatex/generic/lua-widow-control/lua-widow-control.tex
+++ b/macros/luatex/generic/lua-widow-control/lua-widow-control.tex
@@ -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{%
diff --git a/macros/luatex/generic/lua-widow-control/lwc-sample.tex b/macros/luatex/generic/lua-widow-control/lwc-manual-samples.tex
index 6951f7a5bb..6951f7a5bb 100644
--- a/macros/luatex/generic/lua-widow-control/lwc-sample.tex
+++ b/macros/luatex/generic/lua-widow-control/lwc-manual-samples.tex
diff --git a/macros/luatex/generic/lua-widow-control/lwc-manual.mkxl b/macros/luatex/generic/lua-widow-control/lwc-manual.mkxl
index 7623b51d1c..e4f49861b9 100644
--- a/macros/luatex/generic/lua-widow-control/lwc-manual.mkxl
+++ b/macros/luatex/generic/lua-widow-control/lwc-manual.mkxl
@@ -93,7 +93,7 @@ fonts.handlers.otf.addfeature {
% 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]
diff --git a/macros/luatex/generic/lua-widow-control/lwc-manual.tex b/macros/luatex/generic/lua-widow-control/lwc-manual.tex
index 7d68de6201..d2eb12c82e 100644
--- a/macros/luatex/generic/lua-widow-control/lwc-manual.tex
+++ b/macros/luatex/generic/lua-widow-control/lwc-manual.tex
@@ -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 @@ same internal mechanisms, \lwc/ removes \waos/ between columns just
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 @@ commands. Specifically, if you give a line a penalty that matches either
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 @@ ultra-short~lines.
\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 @@ ultra-short~lines.
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 @@ Please note that a compiled document is \bold{not} considered to be an \q{Execut
]
\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][
diff --git a/macros/luatex/generic/lua-widow-control/t-lua-widow-control.mkxl b/macros/luatex/generic/lua-widow-control/t-lua-widow-control.mkxl
index 13637756ae..682e8d23c5 100644
--- a/macros/luatex/generic/lua-widow-control/t-lua-widow-control.mkxl
+++ b/macros/luatex/generic/lua-widow-control/t-lua-widow-control.mkxl
@@ -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]
diff --git a/macros/luatex/generic/lua-widow-control/tb133chernoff-widows.pdf b/macros/luatex/generic/lua-widow-control/tb133chernoff-widows.pdf
index da1fbfa7e9..9c9de8bd02 100644
--- a/macros/luatex/generic/lua-widow-control/tb133chernoff-widows.pdf
+++ b/macros/luatex/generic/lua-widow-control/tb133chernoff-widows.pdf
Binary files differ