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

sile-typesetter / sile / 6932773445

20 Nov 2023 04:11PM UTC coverage: 60.703% (-1.6%) from 62.266%
6932773445

Pull #1904

github

alerque
feat(utilities): Add Greek alphabetical (non-arithmetic) numbering

Useful in some context such as biblical annotations etc. where greek
characters are used orderly for numbering.
Pull Request #1904: Merge develop into master (commit to next release being breaking)

66 of 193 new or added lines in 19 files covered. (34.2%)

321 existing lines in 26 files now uncovered.

9452 of 15571 relevant lines covered (60.7%)

2104.43 hits per line

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

74.01
/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()
28✔
10
typesetter.type = "typesetter"
28✔
11
typesetter._name = "base"
28✔
12

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

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

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

29
    __eq = function (self, other)
30
      return self.lskip.width == other.lskip.width and self.rskip.width == other.rskip.width
×
31
    end
32

33
  })
34

35
local warned = false
28✔
36

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

49
function typesetter:_init (frame)
28✔
50
  self:declareSettings()
62✔
51
  self.hooks = {}
62✔
52
  self.breadcrumbs = SU.breadcrumbs()
124✔
53

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

63
function typesetter.declareSettings(_)
28✔
64

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

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

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

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

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

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

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

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

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

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

135
  SILE.settings:declare({
62✔
136
    parameter = "typesetter.italicCorrection",
137
    type = "boolean",
138
    default = false,
NEW
139
    help = "Whether italic correction is activated or not"
×
140
  })
141

142
end
143

144
function typesetter:initState ()
28✔
145
  self.state = {
81✔
146
    nodes = {},
81✔
147
    outputQueue = {},
81✔
148
    lastBadness = awful_bad,
81✔
149
  }
81✔
150
end
151

152
function typesetter:initFrame (frame)
28✔
153
  if frame then
98✔
154
    self.frame = frame
96✔
155
    self.frame:init(self)
96✔
156
  end
157
end
158

159
function typesetter.getMargins ()
28✔
160
  return _margins(SILE.settings:get("document.lskip"), SILE.settings:get("document.rskip"))
1,983✔
161
end
162

163
function typesetter.setMargins (_, margins)
28✔
164
  SILE.settings:set("document.lskip", margins.lskip)
×
165
  SILE.settings:set("document.rskip", margins.rskip)
×
166
end
167

168
function typesetter:pushState ()
28✔
169
  self.stateQueue[#self.stateQueue+1] = self.state
19✔
170
  self:initState()
19✔
171
end
172

173
function typesetter:popState (ncount)
28✔
174
  local offset = ncount and #self.stateQueue - ncount or nil
19✔
175
  self.state = table.remove(self.stateQueue, offset)
38✔
176
  if not self.state then SU.error("Typesetter state queue empty") end
19✔
177
end
178

179
function typesetter:isQueueEmpty ()
28✔
180
  if not self.state then return nil end
486✔
181
  return #self.state.nodes == 0 and #self.state.outputQueue == 0
486✔
182
end
183

184
function typesetter:vmode ()
28✔
185
  return #self.state.nodes == 0
50✔
186
end
187

188
function typesetter:debugState ()
28✔
189
  print("\n---\nI am in "..(self:vmode() and "vertical" or "horizontal").." mode")
×
190
  print("Writing into " .. tostring(self.frame))
×
191
  print("Recent contributions: ")
×
192
  for i = 1, #(self.state.nodes) do
×
193
    io.stderr:write(self.state.nodes[i].. " ")
×
194
  end
195
  print("\nVertical list: ")
×
196
  for i = 1, #(self.state.outputQueue) do
×
197
    print("  "..self.state.outputQueue[i])
×
198
  end
199
end
200

201
-- Boxy stuff
202
function typesetter:pushHorizontal (node)
28✔
203
  self:initline()
794✔
204
  self.state.nodes[#self.state.nodes+1] = node
794✔
205
  return node
794✔
206
end
207

208
function typesetter:pushVertical (vbox)
28✔
209
  self.state.outputQueue[#self.state.outputQueue+1] = vbox
870✔
210
  return vbox
870✔
211
end
212

213
function typesetter:pushHbox (spec)
28✔
214
  -- if SU.type(spec) ~= "table" then SU.warn("Please use pushHorizontal() to pass a premade node instead of a spec") end
215
  local ntype = SU.type(spec)
41✔
216
  local node = (ntype == "hbox" or ntype == "zerohbox") and spec or SILE.nodefactory.hbox(spec)
41✔
217
  return self:pushHorizontal(node)
41✔
218
end
219

220
function typesetter:pushUnshaped (spec)
28✔
221
  -- if SU.type(spec) ~= "table" then SU.warn("Please use pushHorizontal() to pass a premade node instead of a spec") end
222
  local node = SU.type(spec) == "unshaped" and spec or SILE.nodefactory.unshaped(spec)
348✔
223
  return self:pushHorizontal(node)
174✔
224
end
225

226
function typesetter:pushGlue (spec)
28✔
227
  -- if SU.type(spec) ~= "table" then SU.warn("Please use pushHorizontal() to pass a premade node instead of a spec") end
228
  local node = SU.type(spec) == "glue" and spec or SILE.nodefactory.glue(spec)
710✔
229
  return self:pushHorizontal(node)
355✔
230
end
231

232
function typesetter:pushExplicitGlue (spec)
28✔
233
  -- if SU.type(spec) ~= "table" then SU.warn("Please use pushHorizontal() to pass a premade node instead of a spec") end
234
  local node = SU.type(spec) == "glue" and spec or SILE.nodefactory.glue(spec)
×
235
  node.explicit = true
×
236
  node.discardable = false
×
237
  return self:pushHorizontal(node)
×
238
end
239

240
function typesetter:pushPenalty (spec)
28✔
241
  -- if SU.type(spec) ~= "table" then SU.warn("Please use pushHorizontal() to pass a premade node instead of a spec") end
242
  local node = SU.type(spec) == "penalty" and spec or SILE.nodefactory.penalty(spec)
350✔
243
  return self:pushHorizontal(node)
175✔
244
end
245

246
function typesetter:pushMigratingMaterial (material)
28✔
247
  local node = SILE.nodefactory.migrating({ material = material })
2✔
248
  return self:pushHorizontal(node)
2✔
249
end
250

251
function typesetter:pushVbox (spec)
28✔
252
  -- if SU.type(spec) ~= "table" then SU.warn("Please use pushVertical() to pass a premade node instead of a spec") end
253
  local node = SU.type(spec) == "vbox" and spec or SILE.nodefactory.vbox(spec)
×
254
  return self:pushVertical(node)
×
255
end
256

257
function typesetter:pushVglue (spec)
28✔
258
  -- if SU.type(spec) ~= "table" then SU.warn("Please use pushVertical() to pass a premade node instead of a spec") end
259
  local node = SU.type(spec) == "vglue" and spec or SILE.nodefactory.vglue(spec)
362✔
260
  return self:pushVertical(node)
181✔
261
end
262

263
function typesetter:pushExplicitVglue (spec)
28✔
264
  -- if SU.type(spec) ~= "table" then SU.warn("Please use pushVertical() to pass a premade node instead of a spec") end
265
  local node = SU.type(spec) == "vglue" and spec or SILE.nodefactory.vglue(spec)
236✔
266
  node.explicit = true
118✔
267
  node.discardable = false
118✔
268
  return self:pushVertical(node)
118✔
269
end
270

271
function typesetter:pushVpenalty (spec)
28✔
272
  -- if SU.type(spec) ~= "table" then SU.warn("Please use pushVertical() to pass a premade node instead of a spec") end
273
  local node = SU.type(spec) == "penalty" and spec or SILE.nodefactory.penalty(spec)
92✔
274
  return self:pushVertical(node)
46✔
275
end
276

277
-- Actual typesetting functions
278
function typesetter:typeset (text)
28✔
279
  text = tostring(text)
293✔
280
  if text:match("^%\r?\n$") then return end
293✔
281
  local pId = SILE.traceStack:pushText(text)
206✔
282
  for token in SU.gtoke(text, SILE.settings:get("typesetter.parseppattern")) do
873✔
283
    if token.separator then
255✔
284
      self:endline()
158✔
285
    else
286
      self:setpar(token.string)
176✔
287
    end
288
  end
289
  SILE.traceStack:pop(pId)
206✔
290
end
291

292
function typesetter:initline ()
28✔
293
  if self.state.hmodeOnly then return end -- https://github.com/sile-typesetter/sile/issues/1718
918✔
294
  if (#self.state.nodes == 0) then
909✔
295
    self.state.nodes[#self.state.nodes+1] = SILE.nodefactory.zerohbox()
354✔
296
    SILE.documentState.documentClass.newPar(self)
177✔
297
  end
298
end
299

300
function typesetter:endline ()
28✔
301
  self:leaveHmode()
180✔
302
  SILE.documentState.documentClass.endPar(self)
180✔
303
end
304

305
-- Takes string, writes onto self.state.nodes
306
function typesetter:setpar (text)
28✔
307
  text = text:gsub("\r?\n", " "):gsub("\t", " ")
176✔
308
  if (#self.state.nodes == 0) then
176✔
309
    if not SILE.settings:get("typesetter.obeyspaces") then
248✔
310
      text = text:gsub("^%s+", "")
124✔
311
    end
312
    self:initline()
124✔
313
  end
314
  if #text >0 then
176✔
315
    self:pushUnshaped({ text = text, options= SILE.font.loadDefaults({})})
348✔
316
  end
317
end
318

319
function typesetter:breakIntoLines (nodelist, breakWidth)
28✔
320
  self:shapeAllNodes(nodelist)
175✔
321
  local breakpoints = SILE.linebreak:doBreak(nodelist, breakWidth)
175✔
322
  return self:breakpointsToLines(breakpoints)
175✔
323
end
324

325
local function getLastShape(nodelist)
326
  local hasGlue
327
  local last
NEW
328
  if nodelist then
×
329
    -- The node list may contain nnodes, penalties, kern and glue
330
    -- We skip the latter, and retrieve the last shaped item.
NEW
331
    for i = #nodelist, 1, -1 do
×
NEW
332
      local n = nodelist[i]
×
NEW
333
      if n.is_nnode then
×
NEW
334
        local items = n.nodes[#n.nodes].value.items
×
NEW
335
        last = items[#items]
×
336
        break
337
      end
NEW
338
      if n.is_kern and n.subtype == "punctspace" then
×
339
        -- Some languages such as French insert a special space around
340
        -- punctuations. In those case, we should not need italic correction.
341
        break
342
      end
NEW
343
      if n.is_glue then hasGlue = true end
×
344
    end
345
  end
NEW
346
  return last, hasGlue
×
347
end
348
local function getFirstShape(nodelist)
349
  local first
350
  local hasGlue
NEW
351
  if nodelist then
×
352
    -- The node list may contain nnodes, penalties, kern and glue
353
    -- We skip the latter, and retrieve the first shaped item.
NEW
354
    for i = 1, #nodelist do
×
NEW
355
      local n = nodelist[i]
×
NEW
356
      if n.is_nnode then
×
NEW
357
        local items = n.nodes[1].value.items
×
NEW
358
        first = items[1]
×
359
        break
360
      end
NEW
361
      if n.is_kern and n.subtype == "punctspace" then
×
362
        -- Some languages such as French insert a special space around
363
        -- punctuations. In those case, we should not need italic correction.
364
        break
365
      end
NEW
366
      if n.is_glue then hasGlue = true end
×
367
    end
368
  end
NEW
369
  return first, hasGlue
×
370
end
371

372
local function fromItalicCorrection (precShape, curShape)
373
  local xOffset
NEW
374
  if not curShape or not precShape then
×
NEW
375
    xOffset = 0
×
376
  else
377
    -- Computing italic correction is at best heuristics.
378
    -- The strong assumption is that italic is slanted to the right.
379
    -- Thus, the part of the character that goes beyond its width is usually
380
    -- maximal at the top of the glyph.
381
    -- E.g. consider a "f", that would be the top hook extent.
382
    -- Pathological cases exist, such as fonts with a Q with a long tail,
383
    -- but these will rarely occur in usual languages. For instance, Klingon's
384
    -- "QaQ" might be an issue, but there's not much we can do...
385
    -- Another assumption is that we can distribute that extent in proportion
386
    -- with the next character's height.
387
    -- This might not work that well with non-Latin scripts.
NEW
388
    local d = precShape.glyphWidth + precShape.x_bearing
×
NEW
389
    local delta = d > precShape.width and d - precShape.width or 0
×
NEW
390
    xOffset = precShape.height <= curShape.height
×
NEW
391
      and delta
×
NEW
392
      or delta * curShape.height / precShape.height
×
393
  end
NEW
394
  return xOffset
×
395
end
396

397
local function toItalicCorrection (precShape, curShape)
NEW
398
  if not SILE.settings:get("typesetter.italicCorrection") then return end
×
399
  local xOffset
NEW
400
  if not curShape or not precShape then
×
NEW
401
    xOffset = 0
×
402
  else
403
    -- Same assumptions as fromItalicCorrection(), but on the starting side of
404
    -- the glyph.
NEW
405
    local d = curShape.x_bearing
×
NEW
406
    local delta = d < 0 and -d or 0
×
NEW
407
    xOffset = precShape.depth >= curShape.depth
×
NEW
408
      and delta
×
NEW
409
      or delta * precShape.depth / curShape.depth
×
410
  end
NEW
411
  return xOffset
×
412
end
413

414
local function isItalicLike(nnode)
415
  -- We could do...
416
  --  return nnode and string.lower(nnode.options.style) == "italic"
417
  -- But it's probably more robust to use the italic angle, so that
418
  -- thin italic, oblique or slanted fonts etc. may work too.
NEW
419
  local ot = require("core.opentype-parser")
×
NEW
420
  local face = SILE.font.cache(nnode.options, SILE.shaper.getFace)
×
NEW
421
  local font = ot.parseFont(face)
×
NEW
422
  return font.post.italicAngle ~= 0
×
423
end
424

425
function typesetter.shapeAllNodes (_, nodelist, inplace)
28✔
426
  inplace = SU.boolean(inplace, true) -- Compatibility with earlier versions
1,080✔
427
  local newNodelist = {}
540✔
428
  local prec
429
  local precShapedNodes
430
  for _, current in ipairs(nodelist) do
6,726✔
431
    if current.is_unshaped then
6,186✔
432
      local shapedNodes = current:shape()
184✔
433

434
      if SILE.settings:get("typesetter.italicCorrection") and prec then
368✔
435
        local itCorrOffset
436
        local isGlue
NEW
437
        if isItalicLike(prec) and not isItalicLike(current) then
×
NEW
438
          local precShape, precHasGlue = getLastShape(precShapedNodes)
×
NEW
439
          local curShape, curHasGlue = getFirstShape(shapedNodes)
×
NEW
440
          isGlue = precHasGlue or curHasGlue
×
NEW
441
          itCorrOffset = fromItalicCorrection(precShape, curShape)
×
NEW
442
        elseif not isItalicLike(prec) and isItalicLike(current) then
×
NEW
443
          local precShape, precHasGlue = getLastShape(precShapedNodes)
×
NEW
444
          local curShape, curHasGlue = getFirstShape(shapedNodes)
×
NEW
445
          isGlue = precHasGlue or curHasGlue
×
NEW
446
          itCorrOffset = toItalicCorrection(precShape, curShape)
×
447
        end
NEW
448
        if itCorrOffset and itCorrOffset ~= 0 then
×
449
          -- If one of the node contains a glue (e.g. "a \em{proof} is..."),
450
          -- line breaking may occur between them, so our correction shall be
451
          -- a glue too.
452
          -- Otherwise, the font change is considered to occur at a non-breaking
453
          -- point (e.g. "\em{proof}!") and the correction shall be a kern.
NEW
454
          local makeItCorrNode = isGlue and SILE.nodefactory.glue or SILE.nodefactory.kern
×
NEW
455
          newNodelist[#newNodelist+1] = makeItCorrNode({
×
456
            width = SILE.length(itCorrOffset),
NEW
457
            subtype = "itcorr"
×
458
          })
459
        end
460
      end
461

462
      pl.tablex.insertvalues(newNodelist, shapedNodes)
184✔
463

464
      prec = current
184✔
465
      precShapedNodes = shapedNodes
184✔
466
    else
467
      prec = nil
6,002✔
468
      newNodelist[#newNodelist+1] = current
6,002✔
469
    end
470
  end
471

472
  if not inplace then
540✔
473
    return newNodelist
16✔
474
  end
475

476
  for i =1, #newNodelist do nodelist[i] = newNodelist[i] end
8,209✔
477
  if #nodelist > #newNodelist then
524✔
NEW
478
    for i= #newNodelist + 1, #nodelist do nodelist[i] = nil end
×
479
  end
480
end
481

482
-- Empties self.state.nodes, breaks into lines, puts lines into vbox, adds vbox to
483
-- Turns a node list into a list of vboxes
484
function typesetter:boxUpNodes ()
28✔
485
  local nodelist = self.state.nodes
413✔
486
  if #nodelist == 0 then return {} end
413✔
487
  for j = #nodelist, 1, -1 do
220✔
488
    if not nodelist[j].is_migrating then
221✔
489
      if nodelist[j].discardable then
220✔
490
        table.remove(nodelist, j)
90✔
491
      else
492
        break
493
      end
494
    end
495
  end
496
  while (#nodelist > 0 and nodelist[1].is_penalty) do table.remove(nodelist, 1) end
175✔
497
  if #nodelist == 0 then return {} end
175✔
498
  self:shapeAllNodes(nodelist)
175✔
499
  local parfillskip = SILE.settings:get("typesetter.parfillskip")
175✔
500
  parfillskip.discardable = false
175✔
501
  self:pushGlue(parfillskip)
175✔
502
  self:pushPenalty(-inf_bad)
175✔
503
  SU.debug("typesetter", function ()
350✔
504
    return "Boxed up "..(#nodelist > 500 and (#nodelist).." nodes" or SU.contentToString(nodelist))
×
505
  end)
506
  local breakWidth = SILE.settings:get("typesetter.breakwidth") or self.frame:getLineWidth()
350✔
507
  local lines = self:breakIntoLines(nodelist, breakWidth)
175✔
508
  local vboxes = {}
175✔
509
  for index=1, #lines do
423✔
510
    local line = lines[index]
248✔
511
    local migrating = {}
248✔
512
    -- Move any migrating material
513
    local nodes = {}
248✔
514
    for i =1, #line.nodes do
4,719✔
515
      local node = line.nodes[i]
4,471✔
516
      if node.is_migrating then
4,471✔
517
        for j=1, #node.material do migrating[#migrating+1] = node.material[j] end
2✔
518
      else
519
        nodes[#nodes+1] = node
4,469✔
520
      end
521
    end
522
    local vbox = SILE.nodefactory.vbox({ nodes = nodes, ratio = line.ratio })
248✔
523
    local pageBreakPenalty = 0
248✔
524
    if (#lines > 1 and index == 1) then
248✔
525
      pageBreakPenalty = SILE.settings:get("typesetter.widowpenalty")
30✔
526
    elseif (#lines > 1 and index == (#lines-1)) then
233✔
527
      pageBreakPenalty = SILE.settings:get("typesetter.orphanpenalty")
26✔
528
    end
529
    vboxes[#vboxes+1] = self:leadingFor(vbox, self.state.previousVbox)
496✔
530
    vboxes[#vboxes+1] = vbox
248✔
531
    for i=1, #migrating do vboxes[#vboxes+1] = migrating[i] end
249✔
532
    self.state.previousVbox = vbox
248✔
533
    if pageBreakPenalty > 0 then
248✔
534
      SU.debug("typesetter", "adding penalty of", pageBreakPenalty, "after", vbox)
28✔
535
      vboxes[#vboxes+1] = SILE.nodefactory.penalty(pageBreakPenalty)
56✔
536
    end
537
  end
538
  return vboxes
175✔
539
end
540

541
function typesetter.pageTarget (_)
28✔
542
  SU.deprecated("SILE.typesetter:pageTarget", "SILE.typesetter:getTargetLength", "0.13.0", "0.14.0")
×
543
end
544

545
function typesetter:getTargetLength ()
28✔
546
  return self.frame:getTargetLength()
410✔
547
end
548

549
function typesetter:registerHook (category, func)
28✔
550
  if not self.hooks[category] then self.hooks[category] = {} end
36✔
551
  table.insert(self.hooks[category], func)
36✔
552
end
553

554
function typesetter:runHooks (category, data)
28✔
555
  if not self.hooks[category] then return data end
426✔
556
  for _, func in ipairs(self.hooks[category]) do
143✔
557
    data = func(self, data)
162✔
558
  end
559
  return data
62✔
560
end
561

562
function typesetter:registerFrameBreakHook (func)
28✔
563
  self:registerHook("framebreak", func)
4✔
564
end
565

566
function typesetter:registerNewFrameHook (func)
28✔
567
  self:registerHook("newframe", func)
×
568
end
569

570
function typesetter:registerPageEndHook (func)
28✔
571
  self:registerHook("pageend", func)
32✔
572
end
573

574
function typesetter:buildPage ()
28✔
575
  local pageNodeList
576
  local res
577
  if self:isQueueEmpty() then return false end
748✔
578
  if SILE.scratch.insertions then SILE.scratch.insertions.thisPage = {} end
352✔
579
  pageNodeList, res = SILE.pagebuilder:findBestBreak({
704✔
580
    vboxlist = self.state.outputQueue,
352✔
581
    target   = self:getTargetLength(),
704✔
582
    restart  = self.frame.state.pageRestart
352✔
583
  })
352✔
584
  if not pageNodeList then -- No break yet
352✔
585
    -- self.frame.state.pageRestart = res
586
    self:runHooks("noframebreak")
296✔
587
    return false
296✔
588
  end
589
  SU.debug("pagebuilder", "Buildding page for", self.frame.id)
56✔
590
  self.state.lastPenalty = res
56✔
591
  self.frame.state.pageRestart = nil
56✔
592
  pageNodeList = self:runHooks("framebreak", pageNodeList)
112✔
593
  self:setVerticalGlue(pageNodeList, self:getTargetLength())
112✔
594
  self:outputLinesToPage(pageNodeList)
56✔
595
  return true
56✔
596
end
597

598
function typesetter:setVerticalGlue (pageNodeList, target)
28✔
599
  local glues = {}
56✔
600
  local gTotal = SILE.length()
56✔
601
  local totalHeight = SILE.length()
56✔
602

603
  local pastTop = false
56✔
604
  for _, node in ipairs(pageNodeList) do
753✔
605
    if not pastTop and not node.discardable and not node.explicit then
697✔
606
      -- "Ignore discardable and explicit glues at the top of a frame."
607
      -- See typesetter:outputLinesToPage()
608
      -- Note the test here doesn't check is_vglue, so will skip other
609
      -- discardable nodes (e.g. penalties), but it shouldn't matter
610
      -- for the type of computing performed here.
611
      pastTop = true
56✔
612
    end
613
    if pastTop then
697✔
614
      if not node.is_insertion then
626✔
615
        totalHeight:___add(node.height)
626✔
616
        totalHeight:___add(node.depth)
626✔
617
      end
618
      if node.is_vglue then
626✔
619
        table.insert(glues, node)
397✔
620
        gTotal:___add(node.height)
397✔
621
      end
622
    end
623
  end
624

625
  if totalHeight:tonumber() == 0 then
112✔
626
   return SU.debug("pagebuilder", "No glue adjustment needed on empty page")
4✔
627
  end
628

629
  local adjustment = target - totalHeight
52✔
630
  if adjustment:tonumber() > 0 then
104✔
631
    if adjustment > gTotal.stretch then
38✔
632
      if (adjustment - gTotal.stretch):tonumber() > SILE.settings:get("typesetter.underfulltolerance"):tonumber() then
10✔
633
        SU.warn("Underfull frame " .. self.frame.id .. ": " .. adjustment .. " stretchiness required to fill but only " .. gTotal.stretch .. " available")
1✔
634
      end
635
      adjustment = gTotal.stretch
2✔
636
    end
637
    if gTotal.stretch:tonumber() > 0 then
76✔
638
      for i = 1, #glues do
423✔
639
        local g = glues[i]
386✔
640
        g:adjustGlue(adjustment:tonumber() * g.height.stretch:absolute() / gTotal.stretch)
1,930✔
641
      end
642
    end
643
  elseif adjustment:tonumber() < 0 then
28✔
644
    adjustment = 0 - adjustment
14✔
645
    if adjustment > gTotal.shrink then
14✔
646
      if (adjustment - gTotal.shrink):tonumber() > SILE.settings:get("typesetter.overfulltolerance"):tonumber() then
70✔
647
        SU.warn("Overfull frame " .. self.frame.id .. ": " .. adjustment .. " shrinkability required to fit but only " .. gTotal.shrink .. " available")
×
648
      end
649
      adjustment = gTotal.shrink
14✔
650
    end
651
    if gTotal.shrink:tonumber() > 0 then
28✔
652
      for i = 1, #glues do
×
653
        local g  = glues[i]
×
654
        g:adjustGlue(-adjustment:tonumber() * g.height.shrink:absolute() / gTotal.shrink)
×
655
      end
656
    end
657
  end
658
  SU.debug("pagebuilder", "Glues for this page adjusted by", adjustment, "drawn from", gTotal)
52✔
659
end
660

661
function typesetter:initNextFrame ()
28✔
662
  local oldframe = self.frame
30✔
663
  self.frame:leave(self)
30✔
664
  if #self.state.outputQueue == 0 then
30✔
665
    self.state.previousVbox = nil
21✔
666
  end
667
  if self.frame.next and self.state.lastPenalty > supereject_penalty then
30✔
668
    self:initFrame(SILE.getFrame(self.frame.next))
×
669
  elseif not self.frame:isMainContentFrame() then
60✔
670
    if #self.state.outputQueue > 0 then
14✔
671
      SU.warn("Overfull content for frame " .. self.frame.id)
2✔
672
      self:chuck()
2✔
673
    end
674
  else
675
    self:runHooks("pageend")
16✔
676
    SILE.documentState.documentClass:endPage()
16✔
677
    self:initFrame(SILE.documentState.documentClass:newPage())
32✔
678
  end
679

680
  if not SU.feq(oldframe:getLineWidth(), self.frame:getLineWidth()) then
120✔
681
    self:pushBack()
×
682
    -- Some what of a hack below.
683
    -- Before calling this method, we were in vertical mode...
684
    -- pushback occurred, and it seems it messes up a bit...
685
    -- Regardless what it does, at the end, we ought to be in vertical mode
686
    -- again:
687
    self:leaveHmode()
×
688
  else
689
    -- If I have some things on the vertical list already, they need
690
    -- proper top-of-frame leading applied.
691
    if #self.state.outputQueue > 0 then
30✔
692
      local lead = self:leadingFor(self.state.outputQueue[1], nil)
7✔
693
      if lead then
7✔
694
        table.insert(self.state.outputQueue, 1, lead)
6✔
695
      end
696
    end
697
  end
698
  self:runHooks("newframe")
30✔
699

700
end
701

702
function typesetter:pushBack ()
28✔
703
  SU.debug("typesetter", "Pushing back", #self.state.outputQueue, "nodes")
×
704
  local oldqueue = self.state.outputQueue
×
705
  self.state.outputQueue = {}
×
706
  self.state.previousVbox = nil
×
707
  local lastMargins = self:getMargins()
×
708
  for _, vbox in ipairs(oldqueue) do
×
709
    SU.debug("pushback", "process box", vbox)
×
710
    if vbox.margins and vbox.margins ~= lastMargins then
×
711
      SU.debug("pushback", "new margins", lastMargins, vbox.margins)
×
712
      if not self.state.grid then self:endline() end
×
713
      self:setMargins(vbox.margins)
×
714
    end
715
    if vbox.explicit then
×
716
      SU.debug("pushback", "explicit", vbox)
×
717
      self:endline()
×
718
      self:pushExplicitVglue(vbox)
×
719
    elseif vbox.is_insertion then
×
720
      SU.debug("pushback", "pushBack", "insertion", vbox)
×
721
      SILE.typesetter:pushMigratingMaterial({ material = { vbox } })
×
722
    elseif not vbox.is_vglue and not vbox.is_penalty then
×
723
      SU.debug("pushback", "not vglue or penalty", vbox.type)
×
724
      local discardedFistInitLine = false
×
725
      if (#self.state.nodes == 0) then
×
726
        -- Setup queue but avoid calling newPar
727
        self.state.nodes[#self.state.nodes+1] = SILE.nodefactory.zerohbox()
×
728
      end
729
      for i, node in ipairs(vbox.nodes) do
×
730
        if node.is_glue and not node.discardable then
×
731
          self:pushHorizontal(node)
×
732
        elseif node.is_glue and node.value == "margin" then
×
733
          SU.debug("pushback", "discard", node.value, node)
×
734
        elseif node.is_discretionary then
×
735
          SU.debug("pushback", "re-mark discretionary as unused", node)
×
736
          node.used = false
×
737
          if i == 1 then
×
738
            SU.debug("pushback", "keep first discretionary", node)
×
739
            self:pushHorizontal(node)
×
740
          else
741
            SU.debug("pushback", "discard all other discretionaries", node)
×
742
          end
743
        elseif node.is_zero then
×
744
          if discardedFistInitLine then self:pushHorizontal(node) end
×
745
          discardedFistInitLine = true
×
746
        elseif node.is_penalty then
×
747
          if not discardedFistInitLine then self:pushHorizontal(node) end
×
748
        else
749
          node.bidiDone = true
×
750
          self:pushHorizontal(node)
×
751
        end
752
      end
753
    else
754
      SU.debug("pushback", "discard", vbox.type)
×
755
    end
756
    lastMargins = vbox.margins
×
757
    -- self:debugState()
758
  end
759
  while self.state.nodes[#self.state.nodes]
×
760
  and (self.state.nodes[#self.state.nodes].is_penalty
×
761
    or self.state.nodes[#self.state.nodes].is_zero) do
×
762
    self.state.nodes[#self.state.nodes] = nil
×
763
  end
764
end
765

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

791
function typesetter:leaveHmode (independent)
28✔
792
  if self.state.hmodeOnly then
413✔
NEW
793
    SU.error([[Paragraphs are forbidden in restricted horizontal mode.]])
×
794
  end
795
  SU.debug("typesetter", "Leaving hmode")
413✔
796
  local margins = self:getMargins()
413✔
797
  local vboxlist = self:boxUpNodes()
413✔
798
  self.state.nodes = {}
413✔
799
  -- Push output lines into boxes and ship them to the page builder
800
  for _, vbox in ipairs(vboxlist) do
938✔
801
    vbox.margins = margins
525✔
802
    self:pushVertical(vbox)
525✔
803
  end
804
  if independent then return end
413✔
805
  if self:buildPage() then
698✔
806
    self:initNextFrame()
29✔
807
  end
808
end
809

810
function typesetter:inhibitLeading ()
28✔
811
  self.state.previousVbox = nil
2✔
812
end
813

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

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

835
function typesetter:addrlskip (slice, margins, hangLeft, hangRight)
28✔
836
  local LTR = self.frame:writingDirection() == "LTR"
496✔
837
  local rskip = margins[LTR and "rskip" or "lskip"]
248✔
838
  if not rskip then rskip = SILE.nodefactory.glue(0) end
248✔
839
  if hangRight and hangRight > 0 then
248✔
840
    rskip = SILE.nodefactory.glue({ width = rskip.width:tonumber() + hangRight })
30✔
841
  end
842
  rskip.value = "margin"
248✔
843
  -- while slice[#slice].discardable do table.remove(slice, #slice) end
844
  table.insert(slice, rskip)
248✔
845
  table.insert(slice, SILE.nodefactory.zerohbox())
496✔
846
  local lskip = margins[LTR and "lskip" or "rskip"]
248✔
847
  if not lskip then lskip = SILE.nodefactory.glue(0) end
248✔
848
  if hangLeft and hangLeft > 0 then
248✔
849
    lskip = SILE.nodefactory.glue({ width = lskip.width:tonumber() + hangLeft })
24✔
850
  end
851
  lskip.value = "margin"
248✔
852
  while slice[1].discardable do table.remove(slice, 1) end
248✔
853
  table.insert(slice, 1, lskip)
248✔
854
  table.insert(slice, 1, SILE.nodefactory.zerohbox())
496✔
855
end
856

857
function typesetter:breakpointsToLines (breakpoints)
28✔
858
  local linestart = 1
175✔
859
  local lines = {}
175✔
860
  local nodes = self.state.nodes
175✔
861

862
  for i = 1, #breakpoints do
427✔
863
    local point = breakpoints[i]
252✔
864
    if point.position ~= 0 then
252✔
865
      local slice = {}
252✔
866
      local seenNonDiscardable = false
252✔
867
      for j = linestart, point.position do
3,735✔
868
        slice[#slice+1] = nodes[j]
3,483✔
869
        if nodes[j] then
3,483✔
870
          if not nodes[j].discardable then
3,483✔
871
            seenNonDiscardable = true
2,220✔
872
          end
873
        end
874
      end
875
      if not seenNonDiscardable then
252✔
876
        -- Slip lines containing only discardable nodes (e.g. glues).
877
        SU.debug("typesetter", "Skipping a line containing only discardable nodes")
4✔
878
        linestart = point.position + 1
4✔
879
      else
880
        -- If the line ends with a discretionary, repeat it on the next line,
881
        -- so as to account for a potential postbreak.
882
        if slice[#slice].is_discretionary then
248✔
883
          linestart = point.position
11✔
884
        else
885
          linestart = point.position + 1
237✔
886
        end
887

888
        -- Then only we can add some extra margin glue...
889
        local mrg = self:getMargins()
248✔
890
        self:addrlskip(slice, mrg, point.left, point.right)
248✔
891

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

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

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

948
  local seenNodes = {}
248✔
949
  local skipping = true
248✔
950
  for i, node in ipairs(slice) do
4,719✔
951
    if node.is_box then
4,471✔
952
      skipping = false
2,256✔
953
      if node.parent and not node.parent.hyphenated then
2,256✔
954
        if not seenNodes[node.parent] then
414✔
955
          naturalTotals:___add(node.parent:lineContribution())
346✔
956
        end
957
        seenNodes[node.parent] = true
414✔
958
      else
959
        naturalTotals:___add(node:lineContribution())
3,684✔
960
      end
961
    elseif node.is_penalty and node.penalty == -inf_bad then
2,215✔
962
      skipping = false
172✔
963
    elseif node.is_discretionary then
2,043✔
964
      skipping = false
268✔
965
      local seen = node.parent and seenNodes[node.parent]
268✔
966
      if not seen and not node.used then
268✔
967
        naturalTotals:___add(node:replacementWidth():absolute())
15✔
968
        slice[i].height = slice[i]:replacementHeight():absolute()
15✔
969
      end
970
    elseif not skipping then
1,775✔
971
      naturalTotals:___add(node.width)
1,775✔
972
    end
973
  end
974

975
  -- From the line start, skip glues and margins, and check if it then starts
976
  -- with a used discretionary. If so, account for a postbreak.
977
  n = 1
248✔
978
  while n < #slice do
1,098✔
979
    if slice[n].is_discretionary and slice[n].used then
1,097✔
980
      naturalTotals:___add(slice[n]:postbreakWidth())
22✔
981
      slice[n].height = slice[n]:postbreakHeight()
22✔
982
      break
11✔
983
    elseif not (slice[n].is_glue or slice[n].is_zero) then
1,086✔
984
      break
236✔
985
    end
986
    n = n + 1
850✔
987
  end
988

989
  local _left = breakwidth:tonumber() - naturalTotals:tonumber()
744✔
990
  local ratio = _left / naturalTotals[_left < 0 and "shrink" or "stretch"]:tonumber()
496✔
991
  ratio = math.max(ratio, -1)
248✔
992
  return ratio, naturalTotals
248✔
993
end
994

995
function typesetter:chuck () -- emergency shipout everything
28✔
996
  self:leaveHmode(true)
31✔
997
  if (#self.state.outputQueue > 0) then
31✔
998
    SU.debug("typesetter", "Emergency shipout", #self.state.outputQueue, "lines in frame", self.frame.id)
19✔
999
    self:outputLinesToPage(self.state.outputQueue)
19✔
1000
    self.state.outputQueue = {}
19✔
1001
  end
1002
end
1003

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

1024
  self:pushState()
16✔
1025
  self.state.hmodeOnly = true
16✔
1026
  SILE.process(content)
16✔
1027

1028
  -- We must do a first pass for shaping the nnodes:
1029
  -- This is also where italic correction may occur.
1030
  local nodes = self:shapeAllNodes(self.state.nodes, false)
16✔
1031

1032
  -- Then we can process and measure the nodes.
1033
  local l = SILE.length()
16✔
1034
  local h, d = SILE.length(), SILE.length()
32✔
1035
  for i = 1, #nodes do
24✔
1036
    local node = nodes[i]
8✔
1037
    if node.is_migrating then
8✔
1038
      migratingNodes[#migratingNodes+1] = node
×
1039
    elseif node.is_discretionary then
8✔
1040
      -- HACK https://github.com/sile-typesetter/sile/issues/583
1041
      -- Discretionary nodes have a null line contribution...
1042
      -- But if discretionary nodes occur inside an hbox, since the latter
1043
      -- is not line-broken, they will never be marked as 'used' and will
1044
      -- evaluate to the replacement content (if any)...
1045
      recentContribution[#recentContribution+1] = node
×
1046
      l = l + node:replacementWidth():absolute()
×
1047
      -- The replacement content may have ascenders and descenders...
1048
      local hdisc = node:replacementHeight():absolute()
×
1049
      local ddisc = node:replacementDepth():absolute()
×
1050
      h = hdisc > h and hdisc or h
×
1051
      d = ddisc > d and ddisc or d
×
1052
      -- By the way it's unclear how this is expected to work in TTB
1053
      -- writing direction. For other type of nodes, the line contribution
1054
      -- evaluates to the height rather than the width in TTB, but the
1055
      -- whole logic might then be dubious there too...
1056
    else
1057
      recentContribution[#recentContribution+1] = node
8✔
1058
      l = l + node:lineContribution():absolute()
24✔
1059
      h = node.height > h and node.height or h
13✔
1060
      d = node.depth > d and node.depth or d
14✔
1061
    end
1062
  end
1063
  self:popState()
16✔
1064

1065
  local hbox = SILE.nodefactory.hbox({
32✔
1066
      height = h,
16✔
1067
      width = l,
16✔
1068
      depth = d,
16✔
1069
      value = recentContribution,
16✔
1070
      outputYourself = function (box, atypesetter, line)
1071
        local _post = _rtl_pre_post(box, atypesetter, line)
15✔
1072
        local ox = atypesetter.frame.state.cursorX
15✔
1073
        local oy = atypesetter.frame.state.cursorY
15✔
1074
        SILE.outputter:setCursor(atypesetter.frame.state.cursorX, atypesetter.frame.state.cursorY)
15✔
1075
        for _, node in ipairs(box.value) do
23✔
1076
          node:outputYourself(atypesetter, line)
8✔
1077
        end
1078
        atypesetter.frame.state.cursorX = ox
15✔
1079
        atypesetter.frame.state.cursorY = oy
15✔
1080
        _post()
15✔
1081
        SU.debug("hboxes", function ()
30✔
1082
          SILE.outputter:debugHbox(box, box:scaledWidth(line))
×
1083
          return "Drew debug outline around hbox"
×
1084
        end)
1085
      end
1086
    })
1087
  return hbox, migratingNodes
16✔
1088
end
1089

1090
function typesetter:pushHlist (hlist)
28✔
1091
  for _, h in ipairs(hlist) do
4✔
1092
    self:pushHorizontal(h)
×
1093
  end
1094
end
1095

1096
return typesetter
28✔
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

© 2025 Coveralls, Inc