From aac7fdc4c391b318efbdee17381ac83783b2fbbb Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Mon, 11 Oct 2021 03:02:08 +0000 Subject: CTAN sync 202110110302 --- .../lua-widow-control/lua-widow-control.lua | 363 +++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 macros/luatex/generic/lua-widow-control/lua-widow-control.lua (limited to 'macros/luatex/generic/lua-widow-control/lua-widow-control.lua') diff --git a/macros/luatex/generic/lua-widow-control/lua-widow-control.lua b/macros/luatex/generic/lua-widow-control/lua-widow-control.lua new file mode 100644 index 0000000000..a85793d3a9 --- /dev/null +++ b/macros/luatex/generic/lua-widow-control/lua-widow-control.lua @@ -0,0 +1,363 @@ +--[[ + lua-widow-control + https://github.com/gucci-on-fleek/lua-widow-control + SPDX-License-Identifier: MPL-2.0+ + SPDX-FileCopyrightText: 2021 gucci-on-fleek + ]] + +lwc = {} +lwc.name = "lua-widow-control" + +--[[ + \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. + ]] +local format = tex.formatname + +if format:find('cont') then -- cont-en, cont-fr, cont-nl, ... + lwc.context = true +elseif format:find('latex') then -- lualatex, lualatex-dev, ... + lwc.latex = true +elseif format == 'luatex' then -- Plain + lwc.plain = true +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. + ]] +local last = node.slide +local copy = node.copy_list +local par_id = node.id("par") +local glue_id = node.id("glue") +local set_attribute = node.set_attribute +local has_attribute = node.has_attribute +local flush_list = node.flush_list or node.flushlist +local min_col_width = tex.sp("25em") +local maxdimen = 1073741823 -- \\maxdimen in sp + +--[[ + This error is raised in the following circumstances: + - When the user manually loads the Lua module without loading Lua\TeX{}Base + - When the package is used with an unsupported format + Both of these are pretty unlikely, but it can't hurt to check. + ]] +assert(lwc.context or luatexbase, [[ + + This module requires a supported callback library. Please + follow the following format-dependant instructions: + - LaTeX: Use a version built after 2015-01-01, or include + `\usepackage{luatexbase}' before loading this module. + - Plain: Include `\input ltluatex' before loading this module. + - ConTeXt: Use the LMTX version. +]]) + +--[[ + Package/module initialization + ]] +if lwc.context then + lwc.warning = logs.reporter("module", lwc.name) + lwc.attribute = attributes.public(lwc.name) + lwc.contrib_head = 'contribute_head' -- For \LuaMetaTeX{} +elseif lwc.plain or lwc.latex then + luatexbase.provides_module { + name = lwc.name, + date = "2021/10/09", --%%date + version = "1.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. + ]], + } + lwc.warning = function(str) luatexbase.module_warning(lwc.name, str) end + lwc.attribute = luatexbase.new_attribute(lwc.name) + lwc.contrib_head = 'contrib_head' -- For \LuaTeX{} +else -- uh oh + error("Unsupported format. Please use (Lua)LaTeX, Plain (Lua)TeX, or ConTeXt (MKXL/LMTX)") +end + +lwc.paragraphs = {} -- List to hold the alternate paragraph versions + +if tex.interlinepenalty ~= 0 then + lwc.warning [[ +\interlinepenalty is set to a non-zero value. +This may prevent lua-widow-control from +properly functioning. +]] +end + + +--[[ + Function definitions + ]] + +--- Create a table of functions to enable or disable a given callback +--- @param t table Parameters of the callback to create +--- callback: str = The \LuaTeX{} callback name +--- func: function = The function to call +--- name: str = The name/ID of the callback +--- category: str = The category for a \ConTeXt{} "Action" +--- position: str = The "position" for a \ConTeXt{} "Action" +--- lowlevel: bool = 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 +function lwc.register_callback(t) + if lwc.plain or lwc.latex then -- Both use \LuaTeX{}Base for callbacks + return { + enable = function() + luatexbase.add_to_callback(t.callback, t.func, t.name) + end, + disable = function() + luatexbase.remove_from_callback(t.callback, t.name) + end, + } + elseif lwc.context and not t.lowlevel then + return { + -- 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) + end, + disable = function() + nodes.tasks.disableaction(t.category, "lwc." .. t.name) + end, + } + elseif lwc.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. + ]] + return { + enable = function() callback.register(t.callback, t.func) end, + disable = function() callback.register(t.callback, nil) end, + } + end +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 + lwc.callbacks.disable_box_warnings.enable() + + -- Ensure that we were actually given a par (only under \ConTeXt{} for some reason) + if head.id ~= par_id and lwc.context then + return head + end + + -- We need to return the unmodified head at the end, so we make a copy here + local new_head = copy(head) + + -- Prevent ultra-short last lines (\TeX{}Book p. 104), except with narrow columns + local parfillskip = last(new_head) + if parfillskip.id == glue_id and tex.hsize > min_col_width then + parfillskip.stretch_order = 0 + parfillskip.stretch = 0.9 * tex.hsize -- Last line must be at least 10% long + end + + -- Break the paragraph 1 line longer than natural + local long_node, long_info = tex.linebreak(new_head, { + looseness = 1, + emergencystretch = tex.dimen.lwcemergencystretch, + }) + + -- Break the natural paragraph so we know how long it was + local natural_node, natural_info = tex.linebreak(copy(head)) + flush_list(natural_node) + + lwc.callbacks.disable_box_warnings.disable() + + -- 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 = -1 * long_info.prevdepth + last(long_node).next = prevdepth + + table.insert(lwc.paragraphs, {demerits = long_demerits, node = long_node}) + + -- \LuaMetaTeX{} crashes if we return `true` + 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. +function lwc.mark_paragraphs(head) + set_attribute(head, lwc.attribute, #lwc.paragraphs) + set_attribute(last(head), lwc.attribute, -1 * #lwc.paragraphs) + + return head +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. +function lwc.remove_widows(head) + local penalty = tex.outputpenalty + local paragraphs = lwc.paragraphs + + --[[ + We only need to process pages that have orphans or widows. If `paragraphs` + is empty, then there is nothing that we can do. + ]] + if penalty >= 10000 or + penalty <= -10000 or + penalty == 0 or + #paragraphs == 0 then + return head + end + + 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. + ]] + local paragraph_index = 1 + local minimum_demerits = paragraphs[paragraph_index].demerits + + -- 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 + flush_list(paragraphs[paragraph_index].node) + paragraphs[paragraph_index].node = nil + paragraph_index, minimum_demerits = i, paragraph.demerits + elseif i > 1 then + -- Not sure why `i > 1` is required? + flush_list(paragraph.node) + paragraph.node = nil + end + end + + local target_node = paragraphs[paragraph_index].node + local clear_flag = false + + -- Loop through all of the nodes on the page + while head do + -- Insert the start of the replacement paragraph + if has_attribute(head, lwc.attribute, paragraph_index) then + head.prev.next = target_node + clear_flag = true + end + + -- Insert the end of the replacement paragraph + if has_attribute(head, lwc.attribute, -1 * paragraph_index) then + last(target_node).next = head.next + clear_flag = false + end + + -- Start of final paragraph + if has_attribute(head, lwc.attribute, #paragraphs) then + local last_line = copy(last(head)) + + last(last_line).next = copy(tex.lists[lwc.contrib_head]) + + last(head).prev.prev.next = nil + -- Move the last line to the next page + tex.lists[lwc.contrib_head] = last_line + end + + if clear_flag then + head = node.free(head) + else + head = head.next + end + end + + lwc.paragraphs = {} -- Clear paragraphs array at the end of the page + + return head_save +end + + +-- Add all of the callbacks +lwc.callbacks = { + disable_box_warnings = lwc.register_callback({ + callback = "hpack_quality", + func = function() end, + name = "disable_box_warnings", + lowlevel = true, + }), + remove_widows = lwc.register_callback({ + callback = "pre_output_filter", + func = lwc.remove_widows, + name = "remove_widows", + lowlevel = true, + }), + save_paragraphs = lwc.register_callback({ + callback = "pre_linebreak_filter", + func = lwc.save_paragraphs, + name = "save_paragraphs", + category = "processors", + position = "after", + }), + mark_paragraphs = lwc.register_callback({ + callback = "post_linebreak_filter", + func = lwc.mark_paragraphs, + name = "mark_paragraphs", + category = "finalizers", + position = "after", + }), +} + + +local enabled = false +function lwc.enable_callbacks() + if not enabled then + lwc.callbacks.remove_widows.enable() + lwc.callbacks.save_paragraphs.enable() + lwc.callbacks.mark_paragraphs.enable() + + enabled = true + else + lwc.warning("Already enabled") + end +end + + +function lwc.disable_callbacks() + if 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. + ]] + + enabled = false + else + lwc.warning("Already disabled") + end +end + + +return lwc -- cgit v1.2.3