% \iffalse meta-comment
%
%% Copyright (C) 2020 by Marcel Krueger
%%
%% 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.3 or later is part of all distributions of
%% LaTeX version 2005/12/01 or later.
%
%<*batch>
%<*gobble>
\ifx\jobname\relax\let\documentclass\undefined\fi
\ifx\documentclass\undefined
\csname fi\endcsname
%</gobble>
\input docstrip.tex
\keepsilent
\let\MetaPrefix\relax
\preamble
\endpreamble
\postamble
\endpostamble
\def\MetaPrefix{--}
\generate{%
  \file{lua-ul.lua}{\from{lua-ul.dtx}{luacode}}%
  \file{pre_append_to_vlist_filter.lua}{\from{lua-ul.dtx}{callback}}%
}
\let\MetaPrefix\DoubleperCent
\generate{\file{lua-ul.sty}{\from{lua-ul.dtx}{package}}}
\endbatchfile
%</batch>
%<*gobble>
\fi
\expandafter\ifx\csname @currname\endcsname\empty
\csname fi\endcsname
%</gobble>
%<*driver>
\documentclass{article}
\usepackage{lua-ul}
\usepackage{csquotes,doc,framed,metalogo,hyperref,luacolor,tikzducks,pict2e}
\begin{document}
\DocInput{lua-ul.dtx}
\PrintIndex
\PrintChanges
\end{document}
%</driver>
%<*gobble>
\fi
%</gobble>
% \fi
%
%
% \catcode`\§=14
% \newunderlinetype\beginUnderDuck{\cleaders\hbox{§
%   \begin{tikzpicture}[x=.5ex,y=.5ex,baseline=.8ex]§
%     \duck
%   \end{tikzpicture}§
% }}
% \newcommand\underDuck[1]{{\beginUnderDuck#1}}
% \newunderlinetype\beginUnderWavy[\number\dimexpr1ex]{\cleaders\hbox{§
%     \setlength\unitlength{.3ex}§
%     \begin{picture}(4,0)(0,1)
%       \thicklines
%       \color{red}§
%       \qbezier(0,0)(1,1)(2,0)
%       \qbezier(2,0)(3,-1)(4,0)
%     \end{picture}§
% }}
% \newcommand\underWavy[1]{{\beginUnderWavy#1}}
% \newunderlinetype\beginStrikeThough{\leaders\hbox{§
%     \normalfont\bfseries/§
% }}
% \newcommand\StrikeThough[1]{{\beginStrikeThough#1}}
%
% \newcommand\Luaul{Lua-UL}
% \newcommand\luaul{Lua-UL}
% \GetFileInfo{lua-ul.sty}
% \title{The \luaul\ package\thanks{This document
%        corresponds to \luaul~\fileversion, dated~\filedate.}}
% \author{Marcel Kr\"uger \\ \href{mailto:tex@2krueger.de}{tex@2krueger.de}}
%
% \maketitle
% \section{User-level interface}
% \Luaul\ uses new capabilities of the LuaTeX engine to provide
% underlining/\hspace{0pt}strikethrough/\hspace{0pt}highlighting
% etc.\ support without breaking ligatures, kerning or restricting input.
% The predefined user-level commands are \verb+\underLine+, \verb+\highLight+, and \verb+\strikeThrough+.
% (\verb+\highLight+ will only work correctly if the \texttt{luacolor} package is loaded)
% They are used as
%
% \begin{verbatim}
%   \documentclass{article}
%   \usepackage{lua-ul}
%   \begin{document}
%   This package is \strikeThrough{useless}\underLine{awesome}!
%   \end{document}
% \end{verbatim}
%
% \begin{framed}
%   This package is \strikeThrough{useless}\underLine{awesome}!
% \end{framed}
%
% For limited compatibility with \texttt{soul}, the \texttt{soul} package option allows you
% to use the traditional macro names from \texttt{soul} instead:
%
% \begin{verbatim}
%   \documentclass{article}
%   \usepackage[soul]{lua-ul}
%   \begin{document}
%   This package is \st{useless}\ul{awesome}!
%   \end{document}
% \end{verbatim}
%
% \section{Expert interface}
% \DescribeMacro{\newunderlinetype}§
% Sometimes, you might try to solve more interesting problems than boring underlining,
% strikethrough or highlighting. Maybe you always wanted \underWavy{to be your own
% spell checker}, want to demonstrate \underDuck{your love for ducks} or you think
% that the traditional \verb+\strikeThrough+ \StrikeThough{is not visible enough}.
% For all these cases, you can define your own \enquote{underlining} command based on a \TeX\
% \verb+\[cxg]leaders+ command.
% 
% For this, use\\
% \verb+\newunderlinetype+\meta{macro name}\verb+[+\meta{context specifier}\verb+]{+\meta{\texttt{\string\leaders} command}\verb+}+
%
% First, you have to pass the name of the command which should enable your new kind of underlining.
% (If you want to have an additional command which takes an argument and underlines this argument,
% you will have to define this manually.)
% The optional argument provides a \enquote{context}: This \enquote{context} has to expand to a string
% (something that can appear in a csname) which changes if the leaders box should be recalculated.
% The leader will be chached and reused whenever the context evaluates to the same string.
% So if your leaders should depend on the fontsize,
% the expansion of the context should contain the font size. If you leaders contain text in the current font,
% your context should include \verb+\fontname+. The default context includes the current size of \texttt{1ex}
% and the current color if luacolor is loaded.
%
% The final argument contains the actual leader command. You should omit the final glue normally passed to \verb+\leaders+,
% so e.g.\ write \verb+\leaders\hbox{ . }+ without appending \verb+\hfill+ or \verb+\hskip1pt+ etc.
% The height and depth of your leaders is ignored to ensure that adding underlines etc. does not change the general text layout.
% It is your responsibility to ensure that you are not too high or too low and intercept other lines. On the other hand,
% running dimensions work fine if you use a rule.
%
% For example, the special underline commands demonstrated above are implemented as
%
% \begin{verbatim}
% \usepackage{luacolor,tikzducks,pict2e}
% \newunderlinetype\beginUnderDuck{\cleaders\hbox{%
%   \begin{tikzpicture}[x=.5ex,y=.5ex,baseline=.8ex]%
%     \duck
%   \end{tikzpicture}%
% }}
% \newcommand\underDuck[1]{{\beginUnderDuck#1}}
% \newunderlinetype\beginUnderWavy[\number\dimexpr1ex]{\cleaders\hbox{%
%     \setlength\unitlength{.3ex}%
%     \begin{picture}(4,0)(0,1)
%       \thicklines
%       \color{red}%
%       \qbezier(0,0)(1,1)(2,0)
%       \qbezier(2,0)(3,-1)(4,0)
%     \end{picture}%
% }}
% \newcommand\underWavy[1]{{\beginUnderWavy#1}}
% \newunderlinetype\beginStrikeThough{\leaders\hbox{%
%     \normalfont\bfseries/%
% }}
% \newcommand\StrikeThough[1]{{\beginStrikeThough#1}}
% \end{verbatim}
%
% Here \verb+\underWavy+ uses a custom context because it doesn't change depending on the current font color.
%
% If you only want to use \verb+\newunderlinetype+ and do not want to use the predefined underline types, you
% can use the \texttt{minimal} package option to disable them.
% 
% \StopEventually{}
% \section{The implementation}
% \subsection{Helper modules}
% First we need a separate Lua module \verb+pre_append_to_vlist_filter+ which
% provides a variant of the \verb+append_to_vlist_filter+ callback which can be
% used by multiple packages. This ensures that we are compatible with other packages
% implementing \verb+append_to_vlist_filter+.
% \iffalse
%<*gobble>
\RequirePackage{docstrip-luacode}
\begin{docstrip-luacode}{pre_append_to_vlist_filter}
%</gobble>
%<*callback>
% \fi
% First check if an equivalent to \verb+pre_append_to_vlist_filter+ already exists.
% The idea is that this might eventually get added to the kernel directly.
%    \begin{macrocode}
if luatexbase.callbacktypes.pre_append_to_vlist_filter then
  return
end

local call_callback = luatexbase.call_callback
local flush_node = node.flush_node
local prepend_prevdepth = node.prepend_prevdepth
local callback_define

%    \end{macrocode}
% HACK: Do not do this at home! We need to define the engine callback directly,
% so we use the debug library to get the ``real'' \verb+callback.define+:
%    \begin{macrocode}
for i=1,5 do
  local name, func = debug.getupvalue(luatexbase.disable_callback, i)
  if name == 'callback_register' then
    callback_define = func
    break
  end
end
if not callback_define then
  error[[Unable to find callback.define]]
end

local function filtered_append_to_vlist_filter(box,
                                               locationcode,
                                               prevdepth,
                                               mirrored)
  local current = call_callback("pre_append_to_vlist_filter",
                                box, locationcode, prevdepth,
                                mirrored)
  if not current then
    flush_node(box)
    return
  elseif current == true then
    current = box
  end
  return call_callback("append_to_vlist_filter",
                       box, locationcode, prevdepth, mirrored)
end

callback_define('append_to_vlist_filter',
                filtered_append_to_vlist_filter)
luatexbase.callbacktypes.append_to_vlist_filter = nil
luatexbase.create_callback('append_to_vlist_filter', 'exclusive',
                           function(n, _, prevdepth)
                             return prepend_prevdepth(n, prevdepth)
                           end)
luatexbase.create_callback('pre_append_to_vlist_filter',
                           'list', false)
%    \end{macrocode}
% \iffalse
%</callback>
%<*gobble>
\end{docstrip-luacode}
%</gobble>
% \fi
% \subsection{Lua module}
% Now we can define our main Lua module:
% \iffalse
%<*gobble>
\begin{docstrip-luacode}{lua-ul}
%</gobble>
%<*luacode>
% \fi
%    \begin{macrocode}
local hlist_t = node.id'hlist'
local vlist_t = node.id'vlist'
local kern_t = node.id'kern'
local glue_t = node.id'glue'

local char_given = token.command_id'char_given'

local underlineattrs = {}
local underline_types = {}
local saved_values = {}
local function new_underline_type()
  for i=1,#underlineattrs do
    local attr = underlineattrs[i]
    saved_values[i] = tex.attribute[attr]
    tex.attribute[attr] = -0x7FFFFFFF
  end
  local b = token.scan_list()
  for i=1,#underlineattrs do
    tex.attribute[underlineattrs[i]] = saved_values[i]
  end
  local lead = b.head
  if not lead.leader then
    tex.error("Leader required", {"An underline type has to \z
      be defined by leader. You should use one of the", "commands \z
      \\leaders, \\cleaders, or \\xleader, or \\gleaders here."})
  else
    if lead.next then
    tex.error("Too many nodes", {"An underline type can only be \z
        defined by a single leaders specification,", "not by \z
        multiple nodes. Maybe you supplied an additional glue?",
        "Anyway, the additional nodes will be ignored"})
    end
    table.insert(underline_types, lead)
    b.head = lead.next
    node.flush_node(b)
  end
  token.put_next(token.new(#underline_types, char_given))
end
local function set_underline()
  local j
  for i=1,#underlineattrs do
    local attr = underlineattrs[i]
    if tex.attribute[attr] == -0x7FFFFFFF then
      j = attr
      break
    end
  end
  if not j then
    j = luatexbase.new_attribute(
        "luaul" .. tostring(#underlineattrs+1))
    underlineattrs[#underlineattrs+1] = j
  end
  tex.attribute[j] = token.scan_int()
end
local functions = lua.get_functions_table()
local new_underline_type_func =
    luatexbase.new_luafunction"luaul.new_underline_type"
local set_underline_func =
    luatexbase.new_luafunction"luaul.set_underline_func"
token.set_lua("LuaULNewUnderlineType", new_underline_type_func)
token.set_lua("LuaULSetUnderline", set_underline_func, "protected")
functions[new_underline_type_func] = new_underline_type
functions[set_underline_func] = set_underline

local add_underline_h
local function add_underline_v(head, attr)
  for n in node.traverse(head) do
    if head.id == hlist_t then
      add_underline_h(n, attr)
    elseif head.id == vlist_t then
      add_underline_v(n.head, attr)
    end
  end
end
function add_underline_h(head, attr)
  local used = false
  node.slide(head.head)
  local last_value
  local first
  for n in node.traverse(head.head) do
    local new_value = node.has_attribute(n, attr)
    if n.id == hlist_t then
      new_value = nil
      add_underline_h(n, attr)
    elseif n.id == vlist_t then
      new_value = nil
      add_underline_v(n.head, attr)
    elseif n.id == kern_t and n.subtype == 0 then
      if n.next and not node.has_attribute(n.next, attr) then
        new_value = nil
      else
        new_value = last_value
      end
    elseif n.id == glue_t and (
        n.subtype == 8 or
        n.subtype == 9 or
        n.subtype == 15 or
    false) then
      new_value = nil
    end
    if last_value ~= new_value then
      if last_value then
        local width = node.rangedimensions(head, first, n)
        local kern = node.new(kern_t)
        kern.kern = -width
        local lead = node.copy(underline_types[last_value])
        lead.width = width
        head.head = node.insert_before(head.head, first, lead)
        node.insert_after(head, lead, kern)
      end
      if new_value then
        first = n
      end
      last_value = new_value
    end
  end
  if last_value then
    local width = node.rangedimensions(head, first)
    local kern = node.new(kern_t)
    kern.kern = -width
    kern.next = node.copy(underline_types[last_value])
    kern.next.width = width
    node.tail(head.head).next = kern
  end
end
local function filter(b, loc, prev, mirror)
  for i = 1, #underlineattrs do
    add_underline_v(b, underlineattrs[i])
  end
  return true
end
require'pre_append_to_vlist_filter'
luatexbase.add_to_callback('pre_append_to_vlist_filter',
                           filter, 'add underlines to list')
%    \end{macrocode}
% \iffalse
%</luacode>
%<*gobble>
\end{docstrip-luacode}
%</gobble>
% \fi
% \subsection{\TeX\ support package}
% Now only some \LaTeX\ glue code is still needed
% \iffalse
%<*package>
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage
  {lua-ul}
  [2020/03/12 v0.0.1 Underlining and related functionality for LuaTeX]

% \fi
% Only \LuaLaTeX{} is supported.
% For other engines we show an error.
%    \begin{macrocode}
\ifx\directlua\undefined
  \PackageError{lua-ul}{LuaLaTeX required}%
  {Lua-UL requires LuaLaTeX.
   Maybe you forgot to switch the engine in your editor?}
\fi
\directlua{require'lua-ul'}
\RequirePackage{xparse}
%    \end{macrocode}
% We support some options. Especially \verb+minimal+ will disable the predefined commands \verb+\underLine+ and \verb+\strikeThrough+ and allow you to define similar commands with your custom settings instead, \verb+soul+ tries to replicate names of the \verb soul  package.
%    \begin{macrocode}
\newif\ifluaul@predefined
\newif\ifluaul@soulnames
\luaul@predefinedtrue
\DeclareOption{minimal}{\luaul@predefinedfalse}
\DeclareOption{soul}{\luaul@soulnamestrue}
\ProcessOptions\relax
%    \end{macrocode}
% Just one more tiny helper.
%    \begin{macrocode}
\protected\def\luaul@maybedefineuse#1#2{%
  \unless\ifcsname#1\endcsname
    \expandafter\xdef\csname#1\endcsname{#2}%
  \fi
  \csname#1\endcsname
}

%    \end{macrocode}
% The main macro.
%    \begin{macrocode}
\NewDocumentCommand\newunderlinetype{mO{\luaul@defaultcontext}m}{%
  \newcommand#1{}% "Reserve" the name
  \protected\def#1{%
    \expandafter\luaul@maybedefineuse
      \expanded{{\csstring#1@@#2}}%
      {\LuaULSetUnderline
        \LuaULNewUnderlineType\hbox{#3\hskip0pt}%
  }}%
}
\ifluaul@predefined
  \newcommand\luaul@defaultcontext{%
    \number\dimexpr1ex
    @\unless\ifx\undefined\LuaCol@Attribute
      \the\LuaCol@Attribute
    \fi
  }
  \newunderlinetype\@underLine%
    {\leaders\vrule height -.65ex depth .75ex}
  \newcommand\underLine[1]{{\@underLine#1}}
  \newunderlinetype\@strikeThrough%
    {\leaders\vrule height .55ex depth -.45ex}
  \newcommand\strikeThrough[1]{{\@strikeThrough#1}}
  \newunderlinetype\@highLight[\number\dimexpr1ex]%
    {\color{yellow}\leaders\vrule height 1.75ex depth .75ex}
  \newcommand\highLight[1]{{\@highLight#1}}
  \ifluaul@soulnames
    \let\textul\underLine \let\ul\textul
    \let\textst\strikeThrough \let\st\textst
    \let\texthl\highLight \let\hl\texthl
  \fi
\fi
%    \end{macrocode}
% \iffalse
%</package>
% \fi
% \Finale
