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

sile-typesetter / sile / 9409557472

07 Jun 2024 12:09AM UTC coverage: 69.448% (-4.5%) from 73.988%
9409557472

push

github

alerque
fix(build): Distribute vendored compat-5.3.c source file

12025 of 17315 relevant lines covered (69.45%)

6023.46 hits per line

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

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

3
local package = pl.class(base)
15✔
4
package._name = "insertions"
15✔
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")
16✔
46
   SU.required(options, "stealFrom", "initializing insertions")
16✔
47
   SU.required(options, "maxHeight", "initializing insertions")
16✔
48
   if not options.topSkip and not options.topBox then
16✔
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
32✔
54
      options.stealFrom = { options.stealFrom }
×
55
   end
56
   if options.stealFrom[1] then
16✔
57
      local rl = {}
15✔
58
      for i = 1, #options.stealFrom do
30✔
59
         rl[options.stealFrom[i]] = 1
15✔
60
      end
61
      options.stealFrom = rl
15✔
62
   end
63

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

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

70
   SILE.scratch.insertions.classes[classname] = options
16✔
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 = {}
15✔
82
SILE.types.node.insertionlist = pl.class(SILE.types.node.vbox)
30✔
83

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

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

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

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

103
local thisPageInsertionBoxForClass = function (class)
104
   if not insertionsThisPage[class] then
28✔
105
      insertionsThisPage[class] = SILE.types.node.insertionlist({
14✔
106
         frame = SILE.scratch.insertions.classes[class].insertInto.frame,
7✔
107
      })
7✔
108
   end
109
   return insertionsThisPage[class]
28✔
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)
30✔
122

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

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

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

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

141
function SILE.types.node.insertion:split (materialToSplit, maxsize)
30✔
142
   local firstpage = SILE.pagebuilder:findBestBreak({
4✔
143
      vboxlist = materialToSplit,
2✔
144
      target = maxsize,
2✔
145
      restart = false,
146
      force = true,
147
   })
148
   if firstpage then
2✔
149
      self.nodes = {}
2✔
150
      self:append(materialToSplit)
2✔
151
      self.contentHeight = self.height
2✔
152
      self.contentDepth = self.depth
2✔
153
      self.depth = SILE.types.length(0)
4✔
154
      self.height = SILE.types.length(0)
4✔
155
      return SILE.pagebuilder:collateVboxes(firstpage)
2✔
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
284✔
172
      frame:init()
1✔
173
   end
174
   if not frame.state.totals.shrinkage then
284✔
175
      frame.state.totals.shrinkage = SILE.types.measurement(0)
90✔
176
   end
177
end
178

179
local nextInterInsertionSkip = function (class)
180
   local options = SILE.scratch.insertions.classes[class]
14✔
181
   local stuffSoFar = thisPageInsertionBoxForClass(class)
14✔
182
   if #stuffSoFar.nodes == 0 then
14✔
183
      if options["topBox"] then
7✔
184
         return options["topBox"]:absolute()
7✔
185
      elseif options["topSkip"] then
×
186
         return SILE.types.node.vglue(options["topSkip"]:tonumber())
×
187
      end
188
   else
189
      local skipSize = options["interInsertionSkip"]:tonumber()
7✔
190
      skipSize = skipSize - stuffSoFar.nodes[#stuffSoFar.nodes].depth:tonumber()
14✔
191
      return SILE.types.node.vglue(skipSize)
7✔
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)
212
   local insertion = SILE.scratch.insertions.classes[classname]
12✔
213
   if not insertion then
12✔
214
      SU.error("Uninitialized insertion class " .. classname)
×
215
   end
216
   SILE.typesetter:pushMigratingMaterial({
24✔
217
      SILE.types.node.penalty(SILE.settings:get("insertion.penalty")),
36✔
218
   })
219
   SILE.typesetter:pushMigratingMaterial({
24✔
220
      SILE.types.node.insertion({
24✔
221
         class = classname,
12✔
222
         nodes = vbox.nodes,
12✔
223
         -- actual height and depth must remain zero for page glue calculations
224
         contentHeight = vbox.height,
12✔
225
         contentDepth = vbox.depth,
12✔
226
         frame = insertion.insertInto.frame,
12✔
227
         parent = SILE.typesetter.frame,
12✔
228
      }),
12✔
229
   })
230
end
231

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

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

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

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

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

278
   SILE.insertions.increaseInsertionFrame = function (insertionvbox, classname)
16✔
279
      local amount = insertionvbox.height + insertionvbox.depth
7✔
280
      local opts = SILE.scratch.insertions.classes[classname]
7✔
281
      SU.debug("insertions", "Increasing insertion frame by", amount)
7✔
282
      local stealPosition = opts["steal-position"] or "bottom"
7✔
283
      local insertionFrame = SILE.getFrame(opts["insertInto"].frame)
7✔
284
      local oldHeight = insertionFrame:height()
7✔
285
      amount = amount * opts["insertInto"].ratio
7✔
286
      insertionFrame:constrain("height", oldHeight + amount)
14✔
287
      if stealPosition == "bottom" then
7✔
288
         insertionFrame:relax("top")
7✔
289
      end
290
      SU.debug("insertions", "New height is now", insertionFrame:height())
14✔
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)
16✔
315
      local ins = vboxlist[i]
61✔
316
      if ins.seen then
61✔
317
         return target
47✔
318
      end
319
      local targetFrame = SILE.getFrame(ins.frame)
14✔
320
      local options = SILE.scratch.insertions.classes[ins.class]
14✔
321

322
      ins:dropDiscardables()
14✔
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.
326
      local topBox = nextInterInsertionSkip(ins.class)
14✔
327
      local insertionsHeight = SILE.types.length()
14✔
328
      insertionsHeight:___add(ins.contentHeight)
14✔
329
      insertionsHeight:___add(topBox.height)
14✔
330
      insertionsHeight:___add(topBox.depth)
14✔
331
      insertionsHeight:___add(ins.contentDepth)
14✔
332

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

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

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

348
      local newTarget = target - effectOnThisFrame
14✔
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

354
      if totalHeight + effectOnThisFrame <= target and insbox.height + insertionsHeight <= options.maxHeight then
52✔
355
         SU.debug("insertions", "fits")
12✔
356
         SILE.insertions.setShrinkage(ins.class, insertionsHeight)
12✔
357
         insbox:append(topBox)
12✔
358
         insbox:append(ins)
12✔
359
         ins.seen = true
12✔
360
         return newTarget
12✔
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.
365
      SU.debug("insertions", "splitting")
2✔
366
      local maxsize = SU.min(target - totalHeight, options.maxHeight)
4✔
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.
372
      maxsize = maxsize - topBox.height
2✔
373
      local materialToSplit = {}
2✔
374
      pl.tablex.insertvalues(materialToSplit, ins:unbox())
4✔
375
      local deferredInsertions = ins:split(materialToSplit, maxsize)
2✔
376

377
      if deferredInsertions then
2✔
378
         SU.debug("insertions", "Split. Remaining insertion is", ins)
2✔
379
         SILE.insertions.setShrinkage(
4✔
380
            ins.class,
2✔
381
            topBox.height:absolute() + deferredInsertions.height:absolute() + deferredInsertions.depth:absolute()
10✔
382
         )
383
         insbox:append(topBox)
2✔
384
         -- deferredInsertions.contentHeight = deferredInsertions.height
385
         -- deferredInsertions.contentDepth = deferredInsertions.depth
386
         insbox:append(deferredInsertions)
2✔
387
         deferredInsertions.seen = true
2✔
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

400
         table.insert(vboxlist, i, SILE.types.node.penalty(-20000))
4✔
401
         return target -- Who cares? The penalty is going to cause a split.
2✔
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 (_)
32✔
443
      local typesetter = SILE.typesetter
16✔
444

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

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

458
      typesetter:registerPageEndHook(function (_)
32✔
459
         pl.tablex.foreach(insertionsThisPage, SILE.insertions.increaseInsertionFrame)
34✔
460
         for insertionclass, insertionlist in pairs(insertionsThisPage) do
41✔
461
            insertionlist:outputYourself()
7✔
462
            insertionsThisPage[insertionclass] = nil
7✔
463
         end
464
         if SU.debugging("insertions") then
68✔
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)
16✔
473
   self:export("thisPageInsertionBoxForClass", thisPageInsertionBoxForClass)
16✔
474
   self:export("insert", insert)
16✔
475
end
476

477
function package.declareSettings (_)
15✔
478
   SILE.settings:declare({
15✔
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 realise that insertions are implemented by an external add-on package, rather than being part of the SILE core.
491
\end{document}
492
]]
15✔
493

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