summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/lua-widow-control/lua-widow-control.lua
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2022-05-28 03:01:14 +0000
committerNorbert Preining <norbert@preining.info>2022-05-28 03:01:14 +0000
commit98bbe9f6255283aebab5ce6151393e1ff9014577 (patch)
tree8f436a4696beaff708614cf9d97c28858e6476e4 /macros/luatex/generic/lua-widow-control/lua-widow-control.lua
parent84a5593d3fb9d03aac5677678bcc4c92e8c9a9c4 (diff)
CTAN sync 202205280301
Diffstat (limited to 'macros/luatex/generic/lua-widow-control/lua-widow-control.lua')
-rw-r--r--macros/luatex/generic/lua-widow-control/lua-widow-control.lua815
1 files changed, 550 insertions, 265 deletions
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