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

sile-typesetter / sile / 13437886692

20 Feb 2025 02:44PM UTC coverage: 41.032% (+9.1%) from 31.943%
13437886692

push

github

web-flow
chore(deps): Bump DeterminateSystems/magic-nix-cache-action from 8 to 9 (#2219)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

8188 of 19955 relevant lines covered (41.03%)

3258.14 hits per line

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

76.35
/classes/base.lua
1
--- SILE document class interface.
2
-- @interfaces classes
3

4
local class = pl.class()
261✔
5
class.type = "class"
261✔
6
class._name = "base"
261✔
7

8
class._initialized = false
261✔
9
class.deferredInit = {}
261✔
10
class.pageTemplate = { frames = {}, firstContentFrame = nil }
261✔
11
class.defaultFrameset = {}
261✔
12
class.firstContentFrame = "page"
261✔
13
class.options = setmetatable({}, {
522✔
14
   _opts = {},
261✔
15
   __newindex = function (self, key, value)
16
      local opts = getmetatable(self)._opts
1,350✔
17
      if type(opts[key]) == "function" then
1,350✔
18
         opts[key](class, value)
1,088✔
19
      elseif type(value) == "function" then
806✔
20
         opts[key] = value
806✔
21
      elseif type(key) == "number" then
×
22
         return
×
23
      else
24
         SU.error("Attempted to set an undeclared class option '" .. key .. "'")
×
25
      end
26
   end,
27
   __index = function (self, key)
28
      if key == "super" then
141✔
29
         return nil
×
30
      end
31
      if type(key) == "number" then
141✔
32
         return nil
×
33
      end
34
      local opt = getmetatable(self)._opts[key]
141✔
35
      if type(opt) == "function" then
141✔
36
         return opt(class)
141✔
37
      elseif opt then
×
38
         return opt
×
39
      else
40
         SU.error("Attempted to get an undeclared class option '" .. key .. "'")
×
41
      end
42
   end,
43
})
261✔
44
class.hooks = {
261✔
45
   newpage = {},
261✔
46
   endpage = {},
261✔
47
   finish = {},
261✔
48
}
261✔
49

50
class.packages = {}
261✔
51

52
function class:_init (options)
261✔
53
   SILE.scratch.half_initialized_class = self
133✔
54
   if self == options then
133✔
55
      options = {}
×
56
   end
57
   SILE.languageSupport.loadLanguage("und") -- preload for unlocalized fallbacks
133✔
58
   self:declareOptions()
133✔
59
   self:registerRawHandlers()
133✔
60
   self:declareSettings()
133✔
61
   self:registerCommands()
133✔
62
   self:setOptions(options)
133✔
63
   self:declareFrames(self.defaultFrameset)
133✔
64
   self:registerPostinit(function (self_)
266✔
65
      -- In the event no packages have called \language explicitly or otherwise triggerend the language loader, at this
66
      -- point we'll have a language *setting* but not actually have loaded the language. We put it off as long as we
67
      -- could in case the user changed the default document language and we didn't need to load the system default one,
68
      -- but that time has come at gone at this point. Make sure we've loaded somethnig...
69
      local lang = SILE.settings:get("document.language")
133✔
70
      SILE.languageSupport.loadLanguage(lang)
133✔
71
      if type(self.firstContentFrame) == "string" then
133✔
72
         self_.pageTemplate.firstContentFrame = self_.pageTemplate.frames[self_.firstContentFrame]
133✔
73
      end
74
      local frame = self_:initialFrame()
133✔
75
      SILE.typesetter = SILE.typesetters.base(frame)
266✔
76
      SILE.typesetter:registerPageEndHook(function ()
266✔
77
         SU.debug("frames", function ()
330✔
78
            for _, v in pairs(SILE.frames) do
×
79
               SILE.outputter:debugFrame(v)
×
80
            end
81
            return "Drew debug outlines around frames"
×
82
         end)
83
      end)
84
   end)
85
end
86

87
function class:_post_init ()
261✔
88
   SILE.documentState.documentClass = self
133✔
89
   self._initialized = true
133✔
90
   for i, func in ipairs(self.deferredInit) do
329✔
91
      func(self)
196✔
92
      self.deferredInit[i] = nil
196✔
93
   end
94
   SILE.scratch.half_initialized_class = nil
133✔
95
end
96

97
function class:setOptions (options)
261✔
98
   options = options or {}
133✔
99
   -- Classes that add options with dependencies should explicitly handle them, then exempt them from further processing.
100
   -- The landscape and crop related options are handled explicitly before papersize, then the "rest" of options that are not interdependent.
101
   self.options.landscape = SU.boolean(options.landscape, false)
266✔
102
   options.landscape = nil
133✔
103
   self.options.papersize = options.papersize or "a4"
133✔
104
   options.papersize = nil
133✔
105
   self.options.bleed = options.bleed or "0"
133✔
106
   options.bleed = nil
133✔
107
   self.options.sheetsize = options.sheetsize or nil
133✔
108
   options.sheetsize = nil
133✔
109
   for option, value in pairs(options) do
145✔
110
      self.options[option] = value
12✔
111
   end
112
end
113

114
function class:declareOption (option, setter)
261✔
115
   rawset(getmetatable(self.options)._opts, option, nil)
806✔
116
   self.options[option] = setter
806✔
117
end
118

119
function class:declareOptions ()
261✔
120
   self:declareOption("class", function (_, name)
266✔
121
      if name then
×
122
         if self._legacy then
×
123
            self._name = name
×
124
         elseif name ~= self._name then
×
125
            SU.error("Cannot change class name after instantiation, derive a new class instead")
×
126
         end
127
      end
128
      return self._name
×
129
   end)
130
   self:declareOption("landscape", function (_, landscape)
266✔
131
      if landscape then
267✔
132
         self.landscape = landscape
1✔
133
      end
134
      return self.landscape
267✔
135
   end)
136
   self:declareOption("papersize", function (_, size)
266✔
137
      if size then
133✔
138
         self.papersize = size
133✔
139
         SILE.documentState.paperSize = SILE.papersize(size, self.options.landscape)
399✔
140
         SILE.documentState.orgPaperSize = SILE.documentState.paperSize
133✔
141
         SILE.newFrame({
266✔
142
            id = "page",
143
            left = 0,
144
            top = 0,
145
            right = SILE.documentState.paperSize[1],
133✔
146
            bottom = SILE.documentState.paperSize[2],
133✔
147
         })
148
      end
149
      return self.papersize
133✔
150
   end)
151
   self:declareOption("sheetsize", function (_, size)
266✔
152
      if size then
133✔
153
         self.sheetsize = size
1✔
154
         SILE.documentState.sheetSize = SILE.papersize(size, self.options.landscape)
3✔
155
         if
156
            SILE.documentState.sheetSize[1] < SILE.documentState.paperSize[1]
1✔
157
            or SILE.documentState.sheetSize[2] < SILE.documentState.paperSize[2]
1✔
158
         then
159
            SU.error("Sheet size shall not be smaller than the paper size")
×
160
         end
161
         if SILE.documentState.sheetSize[1] < SILE.documentState.paperSize[1] + SILE.documentState.bleed then
1✔
162
            SU.debug("frames", "Sheet size width augmented to take page bleed into account")
×
163
            SILE.documentState.sheetSize[1] = SILE.documentState.paperSize[1] + SILE.documentState.bleed
×
164
         end
165
         if SILE.documentState.sheetSize[2] < SILE.documentState.paperSize[2] + SILE.documentState.bleed then
1✔
166
            SU.debug("frames", "Sheet size height augmented to take page bleed into account")
×
167
            SILE.documentState.sheetSize[2] = SILE.documentState.paperSize[2] + SILE.documentState.bleed
×
168
         end
169
      else
170
         return self.sheetsize
132✔
171
      end
172
   end)
173
   self:declareOption("bleed", function (_, dimen)
266✔
174
      if dimen then
133✔
175
         self.bleed = dimen
133✔
176
         SILE.documentState.bleed = SU.cast("measurement", dimen):tonumber()
399✔
177
      end
178
      return self.bleed
133✔
179
   end)
180
end
181

182
function class.declareSettings (_)
261✔
183
   SILE.settings:declare({
133✔
184
      parameter = "current.parindent",
185
      type = "glue or nil",
186
      default = nil,
187
      help = "Glue at start of paragraph",
188
   })
189
   SILE.settings:declare({
133✔
190
      parameter = "current.hangIndent",
191
      type = "measurement or nil",
192
      default = nil,
193
      help = "Size of hanging indent",
194
   })
195
   SILE.settings:declare({
133✔
196
      parameter = "current.hangAfter",
197
      type = "integer or nil",
198
      default = nil,
199
      help = "Number of lines affected by handIndent",
200
   })
201
end
202

203
function class:loadPackage (packname, options, reload)
261✔
204
   local pack
205
   -- Allow loading by injecting whole packages as-is, otherwise try to load it with the usual packages path.
206
   if type(packname) == "table" then
940✔
207
      pack, packname = packname, packname._name
176✔
208
   elseif type(packname) == "nil" or packname == "nil" or pl.stringx.strip(packname) == "" then
1,528✔
209
      SU.error(("Attempted to load package with an invalid packname '%s'"):format(packname))
×
210
   else
211
      pack = require(("packages.%s"):format(packname))
764✔
212
      if pack._name ~= packname then
764✔
213
         SU.error(("Loaded module name '%s' does not match requested name '%s'"):format(pack._name, packname))
×
214
      end
215
   end
216
   SILE.packages[packname] = pack
940✔
217
   if type(pack) == "table" and pack.type == "package" then -- current package api
940✔
218
      if self.packages[packname] then
940✔
219
         -- If the same package name has been loaded before, we might be loading a modified version of the same package or
220
         -- we might be re-loading the same package, or we might just be doubling up work because somebody called us twice.
221
         -- The package itself should take care of the difference between load and reload based on the reload flag here,
222
         -- but in addition to that we also want to avoid creating a new instance. We want to run the intitializer from the
223
         -- (possibly different) new module, but not create a new instance ID and loose any connections it made.
224
         -- To do this we add a create function that returns the current instance. This brings along the _initialized flag
225
         -- and of course anything else already setup and running.
226
         local current_instance = self.packages[packname]
95✔
227
         pack._create = function ()
228
            return current_instance
95✔
229
         end
230
         pack(options, true)
190✔
231
      else
232
         self.packages[packname] = pack(options, reload)
1,690✔
233
      end
234
   else -- legacy package
235
      self:initPackage(pack, options)
×
236
   end
237
end
238

239
function class:reloadPackage (packname, options)
261✔
240
   return self:loadPackage(packname, options, true)
×
241
end
242

243
function class:initPackage (pack, options)
261✔
244
   SU.deprecated(
×
245
      "class:initPackage(options)",
246
      "package(options)",
247
      "0.14.0",
248
      "0.16.0",
249
      [[
×
250
         This package appears to be a legacy format package. It returns a table and
251
         expects SILE to guess about what to do. New packages inherit from the base
252
         class and have a constructor function (_init) that automatically handles
253
         setup.
254
      ]]
×
255
   )
256
   if type(pack) == "table" then
×
257
      if pack.exports then
×
258
         pl.tablex.update(self, pack.exports)
×
259
      end
260
      if type(pack.declareSettings) == "function" then
×
261
         pack.declareSettings(self)
×
262
      end
263
      if type(pack.registerRawHandlers) == "function" then
×
264
         pack.registerRawHandlers(self)
×
265
      end
266
      if type(pack.registerCommands) == "function" then
×
267
         pack.registerCommands(self)
×
268
      end
269
      if type(pack.init) == "function" then
×
270
         self:registerPostinit(pack.init, options)
×
271
      end
272
   end
273
end
274

275
--- Register a callback function to be executed after the class initialization has completed.
276
-- Sometimes a class or package may want to run things after the class has been fully initialized. This can be useful
277
-- for setting document settings after packages and all their dependencies have been loaded. For example a package might
278
-- want to trigger something to happen after all frames have been defined, but the package itself doesn't know if it is
279
-- being loaded before or after the document options are processed, frame masters have been setup, etc. Rather than
280
-- relying on the user to load the package after these events, the package can use this callback to defer the action
281
-- until those things can be reasonable expected to have completed.
282
--
283
-- Functions in the deferred initialization queue are run on a first-set first-run basis.
284
--
285
-- Note the typesetter will *not* have been instantiated yet, so is not appropriate to try to output content at this
286
-- point. Injecting content to be processed at the start of a document should be done with preambles. The inputter
287
-- *will* be instantiated at this point, so adding a new preamble is viable.
288
-- If the class has already been initialized the callback function will be run immediately.
289
-- @tparam function func Callback function accepting up to two arguments.
290
-- @tparam[opt] table options Additional table passed as a second argument to the callback.
291
function class:registerPostinit (func, options)
261✔
292
   if self._initialized then
212✔
293
      return func(self, options)
16✔
294
   end
295
   table.insert(self.deferredInit, function (_)
392✔
296
      func(self, options)
196✔
297
   end)
298
end
299

300
function class:registerHook (category, func)
261✔
301
   for _, func_ in ipairs(self.hooks[category]) do
594✔
302
      if func_ == func then
193✔
303
         return
1✔
304
         --[[ See https://github.com/sile-typesetter/sile/issues/1531
305
      return SU.warn("Attempted to set the same function hook twice, probably unintended, skipping.")
306
      -- If the same function signature is already set a package is probably being
307
      -- re-initialized. Ditch the first instance of the hook so that it runs in
308
      -- the order of last initialization.
309
      self.hooks[category][_] = nil
310
      ]]
311
      end
312
   end
313
   table.insert(self.hooks[category], func)
401✔
314
end
315

316
function class:runHooks (category, options)
261✔
317
   for _, func in ipairs(self.hooks[category]) do
720✔
318
      SU.debug("classhooks", "Running hook from", category, options and "with options #" .. #options)
378✔
319
      func(self, options)
378✔
320
   end
321
end
322

323
--- Register a function as a SILE command.
324
-- Takes any Lua function and registers it for use as a SILE command (which will in turn be used to process any content
325
-- nodes identified with the command name.
326
--
327
-- Note that this should only be used to register commands supplied directly by a document class. A similar method is
328
-- available for packages, `packages:registerCommand`.
329
-- @tparam string name Name of cammand to register.
330
-- @tparam function func Callback function to use as command handler.
331
-- @tparam[opt] nil|string help User friendly short usage string for use in error messages, documentation, etc.
332
-- @tparam[opt] nil|string pack Information identifying the module registering the command for use in error and usage
333
-- messages. Usually auto-detected.
334
-- @see SILE.packages:registerCommand
335
function class.registerCommand (_, name, func, help, pack)
261✔
336
   SILE.Commands[name] = func
14,626✔
337
   if not pack then
14,626✔
338
      local where = debug.getinfo(2).source
14,614✔
339
      pack = where:match("(%w+).lua")
14,614✔
340
   end
341
   --if not help and not pack:match(".sil") then SU.error("Could not define command '"..name.."' (in package "..pack..") - no help text" ) end
342
   SILE.Help[name] = {
14,626✔
343
      description = help,
14,626✔
344
      where = pack,
14,626✔
345
   }
14,626✔
346
end
347

348
function class.registerRawHandler (_, format, callback)
261✔
349
   SILE.rawHandlers[format] = callback
141✔
350
end
351

352
function class:registerRawHandlers ()
261✔
353
   self:registerRawHandler("text", function (_, content)
266✔
354
      SILE.settings:temporarily(function ()
2✔
355
         SILE.settings:set("typesetter.parseppattern", "\n")
1✔
356
         SILE.settings:set("typesetter.obeyspaces", true)
1✔
357
         SILE.typesetter:typeset(content[1])
1✔
358
      end)
359
   end)
360
end
361

362
local function packOptions (options)
363
   local relevant = pl.tablex.copy(options)
203✔
364
   relevant.src = nil
203✔
365
   relevant.format = nil
203✔
366
   relevant.module = nil
203✔
367
   relevant.require = nil
203✔
368
   return relevant
203✔
369
end
370

371
function class:registerCommands ()
261✔
372
   local function replaceProcessBy (replacement, tree)
373
      if type(tree) ~= "table" then
157✔
374
         return tree
61✔
375
      end
376
      local ret = pl.tablex.deepcopy(tree)
96✔
377
      if tree.command == "process" then
96✔
378
         return replacement
7✔
379
      else
380
         for i, child in ipairs(tree) do
207✔
381
            ret[i] = replaceProcessBy(replacement, child)
236✔
382
         end
383
         return ret
89✔
384
      end
385
   end
386

387
   self:registerCommand("define", function (options, content)
266✔
388
      SU.required(options, "command", "defining command")
12✔
389
      if type(content) == "function" then
12✔
390
         -- A macro defined as a function can take no argument, so we register
391
         -- it as-is.
392
         self:registerCommand(options["command"], content)
×
393
         return
×
394
      elseif options.command == "process" then
12✔
395
         SU.warn([[
×
396
            Did you mean to re-definine the `\\process` macro?
397

398
            That probably won't go well.
399
         ]])
×
400
      end
401
      self:registerCommand(options["command"], function (_, inner_content)
24✔
402
         SU.debug("macros", "Processing macro \\" .. options["command"])
39✔
403
         local macroArg
404
         if type(inner_content) == "function" then
39✔
405
            macroArg = inner_content
2✔
406
         elseif type(inner_content) == "table" then
37✔
407
            macroArg = pl.tablex.copy(inner_content)
70✔
408
            macroArg.command = nil
35✔
409
            macroArg.id = nil
35✔
410
         elseif inner_content == nil then
2✔
411
            macroArg = {}
2✔
412
         else
413
            SU.error(
×
414
               "Unhandled content type " .. type(inner_content) .. " passed to macro \\" .. options["command"],
×
415
               true
416
            )
417
         end
418
         -- Replace every occurrence of \process in `content` (the macro
419
         -- body) with `macroArg`, then have SILE go through the new `content`.
420
         local newContent = replaceProcessBy(macroArg, content)
39✔
421
         SILE.process(newContent)
39✔
422
         SU.debug("macros", "Finished processing \\" .. options["command"])
39✔
423
      end, options.help, SILE.currentlyProcessingFile)
51✔
424
   end, "Define a new macro. \\define[command=example]{ ... \\process }")
145✔
425

426
   -- A utility function that allows SILE.call() to be used as a noop wrapper.
427
   self:registerCommand("noop", function (_, content)
266✔
428
      SILE.process(content)
9✔
429
   end)
430

431
   -- The document (SIL) or sile (XML) command is always the sigular leaf at the
432
   -- top level of our AST. The work you might expect to see happen here is
433
   -- actually handled by SILE.inputter:classInit() before we get here, so these
434
   -- are just pass through functions. Theoretically, this could be a useful
435
   -- point to hook into-especially for included documents.
436
   self:registerCommand("document", function (_, content)
266✔
437
      SILE.process(content)
×
438
   end)
439
   self:registerCommand("sile", function (_, content)
266✔
440
      SILE.process(content)
×
441
   end)
442

443
   self:registerCommand("comment", function (_, _) end, "Ignores any text within this command's body.")
133✔
444

445
   self:registerCommand("process", function ()
266✔
446
      SU.error("Encountered unsubstituted \\process")
×
447
   end, "Within a macro definition, processes the contents of the macro body.")
133✔
448

449
   self:registerCommand("script", function (options, content)
266✔
450
      local packopts = packOptions(options)
×
451
      local function _deprecated (original, suggested)
452
         SU.deprecated(
×
453
            "\\script",
454
            "\\lua or \\use",
455
            "0.15.0",
456
            "0.16.0",
457
            ([[
×
458
               The \script function has been deprecated. It was overloaded to mean too many
459
               different things and more targeted tools were introduced in SILE v0.14.0. To
460
               load 3rd party modules designed for use with SILE, replace \script[src=...]
461
               with \use[module=...]. To run arbitrary Lua code inline use \lua{}, optionally
462
               with a require= parameter to load a (non-SILE) Lua module using the Lua module
463
               path or src= to load a file by file path.
464

465
               For this use case consider replacing:
466

467
               %s
468

469
               with:
470

471
               %s
472
            ]]):format(original, suggested)
×
473
         )
474
      end
475
      if SU.ast.hasContent(content) then
×
476
         _deprecated("\\script{...}", "\\lua{...}")
×
477
         return SILE.processString(content[1], options.format or "lua", nil, packopts)
×
478
      elseif options.src then
×
479
         local module = options.src:gsub("%/", ".")
×
480
         local original = (("\\script[src=%s]"):format(options.src))
×
481
         local result = SILE.require(options.src)
×
482
         local suggested = (type(result) == "table" and result._name and "\\use[module=%s]" or "\\lua[require=%s]"):format(
×
483
            module
484
         )
485
         _deprecated(original, suggested)
×
486
         return result
×
487
      else
488
         SU.error("\\script function requires inline content or a src file path")
×
489
         return SILE.processString(content[1], options.format or "lua", nil, packopts)
×
490
      end
491
   end, "Runs lua code. The code may be supplied either inline or using src=...")
133✔
492

493
   self:registerCommand("include", function (options, content)
266✔
494
      local packopts = packOptions(options)
1✔
495
      if SU.ast.hasContent(content) then
2✔
496
         local doc = SU.ast.contentToString(content)
×
497
         return SILE.processString(doc, options.format, nil, packopts)
×
498
      elseif options.src then
1✔
499
         return SILE.processFile(options.src, options.format, packopts)
1✔
500
      else
501
         SU.error("\\include function requires inline content or a src file path")
×
502
      end
503
   end, "Includes a content file for processing.")
133✔
504

505
   self:registerCommand(
266✔
506
      "lua",
133✔
507
      function (options, content)
508
         local packopts = packOptions(options)
27✔
509
         if SU.ast.hasContent(content) then
54✔
510
            local doc = SU.ast.contentToString(content)
27✔
511
            return SILE.processString(doc, "lua", nil, packopts)
27✔
512
         elseif options.src then
×
513
            return SILE.processFile(options.src, "lua", packopts)
×
514
         elseif options.require then
×
515
            local module = SU.required(options, "require", "lua")
×
516
            return require(module)
×
517
         else
518
            SU.error("\\lua function requires inline content or a src file path or a require module name")
×
519
         end
520
      end,
521
      "Run Lua code. The code may be supplied either inline, using require=... for a Lua module, or using src=... for a file path"
522
   )
133✔
523

524
   self:registerCommand("sil", function (options, content)
266✔
525
      local packopts = packOptions(options)
×
526
      if SU.ast.hasContent(content) then
×
527
         local doc = SU.ast.contentToString(content)
×
528
         return SILE.processString(doc, "sil")
×
529
      elseif options.src then
×
530
         return SILE.processFile(options.src, "sil", packopts)
×
531
      else
532
         SU.error("\\sil function requires inline content or a src file path")
×
533
      end
534
   end, "Process sil content. The content may be supplied either inline or using src=...")
133✔
535

536
   self:registerCommand("xml", function (options, content)
266✔
537
      local packopts = packOptions(options)
×
538
      if SU.ast.hasContent(content) then
×
539
         local doc = SU.ast.contentToString(content)
×
540
         return SILE.processString(doc, "xml", nil, packopts)
×
541
      elseif options.src then
×
542
         return SILE.processFile(options.src, "xml", packopts)
×
543
      else
544
         SU.error("\\xml function requires inline content or a src file path")
×
545
      end
546
   end, "Process xml content. The content may be supplied either inline or using src=...")
133✔
547

548
   self:registerCommand(
266✔
549
      "use",
133✔
550
      function (options, content)
551
         local packopts = packOptions(options)
175✔
552
         if content[1] and string.len(content[1]) > 0 then
175✔
553
            local doc = SU.ast.contentToString(content)
×
554
            SILE.processString(doc, "lua", nil, packopts)
×
555
         else
556
            if options.src then
175✔
557
               SU.warn([[
×
558
                  Use of 'src' with \\use is discouraged.
559

560
                  Its path handling  will eventually be deprecated.
561
                  Use 'module' instead when possible.
562
               ]])
×
563
               SILE.processFile(options.src, "lua", packopts)
×
564
            else
565
               local module = SU.required(options, "module", "use")
175✔
566
               SILE.use(module, packopts)
175✔
567
            end
568
         end
569
      end,
570
      "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."
571
   )
133✔
572

573
   self:registerCommand("raw", function (options, content)
266✔
574
      local rawtype = SU.required(options, "type", "raw")
5✔
575
      local handler = SILE.rawHandlers[rawtype]
5✔
576
      if not handler then
5✔
577
         SU.error("No inline handler for '" .. rawtype .. "'")
×
578
      end
579
      handler(options, content)
5✔
580
   end, "Invoke a raw passthrough handler")
138✔
581

582
   self:registerCommand("pagetemplate", function (options, content)
266✔
583
      SILE.typesetter:pushState()
6✔
584
      SILE.documentState.thisPageTemplate = { frames = {} }
6✔
585
      SILE.process(content)
6✔
586
      SILE.documentState.thisPageTemplate.firstContentFrame = SILE.getFrame(options["first-content-frame"])
12✔
587
      SILE.typesetter:initFrame(SILE.documentState.thisPageTemplate.firstContentFrame)
6✔
588
      SILE.typesetter:popState()
6✔
589
   end, "Defines a new page template for the current page and sets the typesetter to use it.")
139✔
590

591
   self:registerCommand("frame", function (options, _)
266✔
592
      SILE.documentState.thisPageTemplate.frames[options.id] = SILE.newFrame(options)
54✔
593
   end, "Declares (or re-declares) a frame on this page.")
160✔
594

595
   self:registerCommand("penalty", function (options, _)
266✔
596
      if SU.boolean(options.vertical, false) and not SILE.typesetter:vmode() then
1,411✔
597
         SILE.typesetter:leaveHmode()
113✔
598
      end
599
      if SILE.typesetter:vmode() then
1,070✔
600
         SILE.typesetter:pushVpenalty({ penalty = tonumber(options.penalty) })
986✔
601
      else
602
         SILE.typesetter:pushPenalty({ penalty = tonumber(options.penalty) })
42✔
603
      end
604
   end, "Inserts a penalty node. Option is penalty= for the size of the penalty.")
668✔
605

606
   self:registerCommand("discretionary", function (options, _)
266✔
607
      local discretionary = SILE.types.node.discretionary({})
2✔
608
      if options.prebreak then
2✔
609
         local hbox = SILE.typesetter:makeHbox({ options.prebreak })
2✔
610
         discretionary.prebreak = { hbox }
2✔
611
      end
612
      if options.postbreak then
2✔
613
         local hbox = SILE.typesetter:makeHbox({ options.postbreak })
×
614
         discretionary.postbreak = { hbox }
×
615
      end
616
      if options.replacement then
2✔
617
         local hbox = SILE.typesetter:makeHbox({ options.replacement })
2✔
618
         discretionary.replacement = { hbox }
2✔
619
      end
620
      table.insert(SILE.typesetter.state.nodes, discretionary)
2✔
621
   end, "Inserts a discretionary node.")
135✔
622

623
   self:registerCommand("glue", function (options, _)
266✔
624
      local width = SU.cast("length", options.width):absolute()
62✔
625
      SILE.typesetter:pushGlue(width)
31✔
626
   end, "Inserts a glue node. The width option denotes the glue dimension.")
164✔
627

628
   self:registerCommand("kern", function (options, _)
266✔
629
      local width = SU.cast("length", options.width):absolute()
112✔
630
      SILE.typesetter:pushHorizontal(SILE.types.node.kern(width))
112✔
631
   end, "Inserts a glue node. The width option denotes the glue dimension.")
189✔
632

633
   self:registerCommand("skip", function (options, _)
266✔
634
      options.discardable = SU.boolean(options.discardable, false)
36✔
635
      options.height = SILE.types.length(options.height):absolute()
54✔
636
      SILE.typesetter:leaveHmode()
18✔
637
      if options.discardable then
18✔
638
         SILE.typesetter:pushVglue(options)
×
639
      else
640
         SILE.typesetter:pushExplicitVglue(options)
18✔
641
      end
642
   end, "Inserts vertical skip. The height options denotes the skip dimension.")
151✔
643

644
   self:registerCommand("par", function (_, _)
266✔
645
      SILE.typesetter:endline()
170✔
646
   end, "Ends the current paragraph.")
303✔
647
end
648

649
function class:initialFrame ()
261✔
650
   SILE.documentState.thisPageTemplate = pl.tablex.deepcopy(self.pageTemplate)
342✔
651
   SILE.frames = { page = SILE.frames.page }
171✔
652
   for k, v in pairs(SILE.documentState.thisPageTemplate.frames) do
749✔
653
      SILE.frames[k] = v
578✔
654
   end
655
   if not SILE.documentState.thisPageTemplate.firstContentFrame then
171✔
656
      SILE.documentState.thisPageTemplate.firstContentFrame = SILE.frames[self.firstContentFrame]
×
657
   end
658
   SILE.documentState.thisPageTemplate.firstContentFrame:invalidate()
171✔
659
   return SILE.documentState.thisPageTemplate.firstContentFrame
171✔
660
end
661

662
function class:declareFrame (id, spec)
261✔
663
   spec.id = id
437✔
664
   if spec.solve then
437✔
665
      self.pageTemplate.frames[id] = spec
×
666
   else
667
      self.pageTemplate.frames[id] = SILE.newFrame(spec)
874✔
668
   end
669
   --   next = spec.next,
670
   --   left = spec.left and fW(spec.left),
671
   --   right = spec.right and fW(spec.right),
672
   --   top = spec.top and fH(spec.top),
673
   --   bottom = spec.bottom and fH(spec.bottom),
674
   --   height = spec.height and fH(spec.height),
675
   --   width = spec.width and fH(spec.width),
676
   --   id = id
677
   -- })
678
end
679

680
function class:declareFrames (specs)
261✔
681
   if specs then
133✔
682
      for k, v in pairs(specs) do
557✔
683
         self:declareFrame(k, v)
424✔
684
      end
685
   end
686
end
687

688
-- WARNING: not called as class method
689
function class.newPar (typesetter)
261✔
690
   local parindent = SILE.settings:get("current.parindent") or SILE.settings:get("document.parindent")
1,394✔
691
   -- See https://github.com/sile-typesetter/sile/issues/1361
692
   -- The parindent *cannot* be pushed non-absolutized, as it may be evaluated
693
   -- outside the (possibly temporary) setting scope where it was used for line
694
   -- breaking.
695
   -- Early absolutization can be problematic sometimes, but here we do not
696
   -- really have the choice.
697
   -- As of problematic cases, consider a parindent that would be defined in a
698
   -- frame-related unit (%lw, %fw, etc.). If a frame break occurs and the next
699
   -- frame has a different width, the parindent won't be re-evaluated in that
700
   -- new frame context. However, defining a parindent in such a unit is quite
701
   -- unlikely. And anyway pushback() has plenty of other issues.
702
   typesetter:pushGlue(parindent:absolute())
1,394✔
703
   local hangIndent = SILE.settings:get("current.hangIndent")
697✔
704
   if hangIndent then
697✔
705
      SILE.settings:set("linebreak.hangIndent", hangIndent)
11✔
706
   end
707
   local hangAfter = SILE.settings:get("current.hangAfter")
697✔
708
   if hangAfter then
697✔
709
      SILE.settings:set("linebreak.hangAfter", hangAfter)
11✔
710
   end
711
end
712

713
-- WARNING: not called as class method
714
function class.endPar (typesetter)
261✔
715
   -- If we're already explicitly out of hmode don't do anything special in the way of skips or indents. Assume the user
716
   -- has handled that how they want, e.g. with a skip.
717
   local queue = typesetter.state.outputQueue
751✔
718
   local last_vbox = queue and queue[#queue]
751✔
719
   local last_is_vglue = last_vbox and last_vbox.is_vglue
751✔
720
   local last_is_vpenalty = last_vbox and last_vbox.is_penalty
751✔
721
   if typesetter:vmode() and (last_is_vglue or last_is_vpenalty) then
1,502✔
722
      return
201✔
723
   end
724
   SILE.settings:set("current.parindent", nil)
550✔
725
   typesetter:leaveHmode()
550✔
726
   typesetter:pushVglue(SILE.settings:get("document.parskip"))
1,100✔
727
end
728

729
function class:newPage ()
261✔
730
   SILE.outputter:newPage()
38✔
731
   self:runHooks("newpage")
38✔
732
   -- Any other output-routiney things will be done here by inheritors
733
   return self:initialFrame()
38✔
734
end
735

736
function class:endPage ()
261✔
737
   SILE.typesetter.frame:leave(SILE.typesetter)
171✔
738
   self:runHooks("endpage")
171✔
739
   -- I'm trying to call up a new frame here, don't cause a page break in the current one
740
   -- SILE.typesetter:leaveHmode()
741
   -- Any other output-routiney things will be done here by inheritors
742
end
743

744
function class:finish ()
261✔
745
   SILE.inputter:postamble()
133✔
746
   SILE.typesetter:endline()
133✔
747
   SILE.call("vfill")
133✔
748
   while not SILE.typesetter:isQueueEmpty() do
532✔
749
      SILE.call("supereject")
133✔
750
      SILE.typesetter:leaveHmode(true)
133✔
751
      SILE.typesetter:buildPage()
133✔
752
      if not SILE.typesetter:isQueueEmpty() then
266✔
753
         SILE.typesetter:initNextFrame()
4✔
754
      end
755
   end
756
   SILE.typesetter:runHooks("pageend") -- normally run by the typesetter
133✔
757
   self:endPage()
133✔
758
   if SILE.typesetter and not SILE.typesetter:isQueueEmpty() then
266✔
759
      SU.error("Queues are not empty as expected after ending last page", true)
×
760
   end
761
   SILE.outputter:finish()
133✔
762
   self:runHooks("finish")
133✔
763
end
764

765
return class
261✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc