-
I have a modified --- diglot document class.
-- @use resilient.diglot
local base = require("classes.resilient.base")
local class = pl.class(base)
class._name = "resilient.diglot"
function class:_init(options)
base._init(self, options)
self:loadPackage("counters")
self:registerPostinit(function()
SILE.scratch.counters.folio = { value = 1, display = "arabic" }
-- SILE.call("set-counter", { id = "footnote", value = 1 })
end)
self:declareFrame("a", {
left = "5.0%pw",
right = "50%pw",
top = "3.5%ph",
-- bottom = "93.50%ph",
bottom = "70%ph",
})
self:declareFrame("b", {
left = "55%pw",
right = "100%pw-left(a)",
top = "top(a)",
bottom = "bottom(a)",
})
self:declareFrame("footnotes", {
left = "left(a)",
right = "right(b)",
-- top = "bottom(a)+ 1.50%ph",
-- bottom = "bottom(a)+2.0%ph", -- Adjust as needed for footnote space
top = "bottom(a)+ 2.50%ph",
bottom = "bottom(a)+11.5%ph", -- Adjust as needed for footnote space
})
self:declareFrame("folio", {
left = "left(a)",
right = "right(b)",
-- top = "bottom(a)+2.0%ph",
-- bottom = "bottom(a)+2.5%ph",
top = "bottom(a)+17%ph",
bottom = "bottom(a)+18.0%ph",
})
self:loadPackage("translation", {
frames = {
left = "a",
right = "b",
},
})
self:loadPackage("resilient.footnotes", {
insertInto = "footnotes", -- Specify the frame for footnotes
stealFrom = { "a", "b" }, -- Collect footnotes from both content frames
-- stealFrom = { "content" }, -- Collect footnotes from both content frames
})
end
return class
and a local base = require("packages.base")
local package = pl.class(base)
package._name = "parallel"
-- Typesetter pool for managing typesetters for different frames (e.g., left and right frames).
local typesetterPool = {}
-- Stores layout calculations for each frame, such as height and overflow tracking.
local calculations = {}
-- Specifies the order of frames for synchronizing and page-breaking logic.
local folioOrder = {}
-- Utility function: Iterate through all typesetters and apply a callback function to each.
local allTypesetters = function(callback)
local oldtypesetter = SILE.typesetter -- Save the current typesetter
for frame, typesetter in pairs(typesetterPool) do
-- if SU.debugging(package._name) then
-- SU.debug(package._name, frame .. " frame top: " .. typesetter.frame:top())
-- local height = typesetter.frame:height()
-- SU.debug(package._name, frame .. " frame height: " .. height)
-- end
SILE.typesetter = typesetter -- Switch to the current frame's typesetter
callback(frame, typesetter) -- Execute the callback
end
SILE.typesetter = oldtypesetter -- Restore the original typesetter
end
-- Utility function: Calculate the height of new material for a given frame.
local calculateFrameHeight = function(frame, typesetter)
local height = calculations[frame].cumulativeHeight or SILE.types.length()
for i = calculations[frame].mark + 1, #typesetter.state.outputQueue do
local thisHeight = typesetter.state.outputQueue[i].height + typesetter.state.outputQueue[i].depth
height = height + thisHeight
end
return height
end
-- A null typesetter used as a placeholder. This typesetter doesn't output any content.
local nulTypesetter = pl.class(SILE.typesetters.base)
nulTypesetter.outputLinesToPage = function() end -- Override to suppress output
-- Balances the height of content across frames by adding glue to the shorter frame.
local addBalancingGlue = function(height, overflowHeight)
allTypesetters(function(frame, typesetter)
calculations[frame].heightOfNewMaterial = calculateFrameHeight(frame, typesetter)
local glue = height - calculations[frame].heightOfNewMaterial
if glue:tonumber() > 0 then
table.insert(typesetter.state.outputQueue, SILE.types.node.vglue({ height = glue }))
SU.debug(package._name, "Already added balancing glue of", glue, " to bottom of frame", frame)
end
-- local oppositeFrame = (frame == "left") and "right" or "left"
if overflowHeight then
SU.debug(package._name, "overflowHeight to be added for", oppositeFrame, "of height", overflowHeight)
table.insert(typesetter.state.outputQueue, 1, SILE.types.node.vglue({ height = overflowHeight }))
SU.debug(package._name, "Added overflowHeight as top glue for", frame, "of height", overflowHeight)
end
calculations[frame].mark = #typesetter.state.outputQueue
SU.debug(package._name, "Mark for frame", frame, "is", calculations[frame].mark)
end)
end
-- Handles page-breaking logic for parallel frames.
local parallelPagebreak = function()
for i = 1, #folioOrder do
local thisPageFrames = folioOrder[i]
local hasOverflow = false
-- Initialize all frames at the beginning of processing each page set
for _, frame in ipairs(thisPageFrames) do
local typesetter = typesetterPool[frame]
typesetter:initFrame(typesetter.frame)
end
for j = 1, #thisPageFrames do
local frame = thisPageFrames[j]
local typesetter = typesetterPool[frame]
local thispage = {}
local linesToFit = typesetter.state.outputQueue
local totalHeight = 0
for _, line in ipairs(linesToFit) do
totalHeight = totalHeight + (line.height:tonumber() + line.depth:tonumber())
end
-- Process the frame's content
local targetLength = typesetter:getTargetLength():tonumber()
local currentHeight = 0
while
#linesToFit > 0
and currentHeight + (linesToFit[1].height:tonumber() + linesToFit[1].depth:tonumber())
<= targetLength
do
local line = table.remove(linesToFit, 1)
currentHeight = currentHeight + (line.height:tonumber() + line.depth:tonumber())
table.insert(thispage, line)
end
if #linesToFit > 0 then
hasOverflow = true
-- In theory, the totalHeight is the height of the unprocessed lines and
-- currentHeight is the height of the processed lines.
local overflowHeight = totalHeight - currentHeight
-- local oppositeFrame = (frame == "left") and "right" or "left"
-- calculations[oppositeFrame].overflowHeight = overflowHeight
calculations[frame].overflowHeight = overflowHeight
-- SU.debug(package._name, "totalHeight for frame", frame, "is", totalHeight)
-- SU.debug(package._name, "currentHeight for frame", frame, "is", currentHeight)
-- SU.debug(package._name, "targetLength for frame", frame, "is", targetLength)
SU.debug(package._name, "Overflow height for frame", frame, "is", overflowHeight)
end
typesetter:outputLinesToPage(thispage)
end
SILE.documentState.documentClass:endPage()
if hasOverflow then
-- If the content overflowed, open a new page.
SILE.documentState.documentClass:newPage()
end
end
end
-- Initialization function for the package.
function package:_init(options)
base._init(self, options)
-- Initialize the null typesetter.
SILE.typesetter = nulTypesetter(SILE.getFrame("page"))
-- Ensure the `frames` option is provided.
if type(options.frames) ~= "table" then
SU.error("Package parallel must be initialized with a set of appropriately named frames")
end
-- Set up typesetters for each frame.
for frame, typesetter in pairs(options.frames) do
typesetterPool[frame] = SILE.typesetters.base(SILE.getFrame(typesetter))
typesetterPool[frame].id = typesetter
typesetterPool[frame].buildPage = function() end -- Disable auto page-building
-- Register commands (e.g., \left, \right) for directing content to frames.
local fontcommand = frame .. ":font"
self:registerCommand(frame, function(_, _)
SILE.typesetter = typesetterPool[frame]
SILE.call(fontcommand)
end)
-- Define default font commands for frames if not already defined.
if not SILE.Commands[fontcommand] then
self:registerCommand(fontcommand, function(_, _) end)
end
end
-- Configure the order of frames for the folio (page layout).
if not options.folios then
folioOrder = { {} }
for frame, _ in pl.tablex.sort(options.frames) do
table.insert(folioOrder[1], frame)
end
else
folioOrder = options.folios
end
-- Customize the `newPage` method to synchronize frames.
-- This is where the top glue disappears and I don't know why.
self.class.newPage = function(self_)
allTypesetters(function(frame, _)
calculations[frame] = { mark = 0 }
end)
self.class._base.newPage(self_)
SILE.call("sync")
end
-- Initialize calculations for each frame.
allTypesetters(function(frame, _)
calculations[frame] = { mark = 0 }
end)
-- Override the `finish` method to handle parallel page-breaking.
local oldfinish = self.class.finish
self.class.finish = function(self_)
parallelPagebreak()
oldfinish(self_)
end
end
-- Registers commands for the package.
function package:registerCommands()
-- shortcut for \parskip
self:registerCommand("parskip", function(options, _)
local height = options.height or "12pt plus 3pt minus 3pt"
SILE.typesetter:leaveHmode()
SILE.typesetter:pushVglue(SILE.types.length(height))
end)
self:registerCommand("sync", function(_, _)
local anybreak = false
local maxheight = SILE.types.length()
SU.debug("parallel", "Value of maxheight before balancing for frame ", frame, ": ", maxheight)
-- Check for potential page breaks.
allTypesetters(function(_, typesetter)
typesetter:leaveHmode(true)
local lines = pl.tablex.copy(typesetter.state.outputQueue)
if SILE.pagebuilder:findBestBreak({ vboxlist = lines, target = typesetter:getTargetLength() }) then
anybreak = true
end
end)
-- Perform a page break if necessary.
if anybreak then
parallelPagebreak()
return
end
-- Calculate the height of new material for balancing.
allTypesetters(function(frame, typesetter)
calculations[frame].heightOfNewMaterial = calculateFrameHeight(frame, typesetter)
if calculations[frame].heightOfNewMaterial > maxheight then
maxheight = calculations[frame].heightOfNewMaterial
SU.debug("parallel", "Value of maxheight after balancing for frame ", frame, ": ", maxheight)
end
-- This is not executed and I don't know the reason.
if calculations[frame].overflowHeight then
local overflowHeight = calculations[frame].overflowHeight
SU.degug(package._name, "There is an Overflow for frame", frame, "of height", overflowHeight)
end
end)
-- Add glue to balance frames.
-- local oppositeFrame = (frame == "left") and "right" or "left"
-- if calculations[oppositeFrame].overflowHeight then
if overflowHeight then
addBalancingGlue(maxheight, overflowHeight)
else
addBalancingGlue(maxheight)
end
-- addBalancingGlue(maxheight)
-- Check if parskip is effectively nil
local parskip = SILE.settings:get("document.parskip")
-- SU.debug("parallel", "parsing parskip", parskip.length, parskip.stretch, parskip.shrink)
if not parskip.length then
-- Insert flexible glue to manage space between two successive pairs of frames separated by the \sync command
local additionalGlue = SILE.types.length("12pt plus 3pt minus 3pt")
addBalancingGlue(additionalGlue)
end
end)
end
return package I've managed to add balancing glue to the bottom of shorter frames, but I'm still struggling with overflow handling when a frame spills over to the next page. I've been working on calculating the Another problem I have with Mininal example:
If you find it interesting, please play with it and share the outcome with me. Thank you for you valuable time and interest. |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 2 replies
-
Messing around with This could be a better approach using
This seems to work quite well but some misalignments are still visible in certain cases. Just for fun but parallel typesetting is driving me nut, but it's a very interesting problem ;) Function for glue calculation on page transitions: -- Balance overflow content across the overflow frames.
local balanceOverflow = function(frame, contentHeight)
-- Get the opposite frame
local oppositeFrame = (frame == "left") and "right" or "left"
-- Calculate the height of the opposite frame
local oppositeHeight = calculateFrameHeight(oppositeFrame, typesetterPool[oppositeFrame])
-- Calculate the difference in height between the frames
local glueHeight = contentHeight - oppositeHeight
-- If the current frame is taller, add glue to the opposite frame
if glueHeight:tonumber() > 0 then
SU.debug(package._name, "Adding glue to overflow frame: ", oppositeFrame, " of height:", glueHeight)
table.insert(
typesetterPool[oppositeFrame].state.outputQueue,
SILE.types.node.vglue({ height = SILE.types.length(glueHeight) })
)
elseif glueHeight:tonumber() < 0 then
-- If the opposite frame is taller, add glue to the current frame
SU.debug(package._name, "Adding glue to overflow frame: ", oppositeFrame, " of height: ", -glueHeight)
table.insert(
typesetterPool[frame].state.outputQueue,
SILE.types.node.vglue({ height = SILE.types.length(-glueHeight) })
)
end
end
Modified -- Handles page-breaking logic for parallel frames.
local parallelPagebreak = function()
for _, thisPageFrames in ipairs(folioOrder) do
local hasOverflow = false
-- Collect overflow content for each frame.
-- We don't need to clear it after flushing the captured content,
-- because it will be cleared in the next page.
local overflowContent = {}
-- Initialize and process frames in one loop
for _, frame in ipairs(thisPageFrames) do
local typesetter = typesetterPool[frame]
typesetter:initFrame(typesetter.frame)
local thispage = {}
local linesToFit = typesetter.state.outputQueue
local targetLength = typesetter:getTargetLength():tonumber()
local currentHeight = 0
-- Fit content into the frame
while
#linesToFit > 0
and currentHeight + (linesToFit[1].height:tonumber() + linesToFit[1].depth:tonumber())
<= targetLength
do
local line = table.remove(linesToFit, 1)
currentHeight = currentHeight + (line.height:tonumber() + line.depth:tonumber())
table.insert(thispage, line)
end
if #linesToFit > 0 then
hasOverflow = true
overflowContent[frame] = linesToFit
-- Clear outputQueue for next page, because its content has been added to overflowContent.
typesetter.state.outputQueue = {}
else
-- Make sure that every frame in thisPageFrames has an entry in overflowContent.
-- Otherwise, balancing might fail when restoring overflow content.
overflowContent[frame] = {}
end
-- Output content on the current page
typesetter:outputLinesToPage(thispage)
end
-- End current page
SILE.documentState.documentClass:endPage()
if hasOverflow then
-- Start a new page
SILE.documentState.documentClass:newPage()
-- Restore overflow content to the frames
for _, frame in ipairs(thisPageFrames) do
local typesetter = typesetterPool[frame]
for _, line in ipairs(overflowContent[frame]) do
table.insert(typesetter.state.outputQueue, line)
end
end
-- Balance overflow content across frames after restoring the overflow content.
-- You can combine the balancing mechanism with the overflow content restoration,
-- because the balancing mechanism only needs to be called once the restoration is done.
for _, frame in ipairs(thisPageFrames) do
local typesetter = typesetterPool[frame]
local contentHeight = calculateFrameHeight(frame, typesetter)
balanceOverflow(frame, contentHeight)
end
-- Let \sync handle alignment and marking
SILE.call("sync")
end
end
end
|
Beta Was this translation helpful? Give feedback.
-
I believe I'm about 90% there with parallel frame alignment. I had some hard time dealing with "soft" glue (like While perfect line-by-line alignment might not be achieved with this way, the visual outcome is acceptable, I think. And here is an Example and Another. I have acknowledged the Hope, someone will come up with a better solution. My modification of the original package: local base = require("packages.base")
local package = pl.class(base)
package._name = "parallel"
-- Typesetter pool for managing typesetters for different frames (e.g., left and right frames).
local typesetterPool = {}
-- Stores layout calculations for each frame, such as height, marking and overflow tracking.
local calculations = {}
-- Specifies the order of frames for synchronizing and page-breaking logic.
local folioOrder = {}
-- A null typesetter used as a placeholder. This typesetter doesn't output any content.
local nulTypesetter = pl.class(SILE.typesetters.base)
nulTypesetter.outputLinesToPage = function() end -- Override to suppress output
-- Utility function: Iterate through all typesetters and apply a callback function to each.
local allTypesetters = function(callback)
local oldtypesetter = SILE.typesetter -- Save the current typesetter
for frame, typesetter in pairs(typesetterPool) do
SILE.typesetter = typesetter -- Switch to the current frame's typesetter
callback(frame, typesetter) -- Execute the callback
end
SILE.typesetter = oldtypesetter -- Restore the original typesetter
end
-- Utility function: Calculate the height of new material for a given frame.
local calculateFrameHeight = function(frame, typesetter)
local height = calculations[frame].cumulativeHeight or SILE.types.length()
for i = calculations[frame].mark + 1, #typesetter.state.outputQueue do
local lineHeight = typesetter.state.outputQueue[i].height + typesetter.state.outputQueue[i].depth
height = height + lineHeight
end
return height
end
-- Create dummy content to fill up the overflowed frames.
local createDummyContent = function(height, frame, offset)
-- Retrieve document.baselineskip and document.lineskip
-- which are tables with the following fields: height, depth, is_vglue, adjustment, width
local baselineSkip = SILE.settings:get("document.baselineskip")
SU.debug(package._name, "Baseline skip is ", baselineSkip)
-- Safely retrieve lineskip or default to 0
local lineskip = SILE.settings:get("document.lineskip") or SILE.types.length.new({ length = 0 })
SU.debug(package._name, "Line skip is ", lineskip)
-- Extract the absolute lengths
local baselineHeight = baselineSkip.height and baselineSkip.height:tonumber() or 0
local lineSkipHeight = lineskip.height and lineskip.height:tonumber() or 0
-- Combine the lengths to get the total line height
local lineHeight = baselineHeight + lineSkipHeight
SU.debug(package._name, "textHeight = " .. lineHeight)
-- local typesetter = typesetterPool[frame]
-- local lineHeight = typesetter.state.outputQueue[1].height + typesetter.state.outputQueue[1].depth
-- Calculate the number of lines
local numLines = math.floor(height:tonumber() / lineHeight)
-- Validate offset
offset = offset or 0
if offset >= numLines then
SU.warn("Offset is larger than number of lines available; no dummy content will be generated.")
return
end
-- Get the typesetter for the frame
local typesetter = typesetterPool[frame]
SILE.call("color", { color = "white" }, function()
typesetter:typeset("sile")
for i = 1, numLines - offset do
-- Add dummy content and a line break
SILE.call("break")
typesetter:typeset("sile")
end
end)
end
local balanceFramesWithDummyContent = function(offset)
local frameHeights = {}
local maxHeight = SILE.types.length(0)
-- Step 1: Measure the height of all frames and track the maximum height
allTypesetters(function(frame, typesetter)
local height = calculateFrameHeight(frame, typesetter)
frameHeights[frame] = height
if height > maxHeight then
maxHeight = height -- Track the tallest frame
end
SU.debug(package._name, "Height of frame ", frame, ": ", height)
end)
-- Step 2: Add dummy content to shorter frames to match the maximum height
allTypesetters(function(frame, typesetter)
local heightDifference = maxHeight - frameHeights[frame]
if heightDifference:tonumber() > 0 then
-- Add dummy content to balance the frame height
SILE.typesetter = typesetter
createDummyContent(SILE.types.length(heightDifference), frame, offset or 0)
SU.debug(package._name, "Added dummy content to frame ", frame, " to balance height by: ", heightDifference)
end
end)
end
-- Balances the height of content across frames by adding glue to the shorter frame.
local addBalancingGlue = function(height)
allTypesetters(function(frame, typesetter)
calculations[frame].heightOfNewMaterial = calculateFrameHeight(frame, typesetter)
local glue = height - calculations[frame].heightOfNewMaterial
if glue:tonumber() > 0 then
table.insert(typesetter.state.outputQueue, SILE.types.node.vglue({ height = glue }))
SU.debug(package._name, "Already added balancing glue of", glue, " to bottom of frame", frame)
end
-- calculations[frame].mark = #typesetter.state.outputQueue
-- SU.debug(package._name, "Mark for frame", frame, "is", calculations[frame].mark)
end)
end
-- Adds a flexible glue (parskip) to the bottom of each frame
-- This is decoupled from addBalancingGlue calculations, serving a simple purpose.
local addParskipToFrames = function(parskipHeight)
allTypesetters(function(_, typesetter)
table.insert(typesetter.state.outputQueue, SILE.types.node.vglue({ height = parskipHeight }))
end)
end
-- Handles page-breaking logic for parallel frames.
-- Modify parallelPagebreak to use dummy content
local parallelPagebreak = function()
for _, thisPageFrames in ipairs(folioOrder) do
local hasOverflow = false
local overflowContent = {}
-- Initialize and process frames
for _, frame in ipairs(thisPageFrames) do
local typesetter = typesetterPool[frame]
typesetter:initFrame(typesetter.frame)
local thispage = {}
local linesToFit = typesetter.state.outputQueue
local targetLength = typesetter:getTargetLength():tonumber()
local currentHeight = 0
while
#linesToFit > 0
and currentHeight + (linesToFit[1].height:tonumber() + linesToFit[1].depth:tonumber())
<= targetLength
do
local line = table.remove(linesToFit, 1)
currentHeight = currentHeight + (line.height:tonumber() + line.depth:tonumber())
table.insert(thispage, line)
end
if #linesToFit > 0 then
hasOverflow = true
overflowContent[frame] = linesToFit
typesetter.state.outputQueue = {}
else
overflowContent[frame] = {}
end
typesetter:outputLinesToPage(thispage)
end
SILE.documentState.documentClass:endPage()
if hasOverflow then
-- Start a new page
SILE.documentState.documentClass:newPage()
-- Restore overflow content to the frames
for _, frame in ipairs(thisPageFrames) do
local typesetter = typesetterPool[frame]
for _, line in ipairs(overflowContent[frame]) do
table.insert(typesetter.state.outputQueue, line)
end
end
-- Balance the frames using dummy text
balanceFramesWithDummyContent()
-- Let \sync handle alignment and marking
SILE.call("sync")
end
end
end
-- Initialization function for the package.
function package:_init(options)
base._init(self, options)
-- Initialize the null typesetter.
SILE.typesetter = nulTypesetter(SILE.getFrame("page"))
-- Ensure the `frames` option is provided.
if type(options.frames) ~= "table" then
SU.error("Package parallel must be initialized with a set of appropriately named frames")
end
-- Set up typesetters for each frame.
for frame, typesetter in pairs(options.frames) do
typesetterPool[frame] = SILE.typesetters.base(SILE.getFrame(typesetter))
typesetterPool[frame].id = typesetter
typesetterPool[frame].buildPage = function() end -- Disable auto page-building
-- Register commands (e.g., \left, \right) for directing content to frames.
local fontcommand = frame .. ":font"
self:registerCommand(frame, function(_, _)
SILE.typesetter = typesetterPool[frame]
SILE.call(fontcommand)
end)
-- Define default font commands for frames if not already defined.
if not SILE.Commands[fontcommand] then
self:registerCommand(fontcommand, function(_, _) end)
end
end
-- Configure the order of frames for the folio (page layout).
if not options.folios then
folioOrder = { {} }
for frame, _ in pl.tablex.sort(options.frames) do
table.insert(folioOrder[1], frame)
end
else
folioOrder = options.folios
end
-- Customize the `newPage` method to synchronize frames.
-- Ensure that each new page starts clean but balanced
self.class.newPage = function(self_)
self.class._base.newPage(self_)
-- Reset calculations
allTypesetters(function(frame, _)
calculations[frame] = { mark = 0 }
end)
-- Align and balance frames
SILE.call("sync")
end
-- Initialize calculations for each frame.
allTypesetters(function(frame, _)
calculations[frame] = { mark = 0 }
end)
-- Override the `finish` method to handle parallel page-breaking.
local oldfinish = self.class.finish
self.class.finish = function(self_)
parallelPagebreak()
oldfinish(self_)
end
end
-- Registers commands for the package.
function package:registerCommands()
-- shortcut for \parskip
self:registerCommand("parskip", function(options, _)
local height = options.height or "12pt plus 3pt minus 1pt"
SILE.typesetter:leaveHmode()
SILE.typesetter:pushVglue(SILE.types.length(height))
end)
self:registerCommand("sync", function(_, _)
local anybreak = false
local maxheight = SILE.types.length()
-- Check for potential page breaks.
allTypesetters(function(_, typesetter)
typesetter:leaveHmode(true)
local lines = pl.tablex.copy(typesetter.state.outputQueue)
if SILE.pagebuilder:findBestBreak({ vboxlist = lines, target = typesetter:getTargetLength() }) then
anybreak = true
end
end)
-- Perform a page break if necessary.
if anybreak then
parallelPagebreak()
return
end
-- Calculate the height of new material for balancing.
allTypesetters(function(frame, typesetter)
calculations[frame].heightOfNewMaterial = calculateFrameHeight(frame, typesetter)
if calculations[frame].heightOfNewMaterial > maxheight then
maxheight = calculations[frame].heightOfNewMaterial
SU.debug(package._name, "Value of maxheight after balancing for frame ", frame, ": ", maxheight)
end
end)
-- Add balancing glue
addBalancingGlue(maxheight)
-- Check if parskip is effectively nil
local parskip = SILE.settings:get("document.parskip")
-- SU.debug("parallel", "parsing parskip", parskip.length, parskip.stretch, parskip.shrink)
if not parskip.length then
-- Insert flexible glue to manage space between two successive pairs of frames separated by the \sync command
-- Add parskip to the bottom of both frames
addParskipToFrames(SILE.types.length("12pt plus 3pt minus 1pt"))
else
-- Add the value of parskip set by user
addParskipToFrames(parskip)
end
end)
end
return package
|
Beta Was this translation helpful? Give feedback.
-
Finally, I have seen footnotes in parallel typesetting for the first time: with the following settings: local base = require("packages.base")
local package = pl.class(base)
package._name = "parallel"
-- Typesetter pool for managing typesetters for different frames (e.g., left and right frames).
local typesetterPool, footnotePool = {}, {}
local footnotes = { ftn_left = {}, ftn_right = {} }
-- Stores layout calculations for each frame, such as height, marking and overflow tracking.
local calculations = {}
-- Specifies the order of frames for synchronizing and page-breaking logic.
local folioOrder = {}
-- A null typesetter used as a placeholder. This typesetter doesn't output any content.
-- Its purpose is to make the transtion between frames easier and trouble free.
local nulTypesetter = pl.class(SILE.typesetters.base)
nulTypesetter.outputLinesToPage = function() end -- Override to suppress output
-- Utility function: Iterate through all typesetters and apply a callback function to each.
local allTypesetters = function(callback)
local oldtypesetter = SILE.typesetter -- Save the current typesetter
for frame, typesetter in pairs(typesetterPool) do
SILE.typesetter = typesetter -- Switch to the current frame's typesetter
callback(frame, typesetter) -- Execute the callback
end
SILE.typesetter = oldtypesetter -- Restore the original typesetter
end
-- Utility function: Calculate the height of new material for a given frame.
local calculateFrameHeight = function(frame, typesetter)
local height = calculations[frame].cumulativeHeight or SILE.types.length()
-- typesetter.state.outputQueue now holds actual content reflecting the real layout of lines. Therefore,
-- we can calculate the height of new material by adding the height of each line in the queue.
for i = calculations[frame].mark + 1, #typesetter.state.outputQueue do
local lineHeight = typesetter.state.outputQueue[i].height + typesetter.state.outputQueue[i].depth
height = height + lineHeight
end
-- calculations[frame].cumulativeHeight = height -- Store updated cumulative height
return height
end
-- Create dummy content to fill up the overflowed frames.
local createDummyContent = function(height, frame, offset)
-- Retrieve document.baselineskip and document.lineskip
-- which are tables with the following fields: height, depth, is_vglue, adjustment, width
local baselineSkip = SILE.settings:get("document.baselineskip").height or SILE.types.length.new({ length = 0 })
SU.debug(package._name, "Baseline skip is ", baselineSkip)
-- Safely retrieve lineskip or default to 0
local lineSkip = SILE.settings:get("document.lineskip").height or SILE.types.length.new({ length = 0 })
SU.debug(package._name, "Line skip is ", lineSkip)
-- Combine the lengths to get the total line height
local lineHeight = baselineSkip:tonumber() + lineSkip:tonumber()
SU.debug(package._name, "lineHeight = ", lineHeight)
-- We can't use the same mechanism used for calculating the height of new material
-- in the previous function, because we are now dealing with empty lines. We have to
-- calculate the height this empty space based baseline skip and line skip.
-- local typesetter = typesetterPool[frame]
-- local lineHeight = typesetter.state.outputQueue[1].height + typesetter.state.outputQueue[1].depth
-- Calculate the number of lines
local numLines = math.floor(height:tonumber() / lineHeight)
-- Validate offset
offset = offset or 0
if offset >= numLines then
SU.warn("Offset is larger than number of lines available; no dummy content will be generated.")
return
end
-- Get the typesetter for the frame
local typesetter = typesetterPool[frame]
SILE.call("color", { color = "white" }, function()
typesetter:typeset("sile")
for i = 1, numLines - offset do
-- Add dummy content and a line break
SILE.call("break")
typesetter:typeset("sile")
end
end)
-- This approach works quite well in general, but
-- it fails when the first line of leftover content is empty.
-- SILE.typesetter:pushExplicitVglue(height)
end
local balanceFramesWithDummyContent = function(offset)
local frameHeights = {}
local maxHeight = SILE.types.length(0)
-- Step 1: Measure frame heights and determine the maximum height
allTypesetters(function(frame, typesetter)
local height = calculateFrameHeight(frame, typesetter)
frameHeights[frame] = height
if height > maxHeight then
maxHeight = height
end
end)
-- Step 2: Add dummy content to balance frames
allTypesetters(function(frame, typesetter)
local heightDifference = maxHeight - frameHeights[frame]
if heightDifference:tonumber() > 0 then
SILE.typesetter = typesetter
createDummyContent(SILE.types.length(heightDifference), frame, offset or 0)
end
end)
-- Optional: Log balancing results
SU.debug(package._name, "Balanced frames to height: ", maxHeight)
end
-- Balances the height of content across frames by adding glue to the shorter frame.
local addBalancingGlue = function(height)
allTypesetters(function(frame, typesetter)
calculations[frame].heightOfNewMaterial = calculateFrameHeight(frame, typesetter)
local glue = height - calculations[frame].heightOfNewMaterial
if glue:tonumber() > 0 then
table.insert(typesetter.state.outputQueue, SILE.types.node.vglue({ height = glue }))
SU.debug(package._name, "Already added balancing glue of", glue, " to bottom of frame", frame)
end
-- We would not need to set the marking here, the `\sync` command will take care of that
-- calculations[frame].mark = #typesetter.state.outputQueue
end)
end
-- Adds a flexible glue (parskip) to the bottom of each frame
-- This is decoupled from addBalancingGlue calculations, serving a simple purpose.
local addParskipToFrames = function(parskipHeight)
allTypesetters(function(_, typesetter)
table.insert(typesetter.state.outputQueue, SILE.types.node.vglue({ height = parskipHeight }))
end)
end
local typesetFootnotes = function()
for frame, notes in pairs(footnotes) do
if notes and #notes > 0 then
SU.debug(package._name, "Processing footnotes for frame: " .. frame)
SU.debug(package._name, "Footnotes in frame: ", frame, pl.pretty.write(notes))
-- Initialize the frame's typesetter
local typesetter = footnotePool[frame]
typesetter:initFrame(typesetter.frame)
SILE.typesetter = typesetter -- Switch to this frame's typesetter
-- SU.debug(package._name, "Switched to typesetter for frame: " .. frame)
-- Add a short rule above all the footnotes
SILE.call("parallel_footnote:rule")
-- Temporarily adjust settings for footnotes
SILE.settings:temporarily(function()
SILE.settings:set("font.size", SILE.settings:get("font.size") * 0.80) -- Reduce font size
SILE.call("break") -- To prevent the first footnote being stretched acrross the frame
for _, note in ipairs(notes) do
-- Set up the hanging indent
SILE.settings:set("current.hangAfter", 1)
SILE.settings:set("current.hangIndent", "3.75nspc")
-- Start a paragraph for the footnote
-- SILE.call("noindent") -- Ensure no indent for the first line
-- SILE.typesetter:typeset(note.number .. ".")
-- SILE.call("kern", { width = "3.75nspc" })
SILE.call("footnote:marker", { mark = note.number })
-- Process the footnote content as part of the same paragraph
SILE.process(note.content)
-- End the paragraph
SILE.call("par")
end
end)
-- Pass the output queue directly to `outputLinesToPage` to flush the typesetter's content
if typesetter.state.outputQueue and #typesetter.state.outputQueue > 0 then
typesetter:outputLinesToPage(typesetter.state.outputQueue)
SU.debug(package._name, "Flushed output for frame: " .. frame)
else
SU.warn("No content to output for frame: " .. frame)
end
-- Clear the processed footnotes for this frame
SU.debug(package._name, "Clearing footnotes for frame: " .. frame)
footnotes[frame] = {}
-- Clear the output queue to prevent repeated processing
typesetter.state.outputQueue = {}
else
SU.debug(package._name, "No footnotes to process for frame: " .. frame)
end
end
end
-- Handles page-breaking logic for parallel frames.
-- Modify parallelPagebreak to use dummy content
local parallelPagebreak = function()
for _, thisPageFrames in ipairs(folioOrder) do
local hasOverflow = false
local overflowContent = {}
-- Process each frame for overflow content
allTypesetters(function(frame, typesetter)
typesetter:initFrame(typesetter.frame)
local thispage = {}
local linesToFit = typesetter.state.outputQueue
local targetLength = typesetter:getTargetLength():tonumber()
local currentHeight = 0
while
#linesToFit > 0
and currentHeight + (linesToFit[1].height:tonumber() + linesToFit[1].depth:tonumber())
<= targetLength
do
local line = table.remove(linesToFit, 1)
currentHeight = currentHeight + (line.height:tonumber() + line.depth:tonumber())
table.insert(thispage, line)
end
if #linesToFit > 0 then
hasOverflow = true
overflowContent[frame] = linesToFit
typesetter.state.outputQueue = {}
else
overflowContent[frame] = {}
end
typesetter:outputLinesToPage(thispage)
end)
-- Process footnotes before page break
typesetFootnotes()
-- End the current page
SILE.documentState.documentClass:endPage()
if hasOverflow then
-- Start a new page
SILE.documentState.documentClass:newPage()
-- Restore overflow content to the frames
for frame, overflowLines in pairs(overflowContent) do
local typesetter = typesetterPool[frame]
for _, line in ipairs(overflowLines) do
table.insert(typesetter.state.outputQueue, line)
end
end
-- Rebalance frames
balanceFramesWithDummyContent()
end
end
-- Ensure all frames are synchronized
SILE.call("sync")
end
-- Initialization function for the package.
function package:_init(options)
base._init(self, options)
-- Load the `rules` and `rebox` packages for footnote:rules
-- self:loadPackage("rules")
-- self:loadPackage("rebox")
-- self:loadPackage("counters")
-- self:loadPackage("insertions")
-- Load the `resilient.footnotes` package
self:loadPackage("resilient.footnotes")
self.class:initInsertionClass("footnote_left", {
insertInto = "c", -- Frame for left footnotes
stealFrom = { "a" }, -- Frame to shrink content from
maxHeight = SILE.types.length("75%ph"),
topBox = SILE.types.node.vglue("2ex"),
interInsertionSkip = SILE.types.length("1ex"),
})
self.class:initInsertionClass("footnote_right", {
insertInto = "d", -- Frame for right footnotes
stealFrom = { "b" }, -- Frame to shrink content from
maxHeight = SILE.types.length("75%ph"),
topBox = SILE.types.node.vglue("2ex"),
interInsertionSkip = SILE.types.length("1ex"),
})
-- Initialize the null typesetter.
SILE.typesetter = nulTypesetter(SILE.getFrame("page"))
-- Ensure the `frames` option is provided.
if type(options.frames) ~= "table" or type(options.ftn_frames) ~= "table" then
SU.error("Package parallel must be initialized with a set of appropriately named frames")
end
-- Set up typesetters for each frame.
for frame, typesetter in pairs(options.frames) do
typesetterPool[frame] = SILE.typesetters.base(SILE.getFrame(typesetter))
typesetterPool[frame].id = typesetter
typesetterPool[frame].buildPage = function() end -- Disable auto page-building
-- Register commands (e.g., \left, \right) for directing content to frames.
local fontcommand = frame .. ":font"
self:registerCommand(frame, function(_, _)
SILE.typesetter = typesetterPool[frame]
SILE.call(fontcommand)
end)
-- Define default font commands for frames if not already defined.
if not SILE.Commands[fontcommand] then
self:registerCommand(fontcommand, function(_, _) end)
end
end
-- Set up typesetters for each footnote frame.
for frame, typesetter in pairs(options.ftn_frames) do
footnotePool[frame] = SILE.typesetters.base(SILE.getFrame(typesetter))
footnotePool[frame].id = typesetter
footnotePool[frame].buildPage = function() end -- Disable auto page-building
-- Register commands (e.g., \ftn_left, \ftn_right) for directing content to frames.
local fontcommand = frame .. ":font"
self:registerCommand(frame, function(_, _)
SILE.typesetter = footnotePool[frame]
SILE.call(fontcommand)
end)
-- Define default font commands for frames if not already defined.
if not SILE.Commands[fontcommand] then
self:registerCommand(fontcommand, function(_, _) end)
end
end
-- Configure the order of frames for the folio (page layout).
if not options.folios then
folioOrder = { {} }
for frame, _ in pl.tablex.sort(options.frames) do
table.insert(folioOrder[1], frame)
end
else
folioOrder = options.folios
end
-- Customize the `newPage` method to synchronize frames.
-- Ensure that each new page starts clean but balanced
self.class.newPage = function(self_)
self.class._base.newPage(self_)
-- Reset calculations
allTypesetters(function(frame, _)
calculations[frame] = { mark = 0 }
end)
-- Align and balance frames
SILE.call("sync")
-- Reset footnote counter
-- left_footnoteCounter, right_footnoteCounter = 0, 0
end
-- Initialize calculations for each frame.
allTypesetters(function(frame, _)
calculations[frame] = { mark = 0 }
end)
-- Override the `finish` method to handle parallel page-breaking.
local oldfinish = self.class.finish
self.class.finish = function(self_)
parallelPagebreak()
oldfinish(self_)
end
end
-- Registers commands for the package.
function package:registerCommands()
-- shortcut for \parskip
self:registerCommand("parskip", function(options, _)
local height = options.height or "12pt plus 3pt minus 1pt"
SILE.typesetter:leaveHmode()
SILE.typesetter:pushExplicitVglue(SILE.types.length(height))
end)
self:registerCommand("sync", function(_, _)
local anybreak = false
local maxheight = SILE.types.length()
-- Check for potential page breaks.
allTypesetters(function(_, typesetter)
typesetter:leaveHmode(true)
local lines = pl.tablex.copy(typesetter.state.outputQueue)
if SILE.pagebuilder:findBestBreak({ vboxlist = lines, target = typesetter:getTargetLength() }) then
anybreak = true
end
end)
-- Perform a page break if necessary.
if anybreak then
parallelPagebreak()
return
end
-- Calculate the height of new material for balancing.
allTypesetters(function(frame, typesetter)
calculations[frame].heightOfNewMaterial = calculateFrameHeight(frame, typesetter)
if calculations[frame].heightOfNewMaterial > maxheight then
maxheight = calculations[frame].heightOfNewMaterial
SU.debug(package._name, "Value of maxheight after balancing for frame ", frame, ": ", maxheight)
end
end)
-- Add balancing glue
addBalancingGlue(maxheight)
-- Check if parskip is effectively nil
local parskip = SILE.settings:get("document.parskip")
-- SU.debug("parallel", "parsing parskip", parskip.length, parskip.stretch, parskip.shrink)
if not parskip.length then
-- Insert flexible glue to manage space between two successive pairs of frames separated by the \sync command
-- Add parskip to the bottom of both frames
addParskipToFrames(SILE.types.length("12pt plus 3pt minus 1pt"))
else
-- Add the value of parskip set by user
addParskipToFrames(parskip)
end
end)
self:registerCommand("smaller", function(_, content)
SILE.settings:temporarily(function()
local currentSize = SILE.settings:get("font.size")
SILE.settings:set("font.size", currentSize * 0.75) -- Scale down to 75%
SILE.settings:set("font.weight", 800)
SILE.process(content)
end)
end)
-- Before we can use the \rase command, we need to load the `raiselower` package:
self:loadPackage("raiselower")
self:registerCommand("footnoteNumber", function(options, content)
local height = options.height or "0.3em" -- Default height for superscripts
SILE.call("raise", { height = height }, function()
SILE.call("smaller", {}, function()
SILE.process(content)
end)
end)
end)
-- Stolen from `resilient.footnotes` package
self:registerCommand("parallel_footnote:rule", function(options, _)
local width = SU.cast("measurement", options.width or "20%fw") -- "Usually 1/5 of the text block"
local beforeskipamount = SU.cast("vglue", options.beforeskipamount or "2ex")
local afterskipamount = SU.cast("vglue", options.afterskipamount or "1ex")
local thickness = SU.cast("measurement", options.thickness or "0.5pt")
SILE.call("noindent")
SILE.typesetter:pushExplicitVglue(beforeskipamount)
SILE.call("rebox", {}, function()
SILE.call("hrule", { width = width, height = thickness })
end)
SILE.typesetter:leaveHmode()
SILE.typesetter:pushExplicitVglue(afterskipamount)
end, "Small helper command to set a footnote rule.")
self:registerCommand("parallel_footnote", function(options, content)
local currentFrame = SILE.typesetter.frame.id
local targetFrame = currentFrame == "a" and "ftn_left" or "ftn_right"
-- Increment or retrieve the footnote counter for the target frame
local footnoteNumber
if not options.mark then
SILE.call("increment-counter", { id = targetFrame })
footnoteNumber = self.class.packages.counters:formatCounter(SILE.scratch.counters[targetFrame])
else
footnoteNumber = options.mark
end
-- Add the footnote marker to the text
SILE.call("footnoteNumber", {}, function()
SILE.typesetter:typeset(footnoteNumber)
end)
-- Add the footnote content to the frame's list
if footnotes[targetFrame] then
table.insert(footnotes[targetFrame], {
number = footnoteNumber,
content = content,
})
SU.debug("parallel", "Added footnote " .. footnoteNumber .. " to frame " .. targetFrame)
else
SU.warn("Footnote attempted in an unsupported frame: " .. targetFrame)
end
end)
-- self:registerCommand("parallel_footnote", function(options, content)
-- local currentFrame = SILE.typesetter.frame.id
-- local insertionClass = currentFrame == "a" and "footnote_left" or "footnote_right"
--
-- -- Add footnote reference in text
-- SILE.call("footnote:reference", options)
--
-- -- Insert footnote content into the insertion queue
-- self.class:insert(
-- insertionClass,
-- SILE.call("vbox", {}, function()
-- SILE.call("footnote:marker", options)
-- SILE.process(content)
-- end)
-- )
-- end)
end
return package
However, I'm still having problem with handling overflow when footnotes don't fit their designated frame. I've even tried using insertion classes with an adapted package inherited from For the time being, we can only typeset short footnotes. A pair of insertion classes is the way forward, but I don't how to make it work yet. Here is the
```lua
local base = require("packages.resilient.base")
local utils = require("resilient.utils")
local package = pl.class(base) function package:_init(options)
end function package:registerCommands()
end function package:registerStyles() return package
|
Beta Was this translation helpful? Give feedback.
-
Using insertion classes is still a mystery to me, so I came up with an alternative method for handling overflowed footnote content. And here is the home-made remedy: local base = require("packages.base")
local package = pl.class(base)
package._name = "parallel"
-- Typesetter pool for managing typesetters for different frames (e.g., left and right frames).
local typesetterPool, footnotePool = {}, {}
local footnotes = { ftn_left = {}, ftn_right = {} }
-- Cache for footnote heights
local footnoteHeightCache = {}
-- Stores layout calculations for each frame, such as height, marking and overflow tracking.
local calculations = {}
-- Specifies the order of frames for synchronizing and page-breaking logic.
local folioOrder = {}
-- A null typesetter used as a placeholder. This typesetter doesn't output any content.
-- Its purpose is to make the transtion between frames easier and trouble free.
local nulTypesetter = pl.class(SILE.typesetters.base)
nulTypesetter.outputLinesToPage = function() end -- Override to suppress output
-- Utility function: Iterate through all typesetters and apply a callback function to each.
local allTypesetters = function(callback)
local oldtypesetter = SILE.typesetter -- Save the current typesetter
for frame, typesetter in pairs(typesetterPool) do
SILE.typesetter = typesetter -- Switch to the current frame's typesetter
callback(frame, typesetter) -- Execute the callback
end
SILE.typesetter = oldtypesetter -- Restore the original typesetter
end
-- Utility function: Calculate the height of new material for a given frame.
local calculateFrameHeight = function(frame, typesetter)
local height = calculations[frame].cumulativeHeight or SILE.types.length()
-- typesetter.state.outputQueue now holds actual content reflecting the real layout of lines.
-- Therefore, we can calculate the height of new material by adding the height of each line
-- in the queue.
for i = calculations[frame].mark + 1, #typesetter.state.outputQueue do
local lineHeight = typesetter.state.outputQueue[i].height + typesetter.state.outputQueue[i].depth
height = height + lineHeight
end
-- calculations[frame].cumulativeHeight = height -- Store updated cumulative height
return height
end
-- Create dummy content to fill up the overflowed frames.
local createDummyContent = function(height, frame, offset)
-- Get the typesetter for the frame
local typesetter = typesetterPool[frame]
local lineHeight = nil
-- Calculate precise line height by typesetting a sample line
typesetter:pushState()
SILE.settings:temporarily(function()
-- Typeset a sample line and measure its height
local dummyQueue = {}
-- Redirect the output queue to the dummyQueue to prevent interfering the real content of the document
typesetter.state.outputQueue = dummyQueue
SILE.call("color", { color = "white" }, function()
typesetter:typeset("This is a sample line height.") -- Typeset dummy content
SILE.call("break") -- Add a line break
end)
-- Measure the line height
if #dummyQueue > 0 then
local firstLine = dummyQueue[1]
lineHeight = (firstLine.height:absolute():tonumber() + firstLine.depth:absolute():tonumber())
end
end)
typesetter:popState()
-- If lineHeight could not be calculated, fall back to baselineSkip and lineSkip of the document
if not lineHeight then
local baselineSkip = SILE.settings:get("document.baselineskip").height or SILE.types.length({ length = 0 })
local lineSkip = SILE.settings:get("document.lineskip").height or SILE.types.length({ length = 0 })
lineHeight = baselineSkip:tonumber() + lineSkip:tonumber()
end
-- SU.debug(package._name, "Precise lineHeight = ", lineHeight)
-- Calculate the number of lines needed
local numLines = math.floor(height:tonumber() / lineHeight)
-- Validate offset
offset = offset or 0
if offset >= numLines then
SU.warn("Offset is larger than the number of lines available; no dummy content will be generated.")
return
end
-- Add dummy content to fill the frame
SILE.call("color", { color = "white" }, function()
typesetter:typeset("sile")
for i = 1, numLines - offset do
-- Add dummy content and a line break
SILE.call("break")
typesetter:typeset("sile")
end
end)
end
local balanceFramesWithDummyContent = function(offset)
local frameHeights = {}
local maxHeight = SILE.types.length(0)
-- Step 1: Measure frame heights and determine the maximum height
allTypesetters(function(frame, typesetter)
local height = calculateFrameHeight(frame, typesetter)
frameHeights[frame] = height
if height > maxHeight then
maxHeight = height
end
end)
-- Step 2: Add dummy content to balance frames
allTypesetters(function(frame, typesetter)
local heightDifference = maxHeight - frameHeights[frame]
if heightDifference:tonumber() > 0 then
SILE.typesetter = typesetter
createDummyContent(SILE.types.length(heightDifference), frame, offset or 0)
end
end)
-- Optional: Log balancing results
SU.debug(package._name, "Balanced frames to height: ", maxHeight)
end
-- Balances the height of content across frames by adding glue to the shorter frame.
local addBalancingGlue = function(height)
allTypesetters(function(frame, typesetter)
calculations[frame].heightOfNewMaterial = calculateFrameHeight(frame, typesetter)
local glue = height - calculations[frame].heightOfNewMaterial
if glue:tonumber() > 0 then
table.insert(typesetter.state.outputQueue, SILE.types.node.vglue({ height = glue }))
SU.debug(package._name, "Already added balancing glue of", glue, " to bottom of frame", frame)
end
-- We would not need to set the marking here, the `\sync` command will take care of that
-- calculations[frame].mark = #typesetter.state.outputQueue
end)
end
-- Adds a flexible glue (parskip) to the bottom of each frame
-- This is decoupled from addBalancingGlue calculations, serving a simple purpose.
local addParskipToFrames = function(parskipHeight)
allTypesetters(function(_, typesetter)
table.insert(typesetter.state.outputQueue, SILE.types.node.vglue({ height = parskipHeight }))
end)
end
-- Create a unique id for each footnote
local function generateFootnoteId(frame, note)
return frame .. ":" .. note.number
end
local function getFootnoteHeight(frame, note, typesetter)
local noteId = generateFootnoteId(frame, note)
-- Simulate typesetting to calculate height
local noteQueue = {}
typesetter:pushState()
-- Redirect the output queue to the noteQueue
typesetter.state.outputQueue = noteQueue
SILE.settings:set("current.hangAfter", 1)
SILE.settings:set("current.hangIndent", "3.75nspc")
SILE.call("footnote:marker", { mark = note.number })
SILE.process(note.content)
SILE.call("par")
typesetter:popState()
-- Measure the height of the simulated queue
local noteHeight = 0
for _, node in ipairs(noteQueue) do
noteHeight = noteHeight + node.height:absolute():tonumber() + node.depth:absolute():tonumber()
end
-- Cache the calculated height
footnoteHeightCache[noteId] = noteHeight
-- Return the calculated height and the simulated noteQueue for footnote content
-- the noteQueue will be used later to for spliting if needed
return noteHeight, noteQueue
end
local typesetFootnotes = function()
for frame, notes in pairs(footnotes) do
if notes and #notes > 0 then
SU.debug(package._name, "Processing footnotes for frame: " .. frame)
local typesetter = footnotePool[frame]
typesetter:initFrame(typesetter.frame)
SILE.typesetter = typesetter
-- Add a rule above the footnotes
SILE.call("parallel_footnote:rule")
local nextPageNotes = {}
SILE.settings:temporarily(function()
SILE.settings:set("font.size", SILE.settings:get("font.size") * 0.80)
SILE.call("break") -- To prevent the firt footnote being streched across the frame
local targetHeight = typesetter:getTargetLength():tonumber()
local currentHeight = 0
local baselineSkip = math.ceil(SILE.settings:get("document.baselineskip").height:tonumber() * 0.40)
for i, note in ipairs(notes) do
-- Get the cached or calculated height and simulated noteQueue
local noteHeight, noteQueue = getFootnoteHeight(frame, note, typesetter)
-- Adjust for baseline skip
if i > 1 then
noteHeight = noteHeight + baselineSkip
end
if currentHeight + noteHeight <= targetHeight then
-- Add baseline skip before adding the note (except the first note)
if i > 1 then
table.insert(
typesetter.state.outputQueue,
SILE.types.node.vglue(SILE.types.length(baselineSkip))
)
end
-- Note fits entirely
currentHeight = currentHeight + noteHeight
for _, node in ipairs(noteQueue) do
table.insert(typesetter.state.outputQueue, node)
end
else
-- Note needs to be split
local fittedQueue = {}
local remainingQueue = {}
local fittedHeight = 0
for _, node in ipairs(noteQueue) do
local nodeHeight = node.height:absolute():tonumber() + node.depth:absolute():tonumber()
if fittedHeight + nodeHeight <= (targetHeight - currentHeight) then
table.insert(fittedQueue, node)
fittedHeight = fittedHeight + nodeHeight
else
-- Whatever does not fit is sent to the remaining queue
table.insert(remainingQueue, node)
end
end
-- Flush noteQueue from the memory for optimization
noteQueue = {}
-- Add fitted part to the current frame
if #typesetter.state.outputQueue > 0 then
table.insert(
typesetter.state.outputQueue,
SILE.types.node.vglue(SILE.types.length(baselineSkip))
)
end
currentHeight = currentHeight + fittedHeight
for _, node in ipairs(fittedQueue) do
table.insert(typesetter.state.outputQueue, node)
end
-- Typeset the fitted part to the current frame
typesetter:outputLinesToPage(typesetter.state.outputQueue)
-- Reset output queue and move on
typesetter.state.outputQueue = {}
-- Create a new "split" note and add notes to the next page
if #remainingQueue > 0 then
local contentFunc = function()
for _, node in ipairs(remainingQueue) do
table.insert(SILE.typesetter.state.outputQueue, node)
end
end
table.insert(nextPageNotes, {
-- Suppress the footnote marker for the overflowed note
number = "",
content = contentFunc,
})
end
end
end
-- Output any remaining content
if typesetter.state.outputQueue and #typesetter.state.outputQueue > 0 then
typesetter:outputLinesToPage(typesetter.state.outputQueue)
else
SU.warn("No content to output for frame: " .. frame)
end
-- Add remaining notes to the next page
footnotes[frame] = nextPageNotes
-- Reset output queue after typesetting the remaining footnote content
typesetter.state.outputQueue = {}
end)
else
SU.debug(package._name, "No footnotes to process for frame: " .. frame)
end
end
end
-- Handles page-breaking logic for parallel frames.
local parallelPagebreak = function()
for _, thisPageFrames in ipairs(folioOrder) do
local hasOverflow = false
local overflowContent = {}
-- Process each frame for overflow content
allTypesetters(function(frame, typesetter)
typesetter:initFrame(typesetter.frame)
local thispage = {}
local linesToFit = typesetter.state.outputQueue
local targetLength = typesetter:getTargetLength():tonumber()
local currentHeight = 0
while
#linesToFit > 0
and currentHeight + (linesToFit[1].height:tonumber() + linesToFit[1].depth:tonumber())
<= targetLength
do
local line = table.remove(linesToFit, 1)
currentHeight = currentHeight + (line.height:tonumber() + line.depth:tonumber())
table.insert(thispage, line)
end
if #linesToFit > 0 then
hasOverflow = true
overflowContent[frame] = linesToFit
typesetter.state.outputQueue = {}
else
overflowContent[frame] = {}
end
typesetter:outputLinesToPage(thispage)
end)
-- Process footnotes before page break
typesetFootnotes()
-- End the current page
SILE.documentState.documentClass:endPage()
if hasOverflow then
-- Start a new page
SILE.documentState.documentClass:newPage()
-- Restore overflow content to the frames
for frame, overflowLines in pairs(overflowContent) do
local typesetter = typesetterPool[frame]
for _, line in ipairs(overflowLines) do
table.insert(typesetter.state.outputQueue, line)
end
end
-- Rebalance frames
balanceFramesWithDummyContent()
end
end
-- Ensure all the first pair of frames on the new page are synchronized
SILE.call("sync")
end
-- Initialization function for the package.
function package:_init(options)
base._init(self, options)
-- Load the `resilient.footnotes` package for the footenot:mark style.
self:loadPackage("resilient.footnotes")
-- Initialize the null typesetter.
SILE.typesetter = nulTypesetter(SILE.getFrame("page"))
-- Ensure the `frames` option is provided.
if type(options.frames) ~= "table" or type(options.ftn_frames) ~= "table" then
SU.error("Package parallel must be initialized with a set of appropriately named frames")
end
-- Set up typesetters for each frame.
for frame, typesetter in pairs(options.frames) do
typesetterPool[frame] = SILE.typesetters.base(SILE.getFrame(typesetter))
typesetterPool[frame].id = typesetter
typesetterPool[frame].buildPage = function() end -- Disable auto page-building
-- Register commands (e.g., \left, \right) for directing content to frames.
local fontcommand = frame .. ":font"
self:registerCommand(frame, function(_, _)
SILE.typesetter = typesetterPool[frame]
SILE.call(fontcommand)
end)
-- Define default font commands for frames if not already defined.
if not SILE.Commands[fontcommand] then
self:registerCommand(fontcommand, function(_, _) end)
end
end
-- Set up typesetters for each footnote frame.
for frame, typesetter in pairs(options.ftn_frames) do
footnotePool[frame] = SILE.typesetters.base(SILE.getFrame(typesetter))
footnotePool[frame].id = typesetter
-- You should not disable the auto page-building here, otherwise you can't typeset
-- any footnotes on the last page of your document.
end
-- Configure the order of frames for the folio (page layout).
if not options.folios then
folioOrder = { {} }
for frame, _ in pl.tablex.sort(options.frames) do
table.insert(folioOrder[1], frame)
end
else
folioOrder = options.folios
end
-- Customize the `newPage` method to synchronize frames.
-- Ensure that each new page starts clean but balanced
self.class.newPage = function(self_)
self.class._base.newPage(self_)
-- Reset calculations
allTypesetters(function(frame, _)
calculations[frame] = { mark = 0 }
end)
-- Align and balance frames
SILE.call("sync")
end
-- Initialize calculations for each frame.
allTypesetters(function(frame, _)
calculations[frame] = { mark = 0 }
end)
-- Override the `finish` method to handle parallel page-breaking.
local oldfinish = self.class.finish
self.class.finish = function(self_)
parallelPagebreak()
oldfinish(self_)
end
end
-- Registers commands for the package.
function package:registerCommands()
-- shortcut for \parskip
self:registerCommand("parskip", function(options, _)
local height = options.height or "12pt plus 3pt minus 1pt"
SILE.typesetter:leaveHmode()
SILE.typesetter:pushExplicitVglue(SILE.types.length(height))
end)
self:registerCommand("sync", function(_, _)
local anybreak = false
local maxheight = SILE.types.length()
-- Check for potential page breaks.
allTypesetters(function(_, typesetter)
typesetter:leaveHmode(true)
local lines = pl.tablex.copy(typesetter.state.outputQueue)
if SILE.pagebuilder:findBestBreak({ vboxlist = lines, target = typesetter:getTargetLength() }) then
anybreak = true
end
end)
-- Perform a page break if necessary.
if anybreak then
parallelPagebreak()
return
end
-- Calculate the height of new material for balancing.
allTypesetters(function(frame, typesetter)
calculations[frame].heightOfNewMaterial = calculateFrameHeight(frame, typesetter)
if calculations[frame].heightOfNewMaterial > maxheight then
maxheight = calculations[frame].heightOfNewMaterial
SU.debug(package._name, "Value of maxheight after balancing for frame ", frame, ": ", maxheight)
end
end)
-- Add balancing glue
addBalancingGlue(maxheight)
-- Check if parskip is effectively nil
local parskip = SILE.settings:get("document.parskip")
-- SU.debug("parallel", "parsing parskip", parskip.length, parskip.stretch, parskip.shrink)
if not parskip.length then
-- Insert flexible glue to manage space between two successive pairs of frames separated by the \sync command
-- Add parskip to the bottom of both frames
addParskipToFrames(SILE.types.length("12pt plus 3pt minus 1pt"))
else
-- Add the value of parskip set by user
addParskipToFrames(parskip)
end
end)
self:registerCommand("smaller", function(_, content)
SILE.settings:temporarily(function()
local currentSize = SILE.settings:get("font.size")
SILE.settings:set("font.size", currentSize * 0.75) -- Scale down to 75%
SILE.settings:set("font.weight", 800)
SILE.process(content)
end)
end)
-- Before we can use the \raise command, we need to load the `raiselower` package:
self:loadPackage("raiselower")
self:registerCommand("footnoteNumber", function(options, content)
local height = options.height or "0.3em" -- Default height for superscripts
SILE.call("raise", { height = height }, function()
SILE.call("smaller", {}, function()
SILE.process(content)
end)
end)
end)
-- Stolen from `resilient.footnotes` package
self:registerCommand("parallel_footnote:rule", function(options, _)
local width = SU.cast("measurement", options.width or "20%fw") -- "Usually 1/5 of the text block"
local beforeskipamount = SU.cast("vglue", options.beforeskipamount or "1ex")
local afterskipamount = SU.cast("vglue", options.afterskipamount or "1.5ex")
local thickness = SU.cast("measurement", options.thickness or "0.5pt")
SILE.call("noindent")
-- SILE.typesetter:pushExplicitVglue(beforeskipamount)
SILE.call("rebox", {}, function()
SILE.call("hrule", { width = width, height = thickness })
end)
SILE.typesetter:leaveHmode()
SILE.typesetter:pushExplicitVglue(afterskipamount)
end, "Small helper command to set a footnote rule.")
self:registerCommand("parallel_footnote", function(options, content)
local currentFrame = SILE.typesetter.frame.id
local targetFrame = currentFrame == "a" and "ftn_left" or "ftn_right"
-- Increment or retrieve the footnote counter for the target frame
local footnoteNumber
if not options.mark then
SILE.call("increment-counter", { id = targetFrame })
footnoteNumber = self.class.packages.counters:formatCounter(SILE.scratch.counters[targetFrame])
else
footnoteNumber = options.mark
end
-- Add the footnote marker to the text
SILE.call("footnoteNumber", {}, function()
SILE.typesetter:typeset(footnoteNumber)
end)
-- Add the footnote content to the frame's list
if footnotes[targetFrame] then
table.insert(footnotes[targetFrame], {
number = footnoteNumber,
content = content,
})
end
end)
end
package.documentation = [[
\begin{document}
The \autodoc:package{parallel} package provides a mechanism for typesetting diglot or other parallel documents. When used by a class such as \code{classes/diglot.lua}, it registers a command for each parallel frame, allowing users to select which frame to typeset into.
The package defines the \autodoc:command{\sync} command, which adds vertical spacing to the bottom of each frame to ensure that the \em{next} set of text is horizontally aligned. It also supports independent footnote flows and counters for each frame. Footnotes can be typeset using \autodoc:command{\parallel_footnote}, with styles adopted from the \code{resilient.footnotes} package. Note that \code{document.parskip} is not supported due to manual manipulation of \code{typesetter.state.outputQueue}. Therefore, to start a new paragraph within a frame, users must manually use the \autodoc:command{\parskip} command.
This package is under development and not yet fully mature. Testing has shown that it works best with a font size of 12pt from the \strong{Gentium Plus} family. Custom settings for \code{document.parskip}, \code{document.baselineskip}, or using different font sizes between frames may disrupt frame alignment, making precise alignment challenging.
Frame alignment in parallel typesetting is particularly tricky because it involves multiple interdependent variables and processes that must be carefully synchronized to produce visually cohesive results. Each frame may contain varying amounts of content, leading to differences in height between frames. The height of each frame depends on its content, including typeset text, insertions (e.g., footnotes), and vertical glue. Manual adjustments (e.g., custom \code{baselineSkip}, \code{parSkip}, or font sizes) are often required, further complicating alignment.
SILE’s default page builder operates on a single vertical stream, while parallel typesetting demands handling multiple streams (frames) independently while maintaining their horizontal alignment. This requires custom page-breaking and alignment logic to synchronize the streams. Manually tracking and adjusting frame heights by applying stretchy glue is essential for achieving proper alignment.
Insertions like footnotes add further complexity, as they occupy independent frames and their content flows dynamically. Ensuring these dynamic insertions do not disrupt frame alignment is challenging. When footnotes overflow, splitting them across pages can result in misalignment or compressed content if not carefully managed.
Using different font sizes or baselines for frames (e.g., for bilingual text) requires fine-tuning \code{baselineSkip}, \code{lineSkip}, or \code{parskip} settings to maintain alignment. Frames may also have varying widths or layout constraints, making it difficult to directly compare their heights.
Dynamic content, such as varying paragraph lengths, images, or tables, can lead to unpredictable behavior in each frame. Frequent recalibration is necessary to address these issues. Managing overflow content for the main frames and their footnote counterparts without disrupting alignment adds yet another layer of complexity.
To align frames reasonably, dummy content or vertical glue is often added to the shorter frame. However, such calculations must be precise to avoid visual artifacts caused by estimation errors. Even minor inaccuracies in frame height or glue calculations can result in misalignment.
SILE is primarily designed for single-frame typesetting, with limited native support for parallel or multi-frame layouts. Consequently, most parallel typesetting functionality must be implemented manually, requiring a deep understanding of SILE’s internals. Achieving proper frame alignment often involves trial and error, such as adding dummy text or phantom boxes to fine-tune the layout.
Synchronizing frames across pages involves recalculating frame heights when a new page is entered, managing footnotes, and ensuring consistent alignment. Frequent synchronization can be computationally expensive, particularly for complex or lengthy documents.
Parallel typesetting demands pixel-perfect precision to avoid noticeable misalignment. Achieving such precision often sacrifices flexibility when handling variable content. Users may need to create separate document classes tailored to specific documents.
For examples and further details, see \url{https://sile-typesetter.org/examples/parallel.sil} and the source code of \code{classes/diglot.lua}.
\end{document}
]]
return package
|
Beta Was this translation helpful? Give feedback.
Using insertion classes is still a mystery to me, so I came up with an alternative method for handling overflowed footnote content.
And here is the home-made remedy: