% \iffalse meta-comment
%
% 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.
%
% \fi
%
% \iffalse
%<*driver>
\PassOptionsToPackage{inline}{enumitem}
\documentclass{l3doc}

\usepackage{polyglossia}
\setmainlanguage[variant=british]{english}


\makeatletter
\ExplSyntaxOn

\cs_gset:Npn \l@subsection { \@dottedtocline{2}{2.5em}{2.8em} }  % #2 = 1.5em
\cs_gset:Npn \l@subsubsection { \@dottedtocline{3}{5.3em}{3.5em} }  % #2 = 1.5em
\cs_gset:Npn \l@paragraph { \@dottedtocline{4}{8.8em}{3.2em} }  % #2 = 1.5em

\ExplSyntaxOff
\makeatother


\usepackage{xcolor}

\definecolor{linkcolor}{rgb}{0.0,0.4,0.7}
\colorlet{citecolor}{linkcolor}
\colorlet{urlcolor}{linkcolor}

\hypersetup{
  linkcolor=linkcolor,%
  citecolor=citecolor,%
  urlcolor=urlcolor,%
}
\usepackage{xurl}
\renewcommand*\UrlBigBreaks{}

\newcommand*\fullref[2]{%
  \hyperref[#2]{#1\penalty 200\ \ref*{#2}}%
}
\newcommand*\fullpageref[1]{%
  \hyperref[#1]{page\penalty 200\ \pageref*{#1}}%
}


\setcounter{tocdepth}{7}
\numberwithin{figure}{section}



\usepackage{lua-list-hyphen}

\usepackage{lipsum}
\usepackage{tikz}

\newcommand*\key[1]{\texttt{#1}}
\newcommand*\val[1]{\texttt{#1}}
\newcommand*\keyvalue[2]{\texttt{#1=#2}}

\newlist{vallist}{description}{1}
\setlist[vallist]{
  leftmargin=3em,
  style=unboxed,
  labelsep=1em,
  font=\descriptionitemcolon,
  nosep,
}
\newcommand*{\descriptionitemcolon}[1]{\kern 1em #1:}

\NewDocumentCommand{\default}{ m }{(\textit{Default:}\nobreakspace #1)}


\newcommand*\luafunc[1]{\texttt{#1}}
\newcommand*\luavar[1]{\texttt{#1}}



\newcommand*\prefixedurl[1]{\textsc{url}:~\url{#1}}



\begin{document}

\DocInput{lua-list-hyphen.dtx}

\PrintIndex

\end{document}
%</driver>
% \fi
%
%
%
% \GetFileInfo{lua-list-hyphen.sty}
%
%
%
% \title{^^A
% \pkg{lua-list-hyphen} ^^A
%   --- Per-language listing of hyphenated words for Lua\LaTeX^^A
%   \footnote{This document describes \fileversion, last revised \filedate.}^^A
% }
%
% \author{^^A
%  Alan J. Cain\footnote{\texttt{a.j.cain (AT) gmail.com}}^^A
% }
%
% \date{Released \filedate}
%
% \maketitle
%
%
%
% \begin{abstract}
%   This Lua\LaTeX\ package writes each word that has been hyphenated across lines to a file, using a different file for
%   each language, for subsequent external checking.
% \end{abstract}
%
%
%
% \tableofcontents
%
%
%
% \begin{documentation}
%
%
%
% \section{Introduction}
%
% \TeX's algorithm for finding points where a word can be hyphenated is good, but not perfect.\footnote{For a
% description of the algorithm and its limitations, see Knuth's account in Appendix~H of \textit{The \TeX book}
% (Addison-Wesley, 2021. ISBN:~\texttt{978-0-201-13447-6})} The present author writes in British English, where the
% valid division points can depend on the pronunciation of a word and on its internal structure (and hence its
% etymology), and where there are thus many situations where \TeX's pattern-based approach cannot be expected to work;
% for example, \textit{geometry} \(\to\) \textit{geom-etry}, but \textit{geometric} \(\to\)
% \hbox{\textit{geo-met-tric}}.\footnote{See the \textit{New Oxford Spelling Dictionary}, which is the standard
% reference for word divisions in British English (Oxford University Press, 2005. ISBN:~\texttt{978-0-19-860881-3}).}
% For these words, \pkg{babel}'s patterns for British English yield \textit{geo-metry} and \textit{geo-met-ric}, while
% \pkg{polyglossia}'s produce \textit{ge-om-e-try} and \textit{ge-o-met-ric}. Even in languages which have more
% systematic rules for division points can refer to the semantics of compound words, not just to combinations of
% letters.
%
% Easy checking of the chosen hyphenations is desirable. With Lua\TeX, it is possible to extract the hyphenated words.
% The Lua\LaTeX\ package \pkg{lua-check-hyphen} offers this facility. It checks hyphenated words against a whitelist,
% visually flags unknown hyphenations, and writes unknown hyphenations to a file. But it was first written in 2012, when
% Lua\TeX\ was at an earlier stage of development, and so it has certain problems, such as with words containing
% ligatures. It also lacks multi-language support.
%
% This Lua\LaTeX\ package, \pkg{lua-list-hyphen}, uses some ideas from \pkg{lua-check-hyphen} but was written from
% scratch to work with a modern Lua\TeX. It simply writes hyphenated words from each language to a separate file, so
% that they can be checked (manually or by an external program).
%
% [The author has written a simple Python application \texttt{hyphenassist}\footnote{\textsc{url}:
% \url{https://codeberg.org/ajcain/hyphenassist}.} that checks the listed hyphenations against a dictionary of valid
% divisions and allows the user to quickly choose to add entries to the division dictionary, add hyphenation exceptions,
% or ignore particular hyphenations. He has used this program in conjunction with code incorporated into this package to
% check hyphenations in his own books.\footnote{In particular, \textit{Form \& Number: A History of Mathematical
% Beauty}. \textsc{url}: \url{https://archive.org/details/cain_formandnumber_ebook_large}.}]
%
%
%
% \paragraph*{Licence.} \noindent\pkg{lua-list-hyphen} is released under the \LaTeX\ Project Public Licence v1.3c or
% later.\footnote{\textsc{url}: \url{https://www.latex-project.org/lppl.txt}}
%
%
%
% \paragraph*{Feature requests and bug reports}
%
% The development code and issue tracker are hosted at Codeberg.\footnote{\textsc{url}:
% \url{https://codeberg.org/ajcain/lua-list-hyphen}}
%
%
%
% \section{Requirements}
%
% \pkg{lua-list-hyphen} requires
% \begin{enumerate}[label={(\arabic*)}]
%   \item Lua\LaTeX,
%   \item a recent \LaTeX\ kernel with \pkg{expl3} support (any kernel version since 2020-02-02 should suffice).
% \end{enumerate}
% It does not depend on any other packages, but will interface with \pkg{babel} or \pkg{polyglossia} (if one of them is
% loaded) to determine language names.
%
%
%
% \section{Installation}
%
% To install \pkg{lua-list-hyphen} manually, run \texttt{luatex lua-list-hyphen.ins} and copy
% \texttt{lua-list-hyphen.sty} and \texttt{lua-list-hyphen.lua} to somewhere Lua\LaTeX\ can find them.
%
%
%
% \section{Getting started}
%
% Simply load the package; the hyphenated words are by default written to the file
% \cs{jobname}\file{-}\meta{lang-id}\file{.hyph}, without being sorted or having duplicates removed. The \meta{lang-id}
% is either a Lua\TeX\ numerical language~ID, or a \pkg{babel} or \pkg{polyglossia} name of the language, if one of
% these packages is in use. The prefix \cs{jobname}\file{-} and the extension \file{.hyph} can be customized; see
% \fullref{Section}{sec:options}.
%
%
%
% \section{Package options}
% \label{sec:options}
%
% \DescribeOption{verbose}
% The boolean option \key{verbose} controls how much information is written to the file about each hyphenated word.
% When \val{true}, for each hyphenated word, both the undivided original and the divided word are written out (on the
% same line). When \val{false}, only the hyphenated word is written. \default{\val{false}}
%
% \DescribeOption{unique}
% The option \key{unique} controls removal of duplicates from the list of hyphenated words written out. It can be be
% set to one of the following three values:
% \begin{vallist}
%   \item[\val{none}] Duplicate hyphenations are not removed.
%   \item[\val{case}] Hyphenations that are duplicate (case-sensitively) are removed. In this case, the hyphenations
%     \texttt{geo-metry} and \texttt{Geo-metry} are considered to be distinct.
%   \item[\val{nocase}] Hyphenations that are duplicate (case-insensitively) are removed. In this case, the hyphenations
%     \texttt{geo-metry} and \texttt{Geo-metry} are considered to be duplicates. The case of each listed hyphenation
%     will be that of the first appearance of that hyphenation.
% \end{vallist}
% \default{\val{none}}
%
% \DescribeOption{sort}
% The option \key{sort} controls sorting of the list of hyphenated words. It can be be
% set to one of the following three values:
% \begin{vallist}
%   \item[\val{none}] Hyphenations appear in the same order as the occur in the document, or, if duplicates are removed,
%     in the order of first appearance in the document.
%     \item[\val{case}] Hyphenations are sorted case-sensitively. In this case, \texttt{Geo-metry} precedes
%     \texttt{geo-meter}.
%   \item[\val{nocase}] Hyphenations are sorted case-insensitively. In this case, \texttt{geo-meter} precedes
%     \texttt{Geo-metry}.
% \end{vallist}
% \default{\val{none}}
%
% \medskip
% The two options \key{prefix} and \key{extension} specify the files to which hyphenations are written. Between the
% prefix and the extension is either a Lua\TeX\ numerical language~ID, or a \pkg{babel} or \pkg{polyglossia}
% name of the language, if one of these packages is in use.
%
% \DescribeOption{prefix}
% The \key{prefix} is the part of the file name to which the list of hyphenated words is written, before the
% language~ID.
% \default{\cs{jobname}\file{-} (note the hyphen).}
%
% \DescribeOption{extension}
% The extension of the file (including the \file{.}) to which the list of hyphenated words for each language is written.
% \default{\file{.hyph}}
%
% \medskip
% \DescribeOption{debug}
% The boolean option \key{debug} controls whether debugging information is written to the terminal.
% \default{\val{false}}
%
%
%
% \section{Usage notes}
%
% \subsection{Languages}
%
% To determine the language of a word, \pkg{lua-list-hyphen} looks at what language is applied at the first possible
% hyphenation point, first considering the part of the word before it, then the part after it. In the (presumably rare)
% case of a ‘mixed-language’ word like ‘near-Zugzwang’ being specified (using, for example, \pkg{babel}) with
% \texttt{near-\cs{foreignlanguage}\{german\}\{Zugzwang\}}, it would be assigned to the language in which \hbox{‘near-’}
% is set.
%
% Duplicates are removed within each language. If the same hyphenation occurs in two different languages, it will appear
% in both files, regardless of the value of \key{unique}.
%
%
%
% \subsection{Limitations}
%
% \pkg{lua-list-hyphen} uses Lua\TeX's built-in functions for pattern matching and converting between upper and lower
% case, which are based on the \texttt{slnunicode} library. This library has not been updated for some time and is based
% on an out-of-date version of the Unicode standard. Thus there may be problems with languages added to Unicode more
% recently. Hyphenated words from such languages should still be listed, but may contain extraneous characters and may
% not be sorted correctly. Users may prefer to leave sorting and removal of duplicates to an external program that
% adheres to the current Unicode standard.
%
%
%
% \end{documentation}
%
%
%
% \clearpage
% \begin{implementation}
%
%
%
% \section{Implementation (\LaTeX\ package)}
%
%    \begin{macrocode}
%<*package>
%<@@=lualisthyphen>
%    \end{macrocode}
%
%
%
% \subsection{Initial set-up}
%
% Package identification/version information.
%    \begin{macrocode}
\NeedsTeXFormat{LaTeX2e}[2020-02-02]
\ProvidesExplPackage{lua-list-hyphen}{2026-04-11}{0.2.45}
  {Listing hyphenated words for LuaLaTeX}
%    \end{macrocode}
% Check that Lua\TeX\ is in use.
%    \begin{macrocode}
\sys_if_engine_luatex:F
  {
    \msg_new:nnn{ lua-list-hyphen }{ lualatex_required }
      { LuaLaTeX~required.~Package~loading~will~abort. }
    \msg_critical:nn{ lua-list-hyphen }{ lualatex_required }
  }
%    \end{macrocode}
%
%
%
% \subsection{Options}
%
% \begin{macro}{\l_@@_verbose_bool}
%   Boolean option to indicate whether lists of hyphenations should be written verbosely.
%    \begin{macrocode}
\keys_define:nn { lua-list-hyphen }{
  verbose .bool_set:N = \l_@@_verbose_bool,
}
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}{\l_@@_unique_int}
%   Choice option to indicate whether lists of hyphenations should have duplicates removed, case-sensitively or
%   case-insensitively.
%    \begin{macrocode}
\int_new:N\l_@@_unique_int
\keys_define:nn { lua-list-hyphen }{
  unique .choices:nn = { none, case, nocase }{
    \int_set:Nn\l_@@_unique_int{ \l_keys_choice_int - 1 }
  },
}
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}{\l_@@_sort_int}
%   Choice option to indicate whether lists of hyphenations should be sorted, case-sensitively or case-insensitively.
%    \begin{macrocode}
\int_new:N\l_@@_sort_int
\keys_define:nn { lua-list-hyphen }{
  sort .choices:nn = { none, case, nocase }{
    \int_set:Nn\l_@@_sort_int{ \l_keys_choice_int - 1 }
  },
}
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}{\l_@@_file_prefix_str}
%   String option for the prefix of file files.
%    \begin{macrocode}
\keys_define:nn { lua-list-hyphen }{
  prefix .str_set:N = \l_@@_file_prefix_str,
  prefix .initial:e = { \c_sys_jobname_str- },
}
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}{\l_@@_file_extension_str}
%   String option for the file extension of file files.
%    \begin{macrocode}
\keys_define:nn { lua-list-hyphen }{
  extension .str_set:N = \l_@@_file_extension_str,
  extension .initial:n = { .hyph },
}
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}{
%   \l_@@_debug_int
% }
%   Option to specify whether debug information is written to the terminal. Not intended for end users.
%    \begin{macrocode}
\int_new:N\l_@@_debug_int
\keys_define:nn { lua-list-hyphen }{
  debug .code:n = {\int_set_eq:NN\l_@@_debug_int\c_one_int}
}
%    \end{macrocode}
% \end{macro}
%
%
%
% Process package options.
%    \begin{macrocode}
\ProcessKeyOptions [ lua-list-hyphen ]
%    \end{macrocode}
%
%
%
% Convert boolean options to integers (which can be accessed from Lua).
%    \begin{macrocode}
\int_new:N\l_@@_verbose_int
\bool_if:NT\l_@@_verbose_bool
  { \int_set_eq:NN\l_@@_verbose_int\c_one_int }
%    \end{macrocode}
%
%
%
% \subsection{Lua backend and interface}
%
% Load the Lua backend.
%    \begin{macrocode}
\lua_now:n{
  lualisthyphen = require('lua-list-hyphen')
}
%    \end{macrocode}
%
%
%
% \subsection{Saving language names}
%
% At \texttt{enddocument/afterlastpage}, if possible save \pkg{babel}'s language names. (\pkg{polyglossia}'s names can
% be found directly from Lua.)
%    \begin{macrocode}
\hook_gput_code:nnn{ enddocument/afterlastpage }{ lua-list-hyphen } {
  \@@_babel_save_language_names:
}
%    \end{macrocode}
%
% \begin{macro}{\@@_babel_save_language_names:}
%   If \pkg{babel} is in use, get language names from \cs{bbl@languages}.
%    \begin{macrocode}
\cs_new:Npn \@@_babel_save_language_names:
  {
    \cs_if_exist:NT\bbl@languages
      {
%    \end{macrocode}
%   Iterate through \cs{bbl@languages} to get language names. Items stored in this macro are quadruples prefixed with
%   \cs{bbl@elt}, so locally redefine this latter macro to an auxiliary function that passes language ID/name pairs to
%   the Lua backend.
%    \begin{macrocode}
        \group_begin:
        \cs_set_eq:NN
          \bbl@elt
          \@@_babel_save_language_names_elt:nnnn
        \bbl@languages
        \group_end:
      }
  }
%    \end{macrocode}
% \end{macro}
%
% \begin{macro}{\@@_babel_save_language_names_elt:nnnn}
%   Auxiliary function that takes a quadruple stored in \cs{bbl@languages} and passes language ID/name pairs to the Lua
%   backend.
%    \begin{macrocode}
\cs_new:Npn \@@_babel_save_language_names_elt:nnnn #1#2#3#4
  {
    \lua_now:n{
      lualisthyphen.babel_save_language_name(#2,'#1')
    }
  }
%    \end{macrocode}
% \end{macro}
%
%
%
% \subsection{Processing and writing hyphenation lists}
%
% At \texttt{enddocument/info}, write the list of hyphenations for each language.
%    \begin{macrocode}
\hook_gput_code:nnn{ enddocument/info }{ lua-list-hyphen } {
  \@@_process_write_hyphenation_lists:ee
    {\str_use:N\l_@@_file_prefix_str}
    {\str_use:N\l_@@_file_extension_str}
}
%    \end{macrocode}
%
%
%
% \begin{macro}{\@@_process_write_hyphenation_lists:nn}
%   Write hyphenations lists to files with prefix given in the first parameter and suffix in the second.
%    \begin{macrocode}
\cs_new:Npn \@@_process_write_hyphenation_lists:nn #1#2
  {
    \lua_now:e{
      lualisthyphen.process_write_hyphenation_lists(
        '\luaescapestring{#1}',
        '\luaescapestring{#2}'
      )
    }
  }
\cs_generate_variant:Nn
  \@@_process_write_hyphenation_lists:nn
  { ee }
%    \end{macrocode}
% \end{macro}
%
%
%
%    \begin{macrocode}
%</package>
%    \end{macrocode}
%
%
%
% \section{Implementation (Lua backend)}
%
%    \begin{macrocode}
%<*lua>
%    \end{macrocode}
%
%
%
% \subsection{Debugging function}
%
% \begin{macro}[int]{debug}
%   Debugging function. Initially defined to do nothing, then overridden to become a function that actually writes
%   debugging information if the package option was set.
%    \begin{macrocode}
local function debug(s)
end

if tex.count['l__lualisthyphen_debug_int'] ~= 0 then
  debug = function(s)
    print('lua-list-hyphen DEBUG: ' .. s)
  end
end
%    \end{macrocode}
% \end{macro}
%
%
%
% \subsection{Table key constants}
%
% Keys for tables containing hyphenatable/hyphenated word data.
%    \begin{macrocode}
local KEY_WORD = 'word'
local KEY_LANG = 'lang'
local KEY_DIVISION = 'division'
local KEY_INDEX = 'index'
%    \end{macrocode}
%
%
%
% \subsection{Node ID and subtype constants}
%
% Define constants for the node IDs that need to be recognized.
%    \begin{macrocode}
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')
%    \end{macrocode}
% Define a constant for the kern node subtype that needs to be recognized. There seems to be no automatic way to get
% the numerical value fro the subtype other than searching the \luavar{node.subtype('kern')} table.
%    \begin{macrocode}
local NODE_KERN_SUBTYPE_FONTKERN
for k,v in pairs(node.subtypes('kern')) do
  if v == 'fontkern' then
    NODE_KERN_SUBTYPE_FONTKERN = k
    break
  end
end
%    \end{macrocode}
%
%
%
% \subsection{Utility functions}
%
% \begin{macro}[int]{list_filter}
%   Take a list \luavar{t} and remove from it any elements for which the function
%   \luavar{f} does not return true. (The index \luavar{j} is always the destination index to which a ‘keep’ element
%   is moved.)\footnote{Code adapted from \url{https://stackoverflow.com/a/53038524}.}
%    \begin{macrocode}
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
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{list_uniq}
%   Take a list \luavar{t} and remove from it adjacent elements for which the function \luavar{f} returns true. (The
%   index \luavar{j} is always the last ‘kept’ element.)
%    \begin{macrocode}
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
%    \end{macrocode}
% \end{macro}
%
%
%
% \subsection{Getting text from nodes}
%
% Getting the components of the ligatures that have Unicode code points can be problematic, at least for some fonts,
% so define a lookup table for these cases.
%    \begin{macrocode}
local LIGATURE_COMPONENTS = {
  [0xfb00] = {'f','f'},
  [0xfb01] = {'f','i'},
  [0xfb02] = {'f','l'},
  [0xfb03] = {'f','f','i'},
  [0xfb04] = {'f','f','l'},
}
%    \end{macrocode}
%
%
%
% Extracting text from nodes uses two functions that call each other, so the names have to be defined ahead of time.
%    \begin{macrocode}
local get_node_text
local get_nodelist_text
%    \end{macrocode}
%
%
%
% \begin{macro}[int]{get_node_text}
%   Return the text content of a glyph node (which might be a normal glyph, a ligature, etc.).
%    \begin{macrocode}
get_node_text = function(n)

  if n.id == NODE_ID_GLYPH then

    if LIGATURE_COMPONENTS[n.char] ~= nil then
      local text = ''

      for _,c in ipairs(LIGATURE_COMPONENTS[n.char]) do
        text = text .. c
      end
      return text

    elseif n.components then
      return get_nodelist_text(n.components)
    else
      -- See [https://tug.org/pipermail/luatex/2018-March/006786.html]
      local u = fonts.hashes.identifiers[n.font].characters[n.char].tounicode
      return utf8.char(tonumber(u,16))
    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
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{get_nodelist_text}
%   Return the text content of the glyph nodes in the list starting at \luavar{head} up to and including the node
%   \luavar{last}, or up to the end of the list if \luavar{last} is not specified.
%    \begin{macrocode}
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
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{is_possible_word_node}
%  Return boolean indicating if node \luavar{n} could be part of a word. Assume that \luavar{glyph}, \luavar{disc},
%  and \luavar{margin_kern} nodes could be part of a word, as could a \luavar{kern} node with subtype
%  \luavar{fontkern}.
%    \begin{macrocode}
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
%    \end{macrocode}
% \end{macro}
%
%
%
% \subsection{String manipulation}
%
% \begin{macro}[int]{trim_nonletters_both}
%   Remove non-letter characters from both the start and end of a string.
%    \begin{macrocode}
local function trim_nonletters_both(s)

  return unicode.utf8.match(s,'^%A*(.-)%A*$')

end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{trim_nonletters_start}
%   Remove non-letter characters from the start of a string.
%    \begin{macrocode}
local function trim_nonletters_start(s)

  return unicode.utf8.match(s,'^%A*(.-)$')

end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{trim_nonletters_end}
%   Remove non-letter characters from the end of a string.
%    \begin{macrocode}
local function trim_nonletters_end(s)

  return unicode.utf8.match(s,'^(.-)%A*$')

end
%    \end{macrocode}
% \end{macro}
%
%
%
% \subsection{Pre-linebreak processing}
%
% Before each line has been broken, find all potential division points and store the words in which they occur,
% linking each potential break point to the corresponding word.
%
% Declare a new attribute, which will be used to store in each disc node the index of the corresponding word in the
% table \luavar{hlist_hyphenatable_word_list}.
%    \begin{macrocode}
local hyphen_attr = luatexbase.new_attribute('hyphen_attr')
%    \end{macrocode}
%
%
%
% Table to hold hyphenatable words found in the hlist that will be broken. This table will be cleared after the
% post-linebreak processing.
%    \begin{macrocode}
local hlist_hyphenatable_word_list = {}
%    \end{macrocode}
%
%
%
% \begin{macro}[int]{get_first_glyph_lang}
%   Return the lang attribute of the first glyph in the the part of the list starting n that could be part of a word.
%   (Currently unused; see the documentation of \luafunc{get_disc_lang}.)
%    \begin{macrocode}
  -- local function get_first_glyph_lang(n)

  --   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
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{get_disc_lang}
%   Try to find the language ID in force at a given disc node by looking at (1)~the last glyph in the word
%   before the disc node; (2)~the first glyph in the word after the disc node. Default to language ID \luavar{0}.
%
%   (Looking at \luavar{replace}, \luavar{pre}, \luavar{post} is possible, but is unreliable and so disabled for the
%   present. The author has encountered the situation where an explicit hyphen results in the hyphen characters in
%   \luavar{replace} and \luavar{pre} having different language IDs. He has not had time to investigate how this
%   arises from the interaction of \pkg{babel}/\pkg{polyglossia} and Lua\LaTeX.)
%    \begin{macrocode}
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
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{pre_linebreak}
%   For every word containing a disc node (a potential division point) in the hlist at \luavar{hlist_head}, add the
%   word it which it appears, along with the language of that word to \luavar{hlist_hyphenatable_word_list}, and store
%   its index in that table in the \luavar{hyphen_attr} attribute (declared above) of each disc node in the word.
%    \begin{macrocode}
local function pre_linebreak(hlist_head,groupcode)
%    \end{macrocode}
%   When non-\luavar{nil}, \luavar{word_start_node} is the first node of the ‘current’ word. When non-\luavar{nil},
%   \luavar{hyphenatable_index} is the index in \luavar{hlist_hyphenatable_word_list} where the word will be added.
%   \luavar{hyphenatable_count} is the number of potential hyphenatable words found so far, which is used to set
%   \luavar{hyphenatable_index} when the first disc node is found in a new word.
%    \begin{macrocode}
  local word_start_node = nil
  local hyphenatable_index = nil
  local hyphenatable_count = 0
  local lang = nil

  debug('Pre-linebreak processing start')

  for item in node.traverse(hlist_head) do
%    \end{macrocode}
%   If \luavar{item} is a glyph node, check for a new word start.
%    \begin{macrocode}
    if item.id == NODE_ID_GLYPH and not word_start_node then
      word_start_node = item
    end
%    \end{macrocode}
%   If \luavar{item} is a disc node, check whether it is the first one found in the ‘current’ word (indicated by)
%   \luavar{hyphenatable_index} being \luavar{nil}. If so, set \luavar{hyphenatable_index} and determine the language
%   currently being used. Set the attribute of the disc node.
%    \begin{macrocode}
    if item.id == NODE_ID_DISC then
      if not hyphenatable_index then
        hyphenatable_count = hyphenatable_count + 1
        hyphenatable_index = hyphenatable_count
        lang = get_disc_lang(item)
      end
      node.set_attribute(item,hyphen_attr,hyphenatable_index)
    end
%    \end{macrocode}
%   If \luavar{item} is not a node that can appear in a word assume that the word end has been reached.
%    \begin{macrocode}
    if not is_possible_word_node(item) then
      if word_start_node and hyphenatable_index then
%    \end{macrocode}
%   Extract the text of the hyphenatable word. In fact, the ‘word’ might be something other than a genuine word, such
%   as an ISBN (with hyphen separators). So only store the word in \luavar{hlist_hyphenatable_word_list} if something
%   non-empty is left after trimming non-letters from both sides.
%    \begin{macrocode}
        local hyphenatable_word = trim_nonletters_both(
          get_nodelist_text(word_start_node,item.prev)
        )
        if hyphenatable_word ~= '' then
          debug(
            '  Hyphenatable word (index ' .. hyphenatable_index .. ') "'
            .. hyphenatable_word .. '"'
          )
          hlist_hyphenatable_word_list[hyphenatable_index] = {
            [KEY_WORD] = hyphenatable_word,
            [KEY_LANG] = lang,
          }
        end
      end
%    \end{macrocode}
%  Reset \luavar{word_start_node} and \luavar{hyphenatable_index}, ready for the next word.
%    \begin{macrocode}
      word_start_node = nil
      hyphenatable_index = nil
    end

  end

  debug('Pre-linebreak processing finish')

  return true
end
%    \end{macrocode}
% \end{macro}
%
%
%
% \subsection{Post-linebeak processing}
%
% After linebreaking, look for a discretionary node at the end of each line, which indicates that a word has been
% divided between the end of that line and the start of the next. Extract the two word-pieces from the lines and store
% them in the appropriate language table.
%
% \begin{macro}[int]{get_used_disc}
%   If at the tail of the hlist at \luavar{hlist_head} (which will be a line) there is a disc node not followed by a
%   glyph node, return that disc node. Otherwise return \luavar{nil}.
%    \begin{macrocode}
local function get_used_disc(hlist_head)

  local item = node.tail(hlist_head)

  while item and item.id ~= NODE_ID_GLYPH do
    if item.id == NODE_ID_DISC then
      return item
    end
    item = item.prev
  end

  return nil

end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{get_disc_word_start}
%   Return the node starting the word that includes a given disc node \luavar{n}, or \luavar{nil} if there is no such
%   node.
%    \begin{macrocode}
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
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{get_next_hlist}
%   Return the next hlist in the list containing the given node \luavar{n}, or \luavar{nil} if there is no such hlist
%   node.
%    \begin{macrocode}
local function get_next_hlist(n)

  item = n.next

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

  return nil

end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{get_line_first_word}
%   Return the first word in the hlist at \luavar{hlist_head}, or \luavar{nil} if there is no such word.
%    \begin{macrocode}
local function get_line_first_word(hlist_head)
%    \end{macrocode}
%   \luavar{word_start_node} is either \luavar{nil} or the (glyph) node that starts the word.
%    \begin{macrocode}
  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
%    \end{macrocode}
%   It is possible that the word ends at the end of the hlist, so check if a word has been started.
%    \begin{macrocode}
  if word_start_node then
    return get_nodelist_text(word_start_node,node.tail(hlist_head))
  else
    return nil
  end
end
%    \end{macrocode}
% \end{macro}
%
%
%
% Table for lists for hyphenated words in various languages. This table will be indexed by (numerical) language IDs.
% Each value will be a list, and each entry in the list will be a table containing the original word, the hyphenation,
% and the index of the table in the list (which is needed later for stable sorting and sorting into the original
% order).
%    \begin{macrocode}
local hyphenation_table = {}
%    \end{macrocode}
%
%
%
% \begin{macro}[int]{check_line_hyphenation}
%   Check whether there is a hyphenated word at the end of the given hlist; if so, save the word to
%   \luavar{hyphenation_list}.
%    \begin{macrocode}
local function check_line_hyphenation(hlist)
%    \end{macrocode}
%   First, is there a disc node at the end of the list? (‘End’ modulo certain other node types; see the documentation
%   of \luafunc{get_used_disc}.)
%    \begin{macrocode}
  local last_disc = get_used_disc(hlist.head)
  if not last_disc then
    debug('  No disc node found at end of line')
    return
  end
%    \end{macrocode}
%   The \luavar{hyphen_attr} may or may not contain the index of a word in \luavar{hlist_hyphenatable_word_list}. (See
%   the documentation for \luafunc{pre_linebreak}.)
%    \begin{macrocode}
  local hyphenation_index = node.has_attribute(last_disc,hyphen_attr)
  local t = hlist_hyphenatable_word_list[hyphenation_index]
  if not t then
    debug('  Disc node not associated to a stored word')
    return
  end
  local word = t[KEY_WORD]
  local lang = t[KEY_LANG]
%    \end{macrocode}
%   There should always be a next line, since there is a disc node at the end of \luavar{hlist}, but check anyway.
%    \begin{macrocode}
  local next_line = get_next_hlist(hlist)

  if not next_line then
    debug('  No following line found (which should not happen)')
    return
  end
%    \end{macrocode}
%   Get the hyphenation list for the language of the word, ensuring that the list has been created.
%    \begin{macrocode}
  lang_hyphenation_list = hyphenation_table[lang]
  if not lang_hyphenation_list then
    hyphenation_table[lang] = {}
    lang_hyphenation_list = hyphenation_table[lang]
  end
%    \end{macrocode}
%   For the pre-linebreak part of the word, get the word that ends the line, and trim any leading non-letters. This
%   could leave an empty word; for example, if \(n\)-dimensional is broken at the hyphen, the word ending the line is
%   just the hyphen. If an empty word is left, just use the non-trimmed result.
%    \begin{macrocode}
  local pre = get_nodelist_text(get_disc_word_start(hlist.head,last_disc))
  local pre_temp = trim_nonletters_start(pre)
  if pre_temp ~= '' then
    pre = pre_temp
  end
%    \end{macrocode}
%   For the post-linebreak part, just get the word at the start of the next line, and trim and trailing non-letters.
%    \begin{macrocode}
  local post = trim_nonletters_end(get_line_first_word(next_line.head))

  debug(
    '  Hyphenated word found: "' .. word .. '" -> "' .. pre .. '<>' .. post .. '"'
  )
%    \end{macrocode}
%   Store everything in the language hyphenation list.
%    \begin{macrocode}
  table.insert(
    lang_hyphenation_list,
    {
      [KEY_WORD] = word,
      [KEY_DIVISION] = pre .. post,
      [KEY_INDEX] = #lang_hyphenation_list,
    }
  )

end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{post_linebreak}
%   For every line in the vlist at \luavar{vlist_head}, check whether there is a hyphenated word at the end; if so,
%   save the word to \luavar{hyphenation_list}.
%    \begin{macrocode}
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_hyphenatable_word_list = {}

  debug('Post-linebreak processing end')

  return true

end
%    \end{macrocode}
% \end{macro}
%
%
%
% \subsection{Callbacks}
%
% Add \luafunc{pre_linebreak} and \luafunc{post_linebreak} to the relevant callbacks.
%    \begin{macrocode}
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
)
%    \end{macrocode}
%
%
%
%    \begin{macrocode}
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
)
%    \end{macrocode}
%
%
%
% \subsection{Language settings}
%
% Table mapping language IDs to textual names.
%    \begin{macrocode}
local language_table = {}
%    \end{macrocode}
%
% Populating \luavar{language_table} is done differently for \pkg{babel} and \pkg{polyglossia}. If \pkg{babel} is in
% use, the \LaTeX\ frontend iterates through \cs{bbl@languages} and calls \luafunc{babel_save_language_name}. If
% \pkg{polyglossia} is in use, \luavar{language_table} is populated by \luafunc{polyglossia_get_language_names}, which
% is called just before the hyphenation lists are written.
%
% \begin{macro}[int]{babel_save_language_name}
%   Store the association of a language ID to \pkg{babel}'s texual name, if no name has been assigned to that ID
%   already.
%    \begin{macrocode}
local function babel_save_language_name(lang_id,name)

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

end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{polyglossia_get_language_names}
%   If polyglossia has been loaded, use it to build the table mapping language IDs to textual names.
%    \begin{macrocode}
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
%    \end{macrocode}
% \end{macro}
%
%
%
% \subsection{Processing hyphenation lists}
%
% Before writing out hyphenation lists, remove duplicates and/or perform sorting, in accordance with the set options.
%
%
%
% \subsubsection{Comparisons and equality checks}
%
% \begin{macro}[int]{equal_hyphenation_case_sensitive}
%   Equality check for deduplicating the list of hyphenations case-sensitively.
%    \begin{macrocode}
local function equal_hyphenation_case_sensitive(a,b)
  return (
    a[KEY_WORD] == b[KEY_WORD]
    and
    a[KEY_DIVISION] == b[KEY_DIVISION]
  )
end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{equal_hyphenation_case_insensitive}
%   Equality check for deduplicating the list of hyphenations case-insensitively.
%    \begin{macrocode}
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
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{lessthan_hyphenation_case_sensitive}
%   Comparison for sorting the list of hyphenations case-sensitively.
%
%   The comparison of index keys ensures that the sorting is stable.
%    \begin{macrocode}
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
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{lessthan_hyphenation_case_insensitive}
%   Comparison for sorting the list of hyphenations case-insensitively.
%
%   The comparison of index keys ensures that the sorting is stable.
%    \begin{macrocode}
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
%    \end{macrocode}
% \end{macro}
%
%
%
% \subsubsection{Sorting}
%
% \begin{macro}[int]{sort_lang_hyphenation_list_none}
%   Sort \luavar{hyphenation_list} into its original order of appearance.
%    \begin{macrocode}
local function sort_lang_hyphenation_list_none(hyphenation_list)
  table.sort(
    hyphenation_list,
    function(a,b)
      return a[KEY_INDEX] < b[KEY_INDEX]
    end
  )
end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{sort_lang_hyphenation_list_case_sensitive}
%   Sort \luavar{hyphenation_list} case-sensitively.
%    \begin{macrocode}
local function sort_lang_hyphenation_list_case_sensitive(hyphenation_list)
  table.sort(
    hyphenation_list,
    lessthan_hyphenation_case_sensitive
  )
end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{sort_lang_hyphenation_list_case_insensitive}
%   Sort \luavar{hyphenation_list} case-insensitively.
%    \begin{macrocode}
local function sort_lang_hyphenation_list_case_insensitive(hyphenation_list)
  table.sort(
    hyphenation_list,
    lessthan_hyphenation_case_insensitive
  )
end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{process_lang_hyphenation_list_sort}
%   Select the appropriate function for sorting.
%    \begin{macrocode}
local sort_lang_hyphenation_list
if tex.count['l__lualisthyphen_sort_int'] == 1 then
  sort_lang_hyphenation_list = sort_lang_hyphenation_list_case_sensitive
elseif tex.count['l__lualisthyphen_sort_int'] == 2 then
  sort_lang_hyphenation_list = sort_lang_hyphenation_list_case_insensitive
else
  sort_lang_hyphenation_list = sort_lang_hyphenation_list_none
end
%    \end{macrocode}
% \end{macro}
%
%
%
% \subsubsection{Deduplication}
%
% \begin{macro}[int]{deduplicate_lang_hyphenation_list_none}
%   Dummy function; does not deduplicate \luavar{hyphenation_list}.
%    \begin{macrocode}
local function deduplicate_lang_hyphenation_list_none(hyphenation_list)
end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{deduplicate_lang_hyphenation_list_case_sensitive}
%   Remove duplicates from \luavar{hyphenation_list} case-sensitively.
%    \begin{macrocode}
local function deduplicate_lang_hyphenation_list_case_sensitive(hyphenation_list)
  table.sort(
    hyphenation_list,
    lessthan_hyphenation_case_sensitive
  )
  list_uniq(
    hyphenation_list,
    equal_hyphenation_case_sensitive
  )
end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{deduplicate_lang_hyphenation_list_case_insensitive}
%   Remove duplicates from \luavar{hyphenation_list} case-insensitively.
%    \begin{macrocode}
local function deduplicate_lang_hyphenation_list_case_insensitive(hyphenation_list)
  table.sort(
    hyphenation_list,
    lessthan_hyphenation_case_insensitive
  )
  list_uniq(
    hyphenation_list,
    equal_hyphenation_case_insensitive
  )
end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{deduplicate_lang_hyphenation_list}
%   Select the appropriate function for whether duplicates whould be removed.
%    \begin{macrocode}
local deduplicate_lang_hyphenation_list
if tex.count['l__lualisthyphen_unique_int'] == 1 then
  deduplicate_lang_hyphenation_list = deduplicate_lang_hyphenation_list_case_sensitive
elseif tex.count['l__lualisthyphen_unique_int'] == 2 then
  deduplicate_lang_hyphenation_list = deduplicate_lang_hyphenation_list_case_insensitive
else
  deduplicate_lang_hyphenation_list = deduplicate_lang_hyphenation_list_none
end
%    \end{macrocode}
% \end{macro}
%
%
%
% \subsubsection{Combined processing}
%
% \begin{macro}[int]{process_lang_hyphenation_list}
%   Remove duplicates and sort \luavar{hyphenation_list}.
%    \begin{macrocode}
local function process_lang_hyphenation_list(hyphenation_list)
  deduplicate_lang_hyphenation_list(hyphenation_list)
  sort_lang_hyphenation_list(hyphenation_list)
end
%    \end{macrocode}
% \end{macro}
%
%
%
% \subsection{Writing}
%
% \begin{macro}[int]{write_lang_hyphenation_list_standard}
%   Write out just the hyphenated words.
%    \begin{macrocode}
local function write_lang_hyphenation_list_standard(f,hyphenation_list)

  for i,v in ipairs(hyphenation_list) do

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

  end

end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{write_lang_hyphenation_list_verbose}
%   Write out all hyphenation information.
%    \begin{macrocode}
local function write_lang_hyphenation_list_verbose(f,hyphenation_list)

  for i,v in ipairs(hyphenation_list) do

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

  end

end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{write_lang_hyphenation_list}
%   Set \luafunc{write_lang_hyphenation_list} to be either \luafunc{write_lang_hyphenation_list_standard} or
%   \luafunc{write_lang_hyphenation_list_verbose}, depending on the
%   package options.
%    \begin{macrocode}
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
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{get_hyphenation_file_path}
%   Get the file to which the list of hyphenated words will be written, based on the given \luavar{prefix},
%   \luavar{extension}, numerical \luavar{lang_id}, and taking into account any specified output directory for
%   Lua\TeX.
%    \begin{macrocode}
local function get_hyphenation_file_path(prefix,extension,lang_id)

  local lang_name = language_table[lang_id]
  if not lang_name then
    lang_name = lang_id
  end

  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
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{process_write_lang_hyphenation_list}
%   Process and write out the \luavar{hyphenation_list} (which will be for the language with the numerical
%   \luavar{lang_id}) to a file with the given \luavar{prefix} and \luavar{extension}.
%    \begin{macrocode}
local function process_write_lang_hyphenation_list(
    prefix,extension,lang_id,hyphenation_list
)

  process_lang_hyphenation_list(hyphenation_list)

  local f = io.open(get_hyphenation_file_path(prefix,extension,lang_id),'w')
  write_lang_hyphenation_list(f,hyphenation_list)
  f:close()

end
%    \end{macrocode}
% \end{macro}
%
%
%
% \begin{macro}[int]{process_write_hyphenation_lists}
%   If polyglossia is in use, populate \luavar{language_table}. Then, for each language, process and write out the
%   hyphenation lists to a file.
%    \begin{macrocode}
local function process_write_hyphenation_lists(prefix,extension)

  polyglossia_get_language_names()

  for k,v in pairs(hyphenation_table) do
    process_write_lang_hyphenation_list(prefix,extension,k,v)
  end

end
%    \end{macrocode}
% \end{macro}
%
%
%
% \subsection{Export public functions}
%
% Finally, make available the functions that will be called from the \LaTeX\ frontend using \cs{lua_now:n}.
%    \begin{macrocode}
return {
  process_write_hyphenation_lists = process_write_hyphenation_lists,
  babel_save_language_name = babel_save_language_name,
}
%    \end{macrocode}
%
%
%
%    \begin{macrocode}
%</lua>
%    \end{macrocode}

%
%
%
% \clearpage
% \end{implementation}
%
%
%
% \iffalse
%<*metadriver>
\input{lua-list-hyphen.dtx}
%</metadriver>
% \fi
