2

In addition to posting the right answer for the specific example in my question, please feel free to leave short snippets of code that display capabilities of node library

Am new to LuaTeX node library and learning how it can be utilized to post-process lines using post_linebreak_filter. So far I am able to do very basic maneuvering, like in the answer to my own question here. Next I tried to traverse over hlist nodes with the hope to change their leftskip/rightskip and to convert raggedright text to raggedleft. My trail-and-error didn't work, am posting my incorrect attempts below (Caution: Attempt-1 seems to enter infinite loop, so don't try it in your main terminal). Can someone please explain what is wrong with these, and show how the correct code would look like (with some explanation)?

% Attempt-1: Set leftskip/rightskip outside, and rebox the contents using hpack?
\directlua{
    function my_post_lb_filter(head,groupcode)
      local HLIST = node.id("hlist") % node.id for a line of text in vertical list
      for n in node.traverse(head) do % For every subnode within paragraph
        if n.id==HLIST then % If its a line of text
            tex.setglue("rightskip",0,0,0,2,2)
            tex.setglue("leftskip",0,65536,0,2,2)
            local b = node.copy(n)
            b = node.hpack(b.head)
            node.write(b)
        end
      end
      return head
    end
  luatexbase.add_to_callback('post_linebreak_filter', my_post_lb_filter, 'Play with luatex node library')
}

% Attempt-2 Traverse glue nodes within lines, and surgically update them
\directlua{
    function my_post_lb_filter(head,groupcode)
      local HLIST = node.id("hlist") % node.id for a line of text in vertical list
      local GLUE = node.id("glue")
      local RSKIP = node.subtype("rightskip")
      local LSKIP = node.subtype("leftskip")
      for n in node.traverse(head) do % For every subnode within paragraph
        if n.id==HLIST then % If its a line of text
            for g in node.traverse(n) do % For every subnode within line
                if g.id==GLUE then % If its a glue
                    if g.subtype == RSKIP then
                        node.setglue(g,0,0,0,2,2)
                    end
                    if g.subtype == LSKIP then
                        node.setglue(g,0,65536,0,2,2)
                    end
                end
            end
        end
      end
      return head
    end
  luatexbase.add_to_callback('post_linebreak_filter', my_post_lb_filter, 'Play with luatex node library')
}

A comment about Attempt-2: In my observation from internals of hlist nodes, raggedright lines lack 'leftskip' subnode inside them. So how could one add leftskip subnode to such a line? While raggedleft lines have both 'leftskip' & 'rightskip' subnodes within them, so it is just a matter of updating them I guess.

  • In the post_linebreak_filter the glue has already been set. You have to remove it from the end of the hlist and put it in the beginning. – Henri Menke Apr 16 '20 at 11:19
  • The node.write in your first example will result in an infinite loop. Never use node.write unless you know exactly that the generated node is going to end up in the correct place. – Henri Menke Apr 16 '20 at 11:21
4

First let's look at your first attempt:

You used

            tex.setglue("rightskip",0,0,0,2,2)
            tex.setglue("leftskip",0,65536,0,2,2)
            local b = node.copy(n)
            b = node.hpack(b.head)

Here node.hpack has been used to repack in order to pick up the changed rightskip/leftskip settings, but that is not how TeX works: rightskip and leftskip are not applied as part of packing the hbox, they are applied during linebreaking. So for this approach you would have to do use the linebreak callback and change the parameter there.

Also using node.write is almost always going to lead to problems in a callback. That function is adding a node to the current list TeX is working on, this will sometimes not be the list you think TeX is working on. Instead, try to use the list passed as head. In this case this happens to be the same list, but you are copying hlist nodes and append them at the end. So after the regular hbox nodes are processed, the former end of the list is no longer the end but followed by your copied nodes. Then these copied nodes are processed, creating even more copies. This is causing the infinite loop.

So the second approach is more promising. Your code already is quite close, the remaining steps are:

  • node.subtype only works for whatsit nodes, for other nodes you either have to hardcode the values or analyze the table returned by node.subtypes.
  • We have to traverse n.head instead of n, otherwise you never look into the actual content of the lists.
  • We have to ensure that a leftskip glue actually exists. We could use the guarantee that a leftskip glue will always be the first node in the hlist, but it is a bit safer to just remember while iterating if we already had a leftskip. (This way we don't get problems if another package adds some earlier node).

    To actually create the node, we can use node.new and then insert it into the list with node.insert_before.

  • A little simplification is using node.traverse_id instead of node.traverse here. Then you need less if statements and it is also a little bit faster:
\documentclass{article}
\usepackage{blindtext}
\directlua{
  local HLIST = node.id("hlist") % node.id for a line of text in vertical list
  local GLUE = node.id("glue")
  local RSKIP, LSKIP do
    local gluetypes = node.subtypes("glue")
    for i, n in pairs(gluetypes) do
      if n == "leftskip" then LSKIP = i end
      if n == "rightskip" then RSKIP = i end
    end
  end
  function my_post_lb_filter(head,groupcode)
    for n in node.traverse_id(HLIST, head) do % For every subnode within paragraph
      local leftskip_found
      for g, s in node.traverse_id(GLUE, n.head) do % For every subnode within line
        if s == RSKIP then
          node.setglue(g)
        end
        if s == LSKIP then
          node.setglue(g,0,65536,0,2,0)
          leftskip_found = true
        end
      end
      if not leftskip_found then % We have to add a glue node
        local g = node.new(GLUE, LSKIP)
        g.subtype = LSKIP
        node.setglue(g, 0, 65536, 0, 2, 0)
        g.attr = n.attr % Ensure that attributes have some reasonable value
        n.head = node.insert_before(n.head, n.head, g)
      end
    end
    return head
  end
  luatexbase.add_to_callback('post_linebreak_filter', my_post_lb_filter, 'Play with luatex node library')
}
\begin{document}
\showoutput
\raggedright

\blindtext
\end{document}

enter image description here

You might notice a little exercise left for the reader: The last line is currently centered. Explain why this happens and figure out how to fix it.

  • 1
    local gluetypes = table.swapped(node.subtypes("glue")); local RKSIP, LSKIP = gluetypes.rightskip, gluetypes.leftskip – Henri Menke Apr 17 '20 at 3:54
  • @MarcelKrüger Thanks a lot. \showoutput shows that there is a \glue 0.0 plus 1.0fil at the end of last line. It doesn't say what type of glue it is though? I don't see it in the hlist of last line visualized using topskip's viznodelist (gist.github.com/pgundlach/556247/…) If it not a bug there, then I would think this unnamed glue is being placed outside the last hlist? My question is what is it? I did nullify its effect by setting leftskip to node.setglue(g,0,2*65536,0,2,0) only for last line, though still don't know what's going on there. – reportaman Apr 17 '20 at 3:55
  • 1
    @reportaman It's \parfillskip (sorry for spoiling, Marcel). – Henri Menke Apr 17 '20 at 3:58
  • 1
    @reportaman No, I mean the \glue 0.0 plus 1.0fil is vertical glue in the sense that it creates a vertical distance. This is unrelated to horizontal positioning. – Marcel Krüger Apr 17 '20 at 4:30
  • 1
    That the box is still centered after you removed the parfillskip is because the box was not packed again, so the glue hasn't been reset. The leftskip doesn't "know" yet that it has to stretch more. – Marcel Krüger Apr 17 '20 at 4:32

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.