From 1b913ce64901669921736117b2ed8aa36c55110e Mon Sep 17 00:00:00 2001 From: Karl Berry Date: Sat, 14 May 2022 20:33:11 +0000 Subject: lua-widow-control (14may22) git-svn-id: svn://tug.org/texlive/trunk@63302 c570f23f-e606-0410-a88d-b1316a301751 --- .../luatex/lua-widow-control/lua-widow-control.lua | 637 ++++++++++++++++----- .../luatex/lua-widow-control/lua-widow-control.tex | 22 +- 2 files changed, 492 insertions(+), 167 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 94dd85e1451..45360db2092 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,21 +5,45 @@ SPDX-FileCopyrightText: 2022 Max Chernoff ]] +--- Tell the linter about node attributes +--- @class node +--- @field prev node +--- @field next node +--- @field id integer +--- @field subtype integer +--- @field penalty integer +--- @field height integer +--- @field depth integer + +-- Set some default variables lwc = lwc or {} lwc.name = "lua-widow-control" lwc.nobreak_behaviour = "keep" +-- Locals for `debug_print` local write_nl = texio.write_nl local string_rep = string.rep +local write_log +if status.luatex_engine == "luametatex" then + write_log = "logfile" +else + write_log = "log" +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) if not lwc.debug then return end + -- The number of spaces we need local filler = 15 - #title if text then - write_nl("log", "LWC (" .. title .. string_rep(" ", filler) .. "): " .. text) + write_nl(write_log, "LWC (" .. title .. string_rep(" ", filler) .. "): " .. text) else - write_nl("log", "LWC: " .. string_rep(" ", 18) .. title) + write_nl(write_log, "LWC: " .. string_rep(" ", 18) .. title) end end @@ -31,16 +55,16 @@ end local format = tex.formatname local context, latex, plain, optex, lmtx -if format:find('cont') then -- cont-en, cont-fr, cont-nl, ... +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, ... +elseif format:find("latex") then -- lualatex, lualatex-dev, ... latex = true -elseif format == 'luatex' then -- Plain +elseif format == "luatex" then -- Plain plain = true -elseif format == 'optex' then -- OpTeX +elseif format == "optex" then -- OpTeX optex = true end @@ -48,46 +72,81 @@ 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 or node.copylist -local par_id = node.id("par") or node.id("local_par") +-- Node ID's +local baselineskip_subid = 2 local glue_id = node.id("glue") 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 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 versions of globals +local copy = 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 insert_token = token.put_next or token.putnext +local last = node.slide +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 traverse = node.traverse +local traverseid = node.traverse_id or node.traverseid + +-- Misc. Constants +local iffalse = token.create("iffalse") +local iftrue = token.create("iftrue") +local INFINITY = 10000 local min_col_width = tex.sp("250pt") +local SINGLE_LINE = 50 +local PAGE_MULTIPLE = 100 --[[ Package/module initialization ]] -local warning, info, attribute, contrib_head, stretch_order, pagenum, emergencystretch, max_cost +local attribute, + contrib_head, + emergencystretch, + info, + max_cost, + pagenum, + stretch_order, + warning if lmtx then + -- LMTX has removed underscores from most of the Lua parts debug_print("LMTX") - contrib_head = 'contributehead' + contrib_head = "contributehead" stretch_order = "stretchorder" else - contrib_head = 'contrib_head' + 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) + + warning = logs.reporter(lwc.name, "warning") + local _info = logs.reporter(lwc.name, "info") + info = function (text) + logs.pushtarget("logfile") + _info(text) + logs.poptarget() + end attribute = attributes.public(lwc.name) 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 + -- Dimen names if tex.isdimen("g__lwc_emergencystretch_dim") then emergencystretch = "g__lwc_emergencystretch_dim" max_cost = "g__lwc_maxcost_int" @@ -100,8 +159,8 @@ elseif plain or latex or optex then debug_print("Plain/LaTeX") luatexbase.provides_module { name = lwc.name, - date = "2022/04/23", --%%slashdate - version = "2.0.6", --%%version + date = "2022/05/14", --%%dashdate + version = "2.1.0", --%%version description = [[ This module provides a LuaTeX-based solution to prevent @@ -114,11 +173,12 @@ paragraphs.]], 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 +else -- This shouldn't ever happen error [[Unsupported format. Please use LaTeX, Plain TeX, ConTeXt or OpTeX.]] @@ -126,6 +186,8 @@ end local paragraphs = {} -- List to hold the alternate paragraph versions +--- Gets the current paragraph and page locations +--- @return string local function get_location() return "At " .. pagenum() .. "/" .. #paragraphs end @@ -134,78 +196,22 @@ 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 -local function register_callback(t) - if plain or 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 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 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, - } - elseif optex then - return { - enable = function() - callback.add_to_callback(t.callback, t.func, t.name) - end, - disable = function() - callback.remove_from_callback(t.callback, t.name) - end, - } - end -end - +--- Prints the initial glyphs and glue of an hlist +--- @param head node +--- @return nil 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) + if n.char < 127 then -- Only ASCII + chars = chars .. string_char(n.char) else - chars = chars .. "#" + chars = chars .. "#" -- Replacement for an unknown glyph end elseif n.id == glue_id then - chars = chars .. " " + chars = chars .. " " -- Any glue goes to a space end if #chars > 25 then break @@ -215,14 +221,56 @@ local function get_chars(head) debug_print(get_location(), chars) end +--- The "cost function" to use. See the manual. +--- @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) return demerits / math.sqrt(lines) end +--- Checks if the ConTeXt "grid snapping" is active +--- @return boolean +local function grid_mode_enabled() + -- Compare the token "mode" to see if `\\ifgridsnapping` is `\\iftrue` + return token.create("ifgridsnapping").mode == iftrue.mode +end + +--- Gets the next node of a type/subtype in a node list +--- @param head node The head of the node list +--- @param id number The node type +--- @param args table? +--- subtype: number = The node subtype +--- reverse: bool = Whether we should iterate backwards +--- @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 + if (subtype == args.subtype) or (args.subtype == nil) then + return n + end + end + else -- Only LMTX has the built-in backwards traverser + while head do + if head.id == id and + (head.subtype == args.subtype or args.subtype == nil) + then + return head + end + head = head.prev + end + 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) - -- Ensure that we were actually given a par (only under \ConTeXt{} for some reason) - if (head.id ~= par_id and context) or + 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 @@ -231,23 +279,34 @@ function lwc.save_paragraphs(head) -- 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 + #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) -- 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 + -- Equivalent to \\parfillskip=0pt plus 0.8\\hsize + local parfillskip + if lmtx or last(new_head).id ~= glue_id then + -- LMTX does not automatically add the \\parfillskip glue + parfillskip = new_node("glue", "parfillskip") + else + parfillskip = last(new_head) + end + + 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 + if lmtx or last(new_head).id ~= glue_id then + last(new_head).next = parfillskip + end + -- Break the paragraph 1 line longer than natural local long_node, long_info = tex.linebreak(new_head, { looseness = 1, @@ -255,25 +314,41 @@ function lwc.save_paragraphs(head) }) -- Break the natural paragraph so we know how long it was - local natural_node, natural_info = tex.linebreak(copy(head)) + nat_head = copy(head) + + if lmtx then + parfillskip = new_node("glue", "parfillskip") + parfillskip[stretch_order] = 1 + parfillskip.stretch = 1 -- 0pt plus 1fil + last(nat_head).next = parfillskip + end + + local natural_node, natural_info = tex.linebreak(nat_head) flush_list(natural_node) if renable_box_warnings then lwc.callbacks.disable_box_warnings.disable() end - -- Offset the accumulated \\prevdepth - local prevdepth = node.new("glue") - prevdepth.width = natural_info.prevdepth - long_info.prevdepth - last(long_node).next = prevdepth + if not grid_mode_enabled() then + -- Offset the accumulated \\prevdepth + local prevdepth = new_node("glue") + prevdepth.width = natural_info.prevdepth - long_info.prevdepth + last(long_node).next = prevdepth + end + + local long_cost = lwc.paragraph_cost(long_info.demerits, long_info.prevgraf) - if long_info.prevgraf == natural_info.prevgraf + 1 then + if long_info.prevgraf == natural_info.prevgraf + 1 and + long_cost > 10 -- Any paragraph that is "free" to expand is suspicious + then table.insert(paragraphs, { - cost = lwc.paragraph_cost(long_info.demerits, long_info.prevgraf), - node = long_node + cost = long_cost, + node = next_of_type(long_node, hlist_id, { subtype = line_subid }) }) end + -- Print some debugging information get_chars(head) debug_print(get_location(), "nat lines " .. natural_info.prevgraf) debug_print( @@ -288,21 +363,45 @@ function lwc.save_paragraphs(head) lwc.paragraph_cost(long_info.demerits, long_info.prevgraf) ) - -- \LuaMetaTeX{} crashes if we return `true` + -- \ConTeXt{} crashes if we return `true` return head end -local last_paragraph = 0 --- 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. +--- paragraphs index, which is negated for the end of the paragraph. Called by the +--- `post_linebreak_filter` callback. +--- +--- @param head node +--- @return node function lwc.mark_paragraphs(head) - if not status.output_active then - set_attribute(head, attribute, #paragraphs + (100 * pagenum())) - set_attribute(last(head), attribute, -1 * (#paragraphs + (100 * pagenum()))) - last_paragraph = #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) + + if top_para ~= bottom_para then + set_attribute( + top_para, + attribute, + #paragraphs + (PAGE_MULTIPLE * pagenum()) + ) + set_attribute( + bottom_para, + 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 + set_attribute( + top_para, + attribute, + #paragraphs + (PAGE_MULTIPLE * pagenum()) + SINGLE_LINE + ) + end end return head @@ -313,7 +412,11 @@ end --- 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. +--- the loop at the end if the list is circular. Called by the `pre_output_filter` +--- callback. +--- +--- @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 @@ -341,24 +444,15 @@ recover, but your output may be corrupted. 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 head_save = head -- Save the start of the `head` linked-list - - local penalty = tex.outputpenalty - tex.interlinepenalty +--- Checks to see if a penalty matches the widow/orphan/broken penalties +--- @param penalty number +--- @return boolean +function is_matching_penalty(penalty) 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. @@ -366,26 +460,47 @@ 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 ~= 0 and - penalty < 10000 and - (penalty == widowpenalty or - penalty == displaywidowpenalty or - penalty == clubpenalty or - penalty == clubpenalty + widowpenalty or - penalty == clubpenalty + displaywidowpenalty or - penalty == brokenpenalty or - penalty == brokenpenalty + widowpenalty or - penalty == brokenpenalty + displaywidowpenalty or - penalty == brokenpenalty + clubpenalty or - penalty == brokenpenalty + clubpenalty + widowpenalty or - penalty == brokenpenalty + clubpenalty + displaywidowpenalty) and - #paragraphs >= 1 then - else + penalty = penalty - tex.interlinepenalty + + return penalty ~= 0 and + penalty < INFINITY and ( + penalty == widowpenalty or + penalty == displaywidowpenalty or + penalty == clubpenalty or + penalty == clubpenalty + widowpenalty or + penalty == clubpenalty + displaywidowpenalty or + penalty == brokenpenalty or + penalty == brokenpenalty + widowpenalty or + penalty == brokenpenalty + displaywidowpenalty or + penalty == brokenpenalty + clubpenalty or + penalty == brokenpenalty + clubpenalty + widowpenalty or + penalty == brokenpenalty + clubpenalty + displaywidowpenalty + ) +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) + + if not is_matching_penalty(tex.outputpenalty) or + #paragraphs == 0 + then paragraphs = {} return head_save end - info("Widow/orphan detected. Attempting to remove.") + info("Widow/orphan/broken hyphen detected. Attempting to remove") --[[ Find the paragraph on the page with the least cost. @@ -393,10 +508,38 @@ function lwc.remove_widows(head) local paragraph_index = 1 local best_cost = paragraphs[paragraph_index].cost + 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) + if value then + last_paragraph = value % PAGE_MULTIPLE + break + end + + head_last = head_last.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 + ) % PAGE_MULTIPLE + else + first_paragraph = first_attribute_val % PAGE_MULTIPLE + end + -- We find the current "best" replacement, then free the unused ones for i, paragraph in pairs(paragraphs) do if paragraph.cost < best_cost and - i ~= last_paragraph + i < last_paragraph and + i >= first_paragraph then -- Clear the old best paragraph flush_list(paragraphs[paragraph_index].node) @@ -421,10 +564,10 @@ function lwc.remove_widows(head) ) if best_cost > tex.getcount(max_cost) or - paragraph_index == last_paragraph + paragraph_index == last_paragraph then -- If the best replacement is too bad, we can't do anything - warning("Widow/Orphan NOT removed on page " .. pagenum()) + warning("Widow/Orphan/broken hyphen NOT removed on page " .. pagenum()) paragraphs = {} return head_save end @@ -434,12 +577,13 @@ function lwc.remove_widows(head) -- Start of final paragraph debug_print("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 -- Ignore any glue nodes - elseif head.id == penalty_id and head.penalty >= 10000 then + elseif head.id == penalty_id and head.penalty >= INFINITY then -- Infinite break penalty big_penalty_found = true elseif big_penalty_found and head.id == hlist_id then @@ -451,7 +595,7 @@ function lwc.remove_widows(head) head = last(head_save) break elseif lwc.nobreak_behaviour == "warn" then - warning("Widow/Orphan NOT removed on page " .. pagenum()) + warning("Widow/Orphan/broken hyphen NOT removed on page " .. pagenum()) paragraphs = {} return head_save end @@ -467,6 +611,17 @@ function lwc.remove_widows(head) head = head.prev end + local potential_penalty = head.prev.prev + + if potential_penalty and + potential_penalty.id == penalty_id and + potential_penalty.subtype == linebreakpenalty_subid and + is_matching_penalty(potential_penalty.penalty) + then + warning("Making a new widow/orphan/broken hyphen on page " .. pagenum()) + end + + last_line = copy(head) last(last_line).next = copy(tex.lists[contrib_head]) @@ -489,18 +644,50 @@ function lwc.remove_widows(head) debug_print("remove_widows", "found " .. value) -- Insert the start of the replacement paragraph - if value == paragraph_index + (100 * pagenum()) then + 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 + -- Fix the `\\baselineskip` glue between paragraphs + height_difference = ( + next_of_type(head, hlist_id, { subtype = line_subid }).height - + next_of_type(target_node, hlist_id, { subtype = line_subid }).height + ) + + local prev_bls = next_of_type( + head, + glue_id, + { subtype = baselineskip_subid, reverse = true } + ) + + if prev_bls then + prev_bls.width = prev_bls.width + height_difference + end + head.prev.next = target_node free_next_nodes = true end -- Insert the end of the replacement paragraph - if value == -1 * (paragraph_index + (100 * pagenum())) then + if value == -1 * (paragraph_index + (PAGE_MULTIPLE * pagenum())) or + value == paragraph_index + (PAGE_MULTIPLE * pagenum()) + SINGLE_LINE + then debug_print("remove_widows", "replacement end") - safe_last(target_node).next = head.next + local target_node_last = safe_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 + target_node_last.next = after_glue + + after_glue.next = head.next + else + target_node_last.next = head.next + end + break end @@ -512,11 +699,10 @@ function lwc.remove_widows(head) end info( - "Widow/orphan successfully removed at paragraph " + "Widow/orphan/broken hyphen successfully removed at paragraph " .. paragraph_index .. " on page " .. pagenum() - .. "." ) paragraphs = {} -- Clear paragraphs array at the end of the page @@ -524,31 +710,90 @@ function lwc.remove_widows(head) return head_save 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 +--- 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" +--- @return table t Enablers/Disablers for the callback +--- enable: function = Enable the callback +--- disable: function = Disable the callback +local function register_callback(t) + if plain or 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 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 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, + } + elseif optex then -- Op\TeX{} is very similar to luatexbase + return { + enable = function() + callback.add_to_callback(t.callback, t.func, t.name) + end, + disable = function() + callback.remove_from_callback(t.callback, t.name) + end, + } + end +end + -- Add all of the callbacks lwc.callbacks = { disable_box_warnings = register_callback({ callback = "hpack_quality", - func = function() end, - name = "disable_box_warnings", + func = function() end, + name = "disable_box_warnings", lowlevel = true, }), remove_widows = register_callback({ callback = "pre_output_filter", - func = lwc.remove_widows, - name = "remove_widows", + func = lwc.remove_widows, + name = "remove_widows", lowlevel = true, }), save_paragraphs = register_callback({ callback = "pre_linebreak_filter", - func = lwc.save_paragraphs, - name = "save_paragraphs", + func = lwc.save_paragraphs, + name = "save_paragraphs", category = "processors", position = "after", }), mark_paragraphs = register_callback({ callback = "post_linebreak_filter", - func = lwc.mark_paragraphs, - name = "mark_paragraphs", + func = lwc.mark_paragraphs, + name = "mark_paragraphs", category = "finalizers", position = "after", }), @@ -556,6 +801,7 @@ lwc.callbacks = { local enabled = false +--- Enable the paragraph callbacks function lwc.enable_callbacks() debug_print("callbacks", "enabling") if not enabled then @@ -568,6 +814,7 @@ function lwc.enable_callbacks() end end +--- Disable the paragraph callbacks function lwc.disable_callbacks() debug_print("callbacks", "disabling") if enabled then @@ -588,12 +835,88 @@ end function lwc.if_lwc_enabled() debug_print("iflwc") if enabled then - tex.sprint("\\iftrue") + insert_token(iftrue) else - tex.sprint("\\iffalse") + 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 The TeX types of the function arguments +--- @return string The mangled name +local function mangle_name(name, args) + if plain then + return "lwc@" .. name:gsub("_", "@") + elseif optex then + return "_lwc_" .. name + elseif context then + return "lwc_" .. name + elseif latex then + return "__lwc_" .. name .. ":" .. string_rep("n", #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 The TeX types of the function arguments +--- @return nil +local function register_tex_cmd(name, func, args) + local scanning_func + name = mangle_name(name, args) + + if not context then + local scanners = {} + for _, arg in ipairs(args) do + scanners[#scanners+1] = token['scan_' .. arg] + end + + scanning_func = function() + local values = {} + for _, scanner in ipairs(scanners) do + values[#values+1] = scanner() + end + + func(table.unpack(values)) + end + end + + if optex then + define_lua_command(name, scanning_func) + return + elseif plain or latex then + local index = luatexbase.new_luafunction(name) + lua.get_functions_table()[index] = scanning_func + token.set_lua(name, index) + elseif context then + interfaces.implement { + name = name, + public = true, + arguments = args, + actions = func + } 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, {}) +register_tex_cmd( + "nobreak", + function(str) + lwc.nobreak_behaviour = str + end, + { "string" } +) +register_tex_cmd( + "debug", + function(str) + lwc.debug = str ~= "0" and str ~= "false" and str ~= "stop" + end, + { "string" } +) + --- Silence the luatexbase "Enabling/Removing " info messages --- --- Every time that a paragraph is typeset, \lwc/ hooks in @@ -612,12 +935,12 @@ end 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) + for i = 1, nups do + local name, func = debug.getupvalue(luatexbase.add_to_callback, i) if name == "luatexbase_log" then debug.setupvalue( luatexbase.add_to_callback, - x, + i, function(text) if text:match("^Inserting") or text:match("^Removing") then return 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 b667d05c111..78385ad698d 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 v2.0.6} %%version +\wlog{lua-widow-control v2.1.0} %%version \ifx\directlua\undefined \errmessage{% @@ -35,13 +35,8 @@ \expandglyphsinfont\the\font 20 20 5 \adjustspacing=2 -% Define \TeX{} wrappers for Lua functions -\def\lwcenable{\directlua{lwc.enable_callbacks()}} -\def\lwcdisable{\directlua{lwc.disable_callbacks()}} -\def\iflwc{\directlua{lwc.if_lwc_enabled()}} - % Enable \lwc/ by default when the package is loaded. -\lwcenable +\lwc@enable % Expansion of some parts of the document, such as section headings, is quite % undesirable, so we'll disable \lwc/ for certain commands. @@ -50,15 +45,15 @@ \newcount\lwc@disable@count \def\lwc@patch@pre{% - \iflwc% + \lwc@if@enabled% \advance\lwc@disable@count by 1% - \lwcdisable% + \lwc@disable% \fi% } \def\lwc@patch@post{ \ifnum\lwc@disable@count>0% - \lwcenable% + \lwc@enable% \advance\lwc@disable@count by -1% \fi } @@ -89,6 +84,13 @@ \lwcdisablecmd{\beginsection} % Sectioning \endgroup +% Make the commands public +\let\lwcenable=\lwc@enable +\let\lwcdisable=\lwc@disable +\let\lwcdebug=\lwc@debug +\let\iflwc=\lwc@if@enabled +\let\lwcnobreak=\lwc@nobreak + \catcode`@=12 \endinput -- cgit v1.2.3