If the caption is short enough to fit into a single line (i.e. no line
breaking is needed) the processing of the caption does not generate any
callbacks.
As explained by /u/egreg in a comment,
the difference is that in some cases, no paragraph is created.
For this reason the line break algorithm is not invoked on the
horizontal list;
consequently, pre_linebreak_filter
callback is not triggered.
You can observe this difference by putting text into a hbox or vbox:
\hbox {Horizontal list only, without line break.} \par
\vbox {Vertical list, triggers the line break mechanism.} \par
Only the second line is a affected by line breaking.
Is this behavior intended, which would be in contrast to other
single-line content, such as title or author lines, which DO generate
these callbacks?
The behavior is indeed intended:
LuaTeX offers another callback -- the hpack_filter
-- that is
invoked whenever TeX packages an hbox.
You can use it the same way as the pre_linebreak_filter
,
although the callback receives further arguments on the Lua side that
contain information about the state TeX is presently in.
To illustrate the problem I add some example code (tested with Luatex
0.78.2).
First the Lua file; its file name should differ from the TeX code below
only in the extension:
local traverse_nodes = node.traverse
local iowrite = io.write
local tableconcat = table.concat
local utfchar = unicode.utf8.char
local nodecodes = table.mirrored (node.types())
local glyph_t = nodecodes.glyph
local glue_t = nodecodes.glue
local cbk = function (hd, group, size, pack)
--[[--
Demo function collecting text from glyph nodes in the list starting
with ``hd``, printing them to stdout.
--]]--
local text = {
"\n["
.. (group == "" and "par" or group)
--- the line below shows more info from the hpack_filter context
--.. (size and (":" .. tostring (size) .. ":" .. pack) or "")
.. "] "
}
for n in traverse_nodes (hd) do
local ntype = n.id
if ntype == glyph_t then
text [#text + 1] = utfchar (n.char)
elseif ntype == glue_t then
text [#text + 1] = " "
else -- print node type
text [#text + 1] = "<" .. nodecodes [ntype] .. ":" .. tostring (n.subtype) .. ">"
end
end
text [#text + 1] = "\n"
iowrite (tableconcat (text))
return true
end
luatexbase.add_to_callback ("pre_linebreak_filter", cbk, "document.nodeprinter")
luatexbase.add_to_callback ("hpack_filter", cbk, "document.nodeprinter")
And some Latex code that shows different cases:
\documentclass {scrartcl}
\usepackage {luatexbase}
\RequireLuaModule {lualibs}
\directlua {dofile "\jobname.lua"}
\begin {document}
\hbox {Horizontal list only, without line break.} \par
\vbox {Vertical list, triggers the line break mechanism.} \par
\begin {figure}
\input ward
\caption {Some brief caption.} %% same as with the hbox above
\end {figure}
This is some \hbox{ordinary} paragraph material.
\end {document}
You can also clone the code conveniently bundled as
a gist.
Ordinarily, when processing text nodes you want to register your
function in both callbacks.
There are some caveats, though.
The main problem affects the order of execution:
The hpack_filter
s are called in a separate step prior to handing
over the paragraph to the line breaking mechanism.
For instance, in the following snippet:
This is some \hbox{ordinary} paragraph material.
the hbox containing the text “ordinary” is processed in the
hpack_filter
before the rest of the text is passed to the
pre_linebreak_filter
.
Depending on what you want to achieve, this can end up in a mess.
The solution to this problem is to not rely on the hpack_filter
and
traverse the node lists recursively, e. g.:
local traverse_nodes = node.traverse
local iowrite = io.write
local stringformat = string.format
local stringrep = string.rep
local nodelength = node.length
local count_nodes = node.count
local nodecodes = table.mirrored (node.types())
local hlist_t = nodecodes.hlist
local vlist_t = nodecodes.vlist
local rec_cbk rec_cbk = function (hd, _, depth, where)
--[[--
Recursively walk a paragraph node list and print basic info.
--]]--
if not depth then
iowrite "\n"
depth = 1
where = "par"
end
iowrite (stringrep (" ", depth) ..
stringformat ("[%s:%d] %d nodes, %d glyphs\n",
where, depth,
nodelength (hd),
count_nodes (glyph_t, hd)))
for n in traverse_nodes (hd) do
local ntype = n.id
if ntype == vlist_t then
rec_cbk (n.head, nil, depth + 1, "vbox")
elseif ntype == hlist_t then
rec_cbk (n.head, nil, depth + 1, "hbox")
--- else pass
end
end
return true
end
luatexbase.add_to_callback ("pre_linebreak_filter", rec_cbk, "document.recursenodes")
Use this code as above and typeset something slightly more complex:
This is some \vbox {\hbox {significantly}
\hbox {less}
\hbox {ordinary}} paragraph material.
The hbox’ed and vbox’ed material will now be encountered in its natural
order.
Unfortunately this is not an option if you want to process the float
captions.
Also, the combined use of pre_linebreak_filter
and hpack_filter
usually results in better performance, though.
callback
s' code? Thanks.\@makecaption
in thearticle
class doesn't make TeX into starting a paragraph when the caption fits in one line, because it does\hbox to \hsize{\hfil\box\@tempboxa\hfil}
(where\@tempboxa
stores the caption text).\hbox{...}
in vertical mode, no paragraph is started. I'm sure there's a suitable callback that applies also in this case and this one you should use; or you should change\@makecaption
so that it makes a paragraph also in the “fit in one line” case, say{\centering\unhbox\@tempboxa\par}
.pre_linebreak_filter
and thehpack_filter
callback. This combination is quite common, for instance in Context, both are fused into themvlbuilders
node processor and not even available separately unless you resort to Dirty Tricks™.