--
-- This is file `lua-list-hyphen.lua',
-- generated with the docstrip utility.
--
-- The original source files were:
--
-- lua-list-hyphen.dtx  (with options: `lua')
-- 
-- This is a generated file.
-- 
-- Copyright (C) 2026 Alan J. Cain
-- 
-- This file 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.
-- 
local debug

if tex.count['l__lualisthyphen_debug_int'] == 0 then
  debug = function(s)
  end
else
  debug = function(s)
    print('lua-list-hyphen DEBUG: ' .. s)
  end
end
local KEY_TYPE = 'type'
local KEY_WORD = 'word'
local KEY_LANG = 'lang'
local KEY_DIVISION = 'division'
local KEY_INDEX = 'index'
local KEY_CONTEXT = 'context'
local KEY_PAGE = 'page'
local SEGMENT_WORD = 0
local SEGMENT_SPACE = 1
local SEGMENT_MATH = 2
local NODE_ID_HLIST = node.id('hlist')
local NODE_ID_DISC = node.id('disc')
local NODE_ID_GLUE = node.id('glue')
local NODE_ID_KERN = node.id('kern')
local NODE_ID_MARGIN_KERN = node.id('margin_kern')
local NODE_ID_GLYPH = node.id('glyph')
local NODE_ID_MATH = node.id('math')
local NODE_KERN_SUBTYPE_FONTKERN
local NODE_KERN_SUBTYPE_USERKERN
for k,v in pairs(node.subtypes('kern')) do
  if v == 'fontkern' then
    NODE_KERN_SUBTYPE_FONTKERN = k
  elseif v == 'userkern' then
    NODE_KERN_SUBTYPE_USERKERN = k
  end
end
local NODE_MATH_SUBTYPE_BEGIN
local NODE_MATH_SUBTYPE_END
for k,v in pairs(node.subtypes('math')) do
  if v == 'beginmath' then
    NODE_MATH_SUBTYPE_BEGIN = k
  elseif v == 'endmath' then
    NODE_MATH_SUBTYPE_END = k
  end
end
local STR_MATH = '[MATH]'
local STR_SPACE = ' '
local STR_SPACE_TWO = '  '
local STR_ARROW = ' -> '
local STR_PAGE_PREFIX = 'p.'
local STR_PAGE_NONE = '<none>'
local STR_QUOTE_OPEN = '"'
local STR_QUOTE_CLOSE = '"'
local function list_filter(t, f)
  local j = 1
  local n = #t

  for i=1,n do
    if (f(t[i])) then
      if (i ~= j) then
        t[j] = t[i]
        t[i] = nil
      end
      j = j + 1
    else
      t[i] = nil
    end
  end

end
local function list_uniq(t, f)
  local j = 1
  local n = #t

  for i=2,n do
    if (f(t[i],t[j])) then
      t[i] = nil
    else
      j = i
    end
  end

  list_filter(
    t,
    function(a) return a end
  )
end
local LIGATURE_TEXT = {
  [0xfb00] = 'ff',
  [0xfb01] = 'fi',
  [0xfb02] = 'fl',
  [0xfb03] = 'ffi',
  [0xfb04] = 'ffl',
}
local font_characters = {}
local get_node_text
local get_nodelist_text
get_node_text = function(n)

  if n.id == NODE_ID_GLYPH then

    local ligature_text = LIGATURE_TEXT[n.char]
    if ligature_text ~= nil then
      return ligature_text
    elseif n.components then
      return get_nodelist_text(n.components)
    else
      -- See [https://tug.org/pipermail/luatex/2018-March/006786.html]
      local characters = font_characters[n.font]
      if not characters then
        characters = fonts.hashes.identifiers[n.font].characters
        font_characters[n.font] = characters
      end
      local c = characters[n.char]
      if c then
        return utf8.char(tonumber(c.tounicode,16))
      else
        return ''
      end
    end

  elseif n.id == NODE_ID_DISC then

    if n.replace then
      return get_nodelist_text(n.replace)
    else
      return ''
    end

  else
    return ''
  end

end
get_nodelist_text = function (head,last)

  local text = ''

  for item in node.traverse(head) do

    text = text .. get_node_text(item)

    if item == last then
      break
    end
  end

  return text

end
local function is_possible_word_node(n)

  return (
    n.id == NODE_ID_GLYPH
    or
    n.id == NODE_ID_DISC
    or
    (n.id == NODE_ID_KERN and n.subtype == NODE_KERN_SUBTYPE_FONTKERN)
    or
    n.id == NODE_ID_MARGIN_KERN
  )

end
local function is_possible_space_node(n)

  return (
    n.id == NODE_ID_GLUE
    or
    (n.id == NODE_ID_KERN and n.subtype == NODE_KERN_SUBTYPE_USERKERN)
  )

end
local function trim_nonlettershyphens_both(s)

  return unicode.utf8.match(s,'^[^%a-]*(.-)[^%a-]*$')

end
local function trim_nonlettershyphens_start(s)

  return unicode.utf8.match(s,'^[^%a-]*(.-)$')

end
local function trim_nonlettershyphens_end(s)

  return unicode.utf8.match(s,'^(.-)[^%a-]*$')

end
local function rpad(s,n)

   return s .. unicode.utf8.rep(STR_SPACE,n - unicode.utf8.len(s))

end
local function lpad(s,n)

   return unicode.utf8.rep(STR_SPACE,n - unicode.utf8.len(s)) .. s

end
local hyphen_attr = luatexbase.new_attribute('hyphen_attr')
local hlist_segment_list = {}
  -- local function get_first_glyph_lang(n)

  --   local item = n
  --   while item and is_possible_word_node(item) do
  --     if item.id == NODE_ID_GLYPH then
  --       return item.lang
  --     end
  --     item = item.next
  --   end

  --   return nil

  -- end
local function get_disc_lang(n)

  -- lang = get_first_glyph_lang(n.replace)
  -- if lang then
  --   print(lang)
  --   return lang
  -- end

  -- lang = get_first_glyph_lang(n.pre)
  -- if lang then
  --   print(lang)
  --   return lang
  -- end

  -- lang = get_first_glyph_lang(n.post)
  -- if lang then
  --   return lang
  -- end

  local item
  item = n
  while item and is_possible_word_node(item) do
    if item.id == NODE_ID_GLYPH then
      return item.lang
    end
    item = item.prev
  end
  item = n
  while item and is_possible_word_node(item) do
    if item.id == NODE_ID_GLYPH then
      return item.lang
    end
    item = item.next
  end

  return 0

end
local function pre_linebreak(hlist_head,groupcode)

  local word_start_node = nil
  local segment_count = 0
  local lang = nil

  debug('Pre-linebreak processing start')

  local item = hlist_head
  while item do
    if item.id == NODE_ID_MATH then
      assert(item.subtype == NODE_MATH_SUBTYPE_BEGIN)
      while not (
        item.id == NODE_ID_MATH and item.subtype == NODE_MATH_SUBTYPE_END
      ) do
        item = item.next
      end
      item = item.next

      segment_count = segment_count + 1
      hlist_segment_list[segment_count] = {
        [KEY_TYPE] = SEGMENT_MATH
      }

      goto continue
    end
    if is_possible_word_node(item) then
      word_start_node = item
      segment_count = segment_count + 1
      while item and is_possible_word_node(item) do

        if item.id == NODE_ID_DISC then
          if not lang then
            lang = get_disc_lang(item)
          end
          node.set_attribute(item,hyphen_attr,segment_count)
        end

        item = item.next
      end
      local word_end_node
      if item then
        word_end_node = item.prev
      else
        word_end_node = node.tail(word_start_node)
      end

      local word = get_nodelist_text(word_start_node,word_end_node)
      hlist_segment_list[segment_count] = {
        [KEY_TYPE] = SEGMENT_WORD,
        [KEY_WORD] = word,
        [KEY_LANG] = lang,
      }

      word_start_node = nil
      lang = nil

      goto continue
    end
    if is_possible_space_node(item) then
      segment_count = segment_count + 1

      while item and is_possible_space_node(item) do
        item = item.next
      end

      hlist_segment_list[segment_count] = {
        [KEY_TYPE] = SEGMENT_SPACE
      }

      goto continue
    end
    item = item.next

    ::continue::
  end

  debug('Pre-linebreak processing finish')

  return true
end
local USED_DISC_SEARCH_LIMIT = 20

local function get_used_disc(hlist_head)

  debug('  Looking for disc node at end of line')

  local item = node.tail(hlist_head)

  local count = 0
  while item and item.id ~= NODE_ID_GLYPH and count < USED_DISC_SEARCH_LIMIT do
    if item.id == NODE_ID_DISC then
      return item
    end
    item = item.prev
    count = count + 1
  end

  return nil

end
local function get_disc_word_start(hlist_head,n)

  local item = n

  while item do
    local prev = item.prev

    if not (prev and is_possible_word_node(prev)) then
      return item
    end

    item = prev
  end

  return nil
end
local function get_next_hlist(n)

  local item = n.next

  while item do
    if item.id == NODE_ID_HLIST then
      return item
    end
    item = item.next
  end

  return nil

end
local function get_line_first_word(hlist_head)
  local word_start_node = nil

  for item in node.traverse(hlist_head) do

    if item.id == NODE_ID_GLYPH then
      if not word_start_node then
        word_start_node = item
      end
    end

    if not is_possible_word_node(item) then
      if word_start_node then
        return get_nodelist_text(word_start_node,item.prev)
      end
    end

  end
  if word_start_node then
    return get_nodelist_text(word_start_node,node.tail(hlist_head))
  else
    return nil
  end
end
local function get_context(index,incr,target_word_count)

  local result = ''
  local word_count = 0

  local i = index + incr
  while (
    i > 0 and i <= #hlist_segment_list and word_count < target_word_count
  ) do
    local t = hlist_segment_list[i]

    local item

    if t[KEY_TYPE] == SEGMENT_WORD then
      item = t[KEY_WORD]
      word_count = word_count + 1
    elseif t[KEY_TYPE] == SEGMENT_SPACE then
      item = STR_SPACE
    elseif t[KEY_TYPE] == SEGMENT_MATH then
      item = STR_MATH
    end

    if incr > 0 then
      result = result .. item
    else
      result = item .. result
    end

    i = i + incr
  end

  return result

end
local hyphenation_list = {}
local hyphenation_count = 0
local function check_line_hyphenation(hlist)
  local last_disc = get_used_disc(hlist.head)
  if not last_disc then
    debug('  No disc node found at end of line')
    return
  end
  debug('  Disc node found at end of line')
  local hyphenation_index = node.has_attribute(last_disc,hyphen_attr)
  local t = hlist_segment_list[hyphenation_index]
  assert(t)
  assert(t[KEY_TYPE] == SEGMENT_WORD)
  local word = t[KEY_WORD]
  local lang = t[KEY_LANG]
  if not unicode.utf8.match(word,'%a') then
    debug('  Divided "word" contains no letters')
    return
  end
  local next_line = get_next_hlist(hlist)

  if not next_line then
    debug('  No following line found (which should not happen)')
    return
  end
  local pre = get_nodelist_text(get_disc_word_start(hlist.head,last_disc))
  local pre_temp = trim_nonlettershyphens_start(pre)
  if pre_temp ~= '' then
    pre = pre_temp
  end
  local post = trim_nonlettershyphens_end(get_line_first_word(next_line.head))
  local context =
    get_context(
      hyphenation_index,-1,tex.count['l__lualisthyphen_context_before_int']
    )
    .. word ..
    get_context(
      hyphenation_index,1,tex.count['l__lualisthyphen_context_after_int']
    )

  word = trim_nonlettershyphens_both(word)

  debug(
    '  Hyphenated word found: "' .. word .. '" -> "' .. pre .. '<>' .. post .. '"'
  )
  hyphenation_count = hyphenation_count + 1
  hyphenation_list[hyphenation_count] = {
    [KEY_LANG] = lang,
    [KEY_WORD] = word,
    [KEY_DIVISION] = pre .. post,
    [KEY_INDEX] = hyphenation_count,
    [KEY_CONTEXT] = context,
  }
  late_lua_n = node.new('whatsit','late_lua')
  late_lua_n.data =
    'lualisthyphen.set_hyphenation_page(' .. hyphenation_count .. ',tex.count["c@page"])'

  node.insert_after(hlist.head,last_disc,late_lua_n)

end
local function set_hyphenation_page(index,page)

  hyphenation_list[index][KEY_PAGE] = page

end
local function post_linebreak(vlist_head,groupcode)

  debug('Post-linebreak processing start')

  local line_no = 0

  for item in node.traverse(vlist_head) do

    if item.id == NODE_ID_HLIST then
      line_no = line_no + 1
      debug('  Line no.' .. line_no)
      check_line_hyphenation(item)
    end

  end

  hlist_segment_list = {}

  debug('Post-linebreak processing end')

  return true

end
local LUA_LIST_HYPHEN_PRE_LINEBREAK = 'LUA_LIST_HYPHEN_PRE_LINEBREAK'
luatexbase.add_to_callback(
  'pre_linebreak_filter',
  pre_linebreak,
  LUA_LIST_HYPHEN_PRE_LINEBREAK
)

local LUA_LIST_HYPHEN_POST_LINEBREAK = 'LUA_LIST_HYPHEN_POST_LINEBREAK'
luatexbase.add_to_callback(
  'post_linebreak_filter',
  post_linebreak,
  LUA_LIST_HYPHEN_POST_LINEBREAK
)
local language_table = {}
local function babel_save_language_name(lang_id,name)

  if not language_table[lang_id] then
    language_table[lang_id] = name
  end

end
local function polyglossia_get_language_names()

  if not polyglossia then
    return
  end

  for name,language in pairs(polyglossia.newloader_loaded_languages) do
    language_table[lang.id(language)] = name
  end

end
local function equal_hyphenation_case_sensitive(a,b)
  return (
    a[KEY_WORD] == b[KEY_WORD]
    and
    a[KEY_DIVISION] == b[KEY_DIVISION]
  )
end
local function equal_hyphenation_case_insensitive(a,b)
  return (
    unicode.utf8.lower(a[KEY_WORD]) == unicode.utf8.lower(b[KEY_WORD])
    and
    unicode.utf8.lower(a[KEY_DIVISION]) == unicode.utf8.lower(b[KEY_DIVISION])
  )
end
local function lessthan_hyphenation_case_sensitive(a,b)
  return (
    a[KEY_WORD] < b[KEY_WORD]
    or
    (
      a[KEY_WORD] == b[KEY_WORD]
      and
      a[KEY_DIVISION] < b[KEY_DIVISION]
    )
    or
    (
      a[KEY_WORD] == b[KEY_WORD]
      and
      a[KEY_DIVISION] == b[KEY_DIVISION]
      and
      a[KEY_INDEX] < b[KEY_INDEX]
    )
  )
end
local function lessthan_hyphenation_case_insensitive(a,b)
  return (
    unicode.utf8.lower(a[KEY_WORD]) < unicode.utf8.lower(b[KEY_WORD])
    or
    (
      unicode.utf8.lower(a[KEY_WORD]) == unicode.utf8.lower(b[KEY_WORD])
      and
      unicode.utf8.lower(a[KEY_DIVISION]) < unicode.utf8.lower(b[KEY_DIVISION])
    )
    or
    (
      unicode.utf8.lower(a[KEY_WORD]) == unicode.utf8.lower(b[KEY_WORD])
      and
      unicode.utf8.lower(a[KEY_DIVISION]) < unicode.utf8.lower(b[KEY_DIVISION])
      and
      a[KEY_INDEX] < b[KEY_INDEX]
    )
  )
end
local function sort_hyphenation_list_none(hyphenation_list)
  table.sort(
    hyphenation_list,
    function(a,b)
      return a[KEY_INDEX] < b[KEY_INDEX]
    end
  )
end
local function sort_hyphenation_list_case(hyphenation_list)
  table.sort(
    hyphenation_list,
    lessthan_hyphenation_case_sensitive
  )
end
local function sort_hyphenation_list_nocase(hyphenation_list)
  table.sort(
    hyphenation_list,
    lessthan_hyphenation_case_insensitive
  )
end
local sort_hyphenation_list
if tex.count['l__lualisthyphen_sort_int'] == 1 then
  sort_hyphenation_list = sort_hyphenation_list_case
elseif tex.count['l__lualisthyphen_sort_int'] == 2 then
  sort_hyphenation_list = sort_hyphenation_list_nocase
else
  sort_hyphenation_list = sort_hyphenation_list_none
end
local function deduplicate_hyphenation_list_none(hyphenation_list)
end
local function deduplicate_hyphenation_list_case(hyphenation_list)
  table.sort(
    hyphenation_list,
    lessthan_hyphenation_case_sensitive
  )
  list_uniq(
    hyphenation_list,
    equal_hyphenation_case_sensitive
  )
end
local function deduplicate_hyphenation_list_nocase(hyphenation_list)
  table.sort(
    hyphenation_list,
    lessthan_hyphenation_case_insensitive
  )
  list_uniq(
    hyphenation_list,
    equal_hyphenation_case_insensitive
  )
end
local deduplicate_hyphenation_list
if tex.count['l__lualisthyphen_unique_int'] == 1 then
  deduplicate_hyphenation_list = deduplicate_hyphenation_list_case
elseif tex.count['l__lualisthyphen_unique_int'] == 2 then
  deduplicate_hyphenation_list = deduplicate_hyphenation_list_nocase
else
  deduplicate_hyphenation_list = deduplicate_hyphenation_list_none
end
local function process_lang_hyphenation_list(hyphenation_list)
  deduplicate_hyphenation_list(hyphenation_list)
  sort_hyphenation_list(hyphenation_list)
end
local function write_lang_hyphenation_list_standard(f,hyphenation_list,widths)

  for i,v in ipairs(hyphenation_list) do

    if v then
      f:write(v[KEY_DIVISION] .. '\n')
    end

  end

end
local function write_lang_hyphenation_list_verbose(f,hyphenation_list,widths)

  local cols_word = widths[KEY_WORD]
  local cols_division = widths[KEY_DIVISION]
  local cols_page = widths[KEY_PAGE]

  for i,v in ipairs(hyphenation_list) do

    if v then
      local page = v[KEY_PAGE]
      if page then
        page = STR_PAGE_PREFIX .. page
      else
        page = STR_PAGE_NONE
      end

      f:write(
        rpad(v[KEY_WORD],cols_word)
        .. STR_ARROW
        .. rpad(v[KEY_DIVISION],cols_division)
        .. STR_SPACE_TWO
        .. lpad(page,cols_page)
        .. STR_SPACE
        .. STR_QUOTE_OPEN
        .. v[KEY_CONTEXT]
        .. STR_QUOTE_CLOSE
        .. '\n'
      )
    end

  end

end
local write_lang_hyphenation_list
if tex.count['l__lualisthyphen_verbose_int'] == 0 then
  write_lang_hyphenation_list = write_lang_hyphenation_list_standard
else
  write_lang_hyphenation_list = write_lang_hyphenation_list_verbose
end
local settings_desc
if tex.count['l__lualisthyphen_verbose_int'] == 0 then
  settings_desc = 'verbose=false'
else
  settings_desc = 'verbose=true'
    .. ',context-before=' .. tex.count['l__lualisthyphen_context_before_int']
    .. ',context-after=' .. tex.count['l__lualisthyphen_context_after_int']
end
if tex.count['l__lualisthyphen_include_non_output_int'] == 0 then
  settings_desc = settings_desc .. ',include-non-output=false'
else
  settings_desc = settings_desc .. ',include-non-output=true'
end
local NONE_CASE_NOCASE = {
  [0] = 'none',
  [1] = 'case',
  [2] = 'nocase'
}
settings_desc = settings_desc
  .. ',sort=' .. NONE_CASE_NOCASE[tex.count['l__lualisthyphen_sort_int']]
  .. ',unique=' .. NONE_CASE_NOCASE[tex.count['l__lualisthyphen_unique_int']]
local function get_hyphenation_file_path(prefix,extension,lang_name)

  local hyphenation_file_path = prefix .. tostring(lang_name) .. extension

  if not status.output_directory then
    return hyphenation_file_path
  end

  if string.sub(status.output_directory,-1,-1) == '/' then
    hyphenation_file_path = status.output_directory
      .. hyphenation_file_path
  else
    hyphenation_file_path = status.output_directory
      .. '/' .. hyphenation_file_path
  end

  return hyphenation_file_path

end
local function process_write_lang_hyphenation_list(
    prefix,extension,lang_id,hyphenation_list,widths
)

  process_lang_hyphenation_list(hyphenation_list)

  local lang_name = language_table[lang_id]
  local lang_desc
  if not lang_name then
    lang_name = lang_id
    lang_desc = 'language with ID ' .. lang_id
  else
    lang_desc = 'language "' .. lang_name .. '" (ID ' .. lang_id .. ')'
  end

  local f = io.open(get_hyphenation_file_path(prefix,extension,lang_name),'w')

  f:write('% Chosen hyphenations for ' ..  lang_desc .. '\n')
  f:write('% Generated by lua-list-hyphen (' .. settings_desc .. ')\n')

  write_lang_hyphenation_list(f,hyphenation_list,widths)
  f:close()

end
local function process_write_hyphenation_lists(prefix,extension)

  local lang_hyphenation_table = {}
  local lang_widths_table = {}
  for _,h in pairs(hyphenation_list) do

    if h[KEY_PAGE] or tex.count['l__lualisthyphen_include_non_output_int'] == 1 then

      local lang = h[KEY_LANG]

      local t = lang_hyphenation_table[lang]
      if not t then
        lang_hyphenation_table[lang] = {}
        t = lang_hyphenation_table[lang]
      end

      local widths = lang_widths_table[lang]
      if not widths then
        lang_widths_table[lang] = {
          [KEY_WORD] = 0,
          [KEY_DIVISION] = 0,
          [KEY_PAGE] = 0
        }
        widths = lang_widths_table[lang]
      end

      widths[KEY_WORD] = math.max(
        widths[KEY_WORD],
        unicode.utf8.len(h[KEY_WORD])
      )
      widths[KEY_DIVISION] = math.max(
        widths[KEY_DIVISION],
        unicode.utf8.len(h[KEY_DIVISION])
      )
      widths[KEY_PAGE] = math.max(
        widths[KEY_PAGE],
        unicode.utf8.len(tostring(h[KEY_PAGE]))
      )

      table.insert(t,h)

    end

  end
  for _,widths in pairs(lang_widths_table) do
    widths[KEY_PAGE] = math.max(
      widths[KEY_PAGE] + unicode.utf8.len(STR_PAGE_PREFIX),
      unicode.utf8.len(STR_PAGE_NONE)
    )
  end
  polyglossia_get_language_names()
  for k,v in pairs(lang_hyphenation_table) do
    process_write_lang_hyphenation_list(prefix,extension,k,v,lang_widths_table[k])
  end

end
return {
  process_write_hyphenation_lists = process_write_hyphenation_lists,
  set_hyphenation_page = set_hyphenation_page,
  babel_save_language_name = babel_save_language_name,
}

