summaryrefslogtreecommitdiff
path: root/macros/luatex/generic/lua-widow-control/lua-widow-control.lua
diff options
context:
space:
mode:
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.lua824
1 files changed, 569 insertions, 255 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 1ef86174c4..59ae84ee3e 100644
--- a/macros/luatex/generic/lua-widow-control/lua-widow-control.lua
+++ b/macros/luatex/generic/lua-widow-control/lua-widow-control.lua
@@ -44,7 +44,14 @@ local function debug(title, text)
local filler = 15 - #title
if text then
- write_nl(write_log, "LWC (" .. title .. string_rep(" ", filler) .. "): " .. text)
+ write_nl(
+ write_log,
+ "LWC (" ..
+ title ..
+ string_rep(" ", filler) ..
+ "): " ..
+ text
+ )
else
write_nl(write_log, "LWC: " .. string_rep(" ", 18) .. title)
end
@@ -52,24 +59,28 @@ 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
- detect the format name then set some flags for later processing.
+ \lwc/ is intended to be format-agonistic. It only runs on Lua\TeX{} and
+ LuaMeta\TeX{}, but there are still some slight differences between formats.
+ Here, we detect the format name then set some flags for later processing.
]]
local format = tex.formatname
local context, latex, plain, optex, lmtx
+if status.luatex_engine == "luametatex" then
+ lmtx = true
+end
+
if format:find("cont") then -- cont-en, cont-fr, cont-nl, ...
context = true
- if status.luatex_engine == "luametatex" then
- lmtx = true
- end
elseif format:find("latex") then -- lualatex, lualatex-dev, ...
latex = true
-elseif format == "luatex" or format == "luahbtex" then -- Plain
+elseif format == "luatex" or
+ format == "luahbtex" or
+ format:find("plain")
+then -- Plain
plain = true
elseif format:find("optex") then -- OpTeX
- optex = true
+ optex = _G.optex
end
--[[
@@ -89,22 +100,35 @@ local line_subid = 1
local linebreakpenalty_subid = 1
local par_id = id_from_name("par") or id_from_name("local_par")
local penalty_id = id_from_name("penalty")
+local parfill_subids = {
+ parfillleftskip = 17,
+ parfillrightskip = 16,
+ parinitleftskip = 19,
+ parinitrightskip = 18,
+}
+local vlist_id = id_from_name("vlist")
-- Local versions of globals
local abs = math.abs
local copy = node.copy
local copy_list = node.copy_list or node.copylist
+local effective_glue = node.effective_glue or node.effectiveglue
local find_attribute = node.find_attribute or node.findattribute
local free = node.free
local free_list = node.flush_list or node.flushlist
local get_attribute = node.get_attribute or node.getattribute
+local hpack = node.hpack
local insert_token = token.put_next or token.putnext
+local is_node = node.is_node or node.isnode
local last = node.slide
local linebreak = tex.linebreak
local new_node = node.new
local remove = node.remove
local set_attribute = node.set_attribute or node.setattribute
-local string_char = string.char
+local str_byte = string.byte
+local str_char = string.char
+local str_format = string.format
+local subtype = node.subtype
local tex_box = tex.box
local tex_count = tex.count
local tex_dimen = tex.dimen
@@ -119,8 +143,6 @@ local iftrue = token.create("iftrue")
local INFINITY = 10000
local INSERT_CLASS_MULTIPLE = 1000 * 1000
local INSERT_FIRST_MULTIPLE = 1000
-local llap_offset = math.max(tex.dimen.parindent, tex.sp("12pt"))
-local min_col_width = tex.sp("250pt")
local PAGE_MULTIPLE = 100
local SINGLE_LINE = 50
@@ -128,8 +150,10 @@ lwc.colours = {
expanded = {0.00, 0.70, 0.25},
failure = {0.90, 0.00, 0.25},
moved = {0.25, 0.25, 1.00},
+ cost = {0.50, 0.50, 0.50},
}
+
--[[ Package/module initialization.
Here, we replace any format/engine-specific variables/functions with some
@@ -137,13 +161,15 @@ lwc.colours = {
worrying about any format/engine differences.
]]
local contrib_head,
+ draft_offset,
emergencystretch,
hold_head,
info,
insert_attribute,
max_cost,
- pagenum,
+ page_head,
paragraph_attribute,
+ set_whatsit_field,
shrink_order,
stretch_order,
warning
@@ -155,11 +181,15 @@ if lmtx then
shrink_order = "shrinkorder"
stretch_order = "stretchorder"
hold_head = "holdhead"
+ page_head = "pagehead"
+ set_whatsit_field = node.setwhatsitfield
else
contrib_head = "contrib_head"
shrink_order = "shrink_order"
stretch_order = "stretch_order"
hold_head = "hold_head"
+ page_head = "page_head"
+ set_whatsit_field = node.setfield
end
if context then
@@ -177,20 +207,20 @@ if context then
end
paragraph_attribute = attributes.public(lwc.name .. "_paragraph")
insert_attribute = attributes.public(lwc.name .. "_insert")
- pagenum = function() return tex_count["realpageno"] end
- -- Dimen names
+ -- Dimen/count names
emergencystretch = "lwc_emergency_stretch"
+ draft_offset = "lwc_draft_offset"
max_cost = "lwc_max_cost"
elseif plain or latex or optex then
- pagenum = function() return tex_count[0] end
-
-- Dimen names
if tex.isdimen("g__lwc_emergencystretch_dim") then
emergencystretch = "g__lwc_emergencystretch_dim"
+ draft_offset = "g__lwc_draftoffset_dim"
max_cost = "g__lwc_maxcost_int"
else
emergencystretch = "lwcemergencystretch"
+ draft_offset = "lwcdraftoffset"
max_cost = "lwcmaxcost"
end
@@ -198,8 +228,8 @@ elseif plain or latex or optex then
debug("Plain/LaTeX")
luatexbase.provides_module {
name = lwc.name,
- date = "2022/08/23", --%%slashdate
- version = "2.2.2", --%%version
+ date = "2022/11/22", --%%slashdate
+ version = "3.0.0", --%%version
description = [[
This module provides a LuaTeX-based solution to prevent
@@ -225,30 +255,46 @@ else -- This shouldn't ever happen
Please use LaTeX, Plain TeX, ConTeXt or OpTeX.]]
end
+-- We can't get the value of \\horigin from Lua, but we can guess it
+-- based on the format.
+local horigin
+if optex or (lmtx and context) then
+ horigin = 0
+else
+ horigin = tex.sp("1in")
+end
+
+-- Plain is the only format without a `pre_shipout_filter`
+if plain then
+ luatexbase.create_callback('pre_shipout_filter', 'list')
+end
+
+
--[[ Select the fonts
- We want to use cmr7 for the draft mode cost displays, and the easiest
+ We want to use cmr6 for the draft mode cost displays, and the easiest
way to do so is to just hardcode the font id's. This relies on some
- implementation details; however, it is very unlikely to ever be an issue
+ implementation details; however, it is very unlikely to ever be an issue.
]]
local SMALL_FONT
if plain then
- SMALL_FONT = 4
+ SMALL_FONT = 5
elseif latex then
- SMALL_FONT = 7
+ SMALL_FONT = 6
elseif optex then
- SMALL_FONT = 7
+ SMALL_FONT = 8
elseif context then
- SMALL_FONT = 3
+ SMALL_FONT = fonts.definers.define({
+ name = "LMRoman6-Regular",
+ size = tex.sp("6pt"),
+ })
end
---[[ 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 = {}
+-- Global variables
+local paragraphs = {} -- The expanded paragraphs on each page
+local inserts = {} -- Copies of all the inserts on each page
+local costs = {} -- All of the paragraph costs for the document
+local pagenum = 1 -- The current page/column number
--[[ Function definitions
]]
@@ -256,11 +302,13 @@ local inserts = {}
--- Gets the current paragraph and page locations
--- @return string
local function get_location()
- return "At " .. pagenum() .. "/" .. #paragraphs
+ return "At " .. pagenum .. "/" .. #paragraphs
end
---- Prints the starting glyphs and glue of an `hlist`
+--- Prints the starting glyphs and glue of an `hlist`.
+---
+--- Useful for debugging purposes.
---
--- @param head node
--- @return nil
@@ -271,7 +319,7 @@ local function get_chars(head)
for n in traverse(head) do
if n.id == glyph_id then
if n.char < 127 then -- Only ASCII
- chars = chars .. string_char(n.char)
+ chars = chars .. str_char(n.char)
else
chars = chars .. "#" -- Replacement for an unknown glyph
end
@@ -291,8 +339,11 @@ end
---
--- @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
-function lwc.paragraph_cost(demerits, lines)
+--- @param nat_demerits number The demerits of the naturally-broken paragraph
+--- @param nat_lines number The number of lines in the naturally-broken paragraph
+--- @param head node The head of the broken paragraph
+--- @return number cost The cost of the broken paragraph
+function lwc.paragraph_cost(demerits, lines, nat_demerits, nat_lines, head)
return demerits / math.sqrt(lines)
end
@@ -324,9 +375,8 @@ local function next_of_type(head, id, args)
end
end
else
- --[[ Only LMTX has the built-in backwards traverser, so we need to do it
- ourselves here.
- ]]
+ -- Only LMTX has the built-in backwards traverser, so we need to do
+ -- it manually otherwise.
while head do
if head.id == id and
(head.subtype == args.subtype or args.subtype == nil)
@@ -346,33 +396,76 @@ local function next_of_type(head, id, args)
end
+--- Ensures that a paragraph is ready to be broken
+---
+--- Only applies to LuaMetaTeX
+---
+--- @param head node
+--- @return nil
+local function prepare_linebreak(head)
+ if not lmtx then
+ return
+ end
+
+ -- See how many of the par[left/right][init/fill]skips we have
+ local parfills = {}
+ local count = 0
+ for name, subid in pairs(parfill_subids) do
+ parfills[name] = next_of_type(head, glue_id, { subtype = subid })
+ if parfills[name] then
+ count = count + 1
+ end
+ end
+
+ if count == 0 then
+ -- Usual case
+ tex.preparelinebreak(head)
+ elseif count == 4 then
+ -- Already prepared for some reason, ignored
+ else
+ -- Uh oh
+ warning("Weird par(fill/init)skips found!")
+ tex.preparelinebreak(head) -- Try to fix it
+ end
+end
+
+
--- Breaks a paragraph one line longer than natural
---
--- @param head node The unbroken paragraph
+--- @param parfillskip table<number> The {width, stretch, shrink,
+--- stretch_order, shrink_order} to set
+--- for the \\parfillskip
--- @return node long_node The broken paragraph
--- @return table long_info An info table about the broken paragraph
-local function long_paragraph(head)
+local function long_paragraph(head, parfillskip)
-- We can't modify the original paragraph
head = copy_list(head)
- if lmtx then
- tex.preparelinebreak(head)
- end
-
- -- Prevent ultra-short last lines (\TeX{}Book p. 104), except with narrow columns
- -- Equivalent to \\parfillskip=0pt plus 0.8\\hsize
- local parfillskip = last(head)
+ prepare_linebreak(head)
- if tex.hsize > min_col_width then
- parfillskip[stretch_order] = 0
- parfillskip.stretch = 0.8 * tex.hsize -- Last line must be at least 20% long
- end
+ -- TODO node.setglue is broken in LMTX, so we have to do this manually
+ local n = last(head)
+ n.width = parfillskip[1]
+ n.stretch = parfillskip[2]
+ n.shrink = parfillskip[3]
+ n[stretch_order] = parfillskip[4]
+ n[shrink_order] = parfillskip[5]
-- Break the paragraph 1 line longer than natural
- return linebreak(head, {
+ local long_node, long_info = linebreak(head, {
looseness = 1,
emergencystretch = tex_dimen[emergencystretch],
})
+
+ -- Mark the last line for the costs display
+ set_attribute(
+ last(long_node),
+ paragraph_attribute,
+ -1 * (#paragraphs + 1 + (PAGE_MULTIPLE * pagenum))
+ )
+
+ return long_node, long_info
end
@@ -384,9 +477,7 @@ local function natural_paragraph(head)
-- We can't modify the original paragraph
head = copy_list(head)
- if lmtx then
- tex.preparelinebreak(head)
- end
+ prepare_linebreak(head)
-- Break the paragraph naturally to get \\prevgraf
local natural_node, natural_info = linebreak(head)
@@ -396,88 +487,50 @@ local function natural_paragraph(head)
end
-lwc.draft_mode = false
+local show_colours = false
--- Changes the text colour in a node list if draft mode is active
---
--- @param head node The first node to colour
---- @param colour table<number> A 3-tuple of RGB values
+--- @param colour string The name of a colour in `lwc.colours`
--- @return node head The coloured node
local function colour_list(head, colour)
- if not lwc.draft_mode then
+ if not show_colours then
return head
end
- -- Adapted from https://tex.stackexchange.com/a/372437
- -- \\pdfextension colorstack is ignored in LMTX
- local start_colour = new_node("whatsit", "pdf_colorstack")
- start_colour.stack = 0
- start_colour.command = 1
- start_colour.data = string.format("%.2f %.2f %.2f rg", table.unpack(colour))
-
- local end_colour = new_node("whatsit", "pdf_colorstack")
- end_colour.stack = 0
- end_colour.command = 2
-
- start_colour.next = head
- last(head).next = end_colour
-
- return start_colour
-end
+ local pdf_colour = str_format(
+ "%.2f %.2f %.2f rg",
+ table.unpack(lwc.colours[colour])
+ )
+ if optex and optex.set_node_color then
+ for n in node.traverse(head) do
+ optex.set_node_color(n, pdf_colour)
+ end
---- Generate an \\llap'ed box containing the provided string
----
---- @param str string The string to typeset
---- @return node head The box node
-local function llap_string(str)
- local first = new_node("glue")
- first.width = llap_offset
-
- local m = first
- for letter in str:gmatch(".") do
- local n = new_node("glyph")
- n.font = SMALL_FONT
- n.char = string.byte(letter)
-
- m.next = n
- m = n
+ return head
end
- local hss = new_node("glue")
- hss.stretch = 1
- hss[stretch_order] = 1
- hss.shrink = 1
- hss[shrink_order] = 1
- m.next = hss
-
- return node.hpack(first, 0, "exactly")
-end
-
-
---- Typesets the cost of a paragraph beside it in draft mode
----
---- @param paragraph node
---- @param cost number
---- @return nil
-local function show_cost(paragraph, cost)
- if not lwc.draft_mode then
- return
+ if context then
+ nodes.tracers.colors.setlist(head, "lwc_" .. colour)
+ return head
end
- local last_hlist_end = last(next_of_type(
- last(paragraph),
- hlist_id,
- { subtype = line_subid, reverse = true }
- ).list)
+ -- Adapted from https://tex.stackexchange.com/a/372437 and
+ -- https://github.com/zauguin/luametalatex/issues/8.
+ local start_colour = new_node("whatsit", subtype("pdf_colorstack"))
+ set_whatsit_field(start_colour, "stack", 0)
+ set_whatsit_field(start_colour, "command", 1)
+ set_whatsit_field(start_colour, "data", pdf_colour)
- local cost_str
- if cost < math.maxinteger then
- cost_str = string.format("%.0f", cost)
- else
- cost_str = "infinite"
- end
+ local end_colour = new_node("whatsit", subtype("pdf_colorstack"))
+ set_whatsit_field(end_colour, "stack", 0)
+ set_whatsit_field(end_colour, "command", 2)
+
+ start_colour.next = head
+ last(head).next = end_colour
- last_hlist_end.next = llap_string(cost_str)
+ return start_colour
end
@@ -485,10 +538,10 @@ end
---
--- Called by the `pre_linebreak_filter` callback
---
---- @param head node
---- @return node
+--- @param head node The pre-broken paragraph
+--- @return node head The unmodified `head` argument
function lwc.save_paragraphs(head)
- if (head.id ~= par_id and context) or -- Ensure that we were actually given a par
+ if (head.id ~= par_id and context) or -- Make sure that `head` is a paragraph
status.output_active or -- Don't run during the output routine
tex.nest.ptr > 1 -- Don't run inside boxes
then
@@ -504,10 +557,27 @@ function lwc.save_paragraphs(head)
lwc.callbacks.disable_box_warnings.enable()
end
- long_node, long_info = long_paragraph(head)
-
natural_info = natural_paragraph(head)
+ -- Prevent ultra-short last lines (\TeX{}book p. 104). Equivalent to
+ -- \\parfillskip=0.75\\hsize plus 0.05\\hsize minus 0.75\\hsize.
+ -- From http://petr.olsak.net/ftp/olsak/tbn/tbn.pdf p. 234 (via Jan Sustek)
+ long_node, long_info = long_paragraph(
+ head,
+ {0.75 * tex.hsize, 0.05 * tex.hsize, 0.75 * tex.hsize, 0, 0}
+ )
+
+ if long_info.prevgraf ~= natural_info.prevgraf + 1 then
+ -- The \\parfillskip settings with \\looseness=1 can sometimes
+ -- lengthen paragraphs by two lines instead of one. If this happens,
+ -- we fall back to a slightly-worse \\parfillskip setting.
+ free_list(long_node)
+ long_node, long_info = long_paragraph(
+ head,
+ {0, 0.8 * tex.hsize, false, 0, false}
+ )
+ end
+
if renable_box_warnings then
lwc.callbacks.disable_box_warnings.disable()
end
@@ -519,7 +589,13 @@ function lwc.save_paragraphs(head)
last(long_node).next = prevdepth
end
- local long_cost = lwc.paragraph_cost(long_info.demerits, long_info.prevgraf)
+ local long_cost = lwc.paragraph_cost(
+ long_info.demerits,
+ long_info.prevgraf,
+ natural_info.demerits,
+ natural_info.prevgraf,
+ long_node
+ )
if long_info.prevgraf ~= natural_info.prevgraf + 1 or
long_cost < 10 -- Any paragraph that is "free" to expand is suspicious
@@ -528,11 +604,12 @@ function lwc.save_paragraphs(head)
long_cost = math.maxinteger
end
+ -- The initial glue can disappear in ConTeXt's grid mode, so we
+ -- save starting at the first hlsit
local saved_node = next_of_type(long_node, hlist_id, { subtype = line_subid })
- show_cost(saved_node, long_cost)
for n in traverse_id(hlist_id, saved_node) do
- n.list = colour_list(n.list, lwc.colours.expanded)
+ n.list = colour_list(n.list, "expanded")
end
table.insert(paragraphs, {
@@ -542,20 +619,24 @@ function lwc.save_paragraphs(head)
free_list(long_node)
+ costs[#paragraphs + (PAGE_MULTIPLE * pagenum)] = long_cost
+
-- Print some debugging information
- get_chars(head)
- debug(get_location(), "nat lines " .. natural_info.prevgraf)
- debug(
- get_location(),
- "nat cost " ..
- lwc.paragraph_cost(natural_info.demerits, natural_info.prevgraf)
- )
- debug(get_location(), "long lines " .. long_info.prevgraf)
- debug(
- get_location(),
- "long cost " ..
- lwc.paragraph_cost(long_info.demerits, long_info.prevgraf)
- )
+ if lwc.debug then
+ get_chars(head)
+ debug(get_location(), "nat lines " .. natural_info.prevgraf)
+ debug(
+ get_location(),
+ "nat cost " ..
+ lwc.paragraph_cost(natural_info.demerits, natural_info.prevgraf)
+ )
+ debug(get_location(), "long lines " .. long_info.prevgraf)
+ debug(
+ get_location(),
+ "long cost " ..
+ lwc.paragraph_cost(long_info.demerits, long_info.prevgraf)
+ )
+ end
-- \ConTeXt{} crashes if we return `true`
return head
@@ -572,39 +653,40 @@ end
--- @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 = next_of_type(head, hlist_id, { subtype = line_subid })
- local bottom = last(head)
+ if status.output_active then
+ -- Don't run during the output routine
+ return
+ end
- while bottom.id == insert_id do
- bottom = bottom.prev
- end
+ -- Get the start and end of the paragraph
+ local top = next_of_type(head, hlist_id, { subtype = line_subid })
+ local bottom = last(head)
- if top ~= bottom then
- set_attribute(
- top,
- paragraph_attribute,
- #paragraphs + (PAGE_MULTIPLE * pagenum())
- )
- set_attribute(
- bottom,
- paragraph_attribute,
- -1 * (#paragraphs + (PAGE_MULTIPLE * pagenum()))
- )
- else
- -- We need a special tag for a 1-line paragraph since the node can only
- -- have a single attribute value
- set_attribute(
- top,
- paragraph_attribute,
- #paragraphs + (PAGE_MULTIPLE * pagenum()) + SINGLE_LINE
- )
- end
+ -- The inserts disappear before `pre_output_routine`, so we shouldn't
+ -- mark them.
+ while bottom.id == insert_id do
+ bottom = bottom.prev
+ end
- if #paragraphs > 0 then
- show_cost(head, paragraphs[#paragraphs].cost)
- end
+ if top ~= bottom then
+ set_attribute(
+ top,
+ paragraph_attribute,
+ #paragraphs + (PAGE_MULTIPLE * pagenum)
+ )
+ set_attribute(
+ bottom,
+ paragraph_attribute,
+ -1 * (#paragraphs + (PAGE_MULTIPLE * pagenum))
+ )
+ else
+ -- We need a special tag for a 1-line paragraph since the node can only
+ -- have a single attribute value
+ set_attribute(
+ top,
+ paragraph_attribute,
+ #paragraphs + (PAGE_MULTIPLE * pagenum) + SINGLE_LINE
+ )
end
end
@@ -625,14 +707,15 @@ local function mark_inserts(head)
-- Tag the insert's content so that we can find it later
set_attribute(insert.list, insert_attribute, #inserts)
+ -- We need to tag all lines---not just the start and the end---since
+ -- \TeX{} can split the insert between pages at any point.
for n in traverse(insert.list.next) do
set_attribute(n, insert_attribute, -1 * #inserts)
end
- --[[ 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.
- ]]
+ -- 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
@@ -642,18 +725,18 @@ local function mark_inserts(head)
local insert_class
if lmtx then
- -- FIXME: temporarily hardcode the main "footnote" class
- insert_class = 4 -- insert.index
+ insert_class = insert.index
else
insert_class = insert.subtype
end
- --[[ 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.
- ]]
+ -- 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. If this does turn out
+ -- to be an issue, we can get the insert's class from it's copy
+ -- at `pre_output_filter` instead of saving it now.
set_attribute(
hlist_before.list,
insert_attribute,
@@ -672,8 +755,8 @@ end
--- Saves the inserts and tags a typeset paragraph. Called by the
--- `post_linebreak_filter` callback.
---
---- @param head node
---- @return node
+--- @param head node The head of the broken paragraph
+--- @return node head The unmodified `head` parameter
function lwc.mark_paragraphs(head)
mark_paragraphs(head)
mark_inserts(head)
@@ -686,7 +769,7 @@ end
---
--- @param penalty number
--- @return boolean
-function is_matching_penalty(penalty)
+local function is_matching_penalty(penalty)
local widowpenalty = tex.widowpenalty
local clubpenalty = tex.clubpenalty
local displaywidowpenalty = tex.displaywidowpenalty
@@ -694,7 +777,9 @@ function is_matching_penalty(penalty)
penalty = penalty - tex.interlinepenalty
- -- https://tug.org/TUGboat/tb39-3/tb123mitt-widows-code.pdf#subsection.0.2.1
+ -- Adapted from https://tug.org/TUGboat/tb39-3/tb123mitt-widows-code.pdf.
+ -- This only takes into account the original \TeX{} penalties, not the
+ -- "new" \eTeX{} \\(club/widow/broken)penalties commands.
return penalty ~= 0 and
penalty < INFINITY and (
penalty == widowpenalty or
@@ -712,8 +797,24 @@ function is_matching_penalty(penalty)
end
+--- Determines if we should "activate" \lwc/ for the current page/column.
+---
+--- Users can redefine this if they wish.
+---
+--- @param penalty number The \\outputpenalty for the current page/column
+--- @param paragraphs table<table<string, node|number>> The `paragraphs` table
+--- @param head node The head of the current page/column
+--- @return boolean activate True if \lwc/ should move the last line on this page
+function lwc.should_remove_widows(penalty, paragraphs, head)
+ return is_matching_penalty(penalty)
+end
+
--- Reset any state saved between pages
---
+--- This function is *vital* to ensure that we don't leak any nodes.
+--- If we do leak nodes, then very large documents will slow down and
+--- eventually fail to compile.
+---
--- @return nil
local function reset_state()
for _, paragraph in ipairs(paragraphs) do
@@ -725,6 +826,8 @@ local function reset_state()
free(insert)
end
inserts = {}
+
+ pagenum = pagenum + 1
end
@@ -732,15 +835,15 @@ end
---
--- @return nil
local function remove_widows_fail()
- warning("Widow/Orphan/broken hyphen NOT removed on page " .. pagenum())
+ warning("Widow/Orphan/Broken Hyphen NOT removed on page " .. pagenum)
local last_line = next_of_type(
- last(tex_lists.page_head),
+ last(tex_lists[page_head]),
hlist_id,
{ subtype = line_subid, reverse = true }
)
if last_line then
- last_line.list = colour_list(last_line.list, lwc.colours.failure)
+ last_line.list = colour_list(last_line.list, "failure")
end
local next_first_line = next_of_type(
@@ -749,7 +852,7 @@ local function remove_widows_fail()
{ subtype = line_subid }
)
if next_first_line then
- next_first_line.list = colour_list(next_first_line.list, lwc.colours.failure)
+ next_first_line.list = colour_list(next_first_line.list, "failure")
end
reset_state()
@@ -780,10 +883,12 @@ local function first_last_paragraphs(head)
-- Find the first paragraph on the page, from the top
local first_val, first_head = find_attribute(head, paragraph_attribute)
- while abs(first_val) // PAGE_MULTIPLE == pagenum() - 1 do
- --[[ 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.
- ]]
+ while abs(first_val) // PAGE_MULTIPLE == pagenum - 1 do
+ -- If the first complete paragraph on the page was initially broken on the
+ -- previous page, then we can't expand it here. Why can't we expand it?
+ -- Well, expanding it will nearly always change how the first few lines
+ -- are printed, but we can't modify those since they've already been
+ -- shipped out. So, we need to skip these paragraphs.
first_val, first_head = find_attribute(
first_head.next,
paragraph_attribute
@@ -826,11 +931,11 @@ local function best_paragraph(head)
debug(
"selected para",
- pagenum() .. "/" .. best_index .. " (" .. best_cost .. ")"
+ pagenum .. "/" .. best_index .. " (" .. best_cost .. ")"
)
if best_cost > tex_count[max_cost] or
- best_index == last_paragraph_index or
+ best_index == last_paragraph_index or -- Shouldn't happen
best_cost == math.maxinteger
then
return nil
@@ -858,7 +963,8 @@ local function get_inserts(last_line)
-- Demux the insert values
local class = line_value // INSERT_CLASS_MULTIPLE
- local first_index = (line_value % INSERT_CLASS_MULTIPLE) // INSERT_FIRST_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
@@ -866,23 +972,37 @@ local function get_inserts(last_line)
if lmtx then
insert_box = tex.getinsertcontent(class)
+ -- `getinsertcontent` resets the insert box, so we need to
+ -- re-set it.
+ tex.setinsertcontent(class, insert_box)
else
insert_box = tex_box[class]
end
- -- Get any portions of the insert held over until the next page
- local split_insert = next_of_type(
- tex_lists[hold_head],
- insert_id,
- { subtype = class }
- )
+ -- Get any portions of the insert "held over" until the next page
+ local split_insert
+ if lmtx then
+ split_insert = next_of_type(
+ tex_lists[hold_head],
+ insert_id,
+ { index = class }
+ )
+ else
+ split_insert = next_of_type(
+ tex_lists[hold_head],
+ insert_id,
+ { subtype = class }
+ )
+ end
+ -- We use the same procedure for this page and the next page
for i, insert in ipairs { insert_box, split_insert } do
local m = insert and insert.list
while m do -- Iterate through the insert box
local box_value
box_value, m = find_attribute(m, insert_attribute)
+ local next = m.next
if not m then
break
@@ -898,13 +1018,15 @@ local function get_inserts(last_line)
table.insert(selected_inserts, copy(inserts[box_value]))
end
- m = free(m)
- else
- m = m.next
+ free(m)
end
+
+ m = next
end
end
+ -- If the box has no contents, then void it so that any \\ifvoid
+ -- tests work correctly in the output routine.
if not insert_box.list then
tex_box[class] = nil
end
@@ -917,7 +1039,11 @@ local function get_inserts(last_line)
end
if #selected_inserts ~= 0 then
- info("Moving footnotes on page " .. pagenum())
+ -- This is a somewhat-risky process, so we print an info
+ -- message just in case something goes wrong. We can probably
+ -- remove this in the future if we're sure that everything
+ -- is working correctly.
+ info("Moving footnotes on page " .. pagenum)
end
return selected_inserts
@@ -932,7 +1058,7 @@ lwc.nobreak_behaviour = "keep"
--- some low-level node shuffling.
---
--- @param head node The node representing the start of the page
---- @return boolean success
+--- @return boolean success Set to false if something went wrong
local function move_last_line(head)
-- Start of final paragraph
debug("remove_widows", "moving last line")
@@ -977,19 +1103,19 @@ local function move_last_line(head)
potential_penalty.subtype == linebreakpenalty_subid and
is_matching_penalty(potential_penalty.penalty)
then
- warning("Making a new widow/orphan/broken hyphen on page " .. pagenum())
+ warning("Making a new widow/orphan/broken hyphen on page " .. pagenum)
local second_last_line = next_of_type(
potential_penalty,
hlist_id,
{ subtype = line_subid, reverse = true }
)
- second_last_line.list = colour_list(second_last_line.list, lwc.colours.failure)
+ second_last_line.list = colour_list(second_last_line.list, "failure")
end
last_line = copy_list(n)
- last_line.list = colour_list(last_line.list, lwc.colours.moved)
+ last_line.list = colour_list(last_line.list, "moved")
-- Reinsert any inserts originally present in this moved line
local selected_inserts = get_inserts(last_line)
@@ -1019,13 +1145,29 @@ end
--- @param head node
--- @param paragraph_index number
local function replace_paragraph(head, paragraph_index)
- local target_node = copy_list(paragraphs[paragraph_index].node)
+ -- Remove any inserts. They are completely ignored after
+ -- the page has been broken, but they can upset LuaMetaLaTeX if
+ -- they're found somewhere unexpected.
+ local target_node, last_target_node
+ for n in traverse(paragraphs[paragraph_index].node) do
+ -- Just removing the inserts from the list doesn't work properly,
+ -- so we instead copy over everything that isn't an insert.
+ if n.id ~= insert_id then
+ if not target_node then
+ target_node = copy(n)
+ last_target_node = target_node
+ else
+ last_target_node.next = copy(n)
+ last_target_node = last_target_node.next
+ end
+ end
+ end
local start_found = false
local end_found = false
local free_nodes_begin
- -- Loop through all of the nodes on the page with the lwc attribute
+ -- Loop through all of the nodes on the page with the \lwc/ attribute
local n = head
while n do
local value
@@ -1038,8 +1180,8 @@ local function replace_paragraph(head, paragraph_index)
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
+ if value == paragraph_index + (PAGE_MULTIPLE * pagenum) or
+ value == paragraph_index + (PAGE_MULTIPLE * pagenum) + SINGLE_LINE
then
debug("remove_widows", "replacement start")
start_found = true
@@ -1047,7 +1189,9 @@ local function replace_paragraph(head, paragraph_index)
-- Fix the `\\baselineskip` glue between paragraphs
height_difference = (
next_of_type(n, hlist_id, { subtype = line_subid }).height -
- next_of_type(target_node, hlist_id, { subtype = line_subid }).height
+ next_of_type(
+ target_node, hlist_id, { subtype = line_subid }
+ ).height
)
local prev_bls = next_of_type(
@@ -1065,8 +1209,8 @@ local function replace_paragraph(head, paragraph_index)
end
-- Insert the end of the replacement paragraph
- if value == -1 * (paragraph_index + (PAGE_MULTIPLE * pagenum())) or
- value == paragraph_index + (PAGE_MULTIPLE * pagenum()) + SINGLE_LINE
+ if value == -(paragraph_index + (PAGE_MULTIPLE * pagenum)) or
+ value == paragraph_index + (PAGE_MULTIPLE * pagenum) + SINGLE_LINE
then
debug("remove_widows", "replacement end")
end_found = true
@@ -1095,7 +1239,7 @@ local function replace_paragraph(head, paragraph_index)
if start_found and end_found then
free_list(free_nodes_begin)
else
- warning("Paragraph NOT expanded on page " .. pagenum())
+ warning("Paragraph NOT expanded on page " .. pagenum)
end
end
@@ -1114,7 +1258,7 @@ 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
+ if not lwc.should_remove_widows(tex.outputpenalty, paragraphs, head) then
reset_state()
return head
end
@@ -1154,10 +1298,9 @@ function lwc.remove_widows(head)
-- 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.
- ]]
+ -- The final \\box255 needs to be exactly \\vsize tall to avoid
+ -- over/underfull box warnings, so we correct any discrepancies
+ -- here.
local new_vpack = vpack(head)
local new_height_diff = new_vpack.height - vsize
new_vpack.list = nil
@@ -1169,7 +1312,8 @@ function lwc.remove_widows(head)
free(bls)
if abs(net_height_diff) > 0 and
- -- A difference larger than 0.25\\baselineskip is probably not from \lwc/
+ -- A difference larger than 0.25\\baselineskip is probably not from
+ -- \lwc/, so we let those warnings surface
abs(net_height_diff) < bls_width / 4
then
local bottom_glue = new_node("glue")
@@ -1181,7 +1325,7 @@ function lwc.remove_widows(head)
"Widow/orphan/broken hyphen successfully removed at paragraph "
.. paragraph_index
.. " on page "
- .. pagenum()
+ .. pagenum
)
reset_state()
@@ -1190,6 +1334,143 @@ function lwc.remove_widows(head)
end
+local show_costs = false
+--- Add the paragraph to the list of paragraphs on the page.
+---
+--- Called immediately before the page is shipped out so that we can get
+--- the costs on the correct side in multi-column layouts.
+---
+--- To evenly align all of the costs in the margins, we need to know the
+--- the exact position of the start and end of the paragraph on the page.
+--- This is surprisingly complicated.
+---
+--- @param head node The box to be shipped out
+--- @return true
+function lwc.show_costs (head)
+ if not show_costs then
+ return true
+ end
+
+ local pagewidth = tex.pagewidth or layouts.getpagedimensions()
+
+ --- Loop over each sublist, add up the total width, and show the costs.
+ ---
+ --- @param n node The node to loop over
+ --- @param width number The accumulated width so far
+ --- @param parent node The parent node of the current list
+ --- @return nil
+ local function recurse(n, width, parent)
+ for m in traverse(n) do
+ -- Anything with an \\hbox parent and a width is actual width.
+ -- (If it had a \\vbox parent, then n.width would actually be height.)
+ local self_width = 0
+ if m.id == glue_id and parent.id == hlist_id then
+ self_width = effective_glue(m, parent)
+ elseif m.width and parent.id == hlist_id then
+ self_width = m.width
+ end
+
+ -- A node's "shift" attribute is horizontal only if the parent is
+ -- a \\vbox. This corresponds to the primatives \\moveleft and
+ -- \\moveright. (If the parent is a \\hbox, then n.shift is
+ -- vertical and corresponds to \\raise.)
+ local shift = 0
+ if m.shift and
+ (parent.id == vlist_id or
+ not is_node(parent))
+ then
+ shift = m.shift
+ end
+
+ width = width + self_width
+
+ local attr = get_attribute(m, paragraph_attribute)
+ if attr and abs(attr) % PAGE_MULTIPLE >= SINGLE_LINE then
+ attr = -1 * (abs(attr) - SINGLE_LINE)
+ end
+
+ local cost = costs[abs(attr or 0)]
+
+ if attr and attr < 0 and cost and m.list then
+ -- We've found the end of a marked paragraph!
+
+ -- Generate the \\hbox containing the formatted cost
+ local cost_str
+ if not cost then
+ return
+ elseif cost < math.maxinteger then
+ cost_str = str_format("%.0f", cost)
+ else
+ cost_str = "infinite"
+ end
+
+ local prev, first
+ for letter in cost_str:gmatch(".") do
+ local curr = new_node("glyph")
+ curr.font = SMALL_FONT
+ curr.char = str_byte(letter)
+
+ if not first then
+ first = curr
+ else
+ prev.next = curr
+ end
+ prev = curr
+ end
+
+ local text = hpack(colour_list(first, "cost"))
+
+ -- Make an \\hss to make sure that our `\\hbox`es aren't overfull
+ local hss = new_node("glue")
+ hss.stretch = 1
+ hss[stretch_order] = 1
+ hss.shrink = 1
+ hss[shrink_order] = 1
+
+ local hbox
+ local offset = new_node("glue")
+
+ if (width >= pagewidth / 2) or
+ (m.width >= 0.4 * pagewidth)
+ then -- Right column or single-column
+ -- Costs in the right margin
+ offset.width = (
+ pagewidth -
+ width -
+ m.width -
+ shift -
+ tex_dimen[draft_offset]
+ )
+ text.next = hss
+ hbox = hpack(text, 0, "exactly")
+ else -- Left column
+ -- Costs in the left margin
+ offset.width = (
+ tex_dimen[draft_offset] -
+ m.width -
+ width -
+ shift
+ )
+ hss.next = text
+ hbox = hpack(hss, 0, "exactly")
+ end
+
+ last(m.list).next = offset
+ offset.next = hbox
+ elseif m.list then
+ recurse(m.list, width - self_width + shift, m)
+ end
+ end
+ end
+
+ -- Start at the root of the page
+ recurse(head.list, (tex.hoffset or 0) + horigin, {})
+
+ -- LaTeX requires us to always return true here
+ return true
+end
+
+
--- Create a table of functions to enable or disable a given callback
---
--- @param t table Parameters of the callback to create
@@ -1198,8 +1479,8 @@ end
--- name: string = The name/ID of the callback
--- category: string = The category for a \ConTeXt{} "Action"
--- position: string = The "position" for a \ConTeXt{} "Action"
---- lowlevel: boolean = If we should use a lowlevel \LuaTeX{} callback instead of a
---- \ConTeXt{} "Action"
+--- lowlevel: boolean = If we should use a lowlevel \LuaTeX{} callback
+--- instead of a \ConTeXt{} "Action"
--- @return table t Enablers/Disablers for the callback
--- enable: function = Enable the callback
--- disable: function = Disable the callback
@@ -1215,9 +1496,8 @@ 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)
@@ -1227,18 +1507,18 @@ 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,
disable = function() callback.register(t.callback, nil) end,
}
- elseif optex then -- Op\TeX{} is very similar to luatexbase
+ elseif optex then -- Op\TeX{} is luckily very similar to luatexbase
return {
enable = function()
callback.add_to_callback(t.callback, t.func, t.name)
@@ -1279,6 +1559,13 @@ lwc.callbacks = {
category = "finalizers",
position = "after",
}),
+ show_costs = register_callback({
+ callback = "pre_shipout_filter",
+ func = lwc.show_costs,
+ name = "show_costs",
+ category = "shipouts",
+ position = "finishers",
+ }),
}
@@ -1303,11 +1590,10 @@ function lwc.disable_callbacks()
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 *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.
lwc_enabled = false
else
info("Already disabled")
@@ -1358,6 +1644,8 @@ local function register_tex_cmd(name, func, args)
scanners[#scanners+1] = token['scan_' .. arg]
end
+ -- An intermediate function that properly "scans" for its arguments
+ -- in the \TeX{} side.
scanning_func = function()
local values = {}
for _, scanner in ipairs(scanners) do
@@ -1385,7 +1673,8 @@ local function register_tex_cmd(name, func, args)
end
end
-
+--[[ Make all of the \lwc/ Lua commands available from \TeX{}
+ ]]
register_tex_cmd("if_enabled", lwc.if_lwc_enabled, {})
register_tex_cmd("enable", lwc.enable_callbacks, {})
register_tex_cmd("disable", lwc.disable_callbacks, {})
@@ -1404,12 +1693,26 @@ register_tex_cmd(
{ "string" }
)
register_tex_cmd(
- "draft",
+ "show_costs",
+ function(str)
+ show_costs = str ~= "0" and str ~= "false" and str ~= "stop"
+ end,
+ { "string" }
+)
+register_tex_cmd(
+ "show_colours",
function(str)
- lwc.draft_mode = str ~= "0" and str ~= "false" and str ~= "stop"
+ show_colours = str ~= "0" and str ~= "false" and str ~= "stop"
end,
{ "string" }
)
+register_tex_cmd(
+ "pre_shipout",
+ function(box)
+ luatexbase.call_callback('pre_shipout_filter', tex_box[box])
+ end,
+ { "int" }
+)
--- Silence the luatexbase "Enabling/Removing <callback>" info messages
---
@@ -1424,8 +1727,8 @@ register_tex_cmd(
--- with useless error messages, so we disable it here.
---
--- 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.
+--- `add_to_callback` function. This is almost certainly a terrible idea, but I
+--- don't know of a better way to do it.
---
--- @return nil
local function silence_luatexbase()
@@ -1451,17 +1754,28 @@ local function silence_luatexbase()
end
---[[ 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.
- ]]
+-- 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
+-- Register colours for ConTeXt
+if context then
+ for colour, values in pairs(lwc.colours) do
+ attributes.colors.defineprocesscolor(
+ "lwc_" .. colour,
+ str_format("r=%.2f, g=%.2f, b=%.2f", table.unpack(values))
+ )
+ end
+end
+
-- Activate \lwc/
lwc.callbacks.remove_widows.enable()
+lwc.callbacks.show_costs.enable()
return lwc
+