-- cloze.lua
-- Copyright 2015-2025 Josef Friedrich
--
-- This work may be distributed and/or modified under the
-- conditions of the LaTeX Project Public License, either version 1.3c
-- of this license or (at your option) any later version.
-- The latest version of this license is in
-- http://www.latex-project.org/lppl.txt
-- and version 1.3c or later is part of all distributions of LaTeX
-- version 2008/05/04 or later.
--
-- This work has the LPPL maintenance status `maintained'.
--
-- The Current Maintainer of this work is Josef Friedrich.
--
-- This work consists of the files cloze.lua, cloze.tex,
-- and cloze.sty.
---
---
Naming conventions
---
---* _Variable_ names for _nodes_ are suffixed with `_node`, for example
--- `head_node`.
---* _Variable_ names for _lengths_ (dimensions) are suffixed with
--- `_length`, for example `width`.
---
---__Initialisation of the function tables__
---It is good form to provide some background informations about this Lua
---module.
if not modules then
modules = {}
end
modules['cloze'] = {
version = '2.0.0',
comment = 'cloze',
author = 'Josef Friedrich, R.-M. Huber',
copyright = 'Josef Friedrich, R.-M. Huber',
license = 'The LaTeX Project Public License Version 1.3c 2008-05-04',
}
local farbe = require('farbe')
local luakeys = require('luakeys')()
local lparse = require('lparse')
local ansi_color = luakeys.utils.ansi_color
local log = luakeys.utils.log
local tex_printf = luakeys.utils.tex_printf
---
---The different types of cloze texts required different processing of
---the node lists.
---@alias ClozeType
---|'basic' # `\cloze`, `\clozenol`
---|'fix' # `\clozefix`
---|'par' # `\begin{clozepar}` and `\end{clozepar}`
---|'strike' # `\clozestrike`
---
---@alias MarkerPosition 'start'|'stop' # The argument `position` is either set to `start` or to `stop`.
---
---@class MarkerData
---@field cloze_type ClozeType # The cloze type, for example `basic` or `fixed`.
---@field position MarkerPosition # Whether the marker node indicates the beginning or the end of a cloze.
---@field finished? boolean # true if the node manipulation for the cloze is finished
---@field local_opts Options
---
---The table `config` bundles functions that deal with the option
---handling. All values and functions, which are related to the option
---management, are stored in this table.
---
---Marker processing (marker)
---
---A marker is a whatsit node of the subtype `user_defined`. A marker
---has two purposes:
---
---* Mark the begin and the end of a gap.
---* Store a index number, that points to a Lua table, which holds some
--- additional data like the local options.
local config = (function()
---
---I didn’t know what value I should take as `user_id`. Therefore I
---took my birthday (3.12.1978) and transformed it into a large number.
local user_id = 3121978
---
---Store all local options of the markers.
---@type {[integer]: MarkerData }
local storage = {}
---@class Options
---@field align? 'left'|'center'|'right' # The alignment of a fixed-size cloze text (`\clozefix`).
---@field box_height? string # The height of a cloze box (`clozebox`).
---@field box_rule? string # The thickness of the line around a cloze box (`clozebox`).
---@field box_width? string # The width of a cloze box (`clozebox`).
---@field debug? number # The debug or log level.
---@field distance? string # The distance between the cloze text and the cloze line.
---@field extend_count? integer # The number of extension units (`\clozeextend`).
---@field extend_height? string # The height of one extension unit (`\clozeextend`).
---@field extend_width? string # The width of one extension unit (`\clozeextend`).
---@field font? string # The font of the cloze text.
---@field line_color? string # The color name to colorize the cloze line.
---@field margin? string # The additional margin between the normal and the cloze text.
---@field min_lines? integer # The minimum number of lines a `clozepar` environment must have.
---@field spacing? number # The spacing of the lines in the environment `clozespace`.
---@field spread? number # The magnification or spreading factor of a gap.
---@field text_color? string # The color name to colorize the cloze text.
---@field thickness? string # The thickness of a line.
---@field visibility? boolean # The visibility of the cloze text.
---@field width? string # The width of a fixed size cloze (`\clozefix`).
---The default options.
---@type Options
local defaults = {
align = 'left',
box_height = nil,
box_rule = '0.4pt',
box_width = '\\linewidth',
debug = 0,
distance = '1.5pt',
extend_count = 5,
extend_height = '2ex',
extend_width = '1em',
font = nil,
line_color = 'black',
margin = '3pt',
min_lines = 0,
spacing = 1.6,
spread = 0,
text_color = 'blue',
thickness = '0.4pt',
visibility = true,
width = '2cm',
}
---
---The global options set by the user.
---@type Options
local global_options = {}
---
---The local options.
---@type Options
local local_options = {}
---@type MarkerData|nil
local current_marker_data = nil
---@type integer
local index
---
---`index` is a counter. The functions `get_index()`
---increases the counter by one and then returns it.
---
---@return integer # The index number of the corresponding table in `storage`.
local function get_index()
if not index then
index = 0
end
index = index + 1
return index
end
---
---The function `get_storage()` retrieves values which belong
--- to a whatsit marker.
---
---@param index integer # The argument `index` is a numeric value.
---
---@return MarkerData value
local function get_storage(index)
return storage[index]
end
---
---Write a marker node to TeX's current node list.
---
---@param cloze_type ClozeType # The cloze type, for example `basic` or `fixed`.
---@param position MarkerPosition # Whether the marker node indicates the beginning or the end of a cloze.
---@param local_opts? Options
local function write_marker(cloze_type, position, local_opts)
local index = get_index()
local data = { cloze_type = cloze_type, position = position }
if local_opts ~= nil then
data.local_opts = local_opts
end
storage[index] = data
local marker = node.new('whatsit', 'user_defined') --[[@as UserDefinedWhatsitNode]]
marker.type = 100 -- number
marker.user_id = user_id
marker.value = index
node.write(marker)
end
---
---Test whether the node `n` is a marker and retrieve the
---the corresponding marker data. If the specified node is a start
---marker, the local options of that marker node are loaded.
---
---@param n UserDefinedWhatsitNode # The argument `n` is a node of unspecified type.
---
---@return MarkerData|nil # The marker data or nothing if given node is not a marker.
local function get_marker_data(n)
if n.id == node.id('whatsit') and n.subtype ==
node.subtype('user_defined') and n.user_id == user_id then
local data = get_storage(n.value --[[@as integer]] )
if data.position == 'start' then
if data.local_opts == nil then
local_options = {}
else
local_options = data.local_opts
end
current_marker_data = data
end
return data
end
end
---
---Check if the given node is a marker.
---
---If the specified node is a start marker, the local options of that
---marker node are loaded.
---
---@param head_node Node # The current node.
---@param cloze_type ClozeType # The cloze type, for example `basic` or `fixed`.
---@param position MarkerPosition # Whether the marker node indicates the beginning or the end of a cloze.
---
---@return boolean
local function check_marker(head_node, cloze_type, position)
local data =
get_marker_data(head_node --[[@as UserDefinedWhatsitNode]] )
if data and data.cloze_type == cloze_type and data.position ==
position and not data.finished then
return true
end
return false
end
---
---Return the input node only if it is a marker of the specified
---cloze type and position.
---
---If the specified node is a start marker, the local options of that
---marker node are loaded.
---
---@param head_node Node # The current node.
---@param cloze_type ClozeType # The cloze type, for example `basic` or `fixed`.
---@param position MarkerPosition # Whether the marker node indicates the beginning or the end of a cloze.
---
---@return UserDefinedWhatsitNode|nil # The node if `head_node` is a marker node.
local function get_marker(head_node, cloze_type, position)
---@type UserDefinedWhatsitNode|nil
local marker = nil
if check_marker(head_node, cloze_type, position) then
marker = head_node --[[@as UserDefinedWhatsitNode]]
end
return marker
end
---
---Set the current start marker as completed by setting the `finalized` field to true.
local function finalize_cloze()
if current_marker_data ~= nil then
current_marker_data.finished = true
end
end
---
---Clear the global options storage.
local function reset_global_options()
global_options = {}
end
---
---Test whether the value `value` is not empty and has a
---value.
---
---@param value any # A value of different types.
---
---@return boolean # True is the value is set otherwise false.
local function has_value(value)
if value == nil or value == '' then
return false
else
return true
end
end
---
---Retrieve a value from a given key. First search for the value in the
---local options, then in the global options. If both option storages are
---empty, the default value will be returned.
---
---@param key string # The name of the options key.
---
---@return any # The value of the corresponding option key.
local function get(key)
local value_local = local_options[key]
local value_global = global_options[key]
local value, source
if has_value(value_local) then
source = 'local'
value = local_options[key]
elseif has_value(value_global) then
source = 'global'
value = value_global
else
source = 'default'
value = defaults[key]
end
local g = ansi_color.green
log.debug(
'Get value “%s” from the key “%s” the %s options storage',
g(value), g(key), g(source))
return value
end
---
---Return the default value of the given option.
---
---@param key any # The name of the options key.
---
---@return any # The corresponding value of the options key.
local function get_defaults(key)
return defaults[key]
end
---@type DefinitionCollection
local defs = {
align = {
description = 'The alignment of a fixed-size cloze text (\\clozefix).',
alias = 'alignment',
choices = { 'left', 'center', 'right' },
},
box_height = {
description = 'The height of a cloze box (clozebox).',
alias = { 'boxheight' },
data_type = 'string'
},
box_rule = {
description = 'The thickness of the line around a cloze box (clozebox).',
alias = { 'boxrule' },
data_type = 'string'
},
box_width = {
description = 'The width of a cloze box (clozebox).',
alias = { 'boxwidth' },
data_type = 'string'
},
debug = {
description = 'The debug or log level.',
alias = { 'log' },
data_type = 'integer',
process = function(value)
log.set(value)
end,
},
distance = {
description = 'The distance between the cloze text and the cloze line.',
data_type = 'string'
},
extend_count = {
description = 'The number of extension units (\\clozeextend).',
alias = { 'extension_count', 'extensioncount' },
data_type = 'integer',
},
extend_height = {
-- default = '2ex',
description = 'The height of one extension unit (\\clozeextend).',
alias = { 'extension_height', 'extensionheight' },
data_type = 'string',
},
extend_width = {
-- default = '1em',
description = 'The width of one extension unit (\\clozeextend).',
alias = { 'extension_width', 'extensionwidth' },
data_type = 'string',
},
font = {
description = 'The font of the cloze text.',
data_type = 'string',
},
line_color = {
description = 'The color name to colorize the cloze line.',
alias = 'linecolor',
process = function(value, input)
tex_printf('\\FarbeImport{%s}', value)
end,
data_type = 'string'
},
margin = {
description = 'The additional margin between the normal and the cloze text.',
data_type = 'string',
},
min_lines = {
description = 'The minimum number of lines a `clozepar` environment must have.',
alias = { 'minimum_lines', 'minlines' },
data_type = 'integer',
},
spacing = {
data_type = 'number',
description = 'The spacing of the lines in the environment clozespace',
},
spread = {
alias = { 'openup', 'scale' },
description = 'The magnification or spreading factor of a gap.',
data_type = 'number',
},
text_color = {
description = 'The color name to colorize the cloze text.',
alias = 'textcolor',
data_type = 'string',
process = function(value)
tex_printf('\\FarbeImport{%s}', value)
end,
},
thickness = {
description = 'The thickness of a line.',
data_type = 'string',
},
visibility = {
description = 'The visibility of the cloze text',
opposite_keys = { [true] = 'show', [false] = 'hide' },
},
width = {
description = 'The width of a fixed size cloze (\\clozefix)',
data_type = 'string',
},
}
---
---@param local_opts Options
local function set_local_options(local_opts)
local_options = local_opts
end
---
---Parse local options and set this options to the global variabl local_options
---
---@param kv_string string # A string of key-value pairs that can be parsed by luakeys.
---
---@return Options
local function parse_local_options(kv_string)
local_options = luakeys.parse(kv_string, {
defs = defs,
debug = log.get() > 3,
})
return local_options
end
---
---@param kv_string string # A string of key-value pairs that can be parsed by luakeys.
local function parse_global_options(kv_string)
luakeys.parse(kv_string, {
defs = defs,
accumulated_result = global_options,
debug = log.get() > 3,
})
end
local defs_manager = luakeys.DefinitionManager(defs)
---
---Key-value pair definitions for the environment `clozespace`
local defs_space = (function()
local manager = defs_manager:clone({ include = { 'spacing' } })
manager.defs.spacing.pick = 'number'
return manager
end)()
---
---Parse the oarg (optional argument) of the environment `clozespace`
---
---@param kv_string string for example `2` or `spacing=2`
local function parse_local_space_options(kv_string)
local local_opts = defs_space:parse(kv_string)
set_local_options(local_opts)
end
---
---Key-value pair definitions for the environment `clozebox`
local defs_box = (function()
local manager = defs_manager:clone({ exclude = { 'width' } })
manager.defs.box_height.alias = { 'boxheight', 'height' }
manager.defs.box_rule.alias = { 'boxrule', 'rule' }
manager.defs.box_width.alias = { 'boxwidth', 'width' }
return manager
end)()
---
---Parse the oarg (optional argument) of the environment `clozebox`
---
---@param kv_string string
local function parse_local_box_options(kv_string)
local local_opts = defs_box:parse(kv_string)
set_local_options(local_opts)
end
---
---Key-value pair definitions for the command `clozeextend`
local defs_extend = (function()
local manager = defs_manager:clone({
include = { 'extend_count', 'extend_height', 'extend_width' },
})
manager.defs.extend_count.pick = 'number'
manager.defs.extend_count.alias = {
'extension_count',
'extensioncount',
'count',
}
manager.defs.extend_height.alias = {
'extension_height',
'extensionheight',
'height',
}
manager.defs.extend_width.alias = {
'extension_width',
'extensionwidth',
'width',
}
return manager
end)()
---
---Parse the oarg (optional argument) of the command `clozeextend`
---
---@param kv_string string
local function parse_local_extend_options(kv_string)
local local_opts = defs_extend:parse(kv_string)
set_local_options(local_opts)
end
return {
get = get,
get_defaults = get_defaults,
reset_global_options = reset_global_options,
finalize_cloze = finalize_cloze,
check_marker = check_marker,
write_marker = write_marker,
get_marker = get_marker,
get_marker_data = get_marker_data,
set_local_options = set_local_options,
parse_local_options = parse_local_options,
parse_global_options = parse_global_options,
parse_local_space_options = parse_local_space_options,
parse_local_box_options = parse_local_box_options,
parse_local_extend_options = parse_local_extend_options,
defs_manager = defs_manager,
---
---Get the alignment of a fixed-size cloze text (`\clozefix`).
---
---@return 'left'|'center'|'right' align # The alignment of a fixed-size cloze text (`\clozefix`).
get_align = function()
return get('align')
end,
---
---Get the height of a cloze box (`clozebox`).
---
---@return string|nil box_width # The height of a cloze box (`clozebox`).
get_box_height = function()
return get('box_height')
end,
---
---Get the thickness of the line around a cloze box (`clozebox`).
---
---@return string box_width # The thickness of the line around a cloze box (`clozebox`).
get_box_rule = function()
return get('box_rule')
end,
---
---Get the width of a cloze box (`clozebox`).
---
---@return string box_width # The width of a cloze box (`clozebox`).
get_box_width = function()
return get('box_width')
end,
---
---Get the distance between the cloze text and the cloze line in scaled points.
---
---@return integer distance # The distance between the cloze text and the cloze line in scaled points.
get_distance = function()
return tex.sp(get('distance'))
end,
---
---Get the number of extension units (`\clozeextend`).
---
---@return integer extend_count # The number of extension units (`\clozeextend`).
get_extend_count = function()
return get('extend_count')
end,
---
---Get the height of one extension unit (`\clozeextend`) in scaled points.
---
---@return integer extend_height # The height of one extension unit (`\clozeextend`) in scaled points.
get_extend_height = function()
return tex.sp(get('extend_height'))
end,
---
---Get the width of one extension unit (`\clozeextend`) in scaled points.
---
---@return integer extend_width # The width of one extension unit (`\clozeextend`) in scaled points.
get_extend_width = function()
return tex.sp(get('extend_width'))
end,
---
---Get the font of the cloze text.
---
---@return string|nil font # The font of the cloze text.
get_font = function()
return get('font')
end,
---
---Get the color to colorize the cloze line.
---
---@return Color text_color # The color to colorize the cloze line.
get_line_color = function()
return farbe.Color(get('line_color'))
end,
---
---Get the additional margin in scaled points between the normal and the cloze text.
---
---@return integer min_lines # The additional margin in scaled points between the normal and the cloze text.
get_margin = function()
return tex.sp(get('margin'))
end,
---
---Get the minimum number of lines a `clozepar` environment must have.
---
---@return number min_lines # The minimum number of lines a `clozepar` environment must have.
get_min_lines = function()
return get('min_lines')
end,
---
---Get the spacing of the lines in the environment `clozespace`.
---
---@return number spacing # The spacing of the lines in the environment `clozespace`.
get_spacing = function()
return get('spacing')
end,
---
---Get the magnification or spreading factor of a gap.
---
---@return number spread # The magnification or spreading factor of a gap.
get_spread = function()
return get('spread')
end,
---
---Get the color to colorize the cloze text.
---
---@return Color text_color # The color to colorize the cloze text.
get_text_color = function()
return farbe.Color(get('text_color'))
end,
---
---Get the thickness of a line in scaled points.
---
---@return integer thickness # The thickness of a line in scaled points.
get_thickness = function()
return tex.sp(get('thickness'))
end,
---
---Get the visibility of the cloze text.
---
---@return boolean visibility # The visibility of the cloze text.
get_visibility = function()
return get('visibility')
end,
---
---Get the width of a fixed size cloze (`\clozefix`) in scaled points.
---
---@return integer width # The width of a fixed size cloze (`\clozefix`) in scaled points.
get_width = function()
return tex.sp(get('width'))
end,
}
end)()
local utils = (function()
---
---Create a new PDF colorstack whatsit node.
---
---`utils.create_color()` is a wrapper for the function
---`utils.create_colorstack()`. It queries the current values of the
---options `line_color` and `text_color`.
---
---@param kind 'line'|'text'
---@param command 'push'|'pop'
---
---@return PdfColorstackWhatsitNode
local function create_color(kind, command)
local color
if kind == 'line' then
color = config.get_line_color()
else
color = config.get_text_color()
end
return color:create_pdf_colorstack_node(command)
end
---
---Create a rule node that is used as a line for the cloze texts.
---
---The `depth` and the `height` of the rule are calculated form the options
---`thickness` and `distance`.
---
---@param width number # The argument `width` must have the length unit __scaled points__.
---
---@return RuleNode
local function create_line(width)
local rule = node.new('rule') --[[@as RuleNode]]
local thickness = config.get_thickness()
local distance = config.get_distance()
rule.depth = distance + thickness
rule.height = -distance
rule.width = width
return rule
end
---
---Insert a `list` of nodes after or before the `current` node.
---
---The `head_node` argument is optional. Unfortunately, it is necessary in some edge cases.
---If `head_node` is omitted, the `current` node is used.
---
---@param position 'before'|'after' # The argument `position` can take the values `'after'` or `'before'`.
---@param current Node
---@param list table
---@param head_node? Node
---
---@return Node
local function insert_list(position, current, list, head_node)
if not head_node then
head_node = current
end
for _, insert in ipairs(list) do
if position == 'after' then
head_node, current = node.insert_after(head_node, current,
insert)
elseif position == 'before' then
head_node, current = node.insert_before(head_node, current,
insert)
end
end
return current
end
---
---Enclose a rule node (cloze line) with two PDF colorstack whatsits.
---
---The first colorstack node colors the line, the second resets the
---color.
---
---__Node list__: `whatsit:pdf_colorstack` (line_color) - `rule` (width) - `whatsit:pdf_colorstack` (reset_color)
---
---@param current Node
---@param width number
---
---@return Node
local function insert_line(current, width)
return insert_list('after', current, {
create_color('line', 'push'),
create_line(width),
create_color('line', 'pop'),
})
end
---
---Encloze a rule node with color nodes as the function
-- `utils.insert_line` does.
---
---In contrast to -`utils.insert_line` the three nodes are appended to
---TeX’s ‘current-list’. They are not inserted in a node list, which
---is accessed by a Lua callback.
---
---__Node list__: `whatsit:pdf_colorstack` (line_color) - `rule` (width) - `whatsit:pdf_colorstack` (reset_color)
---
local function write_line_nodes()
node.write(create_color('line', 'push'))
node.write(create_line(config.get_width()))
node.write(create_color('line', 'pop'))
end
---
---Create a line which stretches indefinitely in the
---horizontal direction.
---
---@return GlueNode
local function create_linefil()
local glue = node.new('glue') --[[@as GlueNode]]
glue.subtype = 100
glue.stretch = 65536
glue.stretch_order = 3
local rule = create_line(0)
rule.dir = 'TLT'
glue.leader = rule
return glue
end
---
---Surround a indefinitely strechable line with color whatsits and puts
---it to TeX’s ‘current (node) list’ (write).
local function write_linefil_nodes()
node.write(create_color('line', 'push'))
node.write(create_linefil())
node.write(create_color('line', 'pop'))
end
---
---Create a kern node with a given width.
---
---@param width number # The argument `width` had to be specified in scaled points.
---
---@return KernNode
local function create_kern_node(width)
local kern_node = node.new('kern') --[[@as KernNode]]
kern_node.kern = width
return kern_node
end
---
---Add at the beginning of each `hlist` node list a strut (a invisible
---character).
---
---Now we can add line, color etc. nodes after the first node of a hlist
---not before - after is much more easier.
---
---@param hlist_node HlistNode
---
---@return HlistNode hlist_node
---@return Node strut_node
---@return Node prev_head_node
local function insert_strut_into_hlist(hlist_node)
local prev_head_node = hlist_node.head
local kern_node = create_kern_node(0)
local strut_node = node.insert_before(hlist_node.head,
prev_head_node, kern_node)
hlist_node.head = prev_head_node.prev
return hlist_node, strut_node, prev_head_node
end
---
---Write a kern node to the current node list. This kern node can be
---used to build a margin.
local function write_margin_node()
node.write(create_kern_node(config.get_margin()))
end
---
---Search for a `hlist` (subtype `line`) and insert a strut node into
---the list if a hlist is found.
---
---@param head_node Node # The head of a node list.
---@param insert_strut boolean
---
---@return HlistNode|nil hlist_node
---@return Node|nil strut_node
---@return Node|nil prev_head_node
local function search_hlist(head_node, insert_strut)
while head_node do
if head_node.id == node.id('hlist') and head_node.subtype == 1 then
---@cast head_node HlistNode
if insert_strut then
return insert_strut_into_hlist(head_node)
else
return head_node
end
end
head_node = head_node.next
end
end
---
---See nodetree
---@param n Node
local function debug_node_list(n)
if log.get() < 5 then
return
end
local is_cloze = false
---@param head_node Node
local function get_textual_from_glyph(head_node)
local properties = node.direct.get_properties_table()
local node_id = node.direct.todirect(head_node) -- Convert to node id
local props = properties[node_id]
local info = props and props.glyph_info
local textual
local character_index = node.direct.getchar(node_id)
if info then
textual = info
elseif character_index == 0 then
textual = '^^@'
elseif character_index <= 31 or
(character_index >= 127 and character_index <= 159) then
textual = '???'
elseif character_index < 0x110000 then
textual = utf8.char(character_index)
else
textual = string.format('^^^^^^%06X', character_index)
end
return textual
end
local output = {}
---
---@param value string
local add = function(value)
table.insert(output, value)
end
local red = ansi_color.red
local green = ansi_color.green
local yellow = ansi_color.yellow
local blue = ansi_color.blue
local magenta = ansi_color.magenta
local cyan = ansi_color.cyan
while n do
local marker_data =
config.get_marker_data(n --[[@as UserDefinedWhatsitNode]] )
if marker_data then
if marker_data.position == 'start' then
is_cloze = true
else
is_cloze = false
end
end
if n.id == node.id('glyph') then
if is_cloze then
add(yellow(get_textual_from_glyph(n)))
else
add(get_textual_from_glyph(n))
end
elseif n.id == node.id('glue') then
add(' ')
elseif n.id == node.id('disc') then
add(cyan('|'))
elseif n.id == node.id('kern') then
add(blue('<'))
elseif marker_data then
local char
if marker_data.position == 'start' then
char = 'START'
else
char = 'STOP'
end
add(magenta('[' .. char .. ']'))
elseif n.id == node.id('whatsit') and n.subtype ==
node.subtype('pdf_colorstack') then
local c = n --[[@as PdfColorstackWhatsitNode]]
local command
if c.command == 1 then
command = 'push'
elseif c.command == 2 then
command = 'pop'
end
add(green('[' .. command .. ']'))
elseif n.id == node.id('rule') then
add(red('_'))
elseif n.id == node.id('hlist') then
debug_node_list(n.head)
add(red('└'))
end
n = n.next
end
print(table.concat(output, ''))
end
return {
debug_node_list = debug_node_list,
insert_list = insert_list,
create_color = create_color,
insert_line = insert_line,
write_line_nodes = write_line_nodes,
write_linefil_nodes = write_linefil_nodes,
create_line = create_line,
create_kern_node = create_kern_node,
insert_strut_into_hlist = insert_strut_into_hlist,
write_margin_node = write_margin_node,
search_hlist = search_hlist,
}
end)()
---
---The `traversor` table provides functions to simplify traversing node
---lists. The cloze algorithms can then be implemented in callback
---functions. One callback call is made for each cloze line to be drawn.
local traversor = (function()
---@alias ClozeContinuation 'start-stop' | 'continue-stop' | 'start-continue' | 'continue-continue'
---The enviroment in the node list where the cloze can be inserted.
---The table is passed as an input parameter to the callback function
---`Visitor`.
---@class ClozeNodeEnvironment
---@field parent_hlist HlistNode
---@field start_marker? UserDefinedWhatsitNode
---@field start_node? Node # The start node, which marks the beginning of a cloze.
---@field start Node # The start node, which marks the beginning of a cloze. This field is derived from the fields `start_marker` or `start_node`.
---@field start_continuation boolean # `true` if the cloze must be continued by a line break.
---@field stop_node? Node # The stop node, which marks the end of a cloze. This field is derived from the fields `stop_marker` or `stop_node`.
---@field stop_marker? UserDefinedWhatsitNode
---@field stop Node # The stop node, which marks the end of a cloze. This field is derived from the fields `stop_marker` or `stop_node`.
---@field stop_continuation boolean True if the stop node is not a start marker. The cloze ends because the end of the line is reached.
---@field width integer The width in scaled points from the start to the stop node.
---@field continuation ClozeContinuation
---
---This callback function is called every time a cloze line needs to
---be inserted into the node list.
---@alias Visitor fun(env: ClozeNodeEnvironment): Node|nil
---
---Call the Visitor callback and assemble `ClozeNodeEnvironment` table
---and pass it in.
---
---@param visitor Visitor A callback function that is called each time a cloze line needs to be inserted into the node list.
---@param parent_hlist? HlistNode
---@param start_marker? UserDefinedWhatsitNode
---@param start_node? Node
---@param stop_node? Node
---@param stop_marker? UserDefinedWhatsitNode
---
---@return Node new_head To avoid endless loops if a new node is inserted at the end of a node list we can use this new head to continue
local function call_visitor(visitor,
parent_hlist,
start_marker,
start_node,
stop_node,
stop_marker)
local start --[[@as Node]]
if start_marker ~= nil then
start = start_marker
else
start = start_node
end
if start == nil then
error()
end
local stop --[[@as Node]]
if stop_marker ~= nil then
stop = stop_marker
else
stop = stop_node
end
if stop == nil then
error()
end
local width
if parent_hlist then
width = node.dimensions(parent_hlist.glue_set,
parent_hlist.glue_sign, parent_hlist.glue_order, start, stop)
else
width = node.dimensions(start, stop)
end
---@type ClozeContinuation
local continuation
if start_marker and stop_marker then
continuation = 'start-stop'
elseif start_node and stop_marker then
continuation = 'continue-stop'
elseif start_marker and stop_node then
continuation = 'start-continue'
elseif start_node and stop_node then
continuation = 'continue-continue'
end
---@type ClozeNodeEnvironment
local env = {
parent_hlist = parent_hlist,
start_marker = start_marker,
start_node = start_node,
start = start,
start_continuation = start_marker == nil,
stop_node = stop_node,
stop_marker = stop_marker,
stop = stop,
stop_continuation = stop_marker == nil,
width = width,
continuation = continuation,
}
local new_head = visitor(env)
if new_head then
return new_head
end
return stop
end
---
---Continue a basic cloze gap across line breaks.
---
---@param visitor Visitor # A callback function that is called each time a cloze line needs to be inserted into the node list.
---@param head_node Node # The head of a node list.
local function continue_cloze(visitor, head_node)
while head_node.next do
head_node = head_node.next
if head_node.head then
local start = head_node.head
local n = head_node.head
while n do
local stop_marker = config.get_marker(n, 'basic', 'stop')
if stop_marker then
call_visitor(visitor, head_node, nil, start, nil,
stop_marker)
return
elseif n.next == nil then
n = call_visitor(visitor, head_node, nil, start, n, nil)
end
n = n.next
end
end
end
end
---
---Recurse the node list and search for the marker.
---
---@param visitor Visitor # A callback function that is called each time a cloze line needs to be inserted into the node list.
---@param cloze_type ClozeType # The cloze type, for example `basic` or `fixed`.
---@param head_node Node # The head of a node list.
---@param parent_node? HlistNode # The parent node (hlist) of the head node.
local function traverse(visitor,
cloze_type,
head_node,
parent_node)
---@type UserDefinedWhatsitNode|nil
local start_marker = nil
---@type UserDefinedWhatsitNode|nil
local stop_marker = nil
while head_node do
if head_node.head then
traverse(visitor, cloze_type, head_node.head, head_node --[[@as HlistNode]] )
else
if not start_marker then
start_marker = config.get_marker(head_node, cloze_type,
'start')
end
if not stop_marker then
stop_marker = config.get_marker(head_node, cloze_type, 'stop')
end
if start_marker and stop_marker then
call_visitor(visitor, parent_node, start_marker, nil, nil,
stop_marker)
start_marker, stop_marker = nil, nil
elseif start_marker and not stop_marker and head_node.next ==
nil and parent_node then
--- continue cloze in the next line
call_visitor(visitor, parent_node, start_marker, nil,
head_node, nil)
continue_cloze(visitor, parent_node)
start_marker = nil
end
stop_marker = nil
end
head_node = head_node.next
end
end
return { traverse = traverse }
end)()
local function make_basic(head_node)
traversor.traverse(function(env)
config.finalize_cloze()
local line_color_push = utils.create_color('line', 'push')
local line = utils.create_line(env.width)
line_color_push.next = line
local line_color_pop = utils.create_color('line', 'pop')
line.next = line_color_pop
local first
if env.start_continuation then
-- the first node is attached on head of a hlist
first = env.parent_hlist.head
env.parent_hlist.head = line_color_push
else
first = env.start.next
env.start.next = line_color_push
end
if config.get_visibility() then
-- show cloze text
local kern = utils.create_kern_node(-env.width)
line_color_pop.next = kern
local text_color_push = utils.create_color('text', 'push')
kern.next = text_color_push
local text_color_pop = utils.create_color('text', 'pop')
--- start
text_color_push.next = first
--- stop
if env.stop.next ~= nil then
local tmp = env.stop.next
env.stop.next = text_color_pop
text_color_pop.next = tmp
else
env.stop.next = text_color_pop
-- to avoid endless loop, we have to return the new end of the node list
return text_color_pop
end
else
-- hide cloze text
--- stop
if env.stop_marker ~= nil then
line_color_pop.next = env.stop_marker.prev
else
return line_color_pop
end
end
end, 'basic', head_node)
return head_node
end
---
---Enlarge a basic cloze by a spread factor.
---
---We measure the widths of the cloze, calculate the spread width and
---then simply put half of that to the left and right of the cloze if
---the text is typeset.
---
---@param head_node_input Node # The head of a node list.
local function spread_basic(head_node_input)
local function recurse(head_node)
local n = head_node
local m
while n do
if n.head then
recurse(n.head)
elseif config.check_marker(n, 'basic', 'start') then
local start = n
m = n
while m do
if config.check_marker(m, 'basic', 'stop') then
local stop = m
local width = node.dimensions(start, stop)
local spread = config.get_spread()
if spread == 0 then
break
end
local spread_half_width = (width * spread) / 2
local start_kern = utils.create_kern_node(spread_half_width)
local start_next = start.next
start.next = start_kern
start_kern.next = start_next
local stop_kern = utils.create_kern_node(spread_half_width)
local stop_prev = stop.prev
stop_prev.next = stop_kern
stop_kern.next = stop
break
end
m = m.next
end
end
if n then
n = n.next
else
break
end
end
end
recurse(head_node_input)
return head_node_input
end
---
---Generate a gap with a fixed width. The corresponding LaTeX command to this Lua function is `\clozefix`.
---
---# Node lists
---
---## Show text:
---
---| Variable name | Node type | Node subtype | |
---|--------------------|-----------|--------------------|-------------|
---| `start_node` | `whatsit` | `user_definded` | `index` |
---| `line_node` | `rule` | | `width` |
---| `kern_start_node` | `kern` | Depends on `align` | |
---| `color_text_node` | `whatsit` | `pdf_colorstack` | Text color |
---| | `glyphs` | Text to show | |
---| `color_reset_node` | `whatsit` | `pdf_colorstack` | Reset color |
---| `kern_stop_node` | `kern` | Depends on `align` | |
---| `stop_node` | `whatsit` | `user_definded` | `index` |
---
---## Hide text:
---
---| Variable name | Node type | Node subtype | |
---|---------------|-----------|-----------------|---------|
---| `start_node` | `whatsit` | `user_definded` | `index` |
---| `line_node` | `rule` | | `width` |
---| `stop_node` | `whatsit` | `user_definded` | `index` |
---
---@param head_node_input Node # The head of a node list.
local function make_fix(head_node_input)
traversor.traverse(function(env)
config.finalize_cloze()
---
---Calculate the widths of the whitespace before (`start_width`) and
---after (`stop_width`) the cloze text.
---
---@param start Node
---@param stop Node
---
---@return integer width
---@return integer start_width # The width of the whitespace before the cloze text.
---@return integer stop_width # The width of the whitespace after the cloze text.
local function calculate_widths(start, stop)
local start_width, stop_width
local width = config.get_width()
local text_width = node.dimensions(start, stop)
local align = config.get_align()
if align == 'right' then
start_width = -text_width
stop_width = 0
elseif align == 'center' then
local half = (width - text_width) / 2
start_width = -half - text_width
stop_width = half
else
start_width = -width
stop_width = width - text_width
end
return width, start_width, stop_width
end
local width, kern_start_length, kern_stop_length = calculate_widths(
env.start, env.stop)
local line_node = utils.insert_line(env.start, width)
if config.get_visibility() then
utils.insert_list('after', line_node, {
utils.create_kern_node(kern_start_length),
utils.create_color('text', 'push'),
})
utils.insert_list('before', env.stop, {
utils.create_color('text', 'pop'),
utils.create_kern_node(kern_stop_length),
}, env.start)
else
line_node.next = env.stop.next
end
end, 'fix', head_node_input)
return head_node_input
end
---
---The corresponding LaTeX environment to this lua function is
---`clozepar`.
---
---# Node lists
---
---## Show text:
---
---| Variable name | Node type | Node subtype | |
---|--------------------|-----------|------------------|----------------------------|
---| `strut_node` | `kern` | | width = 0 |
---| `line_node` | `rule` | | `width` (Width from hlist) |
---| `kern_node` | `kern` | | `-width` |
---| `color_text_node` | `whatsit` | `pdf_colorstack` | Text color |
---| | `glyphs` | | Text to show |
---| `tail_node` | `glyph` | | Last glyph in hlist |
---| `color_reset_node` | `whatsit` | `pdf_colorstack` | Reset color
---
---## Hide text:
---
---| Variable name | Node type | Node subtype | |
---|---------------|------------|--------------|----------------------------|
---| `strut_node` | `kern` | | width = 0 |
---| `line_node` | `rule` | | `width` (Width from hlist) |
---
---@param head_node Node # The head of a node list.
local function make_par(head_node)
utils.debug_node_list(head_node)
---
---Add one additional empty line at the end of a paragraph.
---
---All fields from the last hlist node are copied to the created
---hlist.
---
---@param last_hlist_node HlistNode # The last hlist node of a paragraph.
---
---@return HlistNode # The created new hlist node containing the line.
local function add_additional_line(last_hlist_node)
local hlist_node = node.new('hlist') --[[@as HlistNode]]
hlist_node.subtype = 1
local fields = {
'width',
'depth',
'height',
'shift',
'glue_order',
'glue_set',
'glue_sign',
'dir',
}
for _, field in ipairs(fields) do
if last_hlist_node[field] then
hlist_node[field] = last_hlist_node[field]
end
end
local kern_node = utils.create_kern_node(0)
hlist_node.head = kern_node
utils.insert_line(kern_node, last_hlist_node.width)
last_hlist_node.next = hlist_node
hlist_node.prev = last_hlist_node
hlist_node.next = nil
return hlist_node
end
---
---Add multiple empty lines at the end of a paragraph.
---
---@param last_hlist_node HlistNode # The last hlist node of a paragraph.
---@param count number # Count of the lines to add at the end.
local function add_additional_lines(last_hlist_node,
count)
local i = 0
while i < count do
last_hlist_node = add_additional_line(last_hlist_node)
i = i + 1
end
end
---@type Node
local strut_node
---@type Node
local line_node
---@type number
local width
---@type HlistNode
local last_hlist_node
---@type HlistNode
local hlist_node
local line_count = 0
while head_node do
if head_node.id == node.id('hlist') then
---@cast head_node HlistNode
hlist_node = head_node
line_count = line_count + 1
last_hlist_node = hlist_node
width = hlist_node.width
hlist_node, strut_node, _ = utils.insert_strut_into_hlist(
hlist_node)
line_node = utils.insert_line(strut_node, width)
if config.get_visibility() then
utils.insert_list('after', line_node, {
utils.create_kern_node(-width),
utils.create_color('text', 'push'),
})
utils.insert_list('after', node.tail(line_node),
{ utils.create_color('text', 'pop') })
else
line_node.next = nil
end
end
head_node = head_node.next
end
local additional_lines = config.get_min_lines() - line_count
if additional_lines > 0 then
add_additional_lines(last_hlist_node, additional_lines)
end
return true
end
---
---visibilty = true
---
---```
---├─WHATSIT (user_defined) user_id 3121978, type 100, value 3
---├─VLIST (unknown) wd 77.93pt, dp 2.01pt, ht 18.94pt
---│ ╚═head
---│ ├─HLIST (box) wd 21.85pt, dp 0.11pt, ht 6.94pt
---│ │ ╚═head
---│ │ ├─WHATSIT (pdf_colorstack) data '0 0 1 rg 0 0 1 RG'
---│ │ ├─KERN (userkern) 28.04pt
---│ │ ├─GLYPH (glyph) 't', font 19, wd 4.09pt, ht 4.42pt, dp 0.11pt
---│ │ ├─GLYPH (glyph) 'o', font 19, wd 5.11pt, ht 6.94pt, dp 0.11pt
---│ │ ├─GLYPH (glyph) 'p', font 19, wd 5.11pt, ht 4.42pt, dp 0.11pt
---│ │ └─WHATSIT (pdf_colorstack) data ''
---│ ├─GLUE (baselineskip) wd 4.95pt
---│ └─HLIST (box) wd 77.93pt, dp 2.01pt, ht 6.94pt
---│ ╚═head
---│ ├─WHATSIT (pdf_colorstack) data '0 0 1 rg 0 0 1 RG'
---│ ├─RULE (normal) wd 77.93pt, dp -2.31pt, ht 2.71pt
---│ ├─WHATSIT (pdf_colorstack) data ''
---│ ├─KERN (fontkern) -77.93pt
---│ ├─GLYPH (glyph) 'b', font 20, wd 3.19pt, ht 6.94pt
---│ ├─GLYPH (glyph) 'a', font 20, wd 5.75pt, ht 4.53pt, dp 0.06pt
---│ ├─GLYPH (glyph) 's', font 20, wd 6.39pt, ht 4.5pt
---│ ├─GLYPH (glyph) 'e', font 20, wd 5.75pt, ht 4.55pt, dp 2.01pt
---│ └─KERN (italiccorrection)
---├─RULE (normal) dp 3.6pt, ht 8.4pt
---├─WHATSIT (user_defined) user_id 3121978, type 100, value 4
---```
---
---visibilty = false
---
---```
---├─WHATSIT (user_defined) user_id 3121978, type 100, value 3
---├─VLIST (unknown) wd 77.93pt, dp 2.01pt, ht 18.94pt
---│ ╚═head
---│ ├─HLIST (box) wd 21.85pt, dp 0.11pt, ht 6.94pt
---│ ├─GLUE (baselineskip) wd 4.95pt
---│ └─HLIST (box) wd 77.93pt, dp 2.01pt, ht 6.94pt
---│ ╚═head
---│ ├─GLYPH (glyph) 'b', font 20, wd 3.19pt, ht 6.94pt
---│ ├─GLYPH (glyph) 'a', font 20, wd 5.75pt, ht 4.53pt, dp 0.06pt
---│ ├─GLYPH (glyph) 's', font 20, wd 6.39pt, ht 4.5pt
---│ ├─GLYPH (glyph) 'e', font 20, wd 5.75pt, ht 4.55pt, dp 2.01pt
---│ └─KERN (italiccorrection)
---├─RULE (normal) dp 3.6pt, ht 8.4pt
---├─WHATSIT (user_defined) user_id 3121978, type 100, value 4
---```
---
---@param head_node Node # The head of a node list.
---
---@return Node head_node
local function make_strike(head_node)
traversor.traverse(function(env)
-- The pre linebreak filter gets fired multiple times (e.g. by package mdframed)
config.finalize_cloze()
local text_color = config.get_text_color()
local vlist = env.start.next --[[@as VlistNode]]
local top_hlist = vlist.head --[[@as HlistNode]]
local baselineskip = top_hlist.next --[[@as GlueNode]]
local base_hlist = baselineskip.next --[[@as HlistNode]]
local top_kern = top_hlist.head --[[@as KernNode]]
if top_hlist.width > base_hlist.width then
-- top long
-- short
vlist.width = base_hlist.width
top_kern.kern = -(top_hlist.width - base_hlist.width) / 2
else
-- top
-- base long
top_kern.kern = (base_hlist.width - top_hlist.width) / 2
end
-- top
local top_start = top_hlist.head
if config.get_visibility() then
-- top color
top_hlist.head = utils.create_color('text', 'push')
top_hlist.head.next = top_start
local top_stop = node.tail(top_hlist.head)
top_stop.next = utils.create_color('text', 'pop')
else
top_hlist.head = nil
end
-- strike line
if config.get_visibility() then
local base_start = base_hlist.head
local line = node.new('rule') --[[@as RuleNode]]
local thickness = config.get_thickness()
line.depth = -(base_hlist.height / 3)
line.height = (base_hlist.height / 3) + thickness
line.width = base_hlist.width
base_hlist.head = text_color:create_pdf_colorstack_node('push')
local color_pop = text_color:create_pdf_colorstack_node('pop')
line.width = base_hlist.width
local kern = utils.create_kern_node(-base_hlist.width)
base_hlist.head.next = line
line.next = color_pop
color_pop.next = kern
kern.next = base_start
end
end, 'strike', head_node)
return head_node
end
local cb = (function()
---
---@param callback_name CallbackName # The name of a callback
---@param func function # A function to register for the callback
---@param description string # Only used in LuaLatex
local function register(callback_name, func, description)
if luatexbase then
luatexbase.add_to_callback(callback_name, func, description)
else
callback.register(callback_name, func)
end
end
---
---@param callback_name CallbackName # The name of a callback
---@param description string # Only used in LuaLatex
local function unregister(callback_name, description)
if luatexbase then
luatexbase.remove_from_callback(callback_name, description)
else
callback.register(callback_name, nil)
end
end
---
---Store informations if the callbacks are already registered for
---a certain cloze type (`basic`, `fix`, `par`).
---
---@type table<'basic'|'fix'|'par'|'strike', boolean>
local is_registered = {}
return {
---
---Register the functions `make_par`, `make_basic` and
---`make_fix` as callbacks.
---
---`make_par` and `make_basic` are registered to the callback
---`post_linebreak_filter` and `make_fix` to the callback
---`pre_linebreak_filter`. A special treatment is needed for clozes in
---display math mode. The `post_linebreak_filter` is not called on
---display math formulas. I’m not sure if the `pre_output_filter` is the
---right choice to capture the display math formulas.
---
---@param cloze_type ClozeType # The cloze type, for example `basic` or `fixed`.
---
---@return boolean|nil
register_callbacks = function(cloze_type)
if cloze_type == 'par' then
register('post_linebreak_filter', make_par, cloze_type)
return true
end
if not is_registered[cloze_type] then
if cloze_type == 'basic' then
register('post_linebreak_filter', make_basic, cloze_type)
register('pre_linebreak_filter', spread_basic, cloze_type)
register('pre_output_filter', make_basic, cloze_type)
elseif cloze_type == 'fix' then
register('pre_linebreak_filter', make_fix, cloze_type)
elseif cloze_type == 'strike' then
register('pre_linebreak_filter', make_strike, cloze_type)
else
return false
end
is_registered[cloze_type] = true
end
end,
---
---Delete the registered functions from the Lua callbacks.
---
---@param cloze_type ClozeType # The cloze type, for example `basic` or `fixed`.
unregister_callbacks = function(cloze_type)
if cloze_type == 'basic' then
unregister('post_linebreak_filter', cloze_type)
unregister('pre_linebreak_filter', cloze_type)
unregister('pre_output_filter', cloze_type)
elseif cloze_type == 'fix' then
unregister('pre_linebreak_filter', cloze_type)
elseif cloze_type == 'strike' then
unregister('pre_linebreak_filter', cloze_type)
else
unregister('post_linebreak_filter', cloze_type)
end
end,
}
end)()
---
---Variable that can be used to store the previous fbox rule thickness
---to be able to restore the previous thickness.
local fboxrule_restore
---
---@param cloze_type ClozeType
---@param append_opts? string
local function print_cloze(cloze_type, append_opts)
local kv_string, text = lparse.scan('O{} v')
if append_opts ~= nil then
if kv_string == '' then
kv_string = append_opts
else
kv_string = kv_string .. ',' .. append_opts
end
end
tex.print(string.format('\\Cloze{%s}{%s}{%s}', cloze_type, kv_string,
text))
end
---
---This table contains some basic functions which are published to the
---`cloze.tex` and `cloze.sty` file.
return {
register_functions = function()
lparse.register_csname('cloze', function()
print_cloze('basic')
end)
lparse.register_csname('clozefix', function()
print_cloze('fix')
end)
lparse.register_csname('clozenol', function()
print_cloze('basic', 'thickness=0pt')
end)
lparse.register_csname('clozestrike', function()
local kv_string, error_text, solution_text =
lparse.scan('O{} v v')
tex.print(string.format('\\ClozeStrike{%s}{%s}{%s}', kv_string,
error_text, solution_text))
end)
lparse.register_csname('clozeline', function()
local kv_string = lparse.scan('O{}')
tex.print(string.format('\\ClozeLine{%s}', kv_string))
end)
lparse.register_csname('clozelinefil', function()
local kv_string = lparse.scan('O{}')
tex.print(string.format('\\ClozeLinefil{%s}', kv_string))
end)
lparse.register_csname('clozefil', function()
local kv_string, text = lparse.scan('O{} v')
tex.print(string.format('\\ClozeFil{%s}{%s}', kv_string, text))
end)
lparse.register_csname('clozeextend', function()
local kv_string = lparse.scan('O{}')
tex.print(string.format('\\ClozeExtend{%s}', kv_string))
end)
end,
write_linefil_nodes = utils.write_linefil_nodes,
write_line_nodes = utils.write_line_nodes,
write_margin_node = utils.write_margin_node,
reset = config.reset_global_options,
get_defaults = config.get_defaults,
get_option = config.get,
write_marker = config.write_marker,
parse_global_options = config.parse_global_options,
parse_local_options = config.parse_local_options,
register_callback = cb.register_callbacks,
unregister_callback = cb.unregister_callbacks,
initialize_cloze = function(cloze_type, kv_string)
cb.register_callbacks(cloze_type)
config.write_marker(cloze_type, 'start',
config.parse_local_options(kv_string))
end,
---
---Print the TeX markup for the command `\clozeextend`
---
---@param kv_string string
print_extension = function(kv_string)
config.parse_local_extend_options(kv_string)
for _ = 1, config.get_extend_count() do
---ex: vertical measure of x
---px: x height current font (has no effect)
local userskip = node.new('glue', 'userskip') --[[@as GlueNode]]
userskip.width = config.get_extend_width()
node.write(userskip)
local spaceskip = node.new('glue', 'spaceskip') --[[@as GlueNode]]
spaceskip.width = 0
spaceskip.stretch = config.get_extend_width()
spaceskip.shrink = config.get_extend_width()
node.write(spaceskip)
local rule = node.new('rule') --[[@as RuleNode]]
rule.depth = 0
rule.height = config.get_extend_height()
rule.width = 0
node.write(rule)
end
end,
---
---@param text string
---@param kv_string string # A string of key-value pairs that can be parsed by luakeys.
---@param starred string # `\BooleanTrue` `\BooleanFalse`
print_box = function(text, kv_string, starred)
log.debug('text: %s kv_string: %s starred: %s', text, kv_string,
starred)
config.parse_local_box_options(kv_string)
fboxrule_restore = tex.dimen['fboxrule']
local rule = config.get_box_rule()
if rule then
tex.dimen['fboxrule'] = tex.sp(rule)
end
tex.print('\\noindent')
tex.print('\\begin{lrbox}{\\ClozeBox}')
local height = config.get_box_height()
local width = config.get_box_width()
if height then
tex_printf('\\begin{minipage}[t][%s][t]{%s}', height, width)
else
tex_printf('\\begin{minipage}[t]{%s}', width)
end
tex.print('\\setlength{\\parindent}{0pt}')
tex_printf('\\clozenol[margin=0pt]{%s}', text)
tex.print('\\end{minipage}')
tex.print('\\end{lrbox}')
if starred:match('True') then
tex.print('\\usebox{\\ClozeBox}')
else
tex.print('\\fbox{\\usebox{\\ClozeBox}}')
end
end,
---
---Print the required TeX markup for the environment `clozespace` using `tex.print()`
---
---@param kv_string string # A string of key-value pairs that can be parsed by luakeys.
print_space = function(kv_string)
config.parse_local_space_options(kv_string)
tex_printf('\\begin{spacing}{%s}', config.get_spacing())
end,
print_font = function()
local font = config.get_font()
if font ~= nil then
tex.print(font)
else
tex.print('\\clozefont')
end
end,
restore_fboxrule = function()
tex.dimen['fboxrule'] = fboxrule_restore
end,
---
---Print to a minted environment and then as material to be typeset.
---
---verbatim alternativ? https://www.alanshawn.com/tech/2020/07/07/luatex-mimic-input.html#source-code
---
---Only used for the visual test files.
---
---@param markup string unexpanded TeX markup
print_test = function(markup)
markup = string.gsub(markup, '\\obeyedline ', '\n')
tex.print('\\begin{minted}{latex}')
tex.print(string.format('%s', markup))
tex.print('\\end{minted}')
tex.print(markup)
end,
}