local base = require("packages.base")
local package = pl.class(base)
package._name = "insertions"
local initInsertionClass = function (_, classname, options)
SU.required(options, "insertInto", "initializing insertions")
SU.required(options, "stealFrom", "initializing insertions")
SU.required(options, "maxHeight", "initializing insertions")
if not options.topSkip and not options.topBox then
SU.required(options, "topSkip", "initializing insertions")
end
if SU.type(options.stealFrom) ~= "table" then
options.stealFrom = { options.stealFrom }
end
if options.stealFrom[1] then
local rl = {}
for i = 1, #options.stealFrom do
rl[options.stealFrom[i]] = 1
end
options.stealFrom = rl
end
if SU.type(options.insertInto) ~= "table" then
options.insertInto = { frame = options.insertInto, ratio = 1 }
end
options.maxHeight = SILE.types.length(options.maxHeight)
SILE.scratch.insertions.classes[classname] = options
end
local insertionsThisPage = {}
SILE.types.node.insertionlist = pl.class(SILE.types.node.vbox)
SILE.types.node.insertionlist.type = "insertionlist"
SILE.types.node.insertionlist.frame = nil
function SILE.types.node.insertionlist:_init (spec)
SILE.types.node.vbox._init(self, spec)
self.typesetter = SILE.typesetters.default()
end
function SILE.types.node.insertionlist:__tostring ()
return "PI<" .. self.nodes .. ">"
end
function SILE.types.node.insertionlist:outputYourself ()
self.typesetter:initFrame(SILE.getFrame(self.frame))
for _, node in ipairs(self.nodes) do
node:outputYourself(self.typesetter, node)
end
end
local thisPageInsertionBoxForClass = function (class)
if not insertionsThisPage[class] then
insertionsThisPage[class] = SILE.types.node.insertionlist({
frame = SILE.scratch.insertions.classes[class].insertInto.frame,
})
end
return insertionsThisPage[class]
end
SILE.types.node.insertion = pl.class(SILE.types.node.vbox)
SILE.types.node.insertion.discardable = true
SILE.types.node.insertion.type = "insertion"
SILE.types.node.insertion.seen = false
function SILE.types.node.insertion:__tostring ()
return "I<" .. self.nodes[1] .. "...>"
end
function SILE.types.node.insertion:outputYourself () end
function SILE.types.node.insertion:dropDiscardables ()
while #self.nodes > 1 and self.nodes[#self.nodes].discardable do
self.nodes[#self.nodes] = nil
end
end
function SILE.types.node.insertion:split (materialToSplit, maxsize)
local firstpage = SILE.pagebuilder:findBestBreak({
vboxlist = materialToSplit,
target = maxsize,
restart = false,
force = true,
})
if firstpage then
self.nodes = {}
self:append(materialToSplit)
self.contentHeight = self.height
self.contentDepth = self.depth
self.depth = SILE.types.length(0)
self.height = SILE.types.length(0)
return SILE.pagebuilder:collateVboxes(firstpage)
end
end
local initShrinkage = function (frame)
if not frame.state or not frame.state.totals then
frame:init()
end
if not frame.state.totals.shrinkage then
frame.state.totals.shrinkage = SILE.types.measurement(0)
end
end
local nextInterInsertionSkip = function (class)
local options = SILE.scratch.insertions.classes[class]
local stuffSoFar = thisPageInsertionBoxForClass(class)
if #stuffSoFar.nodes == 0 then
if options["topBox"] then
return options["topBox"]:absolute()
elseif options["topSkip"] then
return SILE.types.node.vglue(options["topSkip"]:tonumber())
end
else
local skipSize = options["interInsertionSkip"]:tonumber()
skipSize = skipSize - stuffSoFar.nodes[#stuffSoFar.nodes].depth:tonumber()
return SILE.types.node.vglue(skipSize)
end
end
local debugInsertion = function (ins, insbox, topBox, target, targetFrame, totalHeight)
local insertionsHeight = ins.contentHeight:absolute()
+ topBox.height:absolute()
+ topBox.depth:absolute()
+ ins.contentDepth:absolute()
SU.debug("insertions", "## Incoming insertion")
SU.debug("insertions", "Top box height", topBox.height)
SU.debug("insertions", "Insertion", ins, ins.height, ins.depth)
SU.debug("insertions", "Total incoming height", insertionsHeight)
SU.debug("insertions", "Insertions already in this class", insbox.height, insbox.depth)
SU.debug("insertions", "Page target", target)
SU.debug("insertions", "Page frame", targetFrame)
SU.debug("insertions", totalHeight, "worth of content on page so far")
end
local insert = function (_, classname, vbox)
local insertion = SILE.scratch.insertions.classes[classname]
if not insertion then
SU.error("Uninitialized insertion class " .. classname)
end
SILE.typesetter:pushMigratingMaterial({
SILE.types.node.penalty(SILE.settings:get("insertion.penalty")),
})
SILE.typesetter:pushMigratingMaterial({
SILE.types.node.insertion({
class = classname,
nodes = vbox.nodes,
contentHeight = vbox.height,
contentDepth = vbox.depth,
frame = insertion.insertInto.frame,
parent = SILE.typesetter.frame,
}),
})
end
function package:_init ()
base._init(self)
if not SILE.scratch.insertions then
SILE.scratch.insertions = { classes = {} }
end
if not SILE.insertions then
SILE.insertions = {}
end
SILE.insertions.setShrinkage = function (classname, amount)
local reduceList = SILE.scratch.insertions.classes[classname].stealFrom
for fName, ratio in pairs(reduceList) do
local frame = SILE.getFrame(fName)
if frame then
initShrinkage(frame)
SU.debug("insertions", "Shrinking", fName, "by", amount * ratio)
frame.state.totals.shrinkage = frame.state.totals.shrinkage + amount * ratio
end
end
end
function SILE.insertions.commitShrinkage (_, classname)
local opts = SILE.scratch.insertions.classes[classname]
local reduceList = opts["stealFrom"]
local stealPosition = opts["steal-position"] or "bottom"
for fName, _ in pairs(reduceList) do
local frame = SILE.getFrame(fName)
if frame then
initShrinkage(frame)
local newHeight = frame:height() - frame.state.totals.shrinkage
if stealPosition == "bottom" then
frame:relax("bottom")
else
frame:relax("top")
end
SU.debug("insertions", "Constraining height of", fName, "by", frame.state.totals.shrinkage, "to", newHeight)
frame:constrain("height", newHeight)
frame.state.totals.shrinkage = SILE.types.measurement(0)
end
end
end
SILE.insertions.increaseInsertionFrame = function (insertionvbox, classname)
local amount = insertionvbox.height + insertionvbox.depth
local opts = SILE.scratch.insertions.classes[classname]
SU.debug("insertions", "Increasing insertion frame by", amount)
local stealPosition = opts["steal-position"] or "bottom"
local insertionFrame = SILE.getFrame(opts["insertInto"].frame)
local oldHeight = insertionFrame:height()
amount = amount * opts["insertInto"].ratio
insertionFrame:constrain("height", oldHeight + amount)
if stealPosition == "bottom" then
insertionFrame:relax("top")
end
SU.debug("insertions", "New height is now", insertionFrame:height())
end
SILE.insertions.processInsertion = function (vboxlist, i, totalHeight, target)
local ins = vboxlist[i]
if ins.seen then
return target
end
local targetFrame = SILE.getFrame(ins.frame)
local options = SILE.scratch.insertions.classes[ins.class]
ins:dropDiscardables()
local topBox = nextInterInsertionSkip(ins.class)
local insertionsHeight = SILE.types.length()
insertionsHeight:___add(ins.contentHeight)
insertionsHeight:___add(topBox.height)
insertionsHeight:___add(topBox.depth)
insertionsHeight:___add(ins.contentDepth)
local insbox = thisPageInsertionBoxForClass(ins.class)
initShrinkage(targetFrame)
initShrinkage(SILE.typesetter.frame)
if SU.debugging("insertions") then
debugInsertion(ins, insbox, topBox, target, targetFrame, totalHeight)
end
local effectOnThisFrame = options.stealFrom[SILE.typesetter.frame.id]
if effectOnThisFrame then
effectOnThisFrame = insertionsHeight * effectOnThisFrame
else
effectOnThisFrame = SILE.types.measurement(0)
end
local newTarget = target - effectOnThisFrame
if totalHeight + effectOnThisFrame <= target and insbox.height + insertionsHeight <= options.maxHeight then
SU.debug("insertions", "fits")
SILE.insertions.setShrinkage(ins.class, insertionsHeight)
insbox:append(topBox)
insbox:append(ins)
ins.seen = true
return newTarget
end
SU.debug("insertions", "splitting")
local maxsize = SU.min(target - totalHeight, options.maxHeight)
maxsize = maxsize - topBox.height
local materialToSplit = {}
pl.tablex.insertvalues(materialToSplit, ins:unbox())
local deferredInsertions = ins:split(materialToSplit, maxsize)
if deferredInsertions then
SU.debug("insertions", "Split. Remaining insertion is", ins)
SILE.insertions.setShrinkage(
ins.class,
topBox.height:absolute() + deferredInsertions.height:absolute() + deferredInsertions.depth:absolute()
)
insbox:append(topBox)
insbox:append(deferredInsertions)
deferredInsertions.seen = true
table.insert(vboxlist, i, SILE.types.node.penalty(-20000))
return target end
local lastbox = i
while not vboxlist[lastbox].is_vbox do
lastbox = lastbox - 1
end
while not (vboxlist[i].is_penalty and vboxlist[i].penalty == -20000) do
table.insert(vboxlist, lastbox, SILE.types.node.penalty(-20000))
end
return target
end
self.class:registerPostinit(function (_)
local typesetter = SILE.typesetter
if not typesetter.noinsertion_getTargetLength then
typesetter.noinsertion_getTargetLength = typesetter.getTargetLength
typesetter.getTargetLength = function (self_)
initShrinkage(self_.frame)
return typesetter.noinsertion_getTargetLength(self_) - self_.frame.state.totals.shrinkage
end
end
typesetter:registerFrameBreakHook(function (_, nodelist)
pl.tablex.foreach(insertionsThisPage, SILE.insertions.commitShrinkage)
return nodelist
end)
typesetter:registerPageEndHook(function (_)
pl.tablex.foreach(insertionsThisPage, SILE.insertions.increaseInsertionFrame)
for insertionclass, insertionlist in pairs(insertionsThisPage) do
insertionlist:outputYourself()
insertionsThisPage[insertionclass] = nil
end
if SU.debugging("insertions") then
for _, frame in pairs(SILE.frames) do
SILE.outputter:debugFrame(frame)
end
end
end)
end)
self:export("initInsertionClass", initInsertionClass)
self:export("thisPageInsertionBoxForClass", thisPageInsertionBoxForClass)
self:export("insert", insert)
end
function package:declareSettings ()
SILE.settings:declare({
parameter = "insertion.penalty",
type = "integer",
default = -3000,
help = "Penalty to be applied before insertion",
})
end
package.documentation = [[
\begin{document}
The \autodoc:package{footnotes} package works by taking auxiliary material (the footnote content), shrinking the current frame and inserting it into the footnote frame.
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.
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.
\end{document}
]]
return package