Skip to content

Commit

Permalink
feat: Typesetter (multi-)liner support
Browse files Browse the repository at this point in the history
  • Loading branch information
Omikhleia authored and Didier Willis committed Jan 29, 2024
1 parent f567a08 commit 10c1db1
Showing 1 changed file with 202 additions and 3 deletions.
205 changes: 202 additions & 3 deletions silex/typesetters/base.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,39 @@

-- Typesetter base class

-- BEGIN SILEX LINER
-- These two special nodes are used to track the current liner entry and exit.
-- As Sith Lords, they are always two: they are local here, so no one can
-- use one alone and break the balance of the Force.
local linerEnterNode = pl.class(SILE.nodefactory.zerohbox)
function linerEnterNode:_init(name, outputMethod)
SILE.nodefactory.hbox._init(self)
self.outputMethod = outputMethod
self.name = name
self.enter = self.name
end
function linerEnterNode:clone()
local n = linerEnterNode(self.name, self.outputMethod)
return n
end
function linerEnterNode:__tostring ()
return "+L[" .. self.name .. "]"
end
local linerLeaveNode = pl.class(SILE.nodefactory.zerohbox)
function linerLeaveNode:_init(name)
SILE.nodefactory.hbox._init(self)
self.name = name
self.leave = self.name
end
function linerLeaveNode:clone()
local n = linerLeaveNode(self.name)
return n
end
function linerLeaveNode:__tostring ()
return "-L[" .. self.name .. "]"
end
-- END SILEX LINER

local typesetter = pl.class()
typesetter.type = "typesetter"
typesetter._name = "base"
Expand Down Expand Up @@ -169,6 +202,7 @@ function typesetter:initState ()
nodes = {},
outputQueue = {},
lastBadness = awful_bad,
liner = {}, -- SILEX LINER
}
end

Expand Down Expand Up @@ -965,6 +999,107 @@ function typesetter:addrlskip (slice, margins, hangLeft, hangRight)
table.insert(slice, 1, SILE.nodefactory.zerohbox())
end

-- BEGIN SILEX LINER
--- Any unclosed liner is reopened on the current line, so we clone and repeat
-- it.
-- An assumption is that the inserts are done after the current slice content,
-- supposed to be just before meaningful (visible) content.
---@param slice table Current line nodes
---@return boolean Whether a liner was reopened
function typesetter:repeatEnterLiners (slice)
local m = self.state.liner
if #m > 0 then
for i = 1, #m do
local n = m[i]:clone()
slice[#slice + 1] = n
SU.debug("liner", "Reopening liner", n)
end
return true
end
return false
end

--- All pairs of liners are rebuilt as a single hbox, wrapping its content.
---@param slice table Current line nodes
---@return table New reboxed slice
function typesetter:reboxLiners (slice)
local out = {}
local stack = {}
for i = 1, #slice do
local node = slice[i]
if node.enter then
SU.debug("liner", "Start reboxing", node)
local n = SILE.nodefactory.hbox({
width = SILE.length(),
height = SILE.length(),
depth = SILE.length(),
name = node.name, -- For mere debug
inner = {},
outputYourself = node.outputMethod
})
stack[#stack + 1] = n
elseif node.leave then
SU.debug("liner", "End reboxing", node)
if #stack == 1 then
out[#out + 1] = stack[1]
else
local hbox = stack[#stack - 1]
hbox.inner[#hbox.inner + 1] = stack[#stack]
end
stack[#stack] = nil
else
if #stack > 0 then
local hbox = stack[#stack]
-- Add node and recomputes dimensions
hbox.inner[#hbox.inner + 1] = node
hbox.width = hbox.width + node.width
hbox.height = SU.max(hbox.height, node.height)
hbox.depth = SU.max(hbox.depth, node.depth)
else
out[#out+1] = node
end
end
end
return out -- new reboxed slice
end

--- Check if a node is a liner, and process it if so, in a stack.
---@param node any Current node
---@return boolean Whether a liner was opened
function typesetter:processIfLiner(node)
local entered = false
if node.enter then
SU.debug("liner", "Enter liner", node)
self.state.liner[#self.state.liner+1] = node
entered = true
elseif node.leave then
SU.debug("liner", "Leave liner", node)
if #self.state.liner == 0 then
SU.error("Multiliner stack mismatch" .. node)
elseif self.state.liner[#self.state.liner].enter == node.leave then
self.state.liner[#self.state.liner].link = node
self.state.liner[#self.state.liner] = nil
else
SU.error("Multiliner stack inconsistency"
.. self.state.liner[#self.state.liner] .. "vs. " .. node)
end
end
return entered
end

function typesetter:repeatLeaveLiners(slice, insertIndex)
for i, v in ipairs(self.state.liner) do
if not v.link then
local n = linerLeaveNode(v.name)
SU.debug("liner", "Closing liner", n)
table.insert(slice, insertIndex, n)
else
SU.error("Multiliner stack inconsistency" .. v)
end
end
end
-- END SILEX LINER

function typesetter:breakpointsToLines (breakpoints)
local linestart = 1
local lines = {}
Expand All @@ -975,14 +1110,31 @@ function typesetter:breakpointsToLines (breakpoints)
if point.position ~= 0 then
local slice = {}
local seenNonDiscardable = false
-- BEGIN SILEX LINER
local seenLiner = false
local lastContentNodeIndex

for j = linestart, point.position do
slice[#slice+1] = nodes[j]
if nodes[j] then
if not nodes[j].discardable then
local currentNode = nodes[j]
if not currentNode.discardable
and not currentNode.is_glue and not currentNode.is_zero then
-- actual visible content starts here
lastContentNodeIndex = #slice + 1
end
if not seenLiner and lastContentNodeIndex then
-- Any stacked liner (unclosed from a previous line) is reopened on
-- the current line.
seenLiner = self:repeatEnterLiners(slice)
end
slice[#slice+1] = currentNode
if currentNode then
if not currentNode.discardable then
seenNonDiscardable = true
end
seenLiner = self:processIfLiner(currentNode) or seenLiner
end
end
-- END SILEX LINER
if not seenNonDiscardable then
-- Slip lines containing only discardable nodes (e.g. glues).
SU.debug("typesetter", "Skipping a line containing only discardable nodes")
Expand All @@ -996,6 +1148,11 @@ function typesetter:breakpointsToLines (breakpoints)
linestart = point.position + 1
end

-- BEGIN SILEX LINER
-- Any unclosed liner is closed on the next line in reverse order.
self:repeatLeaveLiners(slice, lastContentNodeIndex + 1)
-- END SILEX LINER

-- BEGIN SILEX HANGED LINES
-- Track hanged lines
if self.state.hangAfter then
Expand All @@ -1017,6 +1174,14 @@ function typesetter:breakpointsToLines (breakpoints)

-- And compute the line...
local ratio = self:computeLineRatio(point.width, slice)

-- BEGIN SILEX LINER
-- Re-shuffle liners, if any, into their own boxes.
if seenLiner then
slice = self:reboxLiners(slice)
end
-- END SILEX LINER

local thisLine = { ratio = ratio, nodes = slice }
lines[#lines+1] = thisLine

Expand Down Expand Up @@ -1230,4 +1395,38 @@ function typesetter:pushHlist (hlist)
end
end

-- BEGIN SILEX LINER
--- A liner is a construct that may span multiple lines.
-- The content may be line-broken, and each bit on each line will be wrapped
-- into a box. These box will be formatted according to some output logic.
-- If we are already in horizontal-restricted mode, the liner is processed
-- immediately, since line breaking won't occur then.
---@param name string Name of the liner (usefull for debugging)
---@param content table SILE AST to process
---@param outputYourself function Output method for wrapped boxes
function typesetter:liner (name, content, outputYourself)
if self.state.hmodeOnly then
SU.debug("liner", "Applying liner in horizontal-restricted mode")
local hbox, hlist = self:makeHbox(content)
self:pushHbox({
width = hbox.width,
height = hbox.height,
depth = hbox.depth,
inner = { hbox },
outputYourself = outputYourself,
})
self:pushHlist(hlist)
else
self.state.linerCount = (self.state.linerCount or 0) + 1
local uname = name .. "_" .. self.state.linerCount
SU.debug("liner", "Applying liner in standard mode")
local enter = linerEnterNode(uname, outputYourself)
local leave = linerLeaveNode(uname)
self:pushHorizontal(enter)
SILE.process(content)
self:pushHorizontal(leave)
end
end
-- END SILEX LINER

return typesetter

0 comments on commit 10c1db1

Please sign in to comment.