12

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} 

2 Answers 2

9

(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
  • @AndrewStacey Beware, this is a simplified version, it might have rough edges. This is what I use in production: github.com/speedata/publisher/blob/develop/src/lua/… (function mknodes()) Commented May 16, 2013 at 17:24
  • I had to apply the same changes as in tex.stackexchange.com/a/422813/73317 in order to avoid the error "You cannot set field spec in a node of type glue". Commented Apr 7, 2018 at 14:18
  • @frougon I have changed the code. Thanks for the hint! Commented Apr 14, 2018 at 19:32
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 
2
  • 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. Commented May 16, 2013 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. Commented May 16, 2013 at 15:17

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.