-- 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.
---
---<h3>Naming conventions</h3>
---
---* _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 = '1.8.1',
  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

---Option handling.
---
---The table `config` bundles functions that deal with the option
---handling.
---
---<h2>Marker processing (marker)</h2>
---
---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.

---
---@alias MarkerMode 'basic'|'strike'|'fix'|'par' # The argument `mode` accepts the string values `basic`, `fix` and `par`.

---
---@alias MarkerPosition 'start'|'stop' # The argument `position` is either set to `start` or to `stop`.

---
---@class MarkerData
---@field mode MarkerMode
---@field position MarkerPosition
---@field local_opts Options

---
---All values and functions, which are related to the option
---management, are stored in a table called `config`.
local config = (function()
  ---
  ---I didn’t know what value I should take as `user_id`. Therefore I
  ---took my birthday 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? 'l'|'r'
  ---@field box_height? string
  ---@field box_rule? string
  ---@field box_width? string
  ---@field distance? string
  ---@field extension_count? integer
  ---@field extension_height? string
  ---@field extension_width? string
  ---@field line_color? string
  ---@field log? number
  ---@field margin? string
  ---@field min_lines? integer
  ---@field spacing? number
  ---@field spread? number
  ---@field text_color? string
  ---@field thickness? string
  ---@field visibility? boolean
  ---@field width? string

  ---The default options.
  local defaults = {
    align = 'l',
    box_height = false,
    box_rule = '0.4pt',
    box_width = '\\linewidth',
    distance = '1.5pt',
    extension_count = 5,
    extension_height = '2ex',
    extension_width = '1em',
    line_color = 'black',
    log = 0,
    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 = {}

  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

  ---
  ---`set_storage()` stores the local options in the Lua table
  --- `storage`.
  ---
  ---It returns a numeric index number. This index number is the key,
  ---where the local options in the Lua table are stored.
  ---
  ---@param mode MarkerMode
  ---@param position MarkerPosition
  ---
  ---@return number # The index number of the corresponding table in
  ---  `storage`.
  local function set_storage(mode, position)
    local index = get_index()
    local data = { mode = mode, position = position }
    if position == 'start' then
      data.local_opts = {}
      for key, value in pairs(local_options) do
        data.local_opts[key] = value
      end
    end
    storage[index] = data
    return index
  end

  ---
  ---We create a user defined whatsit node that can store a number (type
  --- = 100).
  ---
  ---In order to distinguish this node from other user defined whatsit
  ---nodes we set the `user_id` to a large number. We call this whatsit
  ---node a marker.
  ---
  ---@param index number The argument `index` is a number, which is associated to values in the `storage` table.
  ---
  ---@return UserDefinedWhatsitNode
  local function create_marker(index)
    local marker = node.new('whatsit', 'user_defined') --[[@as UserDefinedWhatsitNode]]
    marker.type = 100 -- number
    marker.user_id = user_id
    marker.value = index
    return marker
  end

  ---
  ---Write a marker node to TeX's current node list.
  ---
  ---@param mode MarkerMode
  ---@param position MarkerPosition
  local function write_marker(mode, position)
    local index = set_storage(mode, position)
    local marker = create_marker(index)
    node.write(marker)
  end

  ---
  ---Check if the given node is a marker.
  ---
  ---@param item Node
  ---
  ---@return boolean
  local function is_marker(item)
    local n = item --[[@as UserDefinedWhatsitNode]]
    if n.id == node.id('whatsit') and n.subtype ==
      node.subtype('user_defined') and n.user_id == user_id then
      return true
    end
    return false
  end

  ---
  ---Test whether the node `n` is a marker and retrieve the
  ---the corresponding marker data.
  ---
  ---@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
      end
      return data
    end
  end

  ---
  ---This functions tests, whether the given node `item` is a marker.
  ---
  ---@param head_node Node # The current node.
  ---@param mode MarkerMode
  ---@param position MarkerPosition
  ---
  ---@return boolean
  local function check_marker(head_node, mode, position)
    local data =
      get_marker_data(head_node --[[@as UserDefinedWhatsitNode]] )
    if data and data.mode == mode and data.position == position then
      return true
    end
    return false
  end

  ---
  ---`get_marker` returns the given marker.
  ---
  ---@param head_node Node # The current node.
  ---@param mode MarkerMode
  ---@param position MarkerPosition
  ---
  ---@return false|Node # The node if `head_node` is a marker node.
  local function get_marker(head_node, mode, position)
    local out
    if check_marker(head_node, mode, position) then
      out = head_node
    else
      out = false
    end
    if out and position == 'start' then
      get_marker_data(head_node --[[@as UserDefinedWhatsitNode]] )
    end
    return out
  end

  ---
  ---Remove a whatsit marker.
  ---
  ---It only deletes a node, if a marker is given.
  ---
  ---@param marker Node
  ---
  ---@return Node|nil head
  ---@return Node|nil current
  local function remove_marker(marker)
    if is_marker(marker) then
      return node.remove(marker, marker)
    end
  end

  ---@type 'local'|'global'
  local options_dest

  ---
  ---Store a value `value` and his associated key `key`
  ---either to the global (`global_options`) or to the local
  ---(`local_options`) option table.
  ---
  ---The global boolean variable `local_options` controls in
  ---which table the values are stored.
  ---
  ---@param key string # The option key.
  ---@param value any # The value that is stored under the options key.
  local function set_option(key, value)
    if value == '' then
      return false
    end
    log.info('Set %s option “%s” to “%s”', options_dest, key,
      value)
    if options_dest == 'global' then
      global_options[key] = value
    else
      local_options[key] = value
    end
  end

  ---
  ---Set the variable `options_dest`.
  ---
  ---@param dest 'local'|'global'
  local function set_options_dest(dest)
    options_dest = dest
  end

  ---
  ---Clear the local options storage.
  local function unset_local_options()
    local_options = {}
  end

  ---
  ---Clear the global options storage.
  local function unset_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

  local defs = {
    align = {
      description = 'Align the text of a fixed size cloze.',
      process = function(value)
        set_option('align', value)
      end,
    },
    box_height = {
      description = 'The height of a cloze box.',
      alias = { 'boxheight', 'box_height' },
      process = function(value)
        set_option('box_height', value)
      end,
    },
    box_rule = {
      description = 'The thickness of the rule around a cloze box.',
      alias = { 'boxrule', 'box_rule' },
      process = function(value)
        set_option('box_rule', value)
      end,
    },
    box_width = {
      description = 'The width of a cloze box.',
      alias = { 'boxwidth', 'box_width' },
      process = function(value)
        set_option('box_width', value)
      end,
    },
    debug = {
      data_type = 'integer',
      process = function(value)
        log.set(value)
      end,
    },
    distance = {
      description = 'The distance between the cloze text and the cloze line.',
      process = function(value)
        set_option('distance', value)
      end,
    },
    extension_count = {
      description = 'The number of extension units.',
      alias = 'extensioncount',
      process = function(value)
        set_option('extension_count', value)
      end,
    },
    extension_height = {
      description = 'The height of one extension unit (default: 2ex).',
      alias = 'extensionheight',
      process = function(value)
        set_option('extension_height', value)
      end,

    },
    extension_width = {
      description = 'The width of one extension unit (default: 1em).',
      alias = 'extensionwidth',
      process = function(value)
        set_option('extension_width', value)
      end,
    },
    line_color = {
      description = 'A color name to colorize the cloze line.',
      alias = 'linecolor',
      process = function(value, input)
        tex_printf('\\FarbeImport{%s}', value)
        set_option('line_color', value)
      end,
    },
    log = {
      description = 'Set the log level.',
      data_type = 'integer',
      process = function(value, input)
        log.set(value)
      end,
    },
    margin = {
      description = 'Indicates how far the cloze line sticks up horizontally from the text.',
      process = function(value)
        set_option('margin', value)
      end,
    },
    min_lines = {
      alias = { 'minimum_lines', 'minlines' },
      description = 'How many lines a clozepar at least must have.',
      process = function(value)
        set_option('min_lines', value)
      end,
    },
    spacing = {
      description = 'The spacing between lines (environment clozespace).',
      process = function(value)
        set_option('spacing', value)
      end,
    },
    spread = {
      description = 'Enlarge or spread a gap by a certain factor.',
      process = function(value)
        set_option('spread', value)
      end,
    },
    text_color = {
      description = 'The color (name) of the cloze text.',
      alias = 'textcolor',
      data_type = 'string',
      process = function(value)
        tex_printf('\\FarbeImport{%s}', value)
        set_option('text_color', value)
      end,
    },
    thickness = {
      description = 'The thickness of the cloze line.',
      process = function(value)
        set_option('thickness', value)
      end,
    },
    visibility = {
      description = 'Show or hide the cloze text.',
      opposite_keys = { [true] = 'show', [false] = 'hide' },
      process = function(value)
        set_option('visibility', value)
      end,
    },
    width = {
      description = 'The width of the cloze line of the command \\clozefix.',
      process = function(value)
        set_option('width', value)
      end,
    },
  }

  ---
  ---@param kv_string string
  ---@param options_dest 'local'|'global'
  local function parse_options(kv_string, options_dest)
    unset_local_options()
    set_options_dest(options_dest)
    luakeys.parse(kv_string, { defs = defs, debug = log.get() > 3 })
  end

  local defs_manager = luakeys.DefinitionManager(defs)

  return {
    get = get,
    get_defaults = get_defaults,
    unset_global_options = unset_global_options,
    unset_local_options = unset_local_options,
    set_options_dest = set_options_dest,
    remove_marker = remove_marker,
    check_marker = check_marker,
    set_option = set_option,
    write_marker = write_marker,
    get_marker = get_marker,
    get_marker_data = get_marker_data,
    parse_options = parse_options,
    defs_manager = defs_manager,
  }

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_spec
    if kind == 'line' then
      color_spec = config.get('line_color')
    else
      color_spec = config.get('text_color')
    end
    local color = farbe.Color(color_spec)
    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 = tex.sp(config.get('thickness'))
    local distance = tex.sp(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(tex.sp(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(tex.sp(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)()

local visitor = (function()

  ---The enviroment in the node list where the cloze can be inserted.
  ---@class ClozeNodeEnvironment
  ---@field parent_hlist HlistNode
  ---@field start_marker? UserDefinedWhatsitNode
  ---@field start_node? Node
  ---@field start Node
  ---@field start_continuation boolean
  ---@field stop_node? Node
  ---@field stop_marker? UserDefinedWhatsitNode
  ---@field 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.

  ---@alias Visitor fun(env: ClozeNodeEnvironment): nil

  ---@param visitor Visitor
  ---@param parent_hlist? HlistNode
  ---@param start_marker? UserDefinedWhatsitNode
  ---@param start_node? Node
  ---@param stop_node? Node
  ---@param stop_marker? UserDefinedWhatsitNode
  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

    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,
    }

    visitor(env)

  end

  ---@param n? Node
  local function debug_node(n)
    if n == nil then
      return nil
    end
    return node.type(n.id)
  end

  ---@param parent_hlist Node
  ---@param start_marker? Node
  ---@param start_node? Node
  ---@param stop_node? Node
  ---@param stop_marker? Node
  local function debug_visitor(parent_hlist,
    start_marker,
    start_node,
    stop_node,
    stop_marker)
    print(debug_node(parent_hlist), debug_node(start_marker),
      debug_node(start_node), debug_node(stop_node),
      debug_node(stop_marker))
  end

  ---This local variables are overloaded by functions
  ---calling each other.
  local search_stop

  ---
  ---Search for a stop marker or make a cloze up to the end of the node
  ---list.
  ---
  ---@param visitor Visitor
  ---@param mode MarkerMode
  ---@param parent_node HlistNode # The parent node (hlist) of the start node.
  ---@param start_marker? Node|nil
  ---@param start_node? Node # The node to start a new cloze.
  ---
  ---@return Node|nil head_node # The fast forwarded new head of the node list.
  ---@return Node|nil parent_node # The parent node (hlist) of the head node.
  function search_stop(visitor,
    mode,
    parent_node,
    start_marker,
    start_node)
    ---@type Node|nil
    local n
    if start_marker ~= nil then
      n = start_marker
    else
      n = start_node
    end

    local last_node
    while n do
      if config.check_marker(n, mode, 'stop') then
        call_visitor(visitor, parent_node, start_marker --[[@as UserDefinedWhatsitNode]] ,
          start_node, nil, n --[[@as UserDefinedWhatsitNode]] )
        return n, parent_node
      end
      last_node = n
      n = n.next
    end
    call_visitor(visitor, parent_node, start_marker --[[@as UserDefinedWhatsitNode]] ,
      start_node, last_node, nil)
    if parent_node.next then
      local hlist_node = utils.search_hlist(parent_node.next, false)
      if hlist_node then
        local start_node = hlist_node.head
        return search_stop(visitor, mode, hlist_node, nil, start_node)
      end
    else
      return n, parent_node
    end
  end

  ---
  ---Search for a start marker.
  ---
  ---@param visitor Visitor
  ---@param mode MarkerMode
  ---@param head_node Node # The head of a node list.
  ---@param parent_node? HlistNode # The parent node (hlist) of the head node.
  local function visit(visitor, mode, head_node, parent_node)
    ---@type Node|nil
    local n = head_node

    ---@type Node|nil
    local p = parent_node

    while n do
      if n.head then
        ---@cast n HlistNode
        visit(visitor, mode, n.head, n)
      elseif config.check_marker(n, mode, 'start') and p and p.id ==
        node.id('hlist') then
        ---@cast p HlistNode
        n, p = search_stop(visitor, mode, p, n, nil)
      end
      if n then
        n = n.next
      else
        break
      end
    end
  end

  ---
  ---Traverse a flat node list such as the one in the
  ---`pre_linebreak_filter` callback.
  ---
  ---@param visitor Visitor
  ---@param mode MarkerMode
  ---@param head_node Node # The head of a node list.
  local function visit_pre_linebreak(visitor, mode, head_node)
    ---@type Node|nil
    local n = head_node

    local start_marker = nil
    local stop_marker = nil

    while n do
      if config.check_marker(n, mode, 'start') then
        start_marker = n
      end

      if config.check_marker(n, mode, 'stop') then
        stop_marker = n
      end

      if start_marker and stop_marker then
        call_visitor(visitor, nil, start_marker --[[@as UserDefinedWhatsitNode]] ,
          nil, nil, stop_marker --[[@as UserDefinedWhatsitNode]] )
        start_marker = nil
        stop_marker = nil
      end
      n = n.next

    end
  end

  return { visit = visit, visit_pre_linebreak = visit_pre_linebreak }
end)()

---
---Assemble a possibly multi-line cloze text.
---
---The corresponding LaTeX command to this Lua function is `\cloze`.
---This function is used by other cloze TeX macros too: `\clozenol`,
---`\clozefil`
---
---@param head_node_input Node # The head of a node list.
---
---@return Node # The head of the node list.
local function make_basic(head_node_input)

  utils.debug_node_list(head_node_input)

  ---This local variables are overloaded by functions
  ---calling each other.
  local continue_cloze, search_stop

  ---
  ---Make a single gap.
  ---
  ---@param start_node Node # The node to start / begin a new cloze.
  ---@param stop_node Node # The node to stop / end a new cloze.
  ---@param parent_node HlistNode # The parent node (hlist) of the start and the stop node.
  ---
  ---@return Node|nil stop_node # The stop node.
  ---@return HlistNode parent_node # The parent node (hlist) of the stop node.
  local function make_single(start_node, stop_node, parent_node)
    local node_head = start_node
    local line_width = node.dimensions(parent_node.glue_set,
      parent_node.glue_sign, parent_node.glue_order, start_node,
      stop_node)

    log.info('Make a line of the width of: %s sp', line_width)

    local line_node = utils.insert_line(start_node, line_width)
    local color_text_node = utils.insert_list('after', line_node, {
      utils.create_color('text', 'push'),
    })
    if config.get('visibility') then
      utils.insert_list('after', color_text_node,
        { utils.create_kern_node(-line_width) })
      utils.insert_list('before', stop_node,
        { utils.create_color('text', 'pop') }, node_head)
    else
      line_node.next = stop_node.next
      stop_node.prev = line_node -- not line_node.prev -> line color leaks out
    end
    ---In some edge cases the lua callbacks get fired up twice. After the
    ---cloze has been created, the start and stop whatsit markers can be
    ---deleted.
    config.remove_marker(start_node)
    return config.remove_marker(stop_node), parent_node
  end

  ---
  ---Search for a stop marker or make a cloze up to the end of the node
  ---list.
  ---
  ---@param start_node Node # The node to start a new cloze.
  ---@param parent_node HlistNode # The parent node (hlist) of the start node.
  ---
  ---@return Node|nil head_node # The fast forwarded new head of the node list.
  ---@return Node|nil parent_node # The parent node (hlist) of the head node.
  function search_stop(start_node, parent_node)
    ---@type Node|nil
    local n = start_node

    local last_node
    while n do
      if config.check_marker(n, 'basic', 'stop') then
        return make_single(start_node, n, parent_node)
      end
      last_node = n
      n = n.next
    end
    -- Make a cloze until the end of the node list.
    n = make_single(start_node, last_node, parent_node)
    if parent_node.next then
      return continue_cloze(parent_node.next)
    else
      return n, parent_node
    end
  end

  ---
  ---Continue a multiline cloze.
  ---
  ---@param parent_node Node # A parent node to search for a hlist node.
  ---
  ---@return Node|nil head_node # The fast forwarded new head of the node list.
  ---@return Node|nil parent_node # The parent node (hlist) of the head node.
  function continue_cloze(parent_node)
    local hlist_node = utils.search_hlist(parent_node, true)
    if hlist_node then
      local start_node = hlist_node.head
      return search_stop(start_node, hlist_node)
    end
  end

  ---
  ---Search for a start marker.
  ---
  ---@param head_node Node # The head of a node list.
  ---@param parent_node? HlistNode # The parent node (hlist) of the head node.
  local function search_start(head_node, parent_node)
    ---@type Node|nil
    local n = head_node

    ---@type Node|nil
    local p = parent_node

    while n do
      if n.head then
        ---@cast n HlistNode
        search_start(n.head, n)
      elseif config.check_marker(n, 'basic', 'start') and p and p.id ==
        node.id('hlist') then
        ---Adds also a strut at the first position. It prepars the
        ---hlist and makes it ready to build a cloze.
        ---@cast p HlistNode
        utils.search_hlist(p, true)
        n, p = search_stop(n, p)
      end
      if n then
        n = n.next
      else
        break
      end
    end
  end

  search_start(head_node_input)
  return head_node_input
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

---
---The corresponding LaTeX command to this Lua function is `\clozefix`.
---
---@param head_node_input Node # The head of a node list.
local function make_fix(head_node_input)

  ---
  ---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 = tex.sp(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

  ---
  ---Generate a gap with a fixed width.
  ---
  ---# 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 start Node # The node, where the gap begins
  ---@param stop Node # The node, where the gap ends
  local function make_single(start, stop)
    local width, kern_start_length, kern_stop_length = calculate_widths(
      start, stop)
    local line_node = utils.insert_line(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', stop, {
        utils.create_color('text', 'pop'),
        utils.create_kern_node(kern_stop_length),
      }, start)
    else
      line_node.next = stop.next
    end
    config.remove_marker(start)
    config.remove_marker(stop)
  end

  ---
  ---Recurse the node list and search for the marker.
  ---
  ---@param head_node Node # The head of a node list.
  local function make_fix_recursion(head_node)
    ---@type Node|false
    local start_node = false

    ---@type Node|false
    local stop_node = false
    while head_node do
      if head_node.head then
        make_fix_recursion(head_node.head)
      else
        if not start_node then
          start_node = config.get_marker(head_node, 'fix', 'start')
        end
        if not stop_node then
          stop_node = config.get_marker(head_node, 'fix', 'stop')
        end
        if start_node and stop_node then
          make_single(start_node, stop_node)
          start_node, stop_node = false, false
        end
      end
      head_node = head_node.next
    end
  end

  make_fix_recursion(head_node_input)
  return head_node_input
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
---
---@return Node head_node
local function make_strike(head_node)
  visitor.visit_pre_linebreak(function(env)
    local text_color = farbe.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 = text_color:create_pdf_colorstack_node('push')
      top_hlist.head.next = top_start
      local top_stop = node.tail(top_hlist.head)
      top_stop.next = text_color:create_pdf_colorstack_node('pop')
    else
      top_hlist.head = nil
    end

    -- strike line

    if config.get('visibility') then
      local base_start = base_hlist.head
      local base_stop = node.tail(base_hlist.head)

      local width, height, _ = node.dimensions(base_start, base_stop)

      local line = node.new('rule') --[[@as RuleNode]]
      local thickness = tex.sp(config.get('thickness'))
      line.depth = -(height / 3)
      line.height = (height / 3) + thickness
      line.width = width

      base_hlist.head = text_color:create_pdf_colorstack_node('push')

      local color_pop = text_color:create_pdf_colorstack_node('pop')
      local kern = utils.create_kern_node(-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

---
---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 min_lines = config.get('min_lines')
  local additional_lines = min_lines - line_count

  if additional_lines > 0 then
    add_additional_lines(last_hlist_node, additional_lines)
  end

  return true
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 mode (`basic`, `fix`, `par`).
  ---
  ---@type table<'basic'|'fix'|'par'|'visitor', 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`. The argument `mode` accepts the string values
    ---`basic`, `fix` and `par`. 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 mode MarkerMode
    ---
    ---@return boolean|nil
    register_callbacks = function(mode)
      if mode == 'par' then
        register('post_linebreak_filter', make_par, mode)
        return true
      end
      if not is_registered[mode] then
        if mode == 'basic' then
          register('pre_linebreak_filter', spread_basic, mode)
          register('post_linebreak_filter', make_basic, mode)
          register('pre_output_filter', make_basic, mode)
        elseif mode == 'fix' then
          register('pre_linebreak_filter', make_fix, mode)
        elseif mode == 'strike' then
          register('pre_linebreak_filter', make_strike, mode)
        else
          return false
        end
        is_registered[mode] = true
      end
    end,

    ---
    ---Delete the registered functions from the Lua callbacks.
    ---
    ---@param mode MarkerMode
    unregister_callbacks = function(mode)
      if mode == 'basic' then
        unregister('pre_linebreak_filter', mode)
        unregister('post_linebreak_filter', mode)
        unregister('pre_output_filter', mode)
      elseif mode == 'fix' then
        unregister('pre_linebreak_filter', mode)
      elseif mode == 'strike' then
        unregister('pre_linebreak_filter', mode)
      else
        unregister('post_linebreak_filter', mode)
      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

local function print_cloze()
  local kv_string, text = lparse.scan('O{} v')
  config.parse_options(kv_string, 'local')
  cb.register_callbacks('basic')
  local output = string.format(
    '\\ClozeStartMarker{basic}%s\\ClozeStopMarker{basic}',
    string.format('{\\clozefont\\relax%s}',
      string.format('\\ClozeMargin{%s}', text)))
  tex.print(output)
end

local function print_strike()
  local kv_string, error_text, solution_text = lparse.scan('O{} v v')
  config.parse_options(kv_string, 'local')
  cb.register_callbacks('strike')
  tex.print('\\ClozeStartMarker{strike}' .. string.format(
    '\\vbox{\\hbox{\\kern0pt \\ClozeWrapWithFont{%s}}\\hbox{%s}}',
    solution_text, error_text) .. '\\ClozeStopMarker{strike}')
end

---
---This table contains some basic functions which are published to the
---`cloze.tex` and `cloze.sty` file.
return {
  register_functions = function()
    ---
    ---@param csname string
    ---@param fn function
    local function register_function(csname, fn)
      local index = 376
      local fns = lua.get_functions_table()
      while fns[index] do
        index = index + 1
      end
      fns[index] = fn
      token.set_lua(csname, index, 'global', 'protected')
    end

    register_function('clozeNG', print_cloze)
    register_function('clozestrike', print_strike)
  end,

  write_linefil_nodes = utils.write_linefil_nodes,
  write_line_nodes = utils.write_line_nodes,
  write_margin_node = utils.write_margin_node,
  set_option = config.set_option,
  set_options_dest = config.set_options_dest,
  unset_local_options = config.unset_local_options,
  reset = config.unset_global_options,
  get_defaults = config.get_defaults,
  get_option = config.get,
  marker = config.write_marker,
  parse_options = config.parse_options,
  register_callback = cb.register_callbacks,
  unregister_callback = cb.unregister_callbacks,

  ---@param count string|number
  print_extension = function(count)
    ---@type number|nil
    local c
    if count == '' then
      c = config.get('extension_count')
    end
    c = tonumber(count)

    if not c then
      luakeys.utils.throw_error_message(
        'clozeextend count must be greater than 0.')
    end

    for _ = 1, c do
      ---ex: vertical measure of x
      ---px: x height current font (has no effect)
      tex_printf('\\hspace{%s}\\rule{0pt}{%s}',
        config.get('extension_width'), config.get('extension_height'))
    end
  end,

  ---
  ---@param text string
  ---@param kv_string string
  ---@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.set_options_dest('local')
    config.defs_manager:parse(kv_string, {
      'visibility',
      box_rule = 'rule',
      box_width = 'width',
      box_height = 'height',
    })

    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
  print_space = function(kv_string)
    config.set_options_dest('local')
    local defs = config.defs_manager:include({ 'spacing' }, true)
    defs.spacing.pick = 'number'
    luakeys.parse(kv_string, { defs = defs })
    tex_printf('\\begin{spacing}{%s}', config.get('spacing'))
  end,

  restore_fboxrule = function()
    tex.dimen['fboxrule'] = fboxrule_restore
  end,
}
