10

I'm playing around with luatex and learning about nodes and callbacks. What I'd like to do is be able to add some text via a particular callback. The type of callback that I'm interested in is one where the input is a node list (think hpack_filter) so I want to append the text in the form of a node list. This is simple enough if I have my text to append already stored as a node list (say stuff):

function appendStuff(h)
   local l = node.tail(h)
   l.next = node.copy_list(stuff)
   return h
end

The difficulty is in populating that node list in the first place.

What I'd like to do (because I'm lazy) is to specify a string, say hello world, pass it to TeX to convert it to a node list, and then save the resulting node list. I've figured out how to do this from the TeX side, see code below. What I'd like to do is do it entirely from the lua side. I realise that TeX will have to be involved at some point, but is there a proper way to do this? At the moment, my best guess is to have lua issue the relevant TeX commands via a tex.print(). Using hpack_filter as my callback (is there a better one?) I would have lua install the callback, issue the TeX command to create a box, then uninstall the callback (and, for cleanliness, destroy the box). That just feels cludgey. Is there a better way to accomplish this? If it helps, I can arrange it so that my strings don't need expansion (though, of course, it would be better if that was allowed).

Here's some code to play with:

\documentclass{article}
\usepackage{filecontents}
\usepackage{luatexbase}

\begin{filecontents*}{texttonodes.lua}
local stuff

function saveNodeList(h)
   print("Saving box")
   stuff = h
   luatexbase.remove_from_callback('hpack_filter',"Save a box")
end

function saveNextBox()
    luatexbase.add_to_callback ( 'hpack_filter', saveNodeList, "Save a box" )
end

function useLastBox()
    luatexbase.add_to_callback ( 'hpack_filter', useNodeList, "Use a box" )
end

function useNodeList(h)
   if stuff then
      local l = node.tail(h)
      l.next = node.copy_list(stuff)
   end
   luatexbase.remove_from_callback('hpack_filter',"Use a box")
   return h
end

\end{filecontents*}

\directlua{dofile('texttonodes.lua')}

\newbox\mybox

\newcommand\savetext[1]{%
  \directlua{saveNextBox()}%
  \setbox\mybox=\hbox{#1}%
}
\newcommand\usetext[1]{%
  \directlua{useLastBox()}%
  \setbox\mybox=\hbox{#1}%
  \unhbox\mybox
}

\begin{document}

\savetext{hello world}

\usetext{goodbye earth, }

\end{document}
8

(This is taken from the LuaTeX wiki and updated for LuaTeX shipped with tl2016/17/18/...?)

This look difficult, but it isn't. Well, it is, I don't admit it.

\documentclass{article}
\usepackage{luacode}
\usepackage{libertine}
\begin{document}
\begin{luacode*}
function newglue(parameters)
    local g = node.new("glue")
    local tmp_spec
    if node.has_field(g,"spec") then
        g.spec = node.new("glue_spec")
        tmp_spec = g.spec
    else
        tmp_spec = g
    end
    for k,v in pairs(parameters) do
        tmp_spec[k] = v
    end
    return g
end

function mknodes( text )
  local current_font = font.current()
  local font_parameters = font.getfont(current_font).parameters
  local n, head, last
  -- we should insert the paragraph indentation at the beginning
  head = newglue({width = 20 * 2^16})
  last = head

  for s in string.utfvalues( text ) do
    local char = unicode.utf8.char(s)
    if unicode.utf8.match(char,"%s") then
      -- its a space
      n = newglue({width = font_parameters.space,shrink  = font_parameters.space_shrink, stretch = font_parameters.space_stretch})
    else -- a glyph
      n = node.new("glyph")
      n.font = current_font
      n.subtype = 1
      n.char = s
      n.lang = tex.language
      n.uchyph = 1
      n.left = tex.lefthyphenmin
      n.right = tex.righthyphenmin
    end

    last.next = n
    last = n
  end

  -- now add the final parts: a penalty and the parfillskip glue
  local penalty = node.new("penalty")
  penalty.penalty = 10000

  local parfillskip = newglue({stretch = 2^16,stretch_order = 2})

  last.next = penalty
  penalty.next = parfillskip

  -- just to create the prev pointers for tex.linebreak
  node.slide(head)
  return head
end

local txt = "A wonderful serenity has taken possession of my entire soul, like these sweet mornings of spring which I enjoy with my whole heart. I am alone, and feel the charm of existence in this spot, which was created for the bliss of souls like mine."

tex.baselineskip = newglue({width = 14 * 2^16})

local head = mknodes(txt)
lang.hyphenate(head)
head = node.kerning(head)
head = node.ligaturing(head)

local vbox = tex.linebreak(head,{ hsize = tex.sp("3in")})
node.write(vbox)
\end{luacode*}
\end{document}

I create glyph nodes manually, add some glue and call the internal TeX functions for ligaturing, keming and linebreaking.

The result is a vbox which I write directly into TeX's current list, but you could do other things with it.

resulting paragraph

3

run with luatex. It creates a file xlist2.nodes:

  0    ->nil (hlist->nil)
8,6    ->  0 (whatsit->hlist)
  0    -> 37 (hlist->glyph)
 37"h" -> 37 (glyph->glyph)
 37"e" -> 37 (glyph->glyph)
 37"l" -> 37 (glyph->glyph)
 37"l" -> 37 (glyph->glyph)
 37"o" -> 10 (glyph->glue)
 10    -> 37 (glue->glyph)
 37"w" -> 11 (glyph->kern)
 11    -> 37 (kern->glyph)
 37"o" -> 37 (glyph->glyph)
 37"r" -> 37 (glyph->glyph)
 37"l" -> 37 (glyph->glyph)
 37"d" -> 12 (glyph->penalty)
 12    -> 10 (penalty->glue)
 10    -> 10 (glue->glue)
 10    ->nil (glue->nil)


\nopagenumbers
\begingroup
\catcode`\%=12
\directlua{local out=assert(io.open("xlist2.nodes","w"))
local n=node.types()
function printnode(head)
while head do
  if head.id==8 then out:write(head.id..","..head.subtype)
                else out:write(string.format("%3d",head.id))
  end
  if head.id==37 then
    out:write("\string\"",string.char(head.char),"\string\" ->")
  else out:write("\space\space\space\space->")
  end
  if head.next==nil then out:write("nil")
                    else out:write(string.format("%3d",head.next.id))
  end
  out:write(" ("..n[head.id].."->")
  if head.next==nil then out:write("nil)\string\n")
  else out:write(n[head.next.id]..")\string\n")
  end
  if head.id==0 or head.id==1 then printnode(head.head) end
  head=head.next
end
return true
end
callback.register("post_linebreak_filter",printnode,"printnode")}
\endgroup
hello world
\bye
  • This gets me the list of nodes as an external file. I guess you meant for me to be able to read those back in via lua to recreate the node list so that I could use it in my lua code. This isn't practical in my particular case as I want to build up a large list of these lists to manipulate. However, as I'm new to luatex then I'll learn from looking at your code so thank you. – Loop Space May 16 '13 at 14:32
  • no, it is only an example how one can get the nodes. Instead of writing it into an external file you can use tex.print to write it into TeX's source. – user2478 May 16 '13 at 15:17

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.