From f4f10fd31bb21db819393080ec9c5f3e6733b9db Mon Sep 17 00:00:00 2001 From: Karl Berry Date: Tue, 8 Mar 2022 21:25:59 +0000 Subject: lua-widow-control (8mar22) git-svn-id: svn://tug.org/texlive/trunk@62525 c570f23f-e606-0410-a88d-b1316a301751 --- .../luatex/lua-widow-control/lua-widow-control.lua | 390 +++++++++++++++------ .../luatex/lua-widow-control/lua-widow-control.tex | 9 +- 2 files changed, 295 insertions(+), 104 deletions(-) (limited to 'Master/texmf-dist/tex/luatex') diff --git a/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.lua b/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.lua index f1ecb54c65f..5801feef129 100644 --- a/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.lua +++ b/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.lua @@ -5,8 +5,24 @@ SPDX-FileCopyrightText: 2022 Max Chernoff ]] -lwc = {} +lwc = lwc or {} lwc.name = "lua-widow-control" +lwc.nobreak_behaviour = "keep" + +local write_nl = texio.write_nl +local string_rep = string.rep +local function debug_print(title, text) + if not lwc.debug then return end + + local filler = 15 - #title + + if text then + write_nl("log", "LWC (" .. title .. string_rep(" ", filler) .. "): " .. text) + else + write_nl("log", "LWC: " .. string_rep(" ", 18) .. title) + end +end + --[[ \lwc/ is intended to be format-agonistic. It only runs on Lua\TeX{}, @@ -37,70 +53,90 @@ local last = node.slide local copy = node.copy_list local par_id = node.id("par") or node.id("local_par") local glue_id = node.id("glue") -local set_attribute = node.set_attribute -local has_attribute = node.has_attribute +local glyph_id = node.id("glyph") +local penalty_id = node.id("penalty") +local hlist_id = node.id("hlist") +local traverse = node.traverse +local set_attribute = node.set_attribute or node.setattribute +local find_attribute = node.find_attribute or node.findattribute local flush_list = node.flush_list or node.flushlist local free = node.free -local min_col_width = tex.sp("25em") -local maxdimen = 1073741823 -- \\maxdimen in sp +local min_col_width = tex.sp("250pt") --[[ Package/module initialization ]] -local warning, info, attribute, contrib_head, stretch_order, pagenum - -if context and lmtx then - warning = logs.reporter("module", lwc.name) - info = logs.reporter("module", lwc.name) - attribute = attributes.public(lwc.name) - contrib_head = 'contributehead' -- For \LuaMetaTeX{} +local warning, + info, + attribute, + contrib_head, + stretch_order, + pagenum, + emergencystretch, + max_cost + +if lmtx then + debug_print("LMTX") + contrib_head = 'contributehead' stretch_order = "stretchorder" - pagenum = function() return tex.count["realpageno"] end -elseif context and not lmtx then +else + contrib_head = 'contrib_head' + stretch_order = "stretch_order" +end + +if context then + debug_print("ConTeXt") warning = logs.reporter("module", lwc.name) info = logs.reporter("module", lwc.name) attribute = attributes.public(lwc.name) - contrib_head = 'contrib_head' - stretch_order = "stretch_order" pagenum = function() return tex.count["realpageno"] end -elseif plain or latex then - luatexbase.provides_module { - name = lwc.name, - date = "2022/02/22", --%%date - version = "1.1.6", --%%version - description = [[ - - This module provides a LuaTeX-based solution to prevent - widows and orphans from appearing in a document. It does - so by increasing or decreasing the lengths of previous - 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) - contrib_head = 'contrib_head' -- For \LuaTeX{} - stretch_order = "stretch_order" - pagenum = function() return tex.count[0] end -elseif optex then - local write_nl = texio.write_nl - 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) - contrib_head = 'contrib_head' - stretch_order = "stretch_order" + emergencystretch = "lwc_emergency_stretch" + max_cost = "lwc_max_cost" +elseif plain or latex or optex then pagenum = function() return tex.count[0] end + + if tex.isdimen("g__lwc_emergencystretch_dim") then + emergencystretch = "g__lwc_emergencystretch_dim" + max_cost = "g__lwc_maxcost_int" + else + emergencystretch = "lwcemergencystretch" + max_cost = "lwcmaxcost" + end + + if plain or latex then + debug_print("Plain/LaTeX") + luatexbase.provides_module { + name = lwc.name, + date = "2022/03/07", --%%slashdate + version = "2.0.0", --%%version + description = [[ + +This module provides a LuaTeX-based solution to prevent +widows and orphans from appearing in a document. It does +so by increasing or decreasing the lengths of previous +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) + elseif optex then + debug_print("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) + end else -- uh oh - error [[ - Unsupported format. + error [[Unsupported format. - Please use (Lua)LaTeX, Plain (Lua)TeX, ConTeXt (MKXL/LMTX), - or OpTeX. - ]] +Please use LaTeX, Plain TeX, ConTeXt or OpTeX.]] end local paragraphs = {} -- List to hold the alternate paragraph versions +local function get_location() + return "At " .. pagenum() .. "/" .. #paragraphs +end + --[[ Function definitions @@ -165,11 +201,39 @@ local function register_callback(t) end end +local function get_chars(head) + if not lwc.debug then return end + + local chars = "" + for n in traverse(head) do + if n.id == glyph_id then + if n.char < 127 then + chars = chars .. string.char(n.char) + else + chars = chars .. "#" + end + elseif n.id == glue_id then + chars = chars .. " " + end + if #chars > 25 then + break + end + end + + debug_print(get_location(), chars) +end + + +function lwc.paragraph_cost(demerits, lines) + return demerits / (2 * math.sqrt(lines)) +end + + --- Saves each paragraph, but lengthened by 1 line function lwc.save_paragraphs(head) -- Prevent the "underfull hbox" warnings when we store a potential paragraph local renable_box_warnings - if (latex or plain) and + if (context or optex) or #luatexbase.callback_descriptions("hpack_quality") == 0 then -- See #18 and michal-h21/linebreaker#3 renable_box_warnings = true @@ -194,7 +258,7 @@ function lwc.save_paragraphs(head) -- Break the paragraph 1 line longer than natural local long_node, long_info = tex.linebreak(new_head, { looseness = 1, - emergencystretch = tex.dimen.lwcemergencystretch, + emergencystretch = tex.getdimen(emergencystretch), }) -- Break the natural paragraph so we know how long it was @@ -205,20 +269,31 @@ function lwc.save_paragraphs(head) lwc.callbacks.disable_box_warnings.disable() end - -- If we can't lengthen the paragraph, assign a \emph{very} large demerit value - local long_demerits - if long_info.prevgraf == natural_info.prevgraf then - long_demerits = maxdimen - else - long_demerits = long_info.demerits - end - -- Offset the accumulated \\prevdepth local prevdepth = node.new("glue") prevdepth.width = natural_info.prevdepth - long_info.prevdepth last(long_node).next = prevdepth - table.insert(paragraphs, { demerits = long_demerits, node = long_node }) + if long_info.prevgraf == natural_info.prevgraf + 1 then + table.insert(paragraphs, { + cost = lwc.paragraph_cost(long_info.demerits, long_info.prevgraf), + node = long_node + }) + end + + get_chars(head) + debug_print(get_location(), "nat lines " .. natural_info.prevgraf) + debug_print( + get_location(), + "nat cost " .. + lwc.paragraph_cost(natural_info.demerits, natural_info.prevgraf) + ) + debug_print(get_location(), "long lines " .. long_info.prevgraf) + debug_print( + get_location(), + "long cost " .. + lwc.paragraph_cost(long_info.demerits, long_info.prevgraf) + ) -- \LuaMetaTeX{} crashes if we return `true` return head @@ -251,11 +326,12 @@ local function safe_last(head) if ids[id] then warning [[Circular node list detected! -This should never happen. I'll try and recover, but your output may be -corrupted. As a workaround, disable lua-widow-control for the -affected paragraph or change the page breaks in your document.]] - +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)) + return prev end @@ -276,12 +352,16 @@ end --- 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. function lwc.remove_widows(head) + local head_save = head -- Save the start of the `head` linked-list + local penalty = tex.outputpenalty - tex.interlinepenalty local widowpenalty = tex.widowpenalty local clubpenalty = tex.clubpenalty local displaywidowpenalty = tex.displaywidowpenalty local brokenpenalty = tex.brokenpenalty + debug_print("outputpenalty", tex.outputpenalty .. " " .. #paragraphs) + --[[ We only need to process pages that have orphans or widows. If `paragraphs` is empty, then there is nothing that we can do. @@ -289,7 +369,9 @@ function lwc.remove_widows(head) The list of penalties is from: https://tug.org/TUGboat/tb39-3/tb123mitt-widows-code.pdf#subsection.0.2.1 ]] - if (penalty == widowpenalty or + if penalty ~= 0 and + penalty < 10000 and + (penalty == widowpenalty or penalty == displaywidowpenalty or penalty == clubpenalty or penalty == clubpenalty + widowpenalty or @@ -302,28 +384,26 @@ function lwc.remove_widows(head) penalty == brokenpenalty + clubpenalty + displaywidowpenalty) and #paragraphs >= 1 then else + paragraphs = {} return head end info("Widow/orphan detected. Attempting to remove.") - local head_save = head -- Save the start of the `head` linked-list - --[[ - Find the paragraph on the page with the minimum penalty. - - This would be a 1-liner in Python or JavaScript, but Lua is pretty low-level, - so there's quite a bit of code here. + Find the paragraph on the page with the least cost. ]] local paragraph_index = 1 - local minimum_demerits = paragraphs[paragraph_index].demerits + local best_cost = paragraphs[paragraph_index].cost -- We find the current "best" replacement, then free the unused ones for i, paragraph in pairs(paragraphs) do - if paragraph.demerits < minimum_demerits and i <= #paragraphs - 1 then + if paragraph.cost < best_cost and i <= #paragraphs - 1 then + -- Clear the old best paragraph flush_list(paragraphs[paragraph_index].node) paragraphs[paragraph_index].node = nil - paragraph_index, minimum_demerits = i, paragraph.demerits + -- Set the new best paragraph + paragraph_index, best_cost = i, paragraph.cost elseif i > 1 then -- Not sure why `i > 1` is required? flush_list(paragraph.node) @@ -331,50 +411,113 @@ function lwc.remove_widows(head) end end + debug_print( + "selected para", + pagenum() .. + "/" .. + paragraph_index .. + " (" .. + best_cost .. + ")" + ) + + if best_cost > tex.getcount(max_cost) then + -- If the best replacement is too bad, we can't do anything + warning("Widow/Orphan NOT removed on page " .. pagenum()) + paragraphs = {} + return head_save + end + local target_node = paragraphs[paragraph_index].node - local clear_flag = false - -- Loop through all of the nodes on the page + -- Start of final paragraph + debug_print("remove_widows", "moving last line") + + head = last(head_save).prev + local big_penalty_found, last_line, hlist_head + while head do + if head.id == glue_id then + -- Ignore any glue nodes + elseif head.id == penalty_id and head.penalty >= 10000 then + -- Infinite break penalty + big_penalty_found = true + elseif big_penalty_found and head.id == hlist_id then + -- Line before the penalty + if lwc.nobreak_behaviour == "keep" then + hlist_head = head + big_penalty_found = false + elseif lwc.nobreak_behaviour == "split" then + head = last(head_save) + break + elseif lwc.nobreak_behaviour == "warn" then + warning("Widow/Orphan NOT removed on page " .. pagenum()) + paragraphs = {} + return head_save + end + else + -- Not found + if hlist_head then + head = hlist_head + else + head = last(head_save) + end + break + end + head = head.prev + end + + last_line = copy(head) + last(last_line).next = copy(tex.lists[contrib_head]) + + head.prev.prev.next = nil + -- Move the last line to the next page + tex.lists[contrib_head] = last_line + + local free_next_nodes = false + + -- Loop through all of the nodes on the page with the lwc attribute + head = head_save while head do + local value + value, head = find_attribute(head, attribute) + + if not head then + break + end + + debug_print("remove_widows", "found " .. value) + -- Insert the start of the replacement paragraph - if has_attribute(head, attribute, paragraph_index) then + if value == paragraph_index then + debug_print("remove_widows", "replacement start") safe_last(target_node) -- Remove any loops head.prev.next = target_node - clear_flag = true + free_next_nodes = true end -- Insert the end of the replacement paragraph - if has_attribute(head, attribute, -1 * paragraph_index) then + if value == -1 * paragraph_index then + debug_print("remove_widows", "replacement end") safe_last(target_node).next = head.next - clear_flag = false + break end - -- Start of final paragraph - if has_attribute(head, attribute, #paragraphs) then - local last_line = copy(last(head)) - - last(last_line).next = copy(tex.lists[contrib_head]) - - last(head).prev.prev.next = nil - -- Move the last line to the next page - tex.lists[contrib_head] = last_line - info( - "Widow/orphan successfully removed at paragraph " - .. paragraph_index - .. " on page " - .. pagenum() - .. "." - ) - end - - if clear_flag then + if free_next_nodes then head = free(head) else head = head.next end end + info( + "Widow/orphan successfully removed at paragraph " + .. paragraph_index + .. " on page " + .. pagenum() + .. "." + ) + paragraphs = {} -- Clear paragraphs array at the end of the page return head_save @@ -413,18 +556,19 @@ lwc.callbacks = { local enabled = false function lwc.enable_callbacks() + debug_print("callbacks", "enabling") if not enabled then - lwc.callbacks.remove_widows.enable() lwc.callbacks.save_paragraphs.enable() lwc.callbacks.mark_paragraphs.enable() enabled = true else - warning("Already enabled") + info("Already enabled") end end function lwc.disable_callbacks() + debug_print("callbacks", "disabling") if enabled then lwc.callbacks.save_paragraphs.disable() lwc.callbacks.mark_paragraphs.disable() @@ -436,11 +580,12 @@ function lwc.disable_callbacks() enabled = false else - warning("Already disabled") + info("Already disabled") end end function lwc.if_lwc_enabled() + debug_print("iflwc") if enabled then tex.sprint("\\iftrue") else @@ -448,4 +593,49 @@ function lwc.if_lwc_enabled() end end +--- Silence the luatexbase "Enabling/Removing " info messages +--- +--- Every time that a paragraph is typeset, \lwc/ hooks in +--- and typesets the paragraph 1 line longer. Some of these longer paragraphs +--- will have pretty bad badness values, so TeX will issue an over/underfull +--- hbox warning. To block these warnings, we hook into the `hpack_quality` +--- callback and disable it so that no warning is generated. +--- +--- However, each time that we enable/disable the null `hpack_quality` callback, +--- luatexbase puts an info message in the log. This completely fills the log file +--- 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. +local function silence_luatexbase() + local nups = debug.getinfo(luatexbase.add_to_callback).nups + + for x = 1, nups do + local name, func = debug.getupvalue(luatexbase.add_to_callback, x) + if name == "luatexbase_log" then + debug.setupvalue( + luatexbase.add_to_callback, + x, + function(text) + if text:match("^Inserting") or text:match("^Removing") then + return + else + func(text) + end + end + ) + return + end + end +end + + +-- Activate \lwc/ +if plain or latex then + silence_luatexbase() +end + +lwc.callbacks.remove_widows.enable() + return lwc diff --git a/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.tex b/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.tex index d83e6114f4f..7a75e52132f 100644 --- a/Master/texmf-dist/tex/luatex/lua-widow-control/lua-widow-control.tex +++ b/Master/texmf-dist/tex/luatex/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 v1.1.6} %%version +\wlog{lua-widow-control v2.0.0} %%version \ifx\directlua\undefined \errmessage{% @@ -16,13 +16,14 @@ \clubpenalty=1 \widowpenalty=1 -\displaywidowpenalty=0 -\interlinepenalty=0 -\brokenpenalty=0 +\displaywidowpenalty=1 \newdimen\lwcemergencystretch \lwcemergencystretch=3em +\newcount\lwcmaxcost +\lwcmaxcost=2147483647 + \directlua{require "lua-widow-control"} % Here, we enable font expansion/contraction. It isn't strictly necessary for -- cgit v1.2.3