local class = pl.class()
class.type = "class"
class._name = "base"
class._initialized = false
class.deferredInit = {}
class.pageTemplate = { frames = {}, firstContentFrame = nil }
class.defaultFrameset = {}
class.firstContentFrame = "page"
class.options = setmetatable({}, {
_opts = {},
__newindex = function (self, key, value)
local opts = getmetatable(self)._opts
if type(opts[key]) == "function" then
opts[key](class, value)
elseif type(value) == "function" then
opts[key] = value
elseif type(key) == "number" then
return
else
SU.error("Attempted to set an undeclared class option '" .. key .. "'")
end
end,
__index = function (self, key)
if key == "super" then
return nil
end
if type(key) == "number" then
return nil
end
local opt = getmetatable(self)._opts[key]
if type(opt) == "function" then
return opt(class)
elseif opt then
return opt
else
SU.error("Attempted to get an undeclared class option '" .. key .. "'")
end
end,
})
class.hooks = {
newpage = {},
endpage = {},
finish = {},
}
class.packages = {}
function class:_init (options)
SILE.scratch.half_initialized_class = self
if self == options then
options = {}
end
SILE.languageSupport.loadLanguage("und") self:_declareBaseOptions()
self:declareOptions()
self:registerRawHandlers()
self:_declareBaseSettings()
self:declareSettings()
self:_registerBaseCommands()
self:registerCommands()
self:setOptions(options)
self:declareFrames(self.defaultFrameset)
self:registerPostinit(function (self_)
if type(self.firstContentFrame) == "string" then
self_.pageTemplate.firstContentFrame = self_.pageTemplate.frames[self_.firstContentFrame]
end
local frame = self_:initialFrame()
SILE.typesetter = SILE.typesetters.default(frame)
SILE.typesetter:registerPageEndHook(function ()
SU.debug("frames", function ()
for _, v in pairs(SILE.frames) do
SILE.outputter:debugFrame(v)
end
return "Drew debug outlines around frames"
end)
end)
end)
end
function class:_post_init ()
SILE.documentState.documentClass = self
self._initialized = true
for i, func in ipairs(self.deferredInit) do
func(self)
self.deferredInit[i] = nil
end
SILE.scratch.half_initialized_class = nil
end
function class:setOptions (options)
options = options or {}
self.options.landscape = SU.boolean(options.landscape, false)
options.landscape = nil
self.options.papersize = options.papersize or "a4"
options.papersize = nil
self.options.bleed = options.bleed or "0"
options.bleed = nil
self.options.sheetsize = options.sheetsize or nil
options.sheetsize = nil
for option, value in pairs(options) do
self.options[option] = value
end
end
function class:declareOption (option, setter)
rawset(getmetatable(self.options)._opts, option, nil)
self.options[option] = setter
end
function class:declareOptions () end
function class:_declareBaseOptions ()
self:declareOption("class", function (_, name)
if name then
if self._legacy then
self._name = name
elseif name ~= self._name then
SU.error("Cannot change class name after instantiation, derive a new class instead")
end
end
return self._name
end)
self:declareOption("landscape", function (_, landscape)
if landscape then
self.landscape = landscape
end
return self.landscape
end)
self:declareOption("papersize", function (_, size)
if size then
self.papersize = size
SILE.documentState.paperSize = SILE.papersize(size, self.options.landscape)
SILE.documentState.orgPaperSize = SILE.documentState.paperSize
SILE.newFrame({
id = "page",
left = 0,
top = 0,
right = SILE.documentState.paperSize[1],
bottom = SILE.documentState.paperSize[2],
})
end
return self.papersize
end)
self:declareOption("sheetsize", function (_, size)
if size then
self.sheetsize = size
SILE.documentState.sheetSize = SILE.papersize(size, self.options.landscape)
if
SILE.documentState.sheetSize[1] < SILE.documentState.paperSize[1]
or SILE.documentState.sheetSize[2] < SILE.documentState.paperSize[2]
then
SU.error("Sheet size shall not be smaller than the paper size")
end
if SILE.documentState.sheetSize[1] < SILE.documentState.paperSize[1] + SILE.documentState.bleed then
SU.debug("frames", "Sheet size width augmented to take page bleed into account")
SILE.documentState.sheetSize[1] = SILE.documentState.paperSize[1] + SILE.documentState.bleed
end
if SILE.documentState.sheetSize[2] < SILE.documentState.paperSize[2] + SILE.documentState.bleed then
SU.debug("frames", "Sheet size height augmented to take page bleed into account")
SILE.documentState.sheetSize[2] = SILE.documentState.paperSize[2] + SILE.documentState.bleed
end
else
return self.sheetsize
end
end)
self:declareOption("bleed", function (_, dimen)
if dimen then
self.bleed = dimen
SILE.documentState.bleed = SU.cast("measurement", dimen):tonumber()
end
return self.bleed
end)
end
function class:declareSettings () end
function class:_declareBaseSettings ()
SILE.settings:declare({
parameter = "current.parindent",
type = "glue or nil",
default = nil,
help = "Glue at start of paragraph",
})
SILE.settings:declare({
parameter = "current.hangIndent",
type = "measurement or nil",
default = nil,
help = "Size of hanging indent",
})
SILE.settings:declare({
parameter = "current.hangAfter",
type = "integer or nil",
default = nil,
help = "Number of lines affected by handIndent",
})
end
function class:loadPackage (packname, options, reload)
local pack
if type(packname) == "table" then
pack, packname = packname, packname._name
elseif type(packname) == "nil" or packname == "nil" or pl.stringx.strip(packname) == "" then
SU.error(("Attempted to load package with an invalid packname '%s'"):format(packname))
else
pack = require(("packages.%s"):format(packname))
if pack._name ~= packname then
SU.error(("Loaded module name '%s' does not match requested name '%s'"):format(pack._name, packname))
end
end
SILE.packages[packname] = pack
if type(pack) == "table" and pack.type == "package" then if self.packages[packname] then
local current_instance = self.packages[packname]
pack._create = function ()
return current_instance
end
pack(options, true)
else
self.packages[packname] = pack(options, reload)
end
else self:initPackage(pack, options)
end
end
function class:reloadPackage (packname, options)
return self:loadPackage(packname, options, true)
end
function class.initPackage ()
SU.deprecated(
"class:initPackage(options)",
"package(options)",
"0.14.0",
"0.16.0",
[[
This package appears to be a legacy format package. It returns a table and
expects SILE to guess about what to do. New packages inherit from the base
class and have a constructor function (_init) that automatically handles
setup.
]]
)
end
function class:registerPostinit (func, options)
if self._initialized then
return func(self, options)
end
table.insert(self.deferredInit, function (_)
func(self, options)
end)
end
function class:registerHook (category, func)
for _, func_ in ipairs(self.hooks[category]) do
if func_ == func then
return
end
end
table.insert(self.hooks[category], func)
end
function class:runHooks (category, options)
for _, func in ipairs(self.hooks[category]) do
SU.debug("classhooks", "Running hook from", category, options and "with options #" .. #options)
func(self, options)
end
end
function class:registerCommand (name, func, help, pack)
SILE.Commands[name] = func
if not pack then
local where = debug.getinfo(2).source
pack = where:match("(%w+).lua")
end
SILE.Help[name] = {
description = help,
where = pack,
}
end
function class:registerRawHandler (format, callback)
SILE.rawHandlers[format] = callback
end
function class:registerRawHandlers ()
self:registerRawHandler("text", function (_, content)
SILE.settings:temporarily(function ()
SILE.settings:set("typesetter.parseppattern", "\n")
SILE.settings:set("typesetter.obeyspaces", true)
SILE.typesetter:typeset(content[1])
end)
end)
end
local function packOptions (options)
local relevant = pl.tablex.copy(options)
relevant.src = nil
relevant.format = nil
relevant.module = nil
relevant.require = nil
return relevant
end
function class.registerCommands () end
function class:_registerBaseCommands ()
local function replaceProcessBy (replacement, tree)
if type(tree) ~= "table" then
return tree
end
local ret = pl.tablex.deepcopy(tree)
if tree.command == "process" then
return replacement
else
for i, child in ipairs(tree) do
ret[i] = replaceProcessBy(replacement, child)
end
return ret
end
end
self:registerCommand("define", function (options, content)
SU.required(options, "command", "defining command")
if type(content) == "function" then
self:registerCommand(options["command"], content)
return
elseif options.command == "process" then
SU.warn([[
Did you mean to re-definine the `\\process` macro?
That probably won't go well.
]])
end
self:registerCommand(options["command"], function (_, inner_content)
SU.debug("macros", "Processing macro \\" .. options["command"])
local macroArg
if type(inner_content) == "function" then
macroArg = inner_content
elseif type(inner_content) == "table" then
macroArg = pl.tablex.copy(inner_content)
macroArg.command = nil
macroArg.id = nil
elseif inner_content == nil then
macroArg = {}
else
SU.error(
"Unhandled content type " .. type(inner_content) .. " passed to macro \\" .. options["command"],
true
)
end
local newContent = replaceProcessBy(macroArg, content)
SILE.process(newContent)
SU.debug("macros", "Finished processing \\" .. options["command"])
end, options.help, SILE.currentlyProcessingFile)
end, "Define a new macro. \\define[command=example]{ ... \\process }")
self:registerCommand("noop", function (_, content)
SILE.process(content)
end)
self:registerCommand("document", function (_, content)
SILE.process(content)
end)
self:registerCommand("sile", function (_, content)
SILE.process(content)
end)
self:registerCommand("comment", function (_, _) end, "Ignores any text within this command's body.")
self:registerCommand("process", function ()
SU.error("Encountered unsubstituted \\process")
end, "Within a macro definition, processes the contents of the macro body.")
self:registerCommand("script", function (options, content)
local function _deprecated (original, suggested)
SU.deprecated(
"\\script",
"\\lua or \\use",
"0.15.0",
"0.16.0",
([[
The \script function has been deprecated. It was overloaded to mean too many
different things and more targeted tools were introduced in SILE v0.14.0. To
load 3rd party modules designed for use with SILE, replace \script[src=...]
with \use[module=...]. To run arbitrary Lua code inline use \lua{}, optionally
with a require= parameter to load a (non-SILE) Lua module using the Lua module
path or src= to load a file by file path.
For this use case consider replacing:
%s
with:
%s
]]):format(original, suggested)
)
end
if SU.ast.hasContent(content) then
_deprecated("\\script{...}", "\\lua{...}")
elseif options.src then
local module = options.src:gsub("%/", ".")
local original = (("\\script[src=%s]"):format(options.src))
local result = SILE.require(options.src)
local suggested = (type(result) == "table" and result._name and "\\use[module=%s]" or "\\lua[require=%s]"):format(
module
)
_deprecated(original, suggested)
else
SU.error("\\script function requires inline content or a src file path")
end
end, "Runs lua code. The code may be supplied either inline or using src=...")
self:registerCommand("include", function (options, content)
local packopts = packOptions(options)
if SU.ast.hasContent(content) then
local doc = SU.ast.contentToString(content)
return SILE.processString(doc, options.format, nil, packopts)
elseif options.src then
return SILE.processFile(options.src, options.format, packopts)
else
SU.error("\\include function requires inline content or a src file path")
end
end, "Includes a content file for processing.")
self:registerCommand(
"lua",
function (options, content)
local packopts = packOptions(options)
if SU.ast.hasContent(content) then
local doc = SU.ast.contentToString(content)
return SILE.processString(doc, "lua", nil, packopts)
elseif options.src then
return SILE.processFile(options.src, "lua", packopts)
elseif options.require then
local module = SU.required(options, "require", "lua")
return require(module)
else
SU.error("\\lua function requires inline content or a src file path or a require module name")
end
end,
"Run Lua code. The code may be supplied either inline, using require=... for a Lua module, or using src=... for a file path"
)
self:registerCommand("sil", function (options, content)
local packopts = packOptions(options)
if SU.ast.hasContent(content) then
local doc = SU.ast.contentToString(content)
return SILE.processString(doc, "sil")
elseif options.src then
return SILE.processFile(options.src, "sil", packopts)
else
SU.error("\\sil function requires inline content or a src file path")
end
end, "Process sil content. The content may be supplied either inline or using src=...")
self:registerCommand("xml", function (options, content)
local packopts = packOptions(options)
if SU.ast.hasContent(content) then
local doc = SU.ast.contentToString(content)
return SILE.processString(doc, "xml", nil, packopts)
elseif options.src then
return SILE.processFile(options.src, "xml", packopts)
else
SU.error("\\xml function requires inline content or a src file path")
end
end, "Process xml content. The content may be supplied either inline or using src=...")
self:registerCommand(
"use",
function (options, content)
local packopts = packOptions(options)
if content[1] and string.len(content[1]) > 0 then
local doc = SU.ast.contentToString(content)
SILE.processString(doc, "lua", nil, packopts)
else
if options.src then
SU.warn([[
Use of 'src' with \\use is discouraged.
Its path handling will eventually be deprecated.
Use 'module' instead when possible.
]])
SILE.processFile(options.src, "lua", packopts)
else
local module = SU.required(options, "module", "use")
SILE.use(module, packopts)
end
end
end,
"Load and initialize a SILE module (can be a package, a shaper, a typesetter, or whatever). Use module=... to specif what to load or include module code inline."
)
self:registerCommand("raw", function (options, content)
local rawtype = SU.required(options, "type", "raw")
local handler = SILE.rawHandlers[rawtype]
if not handler then
SU.error("No inline handler for '" .. rawtype .. "'")
end
handler(options, content)
end, "Invoke a raw passthrough handler")
self:registerCommand("pagetemplate", function (options, content)
SILE.typesetter:pushState()
SILE.documentState.thisPageTemplate = { frames = {} }
SILE.process(content)
SILE.documentState.thisPageTemplate.firstContentFrame = SILE.getFrame(options["first-content-frame"])
SILE.typesetter:initFrame(SILE.documentState.thisPageTemplate.firstContentFrame)
SILE.typesetter:popState()
end, "Defines a new page template for the current page and sets the typesetter to use it.")
self:registerCommand("frame", function (options, _)
SILE.documentState.thisPageTemplate.frames[options.id] = SILE.newFrame(options)
end, "Declares (or re-declares) a frame on this page.")
self:registerCommand("penalty", function (options, _)
if SU.boolean(options.vertical, false) and not SILE.typesetter:vmode() then
SILE.typesetter:leaveHmode()
end
if SILE.typesetter:vmode() then
SILE.typesetter:pushVpenalty({ penalty = tonumber(options.penalty) })
else
SILE.typesetter:pushPenalty({ penalty = tonumber(options.penalty) })
end
end, "Inserts a penalty node. Option is penalty= for the size of the penalty.")
self:registerCommand("discretionary", function (options, _)
local discretionary = SILE.types.node.discretionary({})
if options.prebreak then
local hbox = SILE.typesetter:makeHbox({ options.prebreak })
discretionary.prebreak = { hbox }
end
if options.postbreak then
local hbox = SILE.typesetter:makeHbox({ options.postbreak })
discretionary.postbreak = { hbox }
end
if options.replacement then
local hbox = SILE.typesetter:makeHbox({ options.replacement })
discretionary.replacement = { hbox }
end
table.insert(SILE.typesetter.state.nodes, discretionary)
end, "Inserts a discretionary node.")
self:registerCommand("glue", function (options, _)
local width = SU.cast("length", options.width):absolute()
SILE.typesetter:pushGlue(width)
end, "Inserts a glue node. The width option denotes the glue dimension.")
self:registerCommand("kern", function (options, _)
local width = SU.cast("length", options.width):absolute()
SILE.typesetter:pushHorizontal(SILE.types.node.kern(width))
end, "Inserts a glue node. The width option denotes the glue dimension.")
self:registerCommand("skip", function (options, _)
options.discardable = SU.boolean(options.discardable, false)
options.height = SILE.types.length(options.height):absolute()
SILE.typesetter:leaveHmode()
if options.discardable then
SILE.typesetter:pushVglue(options)
else
SILE.typesetter:pushExplicitVglue(options)
end
end, "Inserts vertical skip. The height options denotes the skip dimension.")
self:registerCommand("par", function (_, _)
SILE.typesetter:endline()
end, "Ends the current paragraph.")
end
function class:initialFrame ()
SILE.documentState.thisPageTemplate = pl.tablex.deepcopy(self.pageTemplate)
SILE.frames = { page = SILE.frames.page }
for k, v in pairs(SILE.documentState.thisPageTemplate.frames) do
SILE.frames[k] = v
end
if not SILE.documentState.thisPageTemplate.firstContentFrame then
SILE.documentState.thisPageTemplate.firstContentFrame = SILE.frames[self.firstContentFrame]
end
SILE.documentState.thisPageTemplate.firstContentFrame:invalidate()
return SILE.documentState.thisPageTemplate.firstContentFrame
end
function class:declareFrame (id, spec)
spec.id = id
if spec.solve then
self.pageTemplate.frames[id] = spec
else
self.pageTemplate.frames[id] = SILE.newFrame(spec)
end
end
function class:declareFrames (specs)
if specs then
for k, v in pairs(specs) do
self:declareFrame(k, v)
end
end
end
function class.newPar (typesetter)
local parindent = SILE.settings:get("current.parindent") or SILE.settings:get("document.parindent")
typesetter:pushGlue(parindent:absolute())
local hangIndent = SILE.settings:get("current.hangIndent")
if hangIndent then
SILE.settings:set("linebreak.hangIndent", hangIndent)
end
local hangAfter = SILE.settings:get("current.hangAfter")
if hangAfter then
SILE.settings:set("linebreak.hangAfter", hangAfter)
end
end
function class.endPar (typesetter)
local queue = typesetter.state.outputQueue
local last_vbox = queue and queue[#queue]
local last_is_vglue = last_vbox and last_vbox.is_vglue
local last_is_vpenalty = last_vbox and last_vbox.is_penalty
if typesetter:vmode() and (last_is_vglue or last_is_vpenalty) then
return
end
SILE.settings:set("current.parindent", nil)
typesetter:leaveHmode()
typesetter:pushVglue(SILE.settings:get("document.parskip"))
end
function class:newPage ()
SILE.outputter:newPage()
self:runHooks("newpage")
return self:initialFrame()
end
function class:endPage ()
SILE.typesetter.frame:leave(SILE.typesetter)
self:runHooks("endpage")
end
function class:finish ()
SILE.inputter:postamble()
SILE.typesetter:endline()
SILE.call("vfill")
while not SILE.typesetter:isQueueEmpty() do
SILE.call("supereject")
SILE.typesetter:leaveHmode(true)
SILE.typesetter:buildPage()
if not SILE.typesetter:isQueueEmpty() then
SILE.typesetter:initNextFrame()
end
end
SILE.typesetter:runHooks("pageend") self:endPage()
if SILE.typesetter and not SILE.typesetter:isQueueEmpty() then
SU.error("Queues are not empty as expected after ending last page", true)
end
SILE.outputter:finish()
self:runHooks("finish")
end
return class