• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sile-typesetter / sile / 9304060604

30 May 2024 02:07PM UTC coverage: 74.124% (-0.6%) from 74.707%
9304060604

push

github

alerque
style: Reformat Lua with stylua

8104 of 11995 new or added lines in 184 files covered. (67.56%)

15 existing lines in 11 files now uncovered.

12444 of 16788 relevant lines covered (74.12%)

7175.1 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.25
/typesetters/base.lua
1
--- SILE typesetter (default/base) class.
2
--
3
-- @copyright License: MIT
4
-- @module typesetters.base
5
--
6

7
-- Typesetter base class
8

9
local typesetter = pl.class()
181✔
10
typesetter.type = "typesetter"
181✔
11
typesetter._name = "base"
181✔
12

13
-- This is the default typesetter. You are, of course, welcome to create your own.
14
local awful_bad = 1073741823
181✔
15
local inf_bad = 10000
181✔
16
-- local eject_penalty = -inf_bad
17
local supereject_penalty = 2 * -inf_bad
181✔
18
-- local deplorable = 100000
19

20
-- Local helper class to compare pairs of margins
21
local _margins = pl.class({
362✔
22
   lskip = SILE.nodefactory.glue(),
362✔
23
   rskip = SILE.nodefactory.glue(),
362✔
24

25
   _init = function (self, lskip, rskip)
26
      self.lskip, self.rskip = lskip, rskip
3,460✔
27
   end,
28

29
   __eq = function (self, other)
30
      return self.lskip.width == other.lskip.width and self.rskip.width == other.rskip.width
5✔
31
   end,
32
})
33

34
local warned = false
181✔
35

36
function typesetter:init (frame)
181✔
NEW
37
   SU.deprecated("std.object", "pl.class", "0.13.0", "0.14.0", warned and "" or [[
×
38
  The typesetter instance inheritance system for instances has been
39
  refactored using a different object model. Your instance was created
40
  and initialized using the object copy syntax from the stdlib model.
41
  It has been shimmed for you using the new Penlight model, but this may
42
  lead to unexpected behaviour. Please update your code to use the new
43
  Penlight based inheritance model.]])
×
NEW
44
   warned = true
×
NEW
45
   self:_init(frame)
×
46
end
47

48
function typesetter:_init (frame)
181✔
49
   self:declareSettings()
316✔
50
   self.hooks = {}
316✔
51
   self.breadcrumbs = SU.breadcrumbs()
632✔
52

53
   self.frame = nil
316✔
54
   self.stateQueue = {}
316✔
55
   self:initFrame(frame)
316✔
56
   self:initState()
316✔
57
   -- In case people use stdlib prototype syntax off of the instantiated typesetter...
58
   getmetatable(self).__call = self.init
316✔
59
   return self
316✔
60
end
61

62
function typesetter.declareSettings (_)
181✔
63
   -- Settings common to any typesetter instance.
64
   -- These shouldn't be re-declared and overwritten/reset in the typesetter
65
   -- constructor (see issue https://github.com/sile-typesetter/sile/issues/1708).
66
   -- On the other hand, it's fairly acceptable to have them made global:
67
   -- Any derived typesetter, whatever its implementation, should likely provide
68
   -- some logic for them (= widows, orphans, spacing, etc.)
69

70
   SILE.settings:declare({
316✔
71
      parameter = "typesetter.widowpenalty",
72
      type = "integer",
73
      default = 3000,
74
      help = "Penalty to be applied to widow lines (at the start of a paragraph)",
75
   })
76

77
   SILE.settings:declare({
316✔
78
      parameter = "typesetter.parseppattern",
79
      type = "string or integer",
80
      default = "\r?\n[\r\n]+",
81
      help = "Lua pattern used to separate paragraphs",
82
   })
83

84
   SILE.settings:declare({
316✔
85
      parameter = "typesetter.obeyspaces",
86
      type = "boolean or nil",
87
      default = nil,
88
      help = "Whether to ignore paragraph initial spaces",
89
   })
90

91
   SILE.settings:declare({
316✔
92
      parameter = "typesetter.orphanpenalty",
93
      type = "integer",
94
      default = 3000,
95
      help = "Penalty to be applied to orphan lines (at the end of a paragraph)",
96
   })
97

98
   SILE.settings:declare({
632✔
99
      parameter = "typesetter.parfillskip",
100
      type = "glue",
101
      default = SILE.nodefactory.glue("0pt plus 10000pt"),
632✔
102
      help = "Glue added at the end of a paragraph",
103
   })
104

105
   SILE.settings:declare({
316✔
106
      parameter = "document.letterspaceglue",
107
      type = "glue or nil",
108
      default = nil,
109
      help = "Glue added between tokens",
110
   })
111

112
   SILE.settings:declare({
632✔
113
      parameter = "typesetter.underfulltolerance",
114
      type = "length or nil",
115
      default = SILE.length("1em"),
632✔
116
      help = "Amount a page can be underfull without warning",
117
   })
118

119
   SILE.settings:declare({
632✔
120
      parameter = "typesetter.overfulltolerance",
121
      type = "length or nil",
122
      default = SILE.length("5pt"),
632✔
123
      help = "Amount a page can be overfull without warning",
124
   })
125

126
   SILE.settings:declare({
316✔
127
      parameter = "typesetter.breakwidth",
128
      type = "measurement or nil",
129
      default = nil,
130
      help = "Width to break lines at",
131
   })
132

133
   SILE.settings:declare({
316✔
134
      parameter = "typesetter.softHyphen",
135
      type = "boolean",
136
      default = true,
137
      help = "When true, soft hyphens are rendered as discretionary breaks, otherwise they are ignored",
138
   })
139

140
   SILE.settings:declare({
316✔
141
      parameter = "typesetter.softHyphenWarning",
142
      type = "boolean",
143
      default = false,
144
      help = "When true, a warning is issued when a soft hyphen is encountered",
145
   })
146

147
   SILE.settings:declare({
316✔
148
      parameter = "typesetter.fixedSpacingAfterInitialEmdash",
149
      type = "boolean",
150
      default = true,
151
      help = "When true, em-dash starting a paragraph is considered as a speaker change in a dialogue",
152
   })
153
end
154

155
function typesetter:initState ()
181✔
156
   self.state = {
371✔
157
      nodes = {},
371✔
158
      outputQueue = {},
371✔
159
      lastBadness = awful_bad,
371✔
160
   }
371✔
161
end
162

163
function typesetter:initFrame (frame)
181✔
164
   if frame then
512✔
165
      self.frame = frame
488✔
166
      self.frame:init(self)
488✔
167
   end
168
end
169

170
function typesetter.getMargins ()
181✔
171
   return _margins(SILE.settings:get("document.lskip"), SILE.settings:get("document.rskip"))
10,380✔
172
end
173

174
function typesetter.setMargins (_, margins)
181✔
175
   SILE.settings:set("document.lskip", margins.lskip)
2✔
176
   SILE.settings:set("document.rskip", margins.rskip)
2✔
177
end
178

179
function typesetter:pushState ()
181✔
180
   self.stateQueue[#self.stateQueue + 1] = self.state
55✔
181
   self:initState()
55✔
182
end
183

184
function typesetter:popState (ncount)
181✔
185
   local offset = ncount and #self.stateQueue - ncount or nil
55✔
186
   self.state = table.remove(self.stateQueue, offset)
110✔
187
   if not self.state then
55✔
NEW
188
      SU.error("Typesetter state queue empty")
×
189
   end
190
end
191

192
function typesetter:isQueueEmpty ()
181✔
193
   if not self.state then
2,504✔
NEW
194
      return nil
×
195
   end
196
   return #self.state.nodes == 0 and #self.state.outputQueue == 0
2,504✔
197
end
198

199
function typesetter:vmode ()
181✔
200
   return #self.state.nodes == 0
444✔
201
end
202

203
function typesetter:debugState ()
181✔
NEW
204
   print("\n---\nI am in " .. (self:vmode() and "vertical" or "horizontal") .. " mode")
×
NEW
205
   print("Writing into " .. tostring(self.frame))
×
NEW
206
   print("Recent contributions: ")
×
NEW
207
   for i = 1, #self.state.nodes do
×
NEW
208
      io.stderr:write(self.state.nodes[i] .. " ")
×
209
   end
NEW
210
   print("\nVertical list: ")
×
NEW
211
   for i = 1, #self.state.outputQueue do
×
NEW
212
      print("  " .. self.state.outputQueue[i])
×
213
   end
214
end
215

216
-- Boxy stuff
217
function typesetter:pushHorizontal (node)
181✔
218
   self:initline()
5,555✔
219
   self.state.nodes[#self.state.nodes + 1] = node
5,555✔
220
   return node
5,555✔
221
end
222

223
function typesetter:pushVertical (vbox)
181✔
224
   self.state.outputQueue[#self.state.outputQueue + 1] = vbox
4,972✔
225
   return vbox
4,972✔
226
end
227

228
function typesetter:pushHbox (spec)
181✔
229
   -- if SU.type(spec) ~= "table" then SU.warn("Please use pushHorizontal() to pass a premade node instead of a spec") end
230
   local ntype = SU.type(spec)
284✔
231
   local node = (ntype == "hbox" or ntype == "zerohbox") and spec or SILE.nodefactory.hbox(spec)
284✔
232
   return self:pushHorizontal(node)
284✔
233
end
234

235
function typesetter:pushUnshaped (spec)
181✔
236
   -- if SU.type(spec) ~= "table" then SU.warn("Please use pushHorizontal() to pass a premade node instead of a spec") end
237
   local node = SU.type(spec) == "unshaped" and spec or SILE.nodefactory.unshaped(spec)
3,190✔
238
   return self:pushHorizontal(node)
1,595✔
239
end
240

241
function typesetter:pushGlue (spec)
181✔
242
   -- if SU.type(spec) ~= "table" then SU.warn("Please use pushHorizontal() to pass a premade node instead of a spec") end
243
   local node = SU.type(spec) == "glue" and spec or SILE.nodefactory.glue(spec)
3,554✔
244
   return self:pushHorizontal(node)
1,777✔
245
end
246

247
function typesetter:pushExplicitGlue (spec)
181✔
248
   -- if SU.type(spec) ~= "table" then SU.warn("Please use pushHorizontal() to pass a premade node instead of a spec") end
249
   local node = SU.type(spec) == "glue" and spec or SILE.nodefactory.glue(spec)
168✔
250
   node.explicit = true
84✔
251
   node.discardable = false
84✔
252
   return self:pushHorizontal(node)
84✔
253
end
254

255
function typesetter:pushPenalty (spec)
181✔
256
   -- if SU.type(spec) ~= "table" then SU.warn("Please use pushHorizontal() to pass a premade node instead of a spec") end
257
   local node = SU.type(spec) == "penalty" and spec or SILE.nodefactory.penalty(spec)
1,828✔
258
   return self:pushHorizontal(node)
914✔
259
end
260

261
function typesetter:pushMigratingMaterial (material)
181✔
262
   local node = SILE.nodefactory.migrating({ material = material })
86✔
263
   return self:pushHorizontal(node)
86✔
264
end
265

266
function typesetter:pushVbox (spec)
181✔
267
   -- if SU.type(spec) ~= "table" then SU.warn("Please use pushVertical() to pass a premade node instead of a spec") end
268
   local node = SU.type(spec) == "vbox" and spec or SILE.nodefactory.vbox(spec)
4✔
269
   return self:pushVertical(node)
2✔
270
end
271

272
function typesetter:pushVglue (spec)
181✔
273
   -- if SU.type(spec) ~= "table" then SU.warn("Please use pushVertical() to pass a premade node instead of a spec") end
274
   local node = SU.type(spec) == "vglue" and spec or SILE.nodefactory.vglue(spec)
1,666✔
275
   return self:pushVertical(node)
833✔
276
end
277

278
function typesetter:pushExplicitVglue (spec)
181✔
279
   -- if SU.type(spec) ~= "table" then SU.warn("Please use pushVertical() to pass a premade node instead of a spec") end
280
   local node = SU.type(spec) == "vglue" and spec or SILE.nodefactory.vglue(spec)
1,044✔
281
   node.explicit = true
522✔
282
   node.discardable = false
522✔
283
   return self:pushVertical(node)
522✔
284
end
285

286
function typesetter:pushVpenalty (spec)
181✔
287
   -- if SU.type(spec) ~= "table" then SU.warn("Please use pushVertical() to pass a premade node instead of a spec") end
288
   local node = SU.type(spec) == "penalty" and spec or SILE.nodefactory.penalty(spec)
454✔
289
   return self:pushVertical(node)
227✔
290
end
291

292
-- Actual typesetting functions
293
function typesetter:typeset (text)
181✔
294
   text = tostring(text)
2,683✔
295
   if text:match("^%\r?\n$") then
2,683✔
296
      return
827✔
297
   end
298
   local pId = SILE.traceStack:pushText(text)
1,856✔
299
   for token in SU.gtoke(text, SILE.settings:get("typesetter.parseppattern")) do
7,635✔
300
      if token.separator then
2,067✔
301
         self:endline()
948✔
302
      else
303
         if SILE.settings:get("typesetter.softHyphen") then
3,186✔
304
            local warnedshy = false
1,592✔
305
            for token2 in SU.gtoke(token.string, luautf8.char(0x00AD)) do
4,806✔
306
               if token2.separator then -- soft hyphen support
1,622✔
307
                  local discretionary = SILE.nodefactory.discretionary({})
15✔
308
                  local hbox = SILE.typesetter:makeHbox({ SILE.settings:get("font.hyphenchar") })
30✔
309
                  discretionary.prebreak = { hbox }
15✔
310
                  table.insert(SILE.typesetter.state.nodes, discretionary)
15✔
311
                  if not warnedshy and SILE.settings:get("typesetter.softHyphenWarning") then
16✔
NEW
312
                     SU.warn("Soft hyphen encountered and replaced with discretionary")
×
313
                  end
314
                  warnedshy = true
15✔
315
               else
316
                  self:setpar(token2.string)
1,607✔
317
               end
318
            end
319
         else
320
            if
321
               SILE.settings:get("typesetter.softHyphenWarning") and luautf8.match(token.string, luautf8.char(0x00AD))
2✔
322
            then
NEW
323
               SU.warn("Soft hyphen encountered and ignored")
×
324
            end
325
            text = luautf8.gsub(token.string, luautf8.char(0x00AD), "")
1✔
326
            self:setpar(text)
1✔
327
         end
328
      end
329
   end
330
   SILE.traceStack:pop(pId)
1,856✔
331
end
332

333
function typesetter:initline ()
181✔
334
   if self.state.hmodeOnly then
6,292✔
335
      return
455✔
336
   end -- https://github.com/sile-typesetter/sile/issues/1718
337
   if #self.state.nodes == 0 then
5,837✔
338
      self.state.nodes[#self.state.nodes + 1] = SILE.nodefactory.zerohbox()
1,706✔
339
      SILE.documentState.documentClass.newPar(self)
853✔
340
   end
341
end
342

343
function typesetter:endline ()
181✔
344
   self:leaveHmode()
836✔
345
   SILE.documentState.documentClass.endPar(self)
836✔
346
end
347

348
-- Just compute once, to avoid unicode characters in source code.
349
local speakerChangePattern = "^"
×
350
   .. luautf8.char(0x2014) -- emdash
181✔
NEW
351
   .. "[ "
×
352
   .. luautf8.char(0x00A0)
181✔
353
   .. luautf8.char(0x202F) -- regular space or NBSP or NNBSP
181✔
354
   .. "]+"
181✔
355
local speakerChangeReplacement = luautf8.char(0x2014) .. " "
181✔
356

357
-- Special unshaped node subclass to handle space after a speaker change in dialogues
358
-- introduced by an em-dash.
359
local speakerChangeNode = pl.class(SILE.nodefactory.unshaped)
181✔
360
function speakerChangeNode:shape ()
181✔
361
   local node = self._base.shape(self)
3✔
362
   local spc = node[2]
3✔
363
   if spc and spc.is_glue then
3✔
364
      -- Switch the variable space glue to a fixed kern
365
      node[2] = SILE.nodefactory.kern({ width = spc.width.length })
6✔
366
      node[2].parent = self.parent
3✔
367
   else
368
      -- Should not occur:
369
      -- How could it possibly be shaped differently?
NEW
370
      SU.warn("Speaker change logic met an unexpected case, this might be a bug.")
×
371
   end
372
   return node
3✔
373
end
374

375
-- Takes string, writes onto self.state.nodes
376
function typesetter:setpar (text)
181✔
377
   text = text:gsub("\r?\n", " "):gsub("\t", " ")
1,621✔
378
   if #self.state.nodes == 0 then
1,621✔
379
      if not SILE.settings:get("typesetter.obeyspaces") then
1,474✔
380
         text = text:gsub("^%s+", "")
727✔
381
      end
382
      self:initline()
737✔
383

384
      if
385
         SILE.settings:get("typesetter.fixedSpacingAfterInitialEmdash")
737✔
386
         and not SILE.settings:get("typesetter.obeyspaces")
1,473✔
387
      then
388
         local speakerChange = false
726✔
389
         local dialogue = luautf8.gsub(text, speakerChangePattern, function ()
1,452✔
390
            speakerChange = true
3✔
391
            return speakerChangeReplacement
3✔
392
         end)
393
         if speakerChange then
726✔
394
            local node = speakerChangeNode({ text = dialogue, options = SILE.font.loadDefaults({}) })
6✔
395
            self:pushHorizontal(node)
3✔
396
            return -- done here: speaker change space handling is done after nnode shaping
3✔
397
         end
398
      end
399
   end
400
   if #text > 0 then
1,618✔
401
      self:pushUnshaped({ text = text, options = SILE.font.loadDefaults({}) })
3,190✔
402
   end
403
end
404

405
function typesetter:breakIntoLines (nodelist, breakWidth)
181✔
406
   self:shapeAllNodes(nodelist)
856✔
407
   local breakpoints = SILE.linebreak:doBreak(nodelist, breakWidth)
856✔
408
   return self:breakpointsToLines(breakpoints)
856✔
409
end
410

411
function typesetter.shapeAllNodes (_, nodelist)
181✔
412
   local newNl = {}
2,553✔
413
   for i = 1, #nodelist do
45,167✔
414
      if nodelist[i].is_unshaped then
42,614✔
415
         pl.tablex.insertvalues(newNl, nodelist[i]:shape())
3,918✔
416
      else
417
         newNl[#newNl + 1] = nodelist[i]
41,308✔
418
      end
419
   end
420
   for i = 1, #newNl do
59,246✔
421
      nodelist[i] = newNl[i]
56,693✔
422
   end
423
   if #nodelist > #newNl then
2,553✔
NEW
424
      for i = #newNl + 1, #nodelist do
×
NEW
425
         nodelist[i] = nil
×
426
      end
427
   end
428
end
429

430
-- Empties self.state.nodes, breaks into lines, puts lines into vbox, adds vbox to
431
-- Turns a node list into a list of vboxes
432
function typesetter:boxUpNodes ()
181✔
433
   local nodelist = self.state.nodes
1,971✔
434
   if #nodelist == 0 then
1,971✔
435
      return {}
1,115✔
436
   end
437
   for j = #nodelist, 1, -1 do
1,050✔
438
      if not nodelist[j].is_migrating then
1,061✔
439
         if nodelist[j].discardable then
1,016✔
440
            table.remove(nodelist, j)
320✔
441
         else
442
            break
443
         end
444
      end
445
   end
446
   while #nodelist > 0 and nodelist[1].is_penalty do
856✔
NEW
447
      table.remove(nodelist, 1)
×
448
   end
449
   if #nodelist == 0 then
856✔
NEW
450
      return {}
×
451
   end
452
   self:shapeAllNodes(nodelist)
856✔
453
   local parfillskip = SILE.settings:get("typesetter.parfillskip")
856✔
454
   parfillskip.discardable = false
856✔
455
   self:pushGlue(parfillskip)
856✔
456
   self:pushPenalty(-inf_bad)
856✔
457
   SU.debug("typesetter", function ()
1,712✔
NEW
458
      return "Boxed up " .. (#nodelist > 500 and #nodelist .. " nodes" or SU.contentToString(nodelist))
×
459
   end)
460
   local breakWidth = SILE.settings:get("typesetter.breakwidth") or self.frame:getLineWidth()
1,712✔
461
   local lines = self:breakIntoLines(nodelist, breakWidth)
856✔
462
   local vboxes = {}
856✔
463
   for index = 1, #lines do
2,336✔
464
      local line = lines[index]
1,480✔
465
      local migrating = {}
1,480✔
466
      -- Move any migrating material
467
      local nodes = {}
1,480✔
468
      for i = 1, #line.nodes do
33,546✔
469
         local node = line.nodes[i]
32,066✔
470
         if node.is_migrating then
32,066✔
471
            for j = 1, #node.material do
172✔
472
               migrating[#migrating + 1] = node.material[j]
86✔
473
            end
474
         else
475
            nodes[#nodes + 1] = node
31,980✔
476
         end
477
      end
478
      local vbox = SILE.nodefactory.vbox({ nodes = nodes, ratio = line.ratio })
1,480✔
479
      local pageBreakPenalty = 0
1,480✔
480
      if #lines > 1 and index == 1 then
1,480✔
481
         pageBreakPenalty = SILE.settings:get("typesetter.widowpenalty")
384✔
482
      elseif #lines > 1 and index == (#lines - 1) then
1,288✔
483
         pageBreakPenalty = SILE.settings:get("typesetter.orphanpenalty")
230✔
484
      end
485
      vboxes[#vboxes + 1] = self:leadingFor(vbox, self.state.previousVbox)
2,960✔
486
      vboxes[#vboxes + 1] = vbox
1,480✔
487
      for i = 1, #migrating do
1,566✔
488
         vboxes[#vboxes + 1] = migrating[i]
86✔
489
      end
490
      self.state.previousVbox = vbox
1,480✔
491
      if pageBreakPenalty > 0 then
1,480✔
492
         SU.debug("typesetter", "adding penalty of", pageBreakPenalty, "after", vbox)
307✔
493
         vboxes[#vboxes + 1] = SILE.nodefactory.penalty(pageBreakPenalty)
614✔
494
      end
495
   end
496
   return vboxes
856✔
497
end
498

499
function typesetter.pageTarget (_)
181✔
NEW
500
   SU.deprecated("SILE.typesetter:pageTarget", "SILE.typesetter:getTargetLength", "0.13.0", "0.14.0")
×
501
end
502

503
function typesetter:getTargetLength ()
181✔
504
   return self.frame:getTargetLength()
1,959✔
505
end
506

507
function typesetter:registerHook (category, func)
181✔
508
   if not self.hooks[category] then
263✔
509
      self.hooks[category] = {}
220✔
510
   end
511
   table.insert(self.hooks[category], func)
263✔
512
end
513

514
function typesetter:runHooks (category, data)
181✔
515
   if not self.hooks[category] then
2,018✔
516
      return data
1,712✔
517
   end
518
   for _, func in ipairs(self.hooks[category]) do
693✔
519
      data = func(self, data)
774✔
520
   end
521
   return data
306✔
522
end
523

524
function typesetter:registerFrameBreakHook (func)
181✔
525
   self:registerHook("framebreak", func)
39✔
526
end
527

528
function typesetter:registerNewFrameHook (func)
181✔
529
   self:registerHook("newframe", func)
4✔
530
end
531

532
function typesetter:registerPageEndHook (func)
181✔
533
   self:registerHook("pageend", func)
220✔
534
end
535

536
function typesetter:buildPage ()
181✔
537
   local pageNodeList
538
   local res
539
   if self:isQueueEmpty() then
3,560✔
540
      return false
87✔
541
   end
542
   if SILE.scratch.insertions then
1,693✔
543
      SILE.scratch.insertions.thisPage = {}
472✔
544
   end
545
   pageNodeList, res = SILE.pagebuilder:findBestBreak({
3,386✔
546
      vboxlist = self.state.outputQueue,
1,693✔
547
      target = self:getTargetLength(),
3,386✔
548
      restart = self.frame.state.pageRestart,
1,693✔
549
   })
1,693✔
550
   if not pageNodeList then -- No break yet
1,693✔
551
      -- self.frame.state.pageRestart = res
552
      self:runHooks("noframebreak")
1,431✔
553
      return false
1,431✔
554
   end
555
   SU.debug("pagebuilder", "Buildding page for", self.frame.id)
262✔
556
   self.state.lastPenalty = res
262✔
557
   self.frame.state.pageRestart = nil
262✔
558
   pageNodeList = self:runHooks("framebreak", pageNodeList)
524✔
559
   self:setVerticalGlue(pageNodeList, self:getTargetLength())
524✔
560
   self:outputLinesToPage(pageNodeList)
262✔
561
   return true
262✔
562
end
563

564
function typesetter:setVerticalGlue (pageNodeList, target)
181✔
565
   local glues = {}
262✔
566
   local gTotal = SILE.length()
262✔
567
   local totalHeight = SILE.length()
262✔
568

569
   local pastTop = false
262✔
570
   for _, node in ipairs(pageNodeList) do
4,422✔
571
      if not pastTop and not node.discardable and not node.explicit then
4,160✔
572
         -- "Ignore discardable and explicit glues at the top of a frame."
573
         -- See typesetter:outputLinesToPage()
574
         -- Note the test here doesn't check is_vglue, so will skip other
575
         -- discardable nodes (e.g. penalties), but it shouldn't matter
576
         -- for the type of computing performed here.
577
         pastTop = true
258✔
578
      end
579
      if pastTop then
4,160✔
580
         if not node.is_insertion then
3,805✔
581
            totalHeight:___add(node.height)
3,769✔
582
            totalHeight:___add(node.depth)
3,769✔
583
         end
584
         if node.is_vglue then
3,805✔
585
            table.insert(glues, node)
2,173✔
586
            gTotal:___add(node.height)
2,173✔
587
         end
588
      end
589
   end
590

591
   if totalHeight:tonumber() == 0 then
524✔
592
      return SU.debug("pagebuilder", "No glue adjustment needed on empty page")
7✔
593
   end
594

595
   local adjustment = target - totalHeight
255✔
596
   if adjustment:tonumber() > 0 then
510✔
597
      if adjustment > gTotal.stretch then
231✔
598
         if
599
            (adjustment - gTotal.stretch):tonumber() > SILE.settings:get("typesetter.underfulltolerance"):tonumber()
180✔
600
         then
601
            SU.warn(
50✔
602
               "Underfull frame "
603
                  .. self.frame.id
25✔
604
                  .. ": "
25✔
605
                  .. adjustment
25✔
606
                  .. " stretchiness required to fill but only "
25✔
607
                  .. gTotal.stretch
25✔
608
                  .. " available"
25✔
609
            )
610
         end
611
         adjustment = gTotal.stretch
36✔
612
      end
613
      if gTotal.stretch:tonumber() > 0 then
462✔
614
         for i = 1, #glues do
2,248✔
615
            local g = glues[i]
2,037✔
616
            g:adjustGlue(adjustment:tonumber() * g.height.stretch:absolute() / gTotal.stretch)
10,185✔
617
         end
618
      end
619
   elseif adjustment:tonumber() < 0 then
48✔
620
      adjustment = 0 - adjustment
24✔
621
      if adjustment > gTotal.shrink then
24✔
622
         if (adjustment - gTotal.shrink):tonumber() > SILE.settings:get("typesetter.overfulltolerance"):tonumber() then
120✔
623
            SU.warn(
8✔
624
               "Overfull frame "
625
                  .. self.frame.id
4✔
626
                  .. ": "
4✔
627
                  .. adjustment
4✔
628
                  .. " shrinkability required to fit but only "
4✔
629
                  .. gTotal.shrink
4✔
630
                  .. " available"
4✔
631
            )
632
         end
633
         adjustment = gTotal.shrink
24✔
634
      end
635
      if gTotal.shrink:tonumber() > 0 then
48✔
NEW
636
         for i = 1, #glues do
×
NEW
637
            local g = glues[i]
×
NEW
638
            g:adjustGlue(-adjustment:tonumber() * g.height.shrink:absolute() / gTotal.shrink)
×
639
         end
640
      end
641
   end
642
   SU.debug("pagebuilder", "Glues for this page adjusted by", adjustment, "drawn from", gTotal)
255✔
643
end
644

645
function typesetter:initNextFrame ()
181✔
646
   local oldframe = self.frame
89✔
647
   self.frame:leave(self)
89✔
648
   if #self.state.outputQueue == 0 then
89✔
649
      self.state.previousVbox = nil
45✔
650
   end
651
   if self.frame.next and self.state.lastPenalty > supereject_penalty then
89✔
652
      self:initFrame(SILE.getFrame(self.frame.next))
45✔
653
   elseif not self.frame:isMainContentFrame() then
148✔
654
      if #self.state.outputQueue > 0 then
22✔
655
         SU.warn("Overfull content for frame " .. self.frame.id)
4✔
656
         self:chuck()
4✔
657
      end
658
   else
659
      self:runHooks("pageend")
52✔
660
      SILE.documentState.documentClass:endPage()
52✔
661
      self:initFrame(SILE.documentState.documentClass:newPage())
104✔
662
   end
663

664
   if not SU.feq(oldframe:getLineWidth(), self.frame:getLineWidth()) then
356✔
665
      self:pushBack()
9✔
666
      -- Some what of a hack below.
667
      -- Before calling this method, we were in vertical mode...
668
      -- pushback occurred, and it seems it messes up a bit...
669
      -- Regardless what it does, at the end, we ought to be in vertical mode
670
      -- again:
671
      self:leaveHmode()
18✔
672
   else
673
      -- If I have some things on the vertical list already, they need
674
      -- proper top-of-frame leading applied.
675
      if #self.state.outputQueue > 0 then
80✔
676
         local lead = self:leadingFor(self.state.outputQueue[1], nil)
36✔
677
         if lead then
36✔
678
            table.insert(self.state.outputQueue, 1, lead)
30✔
679
         end
680
      end
681
   end
682
   self:runHooks("newframe")
89✔
683
end
684

685
function typesetter:pushBack ()
181✔
686
   SU.debug("typesetter", "Pushing back", #self.state.outputQueue, "nodes")
9✔
687
   local oldqueue = self.state.outputQueue
9✔
688
   self.state.outputQueue = {}
9✔
689
   self.state.previousVbox = nil
9✔
690
   local lastMargins = self:getMargins()
9✔
691
   for _, vbox in ipairs(oldqueue) do
73✔
692
      SU.debug("pushback", "process box", vbox)
64✔
693
      if vbox.margins and vbox.margins ~= lastMargins then
64✔
694
         SU.debug("pushback", "new margins", lastMargins, vbox.margins)
2✔
695
         if not self.state.grid then
2✔
696
            self:endline()
2✔
697
         end
698
         self:setMargins(vbox.margins)
2✔
699
      end
700
      if vbox.explicit then
64✔
NEW
701
         SU.debug("pushback", "explicit", vbox)
×
NEW
702
         self:endline()
×
NEW
703
         self:pushExplicitVglue(vbox)
×
704
      elseif vbox.is_insertion then
64✔
NEW
705
         SU.debug("pushback", "pushBack", "insertion", vbox)
×
NEW
706
         SILE.typesetter:pushMigratingMaterial({ material = { vbox } })
×
707
      elseif not vbox.is_vglue and not vbox.is_penalty then
64✔
708
         SU.debug("pushback", "not vglue or penalty", vbox.type)
29✔
709
         local discardedFistInitLine = false
29✔
710
         if #self.state.nodes == 0 then
29✔
711
            -- Setup queue but avoid calling newPar
712
            self.state.nodes[#self.state.nodes + 1] = SILE.nodefactory.zerohbox()
10✔
713
         end
714
         for i, node in ipairs(vbox.nodes) do
875✔
715
            if node.is_glue and not node.discardable then
846✔
716
               self:pushHorizontal(node)
10✔
717
            elseif node.is_glue and node.value == "margin" then
841✔
718
               SU.debug("pushback", "discard", node.value, node)
116✔
719
            elseif node.is_discretionary then
783✔
720
               SU.debug("pushback", "re-mark discretionary as unused", node)
108✔
721
               node.used = false
108✔
722
               if i == 1 then
108✔
NEW
723
                  SU.debug("pushback", "keep first discretionary", node)
×
NEW
724
                  self:pushHorizontal(node)
×
725
               else
726
                  SU.debug("pushback", "discard all other discretionaries", node)
108✔
727
               end
728
            elseif node.is_zero then
675✔
729
               if discardedFistInitLine then
61✔
730
                  self:pushHorizontal(node)
32✔
731
               end
732
               discardedFistInitLine = true
61✔
733
            elseif node.is_penalty then
614✔
734
               if not discardedFistInitLine then
5✔
NEW
735
                  self:pushHorizontal(node)
×
736
               end
737
            else
738
               node.bidiDone = true
609✔
739
               self:pushHorizontal(node)
609✔
740
            end
741
         end
742
      else
743
         SU.debug("pushback", "discard", vbox.type)
35✔
744
      end
745
      lastMargins = vbox.margins
64✔
746
      -- self:debugState()
747
   end
NEW
748
   while
×
749
      self.state.nodes[#self.state.nodes]
13✔
750
      and (self.state.nodes[#self.state.nodes].is_penalty or self.state.nodes[#self.state.nodes].is_zero)
13✔
751
   do
752
      self.state.nodes[#self.state.nodes] = nil
4✔
753
   end
754
end
755

756
function typesetter:outputLinesToPage (lines)
181✔
757
   SU.debug("pagebuilder", "OUTPUTTING frame", self.frame.id)
356✔
758
   -- It would have been nice to avoid storing this "pastTop" into a frame
759
   -- state, to keep things less entangled. There are situations, though,
760
   -- we will have left horizontal mode (triggering output), but will later
761
   -- call typesetter:chuck() do deal with any remaining content, and we need
762
   -- to know whether some content has been output already.
763
   local pastTop = self.frame.state.totals.pastTop
356✔
764
   for _, line in ipairs(lines) do
4,797✔
765
      -- Ignore discardable and explicit glues at the top of a frame:
766
      -- Annoyingly, explicit glue *should* disappear at the top of a page.
767
      -- if you don't want that, add an empty vbox or something.
768
      if not pastTop and not line.discardable and not line.explicit then
4,441✔
769
         -- Note the test here doesn't check is_vglue, so will skip other
770
         -- discardable nodes (e.g. penalties), but it shouldn't matter
771
         -- for outputting.
772
         pastTop = true
341✔
773
      end
774
      if pastTop then
4,441✔
775
         line:outputYourself(self, line)
3,991✔
776
      end
777
   end
778
   self.frame.state.totals.pastTop = pastTop
356✔
779
end
780

781
function typesetter:leaveHmode (independent)
181✔
782
   if self.state.hmodeOnly then
1,971✔
783
      -- HACK HBOX
784
      -- This should likely be an error, but may break existing uses
785
      -- (although these are probably already defective).
786
      -- See also comment HACK HBOX in typesetter:makeHbox().
NEW
787
      SU.warn([[Building paragraphs in this context may have unpredictable results.
×
UNCOV
788
It will likely break in future versions]])
×
789
   end
790
   SU.debug("typesetter", "Leaving hmode")
1,971✔
791
   local margins = self:getMargins()
1,971✔
792
   local vboxlist = self:boxUpNodes()
1,971✔
793
   self.state.nodes = {}
1,971✔
794
   -- Push output lines into boxes and ship them to the page builder
795
   for _, vbox in ipairs(vboxlist) do
5,319✔
796
      vbox.margins = margins
3,348✔
797
      self:pushVertical(vbox)
3,348✔
798
   end
799
   if independent then
1,971✔
800
      return
364✔
801
   end
802
   if self:buildPage() then
3,214✔
803
      self:initNextFrame()
84✔
804
   end
805
end
806

807
function typesetter:inhibitLeading ()
181✔
808
   self.state.previousVbox = nil
2✔
809
end
810

811
function typesetter.leadingFor (_, vbox, previous)
181✔
812
   -- Insert leading
813
   SU.debug("typesetter", "   Considering leading between two lines:")
1,420✔
814
   SU.debug("typesetter", "   1)", previous)
1,420✔
815
   SU.debug("typesetter", "   2)", vbox)
1,420✔
816
   if not previous then
1,420✔
817
      return SILE.nodefactory.vglue()
368✔
818
   end
819
   local prevDepth = previous.depth
1,052✔
820
   SU.debug("typesetter", "   Depth of previous line was", prevDepth)
1,052✔
821
   local bls = SILE.settings:get("document.baselineskip")
1,052✔
822
   local depth = bls.height:absolute() - vbox.height:absolute() - prevDepth:absolute()
5,260✔
823
   SU.debug("typesetter", "   Leading height =", bls.height, "-", vbox.height, "-", prevDepth, "=", depth)
1,052✔
824

825
   -- the lineskip setting is a vglue, but we need a version absolutized at this point, see #526
826
   local lead = SILE.settings:get("document.lineskip").height:absolute()
2,104✔
827
   if depth > lead then
1,052✔
828
      return SILE.nodefactory.vglue(SILE.length(depth.length, bls.height.stretch, bls.height.shrink))
1,886✔
829
   else
830
      return SILE.nodefactory.vglue(lead)
109✔
831
   end
832
end
833

834
function typesetter:addrlskip (slice, margins, hangLeft, hangRight)
181✔
835
   local LTR = self.frame:writingDirection() == "LTR"
2,960✔
836
   local rskip = margins[LTR and "rskip" or "lskip"]
1,480✔
837
   if not rskip then
1,480✔
NEW
838
      rskip = SILE.nodefactory.glue(0)
×
839
   end
840
   if hangRight and hangRight > 0 then
1,480✔
841
      rskip = SILE.nodefactory.glue({ width = rskip.width:tonumber() + hangRight })
63✔
842
   end
843
   rskip.value = "margin"
1,480✔
844
   -- while slice[#slice].discardable do table.remove(slice, #slice) end
845
   table.insert(slice, rskip)
1,480✔
846
   table.insert(slice, SILE.nodefactory.zerohbox())
2,960✔
847
   local lskip = margins[LTR and "lskip" or "rskip"]
1,480✔
848
   if not lskip then
1,480✔
NEW
849
      lskip = SILE.nodefactory.glue(0)
×
850
   end
851
   if hangLeft and hangLeft > 0 then
1,480✔
852
      lskip = SILE.nodefactory.glue({ width = lskip.width:tonumber() + hangLeft })
102✔
853
   end
854
   lskip.value = "margin"
1,480✔
855
   while slice[1].discardable do
1,489✔
856
      table.remove(slice, 1)
18✔
857
   end
858
   table.insert(slice, 1, lskip)
1,480✔
859
   table.insert(slice, 1, SILE.nodefactory.zerohbox())
2,960✔
860
end
861

862
function typesetter:breakpointsToLines (breakpoints)
181✔
863
   local linestart = 1
856✔
864
   local lines = {}
856✔
865
   local nodes = self.state.nodes
856✔
866

867
   for i = 1, #breakpoints do
2,347✔
868
      local point = breakpoints[i]
1,491✔
869
      if point.position ~= 0 then
1,491✔
870
         local slice = {}
1,491✔
871
         local seenNonDiscardable = false
1,491✔
872
         for j = linestart, point.position do
27,658✔
873
            slice[#slice + 1] = nodes[j]
26,167✔
874
            if nodes[j] then
26,167✔
875
               if not nodes[j].discardable then
26,167✔
876
                  seenNonDiscardable = true
17,682✔
877
               end
878
            end
879
         end
880
         if not seenNonDiscardable then
1,491✔
881
            -- Slip lines containing only discardable nodes (e.g. glues).
882
            SU.debug("typesetter", "Skipping a line containing only discardable nodes")
11✔
883
            linestart = point.position + 1
11✔
884
         else
885
            -- If the line ends with a discretionary, repeat it on the next line,
886
            -- so as to account for a potential postbreak.
887
            if slice[#slice].is_discretionary then
1,480✔
888
               linestart = point.position
191✔
889
            else
890
               linestart = point.position + 1
1,289✔
891
            end
892

893
            -- Then only we can add some extra margin glue...
894
            local mrg = self:getMargins()
1,480✔
895
            self:addrlskip(slice, mrg, point.left, point.right)
1,480✔
896

897
            -- And compute the line...
898
            local ratio = self:computeLineRatio(point.width, slice)
1,480✔
899
            local thisLine = { ratio = ratio, nodes = slice }
1,480✔
900
            lines[#lines + 1] = thisLine
1,480✔
901
         end
902
      end
903
   end
904
   if linestart < #nodes then
856✔
905
      -- Abnormal, but warn so that one has a chance to check which bits
906
      -- are missing at output.
NEW
907
      SU.warn("Internal typesetter error " .. (#nodes - linestart) .. " skipped nodes")
×
908
   end
909
   return lines
856✔
910
end
911

912
function typesetter.computeLineRatio (_, breakwidth, slice)
181✔
913
   -- This somewhat wrong, see #1362 and #1528
914
   -- This is a somewhat partial workaround, at least made consistent with
915
   -- the nnode and discretionary outputYourself routines
916
   -- (which are somewhat wrong too, or to put it otherwise, the whole
917
   -- logic here, marking nodes without removing/replacing them, likely makes
918
   -- things more complex than they should).
919
   -- TODO Possibly consider a full rewrite/refactor.
920
   local naturalTotals = SILE.length()
1,480✔
921

922
   -- From the line end, check if the line is hyphenated (to account for a prebreak)
923
   -- or contains extraneous glues (e.g. to account for spaces to ignore).
924
   local n = #slice
1,480✔
925
   while n > 1 do
4,850✔
926
      if slice[n].is_glue or slice[n].is_zero then
4,850✔
927
         -- Skip margin glues (they'll be accounted for in the loop below) and
928
         -- zero boxes, so as to reach actual content...
929
         if slice[n].value ~= "margin" then
3,370✔
930
            -- ... but any other glue than a margin, at the end of a line, is actually
931
            -- extraneous. It will however also be accounted for below, so subtract
932
            -- them to cancel their width. Typically, if a line break occurred at
933
            -- a space, the latter is then at the end of the line now, and must be
934
            -- ignored.
935
            naturalTotals:___sub(slice[n].width)
1,890✔
936
         end
937
      elseif slice[n].is_discretionary then
1,480✔
938
         -- Stop as we reached an hyphenation, and account for the prebreak.
939
         slice[n].used = true
191✔
940
         if slice[n].parent then
191✔
941
            slice[n].parent.hyphenated = true
184✔
942
         end
943
         naturalTotals:___add(slice[n]:prebreakWidth())
382✔
944
         slice[n].height = slice[n]:prebreakHeight()
382✔
945
         break
191✔
946
      else
947
         -- Stop as we reached actual content.
948
         break
949
      end
950
      n = n - 1
3,370✔
951
   end
952

953
   local seenNodes = {}
1,480✔
954
   local skipping = true
1,480✔
955
   for i, node in ipairs(slice) do
33,546✔
956
      if node.is_box then
32,066✔
957
         skipping = false
16,208✔
958
         if node.parent and not node.parent.hyphenated then
16,208✔
959
            if not seenNodes[node.parent] then
4,302✔
960
               naturalTotals:___add(node.parent:lineContribution())
3,366✔
961
            end
962
            seenNodes[node.parent] = true
4,302✔
963
         else
964
            naturalTotals:___add(node:lineContribution())
23,812✔
965
         end
966
      elseif node.is_penalty and node.penalty == -inf_bad then
15,858✔
967
         skipping = false
877✔
968
      elseif node.is_discretionary then
14,981✔
969
         skipping = false
3,254✔
970
         local seen = node.parent and seenNodes[node.parent]
3,254✔
971
         if not seen and not node.used then
3,254✔
972
            naturalTotals:___add(node:replacementWidth():absolute())
1,017✔
973
            slice[i].height = slice[i]:replacementHeight():absolute()
1,017✔
974
         end
975
      elseif not skipping then
11,727✔
976
         naturalTotals:___add(node.width)
11,727✔
977
      end
978
   end
979

980
   -- From the line start, skip glues and margins, and check if it then starts
981
   -- with a used discretionary. If so, account for a postbreak.
982
   n = 1
1,480✔
983
   while n < #slice do
6,196✔
984
      if slice[n].is_discretionary and slice[n].used then
6,196✔
985
         naturalTotals:___add(slice[n]:postbreakWidth())
382✔
986
         slice[n].height = slice[n]:postbreakHeight()
382✔
987
         break
191✔
988
      elseif not (slice[n].is_glue or slice[n].is_zero) then
6,005✔
989
         break
1,289✔
990
      end
991
      n = n + 1
4,716✔
992
   end
993

994
   local _left = breakwidth:tonumber() - naturalTotals:tonumber()
4,440✔
995
   local ratio = _left / naturalTotals[_left < 0 and "shrink" or "stretch"]:tonumber()
2,960✔
996
   ratio = math.max(ratio, -1)
1,480✔
997
   return ratio, naturalTotals
1,480✔
998
end
999

1000
function typesetter:chuck () -- emergency shipout everything
181✔
1001
   self:leaveHmode(true)
106✔
1002
   if #self.state.outputQueue > 0 then
106✔
1003
      SU.debug("typesetter", "Emergency shipout", #self.state.outputQueue, "lines in frame", self.frame.id)
86✔
1004
      self:outputLinesToPage(self.state.outputQueue)
86✔
1005
      self.state.outputQueue = {}
86✔
1006
   end
1007
end
1008

1009
-- Logic for building an hbox from content.
1010
-- It returns the hbox and an horizontal list of (migrating) elements
1011
-- extracted outside of it.
1012
-- None of these are pushed to the typesetter node queue. The caller
1013
-- is responsible of doing it, if the hbox is built for anything
1014
-- else than e.g. measuring it. Likewise, the call has to decide
1015
-- what to do with the migrating content.
1016
local _rtl_pre_post = function (box, atypesetter, line)
1017
   local advance = function ()
1018
      atypesetter.frame:advanceWritingDirection(box:scaledWidth(line))
1,210✔
1019
   end
1020
   if atypesetter.frame:writingDirection() == "RTL" then
1,210✔
NEW
1021
      advance()
×
NEW
1022
      return function () end
×
1023
   else
1024
      return advance
605✔
1025
   end
1026
end
1027
function typesetter:makeHbox (content)
181✔
1028
   local recentContribution = {}
360✔
1029
   local migratingNodes = {}
360✔
1030

1031
   -- HACK HBOX
1032
   -- This is from the original implementation.
1033
   -- It would be somewhat cleaner to use a temporary typesetter state
1034
   -- (pushState/popState) rather than using the current one, removing
1035
   -- the processed nodes from it afterwards. However, as long
1036
   -- as leaving horizontal mode is not strictly forbidden here, it would
1037
   -- lead to a possibly different result (the output queue being skipped).
1038
   -- See also HACK HBOX comment in typesetter:leaveHmode().
1039
   local index = #self.state.nodes + 1
360✔
1040
   self.state.hmodeOnly = true
360✔
1041
   SILE.process(content)
360✔
1042
   self.state.hmodeOnly = false -- Wouldn't be needed in a temporary state
360✔
1043

1044
   local l = SILE.length()
360✔
1045
   local h, d = SILE.length(), SILE.length()
720✔
1046
   for i = index, #self.state.nodes do
768✔
1047
      local node = self.state.nodes[i]
408✔
1048
      if node.is_migrating then
734✔
NEW
1049
         migratingNodes[#migratingNodes + 1] = node
×
1050
      elseif node.is_unshaped then
408✔
1051
         local shape = node:shape()
326✔
1052
         for _, attr in ipairs(shape) do
810✔
1053
            recentContribution[#recentContribution + 1] = attr
484✔
1054
            h = attr.height > h and attr.height or h
813✔
1055
            d = attr.depth > d and attr.depth or d
599✔
1056
            l = l + attr:lineContribution():absolute()
1,452✔
1057
         end
1058
      elseif node.is_discretionary then
82✔
1059
         -- HACK https://github.com/sile-typesetter/sile/issues/583
1060
         -- Discretionary nodes have a null line contribution...
1061
         -- But if discretionary nodes occur inside an hbox, since the latter
1062
         -- is not line-broken, they will never be marked as 'used' and will
1063
         -- evaluate to the replacement content (if any)...
1064
         recentContribution[#recentContribution + 1] = node
1✔
1065
         l = l + node:replacementWidth():absolute()
3✔
1066
         -- The replacement content may have ascenders and descenders...
1067
         local hdisc = node:replacementHeight():absolute()
2✔
1068
         local ddisc = node:replacementDepth():absolute()
2✔
1069
         h = hdisc > h and hdisc or h
2✔
1070
         d = ddisc > d and ddisc or d
1✔
1071
      -- By the way it's unclear how this is expected to work in TTB
1072
      -- writing direction. For other type of nodes, the line contribution
1073
      -- evaluates to the height rather than the width in TTB, but the
1074
      -- whole logic might then be dubious there too...
1075
      else
1076
         recentContribution[#recentContribution + 1] = node
81✔
1077
         l = l + node:lineContribution():absolute()
243✔
1078
         h = node.height > h and node.height or h
94✔
1079
         d = node.depth > d and node.depth or d
92✔
1080
      end
1081
      self.state.nodes[i] = nil -- wouldn't be needed in a temporary state
408✔
1082
   end
1083

1084
   local hbox = SILE.nodefactory.hbox({
720✔
1085
      height = h,
360✔
1086
      width = l,
360✔
1087
      depth = d,
360✔
1088
      value = recentContribution,
360✔
1089
      outputYourself = function (box, atypesetter, line)
1090
         local _post = _rtl_pre_post(box, atypesetter, line)
605✔
1091
         local ox = atypesetter.frame.state.cursorX
605✔
1092
         local oy = atypesetter.frame.state.cursorY
605✔
1093
         SILE.outputter:setCursor(atypesetter.frame.state.cursorX, atypesetter.frame.state.cursorY)
605✔
1094
         for _, node in ipairs(box.value) do
1,876✔
1095
            node:outputYourself(atypesetter, line)
1,271✔
1096
         end
1097
         atypesetter.frame.state.cursorX = ox
605✔
1098
         atypesetter.frame.state.cursorY = oy
605✔
1099
         _post()
605✔
1100
         SU.debug("hboxes", function ()
1,210✔
NEW
1101
            SILE.outputter:debugHbox(box, box:scaledWidth(line))
×
NEW
1102
            return "Drew debug outline around hbox"
×
1103
         end)
1104
      end,
1105
   })
1106
   return hbox, migratingNodes
360✔
1107
end
1108

1109
function typesetter:pushHlist (hlist)
181✔
1110
   for _, h in ipairs(hlist) do
19✔
NEW
1111
      self:pushHorizontal(h)
×
1112
   end
1113
end
1114

1115
return typesetter
181✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc