5

The usual ways of underlining text (or at least the ones I know of: be it with \underline or e.g. the soul package) break kerning when used with part of a word. My current solution consists of a two-step process

  1. I type the letters to be underlined (this takes care of the kerning with the preceding letter)
  2. I type the underline
  3. I grab the following letter and add the kerning manually

MWE with picture of the result

\documentclass{article}

\makeatletter
\def\foo#1{%
   #1%
   \setbox0=\hbox{#1}%
   \hb@xt@\z@{\hss\vrule height -1.2pt depth 1.6pt width\wd0}%
   \addkerncorrection{#1}%
}

\def\addkerncorrection#1#2{%
   \setbox0=\hbox{{#1}{#2}}%
   \setbox1=\hbox{#1#2}%
   \dimen@=\wd1
   \advance\dimen@ by -\wd0
   \kern\dimen@ #2%
}
\makeatother

\begin{document}

V\foo{A}V

VAV

V\underline{A}V

\end{document}

enter image description here

The problem is that this kind of fails if the underlined part is the last piece of a word. Of course I can correct this with an empty group but I'd like to avoid that. I have a working trick (which I refuse to post here :-)) based on \new@ifnextchar from amsgen to test for a following space character but I'm starting to grow uncomfortable with the level of hacking. Furthermore, when utf8 encoding is used it breaks down for stuff like l\foo{ie}ß, which should be thus input as l\foo{ie}\ss.

So the question is: Is there a (better) way to underline part of a word while preserving the correct kerning and not swallowing spaces?

For those who might think this is an XY problem: this is about typesetting psalms in the different tones, like

enter image description here

  • 2
    luatex an option? – David Carlisle Aug 17 '18 at 15:28
  • @DavidCarlisle If I have to :-) Jokes aside, yes, of course. My desire of having this working is stronger than my distaste for stuff I don't understand... :-) – campa Aug 17 '18 at 15:32
  • for classic tex I'm pretty sure i'd use a syntax \foo{Kr}{a}{ft} with the pre, underlined, and post groups separate and possibly empty, then you don't have to mess with ifnextchar tests, for luatex you could perhaps use a callback to add the lines after typesetting – David Carlisle Aug 17 '18 at 15:35
  • Point is, I'm working for someone else. The idea would be to have an environment which makes _ active, and use Kr_a_ft. With my current solution (plus the bit I haven't shown) spaces are not a problem, but ß are. But I start to think that the question is ill-posed :-( – campa Aug 17 '18 at 15:38
  • ß should't be a problem I'd have thought, if they are your example should include that case:-) – David Carlisle Aug 17 '18 at 15:42
10

Using LuaTeX, you can use an attribute to mark the characters you want to underline. This does not interfere with kerning/ligaturing/line breaking because it only acts after all this is finished.

I added some extra flexibility for customized line thickness, placing, duck underlines, ... but the callback basically just iterates over the node tree and draws leaders whenever it finds a marked node:

\documentclass{article}
\usepackage{luacode}
\usepackage{tikzducks,pict2e}

\newattribute\underlineattr
\begin{luacode*}
  local underlineattr = token.create'underlineattr'.index
  local underline_types = {}
  function new_underline_type()
    table.insert(underline_types, tex.box[0].head)
    tex.box[0].head = nil
    tex.sprint(#underline_types)
  end
  local add_underline_h
  local function add_underline_v(head)
    for n in node.traverse(head) do
      if head.id == node.id'hlist' then
        add_underline_h(n)
      elseif head.id == node.id'vlist' then
        add_underline_v(n.head)
      end
    end
  end
  function add_underline_h(head)
    local last_value
    local first
    for n in node.traverse(head.head) do
      local new_value = node.has_attribute(n, underlineattr)
      if n.id == node.id'hlist' then
        new_value = nil
        add_underline_h(n)
      elseif n.id == node.id'vlist' then
        new_value = nil
        add_underline_v(n.head)
      elseif n.id == node.id'kern' and n.subtype == 0 then
        if n.next and not node.has_attribute(n.next, underlineattr) then
          new_value = nil
        else
          new_value = last_value
        end
      elseif n.id == node.id'glue' 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'
          kern.kern = -width
          kern.next = node.copy(underline_types[last_value])
          kern.next.width = width
          kern.next.next = n
          n.prev.next = 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'
      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)
    add_underline_v(b)
    local new_prev = mirror and b.height or b.depth
    if prev > -65536000 then
      local lineglue = tex.baselineskip.width - prev - (mirror and b.depth or b.height)
      local skip
      if lineglue < tex.lineskiplimit then
        skip = node.new('glue', 1)
        node.setglue(skip, node.getglue(tex.lineskip))
      else
        skip = node.new('glue', 2)
        node.setglue(skip, node.getglue(tex.baselineskip))
        skip.width = lineglue
      end
      skip.next = b
      b = skip
    end
    return b, new_prev
    -- return node.prepend_prevdepth(b)
  end
  luatexbase.callbacktypes.append_to_vlist_filter = 3 -- This should not be necessary
  luatexbase.add_to_callback('append_to_vlist_filter', filter, 'add underlines to list')
\end{luacode*}

\newcommand\newunderlinetype[2]{%
  \setbox0\hbox{#2\hskip0pt}%
  \chardef#1=\directlua{new_underline_type()}\relax
}
\newunderlinetype\myunderline{\leaders\vrule height-1ptdepth1.5pt}
\newunderlinetype\myunderduck{\cleaders\hbox{%
    \begin{tikzpicture}[baseline=3,scale=0.05]%
      \duck
    \end{tikzpicture}%
}}
\newunderlinetype\myunderwavy{\leaders\hbox{%
    \setlength\unitlength{.3mm}%
    \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\underLine[1]{{\underlineattr=\myunderline#1}}
\newcommand\underDuck[1]{{\underlineattr=\myunderduck#1}}
\newcommand\underWavy[1]{{\underlineattr=\myunderwavy#1}}
\begin{document}
V\underLine{A}V

\underDuck{VAV}

\underDuck{V}\underLine{AV}

\underDuck{These are \underWavy{ucks}}
\end{document}

enter image description here

  • -1 for the ducks:-) – David Carlisle Aug 17 '18 at 20:25
  • +1 but give me some more time to digest this before I accept :-) One thing (besides the whole lua code) is not clear to me: what's the purpose of \newcount? – campa Aug 18 '18 at 11:02
  • @campa A "underlinetype" is basically just a number identifying the stored leader specification. This leads to the question how to store this number under a nice name. There are three options: Creating a macro with the written out number, using a TeX count register or a (math)chardef. Using a macro can lead to parsing problems related to trialing spaces, so I avoided them. chardefs are nice but hard to set from Lua (This will probably change in the next LuaTeX version). So a \count register is used to store the number. Before setting it, it has to be allocated by \newcount. – Marcel Krüger Aug 18 '18 at 11:20
  • I just edited the code, now the \newcount is gone and a \chardef is used instead. – Marcel Krüger Aug 18 '18 at 11:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.