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

sile-typesetter / sile / 11124789710

01 Oct 2024 11:57AM UTC coverage: 29.567% (-31.4%) from 60.926%
11124789710

push

github

web-flow
Merge pull request #2105 from Omikhleia/refactor-collated-sort

0 of 10 new or added lines in 1 file covered. (0.0%)

5252 existing lines in 53 files now uncovered.

5048 of 17073 relevant lines covered (29.57%)

1856.13 hits per line

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

31.55
/packages/insertions/init.lua
1
local base = require("packages.base")
1✔
2

3
local package = pl.class(base)
1✔
4
package._name = "insertions"
1✔
5

6
--[[
7
 Insertions handling is the most complicated and bug-prone part of
8
 SILE, and thus deserves to be well documented. If you plan to work
9
 on this, it would help you a lot to read chapter 29 of "TeX By Topic"
10
 by Victor Eijkhout.
11

12
 Note: While most SILE packages are completely self-contained, this one
13
 requires some support from the SILE core. We'll get to it later, but
14
 when the page builder comes across some of the magic boxes that are
15
 defined as part of insertions handling, it triggers a routine here
16
 which can alter the page builder's operation. So it is not really an
17
 optional package.
18

19
--
20

21
The typical insertion is a footnote but we provide a generic mechanism for
22
handling any kind of insertion. An insertion is material which is added from
23
the main flow of the document into a specific area on the page, and where
24
moving such material alters the shape of the page.
25

26
Because this is a generic mechanism, each insertion is part of a "class"
27
(just like in TeX.); you may have several different classes putting insertions
28
in different places on the page. These classes also define how the page shape
29
is altered by the insertion. So, a class using the footnotes package may specify
30
that insertions get added into the `footnotes' frame and the space used up by
31
those insertions also reduces the 'content' frame. If you have two columns but
32
one footnote frame, then an insertion will steal space equal to half the
33
insertion's height from each column's frame. This is specified as:
34

35
  insertInto = "footnotes",
36
  stealFrom = { leftColumn = 0.5, rightColumn = 0.5 }
37

38
Insertion classes also need to define the maximum height they can stretch to,
39
the skip or box placed before the first insertion on a page, and the skip placed
40
between insertions.
41

42
--]]
43

44
local initInsertionClass = function (_, classname, options)
45
   SU.required(options, "insertInto", "initializing insertions")
1✔
46
   SU.required(options, "stealFrom", "initializing insertions")
1✔
47
   SU.required(options, "maxHeight", "initializing insertions")
1✔
48
   if not options.topSkip and not options.topBox then
1✔
49
      SU.required(options, "topSkip", "initializing insertions")
×
50
   end
51

52
   -- Turn stealFrom into a hash, if it isn't one.
53
   if SU.type(options.stealFrom) ~= "table" then
2✔
54
      options.stealFrom = { options.stealFrom }
×
55
   end
56
   if options.stealFrom[1] then
1✔
57
      local rl = {}
1✔
58
      for i = 1, #options.stealFrom do
2✔
59
         rl[options.stealFrom[i]] = 1
1✔
60
      end
61
      options.stealFrom = rl
1✔
62
   end
63

64
   if SU.type(options.insertInto) ~= "table" then
2✔
65
      options.insertInto = { frame = options.insertInto, ratio = 1 }
1✔
66
   end
67

68
   options.maxHeight = SILE.types.length(options.maxHeight)
2✔
69

70
   SILE.scratch.insertions.classes[classname] = options
1✔
71
end
72

73
--[[
74

75
Each insertion class stores a page's worth of content in a box.
76
In some ways it's a fairly standard vbox, but it also knows its own
77
typesetter and frame.
78

79
--]]
80

81
local insertionsThisPage = {}
1✔
82
SILE.types.node.insertionlist = pl.class(SILE.types.node.vbox)
2✔
83

84
SILE.types.node.insertionlist.type = "insertionlist"
1✔
85
SILE.types.node.insertionlist.frame = nil
1✔
86

87
function SILE.types.node.insertionlist:_init (spec)
2✔
UNCOV
88
   SILE.types.node.vbox._init(self, spec)
×
UNCOV
89
   self.typesetter = SILE.typesetters.base()
×
90
end
91

92
function SILE.types.node.insertionlist:__tostring ()
2✔
93
   return "PI<" .. self.nodes .. ">"
×
94
end
95

96
function SILE.types.node.insertionlist:outputYourself ()
2✔
UNCOV
97
   self.typesetter:initFrame(SILE.getFrame(self.frame))
×
UNCOV
98
   for _, node in ipairs(self.nodes) do
×
UNCOV
99
      node:outputYourself(self.typesetter, node)
×
100
   end
101
end
102

103
local thisPageInsertionBoxForClass = function (class)
UNCOV
104
   if not insertionsThisPage[class] then
×
UNCOV
105
      insertionsThisPage[class] = SILE.types.node.insertionlist({
×
106
         frame = SILE.scratch.insertions.classes[class].insertInto.frame,
107
      })
108
   end
UNCOV
109
   return insertionsThisPage[class]
×
110
end
111

112
--[[
113

114
An insertion vbox, on the other hand, is a place where insertion material
115
is held until we are sure it is going to end up on the current page. (We
116
might be consuming material that will eventually end up on a future page.)
117
So we stick the material into an insertion vbox, and when the pagebuilder
118
sees this, magic will happen.
119

120
--]]
121
SILE.types.node.insertion = pl.class(SILE.types.node.vbox)
2✔
122

123
SILE.types.node.insertion.discardable = true
1✔
124
SILE.types.node.insertion.type = "insertion"
1✔
125
SILE.types.node.insertion.seen = false
1✔
126

127
function SILE.types.node.insertion:__tostring ()
2✔
128
   return "I<" .. self.nodes[1] .. "...>"
×
129
end
130

131
function SILE.types.node.insertion.outputYourself (_) end
1✔
132

133
-- And some utility methods to make the insertion processing code
134
-- easier to read.
135
function SILE.types.node.insertion:dropDiscardables ()
2✔
UNCOV
136
   while #self.nodes > 1 and self.nodes[#self.nodes].discardable do
×
137
      self.nodes[#self.nodes] = nil
×
138
   end
139
end
140

141
function SILE.types.node.insertion:split (materialToSplit, maxsize)
2✔
UNCOV
142
   local firstpage = SILE.pagebuilder:findBestBreak({
×
143
      vboxlist = materialToSplit,
144
      target = maxsize,
145
      restart = false,
146
      force = true,
147
   })
UNCOV
148
   if firstpage then
×
UNCOV
149
      self.nodes = {}
×
UNCOV
150
      self:append(materialToSplit)
×
UNCOV
151
      self.contentHeight = self.height
×
UNCOV
152
      self.contentDepth = self.depth
×
UNCOV
153
      self.depth = SILE.types.length(0)
×
UNCOV
154
      self.height = SILE.types.length(0)
×
UNCOV
155
      return SILE.pagebuilder:collateVboxes(firstpage)
×
156
   end
157
end
158

159
--[[
160

161
Set up a value to track how much smaller/larger to make a frame.
162
We have to track this on the frame, because different insertion
163
classes might affect the same frame; so we can't track it per class.
164
We also have to ensure it's initialized every time because we might
165
be shrinking a frame further down the page that the typesetter hasn't
166
entered yet.
167

168
--]]
169

170
local initShrinkage = function (frame)
171
   if not frame.state or not frame.state.totals then
62✔
UNCOV
172
      frame:init()
×
173
   end
174
   if not frame.state.totals.shrinkage then
62✔
175
      frame.state.totals.shrinkage = SILE.types.measurement(0)
30✔
176
   end
177
end
178

179
local nextInterInsertionSkip = function (class)
UNCOV
180
   local options = SILE.scratch.insertions.classes[class]
×
UNCOV
181
   local stuffSoFar = thisPageInsertionBoxForClass(class)
×
UNCOV
182
   if #stuffSoFar.nodes == 0 then
×
UNCOV
183
      if options["topBox"] then
×
UNCOV
184
         return options["topBox"]:absolute()
×
185
      elseif options["topSkip"] then
×
186
         return SILE.types.node.vglue(options["topSkip"]:tonumber())
×
187
      end
188
   else
189
      local skipSize = options["interInsertionSkip"]:tonumber()
×
190
      skipSize = skipSize - stuffSoFar.nodes[#stuffSoFar.nodes].depth:tonumber()
×
191
      return SILE.types.node.vglue(skipSize)
×
192
   end
193
end
194

195
local debugInsertion = function (ins, insbox, topBox, target, targetFrame, totalHeight)
196
   local insertionsHeight = ins.contentHeight:absolute()
×
197
      + topBox.height:absolute()
×
198
      + topBox.depth:absolute()
×
199
      + ins.contentDepth:absolute()
×
200
   SU.debug("insertions", "## Incoming insertion")
×
201
   SU.debug("insertions", "Top box height", topBox.height)
×
202
   SU.debug("insertions", "Insertion", ins, ins.height, ins.depth)
×
203
   SU.debug("insertions", "Total incoming height", insertionsHeight)
×
204
   SU.debug("insertions", "Insertions already in this class", insbox.height, insbox.depth)
×
205
   SU.debug("insertions", "Page target", target)
×
206
   SU.debug("insertions", "Page frame", targetFrame)
×
207
   SU.debug("insertions", totalHeight, "worth of content on page so far")
×
208
end
209

210
-- This just puts the insertion vbox into the typesetter's queues.
211
local insert = function (_, classname, vbox)
UNCOV
212
   local insertion = SILE.scratch.insertions.classes[classname]
×
UNCOV
213
   if not insertion then
×
214
      SU.error("Uninitialized insertion class " .. classname)
×
215
   end
UNCOV
216
   SILE.typesetter:pushMigratingMaterial({
×
UNCOV
217
      SILE.types.node.penalty(SILE.settings:get("insertion.penalty")),
×
218
   })
UNCOV
219
   SILE.typesetter:pushMigratingMaterial({
×
UNCOV
220
      SILE.types.node.insertion({
×
221
         class = classname,
222
         nodes = vbox.nodes,
223
         -- actual height and depth must remain zero for page glue calculations
224
         contentHeight = vbox.height,
225
         contentDepth = vbox.depth,
226
         frame = insertion.insertInto.frame,
227
         parent = SILE.typesetter.frame,
UNCOV
228
      }),
×
229
   })
230
end
231

232
function package:_init ()
1✔
233
   base._init(self)
1✔
234
   if not SILE.scratch.insertions then
1✔
235
      SILE.scratch.insertions = { classes = {} }
1✔
236
   end
237
   if not SILE.insertions then
1✔
238
      SILE.insertions = {}
1✔
239
   end
240

241
   --[[ Mark a frame for reduction. --]]
242

243
   SILE.insertions.setShrinkage = function (classname, amount)
1✔
UNCOV
244
      local reduceList = SILE.scratch.insertions.classes[classname].stealFrom
×
UNCOV
245
      for fName, ratio in pairs(reduceList) do
×
UNCOV
246
         local frame = SILE.getFrame(fName)
×
UNCOV
247
         if frame then
×
UNCOV
248
            initShrinkage(frame)
×
UNCOV
249
            SU.debug("insertions", "Shrinking", fName, "by", amount * ratio)
×
UNCOV
250
            frame.state.totals.shrinkage = frame.state.totals.shrinkage + amount * ratio
×
251
         end
252
      end
253
   end
254

255
   --[[ Actually shrink the frame. --]]
256

257
   SILE.insertions.commitShrinkage = function (_, classname)
1✔
UNCOV
258
      local opts = SILE.scratch.insertions.classes[classname]
×
UNCOV
259
      local reduceList = opts["stealFrom"]
×
UNCOV
260
      local stealPosition = opts["steal-position"] or "bottom"
×
UNCOV
261
      for fName, _ in pairs(reduceList) do
×
UNCOV
262
         local frame = SILE.getFrame(fName)
×
UNCOV
263
         if frame then
×
UNCOV
264
            initShrinkage(frame)
×
UNCOV
265
            local newHeight = frame:height() - frame.state.totals.shrinkage
×
UNCOV
266
            if stealPosition == "bottom" then
×
UNCOV
267
               frame:relax("bottom")
×
268
            else
269
               frame:relax("top")
×
270
            end
UNCOV
271
            SU.debug("insertions", "Constraining height of", fName, "by", frame.state.totals.shrinkage, "to", newHeight)
×
UNCOV
272
            frame:constrain("height", newHeight)
×
UNCOV
273
            frame.state.totals.shrinkage = SILE.types.measurement(0)
×
274
         end
275
      end
276
   end
277

278
   SILE.insertions.increaseInsertionFrame = function (insertionvbox, classname)
1✔
UNCOV
279
      local amount = insertionvbox.height + insertionvbox.depth
×
UNCOV
280
      local opts = SILE.scratch.insertions.classes[classname]
×
UNCOV
281
      SU.debug("insertions", "Increasing insertion frame by", amount)
×
UNCOV
282
      local stealPosition = opts["steal-position"] or "bottom"
×
UNCOV
283
      local insertionFrame = SILE.getFrame(opts["insertInto"].frame)
×
UNCOV
284
      local oldHeight = insertionFrame:height()
×
UNCOV
285
      amount = amount * opts["insertInto"].ratio
×
UNCOV
286
      insertionFrame:constrain("height", oldHeight + amount)
×
UNCOV
287
      if stealPosition == "bottom" then
×
UNCOV
288
         insertionFrame:relax("top")
×
289
      end
UNCOV
290
      SU.debug("insertions", "New height is now", insertionFrame:height())
×
291
   end
292

293
   --[[
294
  So, this is the magic routine called by the page builder to determine what
295
  do to when an insertion is seen in the vertical list. The key design issue
296
  about this routine is that it needs to be very careful about state; it may
297
  end up processing the same list different times. (if the current list of
298
  vertical items is not tall enough to cause a page break yet) So it should
299
  not commit itself to anything yet. Another interesting complication is that
300
  when the page builder restarts, for optimization purposes it is at liberty
301
  to restart its calculations half-way through the list. So you can't
302
  completely forget the insertions that you've seen either.
303

304
  However, one mitigating factor is: if an insertion fits on the current page,
305
  it will end up on the current page. So if you've seen an insertion and it
306
  fits, you can commit to it at this point. If at some later date we have
307
  page builders which reflow multiple pages, then this may not be true.
308

309
  The main job is this routine is to make a decision about whether the
310
  upcoming insertion can fit on the page; if it needs to be split; or if it
311
  should not appear on this page at all (and hence force the line which
312
  caused the insertion off the page as well).
313
  --]]
314
   SILE.insertions.processInsertion = function (vboxlist, i, totalHeight, target)
1✔
UNCOV
315
      local ins = vboxlist[i]
×
UNCOV
316
      if ins.seen then
×
UNCOV
317
         return target
×
318
      end
UNCOV
319
      local targetFrame = SILE.getFrame(ins.frame)
×
UNCOV
320
      local options = SILE.scratch.insertions.classes[ins.class]
×
321

UNCOV
322
      ins:dropDiscardables()
×
323

324
      -- We look into the page's insertion box and choose the appropriate skip,
325
      -- so we know how high the whole insertion is.
UNCOV
326
      local topBox = nextInterInsertionSkip(ins.class)
×
UNCOV
327
      local insertionsHeight = SILE.types.length()
×
UNCOV
328
      insertionsHeight:___add(ins.contentHeight)
×
UNCOV
329
      insertionsHeight:___add(topBox.height)
×
UNCOV
330
      insertionsHeight:___add(topBox.depth)
×
UNCOV
331
      insertionsHeight:___add(ins.contentDepth)
×
332

UNCOV
333
      local insbox = thisPageInsertionBoxForClass(ins.class)
×
UNCOV
334
      initShrinkage(targetFrame)
×
UNCOV
335
      initShrinkage(SILE.typesetter.frame)
×
336

UNCOV
337
      if SU.debugging("insertions") then
×
338
         debugInsertion(ins, insbox, topBox, target, targetFrame, totalHeight)
×
339
      end
340

UNCOV
341
      local effectOnThisFrame = options.stealFrom[SILE.typesetter.frame.id]
×
UNCOV
342
      if effectOnThisFrame then
×
UNCOV
343
         effectOnThisFrame = insertionsHeight * effectOnThisFrame
×
344
      else
345
         effectOnThisFrame = SILE.types.measurement(0)
×
346
      end
347

UNCOV
348
      local newTarget = target - effectOnThisFrame
×
349

350
      -- We only fit if:
351
      -- the effect of the insertion on this frame doesn't take us over the page target
352
      -- and this doesn't take the target frame over the max height.
353

UNCOV
354
      if totalHeight + effectOnThisFrame <= target and insbox.height + insertionsHeight <= options.maxHeight then
×
UNCOV
355
         SU.debug("insertions", "fits")
×
UNCOV
356
         SILE.insertions.setShrinkage(ins.class, insertionsHeight)
×
UNCOV
357
         insbox:append(topBox)
×
UNCOV
358
         insbox:append(ins)
×
UNCOV
359
         ins.seen = true
×
UNCOV
360
         return newTarget
×
361
      end
362

363
      -- OK, we didn't fit. So now we have to split the insertion to fit the height
364
      -- we have within the insertion frame.
UNCOV
365
      SU.debug("insertions", "splitting")
×
UNCOV
366
      local maxsize = SU.min(target - totalHeight, options.maxHeight)
×
367

368
      -- If we're going to fit this insertion on the page, we will use the
369
      -- whole of topbox, so let's subtract the height of that now.
370
      -- The remaining height will be the amount of inserted material that we
371
      -- intend to put on this page.
UNCOV
372
      maxsize = maxsize - topBox.height
×
UNCOV
373
      local materialToSplit = {}
×
UNCOV
374
      pl.tablex.insertvalues(materialToSplit, ins:unbox())
×
UNCOV
375
      local deferredInsertions = ins:split(materialToSplit, maxsize)
×
376

UNCOV
377
      if deferredInsertions then
×
UNCOV
378
         SU.debug("insertions", "Split. Remaining insertion is", ins)
×
UNCOV
379
         SILE.insertions.setShrinkage(
×
UNCOV
380
            ins.class,
×
UNCOV
381
            topBox.height:absolute() + deferredInsertions.height:absolute() + deferredInsertions.depth:absolute()
×
382
         )
UNCOV
383
         insbox:append(topBox)
×
384
         -- deferredInsertions.contentHeight = deferredInsertions.height
385
         -- deferredInsertions.contentDepth = deferredInsertions.depth
UNCOV
386
         insbox:append(deferredInsertions)
×
UNCOV
387
         deferredInsertions.seen = true
×
388

389
         --[[ The insertion we're dealing with is currently vboxlist[i], and it
390
      now contains all the material that *didn't* make it onto the current
391
      page. We've dealt with the material that did fit on the page. We want
392
      the page builder to a) break the page immediately here - it's full by
393
      definition, or else we would not have needed to split (XXX This is not
394
      true, because we might not be stealing from the current frame.) and
395
      then b) retry the remaining material. By inserting a penalty into the
396
      vboxlist, when we return from here the pagebuilder will first consider
397
      the penalty (and break the page) and then consider the rest of the
398
      insertion. --]]
399

UNCOV
400
         table.insert(vboxlist, i, SILE.types.node.penalty(-20000))
×
UNCOV
401
         return target -- Who cares? The penalty is going to cause a split.
×
402
      end
403

404
      --[[ We couldn't even split the insertion.
405

406
    Assume that previously we have seen these nodes on the vboxlist:
407

408
      i-2 \vbox{Hello world. I am a footnote mark: 1}
409
      i-1 \vglue
410
      i   \insertionbox{1. Footnote}
411

412
    This insertion couldn't fit on the page at all. Therefore we need to
413
    take both the footnote *and* the previous vbox (from which the
414
    insertion has been migrated) onto a new page. The way we achieve this
415
    is by finding the most recent vbox and then continually inserting page
416
    break penalties, pushing the insertion and vbox further down the list
417
    until they are the next things to be considered. The end result of the
418
    vboxlist will be:
419

420
      i-2 \penalty
421
      i-1 \penalty
422
      i   \penalty <- considered now when we return to pagebuilder
423
      i+1 \vbox{Hello world. I am a footnote mark: 1}
424
      i+2 \vglue
425
      i+3 \insertionbox{1. Footnote}
426

427
    The page will be broken right now, and the old vbox will no longer
428
    be part of the material to be output on the page. The new page will
429
    start with the vbox and the insertion.
430

431
    --]]
432
      local lastbox = i
×
433
      while not vboxlist[lastbox].is_vbox do
×
434
         lastbox = lastbox - 1
×
435
      end
436
      while not (vboxlist[i].is_penalty and vboxlist[i].penalty == -20000) do
×
437
         table.insert(vboxlist, lastbox, SILE.types.node.penalty(-20000))
×
438
      end
439
      return target
×
440
   end
441

442
   self.class:registerPostinit(function (_)
2✔
443
      local typesetter = SILE.typesetter
1✔
444

445
      if not typesetter.noinsertion_getTargetLength then
1✔
446
         typesetter.noinsertion_getTargetLength = typesetter.getTargetLength
1✔
447
         typesetter.getTargetLength = function (self_)
448
            initShrinkage(self_.frame)
62✔
449
            return typesetter.noinsertion_getTargetLength(self_) - self_.frame.state.totals.shrinkage
186✔
450
         end
451
      end
452

453
      typesetter:registerFrameBreakHook(function (_, nodelist)
2✔
454
         pl.tablex.foreach(insertionsThisPage, SILE.insertions.commitShrinkage)
15✔
455
         return nodelist
15✔
456
      end)
457

458
      typesetter:registerPageEndHook(function (_)
2✔
459
         pl.tablex.foreach(insertionsThisPage, SILE.insertions.increaseInsertionFrame)
15✔
460
         for insertionclass, insertionlist in pairs(insertionsThisPage) do
15✔
UNCOV
461
            insertionlist:outputYourself()
×
UNCOV
462
            insertionsThisPage[insertionclass] = nil
×
463
         end
464
         if SU.debugging("insertions") then
30✔
465
            for _, frame in pairs(SILE.frames) do
×
466
               SILE.outputter:debugFrame(frame)
×
467
            end
468
         end
469
      end)
470
   end)
471

472
   self:export("initInsertionClass", initInsertionClass)
1✔
473
   self:export("thisPageInsertionBoxForClass", thisPageInsertionBoxForClass)
1✔
474
   self:export("insert", insert)
1✔
475
end
476

477
function package.declareSettings (_)
1✔
478
   SILE.settings:declare({
1✔
479
      parameter = "insertion.penalty",
480
      type = "integer",
481
      default = -3000,
482
      help = "Penalty to be applied before insertion",
483
   })
484
end
485

486
package.documentation = [[
487
\begin{document}
488
The \autodoc:package{footnotes} package works by taking auxiliary material (the footnote content), shrinking the current frame and inserting it into the footnote frame.
489
This is powered by the \autodoc:package{insertions} package; it doesn’t provide any user-visible SILE commands, but provides Lua functionality to other packages.
490
TeX wizards may be interested to realize that insertions are implemented by an external add-on package, rather than being part of the SILE core.
491
\end{document}
492
]]
1✔
493

494
return package
1✔
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