22

Consider the following code, using traditional TeX programming that through a series of boxing and unboxing, grabs the last line of a paragraph and centers it.

\documentclass{article}
\usepackage{lipsum}
\begin{document}
\def\centerlastline#1{%
       \bgroup
       \setbox0=\vbox{\bfseries\noindent #1}%
       \setbox1=\vbox{%
            \unvbox0
            \setbox2=\lastbox
            \hbox to \linewidth{\hfill\unhbox2 \hfill}%
       }%
       \unvbox1
      \egroup
  }%
\centerlastline{\lipsum[3]}
\end{document}

How can achieve the same using LuaTeX's access to the paragraph builder and its nodes?

  • 1
    In light of wipet's answer, perhaps clarify if the aim is the typesetting output of a centred last line or whether that is just an example of something that can be done by having access to information about the last line. – Joseph Wright Oct 16 '14 at 8:01
  • @JosephWright That was just an example to demonstrate LuaTeX technicques for accessing nodes in paragraphs. Wipet's answer however elegant it is does not answer the question. – Yiannis Lazarides Oct 16 '14 at 10:48
14
+100

After too many hours of failed attempts I got a working solution. The problem was in setting correct values to glue nodes. Documentation on this is unfortunately lacking, so I spent too much time hacking node dumps, but this lead to nowhere.

\documentclass{article}
\usepackage{lipsum}
\usepackage{luatexbase}
% we will use node attributes to start/stop processing of paragraphs. 
% This is the most flexible way to support multiple paragraphs at once
\newluatexattribute\centerattr
\directlua{%
  luatexbase.add_to_callback("post_linebreak_filter", function(head)
  local first_glyph = function(h)
        for i in node.traverse_id(37,h) do 
                return i 
           end
        end
        local get_last = function(head, id)
          local x = node.count(id, head)
          for n in node.traverse_id(id,head) do
            x = x - 1
            if x == 0 then return n end
          end
          return false
        end
        % get last line
        local n = get_last(head,0)
        local attr_num = luatexbase.attributes.centerattr
        local attr_val = node.has_attribute(n, attr_num) or 0
        if  attr_val < 1 then return head end
        % make hfill glue
        local make_hfill  = function()
           local hfill = node.new("glue")
           hfill.spec = node.new("glue_spec")
           hfill.spec.stretch = 2^15
            % set stretch_order to glue_order value of parent hlist
           hfill.spec.stretch_order = n.glue_order %3
           return hfill
        end

        local hfill = make_hfill()
        local hfill2 = make_hfill()
        % add first hfill at the beginning
        hfill.next = n.head
        local last_glyph = get_last(hfill, 10)
        hfill2.next = last_glyph.next
        % and second hfill at the end
        last_glyph.next= hfill2
    n.head = hfill 
    return head
   end, "center last line")
  }
\begin{document}
\def\luacenterlastline#1{%
       \bgroup
             \centerattr=1
       \bfseries\noindent #1%
      \egroup
}
\luacenterlastline{\lipsum[3]}

\lipsum[3]

\noindent hello world

\centerattr=1
\noindent hello world
\end{document}

I decided to use different approach than was used in Yannis's example with box processing. Node processing callbacks are used, as it enables much more flexibility. We can have process multiple paragraphs at once, or even whole document. To direct processing, node attributes are used. This is new concept in LuaTeX, we can declare the attribute and assign a value to it and all following nodes will have attribute set to this value.

Using luatexbase package, we can declare new attribute and assign a value with

\newluatexattribute\centerattr
\centerattr=1

on the Lua side, we can get attribute number and value with:

local attr_num = luatexbase.attributes.centerattr
local attr_val = node.has_attribute(n, attr_num) or 0

attributes are numbers, so we first have to get attribute number from name used on LaTeX side, and then get value of the attribute with this number of node n.

if  attr_val < 1 then return head end

with this condition we return node list unchanged if attribute is unset or when we set it to 0 manually.

Now we must decide which node callback we shall use. for processing paragraphs after linebreak, post_linebreak_filter is used. Structure of node list we can access in this callback is following:

 hlist
 glue
 hlist
 ...

hlist is a horizontal box, with child nodes accessible in head field. glue here is from \baselineskip.

It is quite easy to get the last line using this function:

        local get_last = function(head, id)
          local x = node.count(id, head)
          for n in node.traverse_id(id,head) do
            x = x - 1
            if x == 0 then return n end
          end
          return false
        end

head is node list to be processed and id is node type to be processed. So we can get last hlist with

 local n = get_last(head,0)

Now the hard thing, which I don't still ready understand and which I found after lot of experimenting. hfill type of glue:

        local make_hfill  = function()
           local hfill = node.new("glue")
           hfill.spec = node.new("glue_spec")
           hfill.spec.stretch = 2^15
           % set stretch_order to glue_order value of parent hlist
           hfill.spec.stretch_order = n.glue_order %3
           return hfill
        end

With this function, two glue nodes are created, one is added at the beginning of the node list, second at the end, so the contents will be centered.

Now I will explain problems that I had before I found this solution:

when I inspected nodes inserted with \hfill command, they had these values of spec:

 stretch = 2^16
 stretch_order = 3

when I added glue nodes with these values to the node list, nothing happened. later I found that value of stretch_order must be the same as value of glue_order of parent hlist. hlist which contained explicit \hfill had value of glue_order equal to 3, but normal hlists had glue_order set to 2, so no shrinking was applied. I don't really know why, some experts on TeX internals will hopefully explain that.

So I changed value of stretch_order to be the same as glue_order. But now the lines were flush right, ignoring second hfill node. Then I played with stretch value and found that value of 2^15 works. I don't know why doesn't work 2^16, when nodes inserted by \hfill have this value.

After realizing correct values for hfill glue, rest of the work is quite easy, just setting next pointers between nodes.

enter image description here


Old answer

I will post partial solution, because I am stuck with some problem and maybe someone will know a solution.

\documentclass{article}
\usepackage{lipsum}
\begin{document}
\def\luacenterlastline#1{%
       \bgroup
       \setbox0=\vbox{\bfseries\noindent #1}%
             \directlua{%
                local char =unicode.utf8.char
                % get contents of box 0
                local head = tex.box[0].head
                % get last hlist -- this means line
                local x = node.count(0,head)
                for n in node.traverse_id(0, head) do
                  x = x - 1
                    % if x is 0 then we are in the last line
                  if x == 0 then 
                      % make hfill node
                    local hfill = node.new("glue")
                    hfill.spec = node.new("glue_spec")
                    hfill.spec.stretch = 2^16
                    hfill.spec.stretch_order = 3
                        % make testing node, because node inserting doesn't work
                    local pok = node.new(37)
                    pok.char = 64
                    pok.next = n.head
                        % print all glyphs on a line
                        % note the @ at the beginning. our test node is printed
                        for c in  node.traverse_id(37,pok) do
                          print(char(c.char))
                    end
                        n.head = pok 
                        % this doesn't work neither
                    %n.head = node.insert_before(n.head,n.head,pok)
                    end
                end
                tex.box[0].head = head
             }
             \unvbox0
      \egroup
}
\luacenterlastline{\lipsum[3]}
\end{document}

Contents of boxes is accessible with tex.box function. Node list is in head field. In this case, there are several nodes with id 0, which are hlist, nodes broken into lines. We cound these nodes and loop over them, processing is activated only in the last hlist. New node of type glue is created, with stretch and stretch_order fields set to maximal stretchability. Then I tried to insert this node at the beginning of the current hlist and this is where I am stuck, because it doesn't show in the document.

I am sure that this is some stupid problem on my side, but I was unable to solve it. For test, I added another node of glyph type, which should add @ char into the hlist and it doesn't work either. Strangely, if I print contents of the hlist, @ character is printed on the terminal, so this glyph is part of new hlist, but it doesn't find its way to the node lists used in the final document. I am sure that this is just some stupid mistake, but I should go sleep now, so maybe I will figure that tommorow.

  • Should it be print or tex.print? – Yiannis Lazarides Oct 16 '14 at 10:53
  • @YiannisLazarides tex.print output text to the document, I wanted to print to the terminal, as it is just debugging output – michal.h21 Oct 16 '14 at 10:56
  • Thanks, I wasn't sure. Hope you can finish it. This is the type of answer I am looking for. What is pok? – Yiannis Lazarides Oct 16 '14 at 11:01
  • @YiannisLazarides its abbreviation of pokus, which means trial in Czech. it is just meant for debugging, because I spent 3 hours trying to update node list without success yesterday – michal.h21 Oct 16 '14 at 11:08
  • 1
    @YiannisLazarides I added some information, but lot o questions is still unanswered, as I've found the solution with trials – michal.h21 Oct 21 '14 at 19:42
16

enter image description here

\documentclass{article}
\usepackage{lipsum}
\begin{document}


\directlua{function last_line(h)
ll=node.slide(h)
newh = node.remove(h,ll)
callback.register("post_linebreak_filter",nil)
tex.setbox(0,ll)
return h
end
}
\directlua{callback.register("post_linebreak_filter", last_line)}
\lipsum[3]


\begin{center}
\fbox{\unhbox0}
\end{center}
\end{document}
  • Dave wouldn't it be a bit more elegant to add the glue in the box before returning h, rather that using center afterwards? – Yiannis Lazarides Oct 21 '14 at 2:26
  • @YiannisLazarides perhaps, but I figured you could do that bit:-) also as you indicated that the main issue was getting the last line and centering was just an example, I thought returning the (original) last line in a box was the most flexible, the following center with the boxed environment is just a toy demo separate from the lua filter which might have practical use. I thought that you might for example want to measure the last line or colour or whatever and it's easier to do that if you pass it back than if you hard code the modification into the filter – David Carlisle Oct 21 '14 at 8:35
11

You can use traditional TeX programming in LuaTeX too. An you don't need to use the boxing and unboxing for your task. You can try:

\def\centerlastline#1{\par
   {\leftskip=0pt plus1fil \rightskip=0pt plus-1fil \parfillskip=0pt plus2fil
   \noindent #1\par}%
}

\centerlastline{\lipsum[3]}          

enter image description here

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.