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

sile-typesetter / sile / 14284237390

05 Apr 2025 05:33PM UTC coverage: 63.158% (+31.8%) from 31.375%
14284237390

push

github

web-flow
Merge pull request #2248 from alerque/class-warfare

Normalize module layout across all module types

257 of 350 new or added lines in 14 files covered. (73.43%)

71 existing lines in 11 files now uncovered.

13670 of 21644 relevant lines covered (63.16%)

3070.68 hits per line

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

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

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

8
class._initialized = false
306✔
9
class.deferredInit = {}
306✔
10
class.pageTemplate = { frames = {}, firstContentFrame = nil }
306✔
11
class.defaultFrameset = {}
306✔
12
class.firstContentFrame = "page"
306✔
13
class.options = setmetatable({}, {
612✔
14
   _opts = {},
306✔
15
   __newindex = function (self, key, value)
16
      local opts = getmetatable(self)._opts
1,572✔
17
      if type(opts[key]) == "function" then
1,572✔
18
         opts[key](class, value)
1,266✔
19
      elseif type(value) == "function" then
939✔
20
         opts[key] = value
939✔
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
164✔
29
         return nil
×
30
      end
31
      if type(key) == "number" then
164✔
32
         return nil
×
33
      end
34
      local opt = getmetatable(self)._opts[key]
164✔
35
      if type(opt) == "function" then
164✔
36
         return opt(class)
164✔
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
})
306✔
44
class.hooks = {
306✔
45
   newpage = {},
306✔
46
   endpage = {},
306✔
47
   finish = {},
306✔
48
}
306✔
49

50
class.packages = {}
306✔
51

52
function class:_init (options)
306✔
53
   SILE.scratch.half_initialized_class = self
155✔
54
   if self == options then
155✔
55
      options = {}
×
56
   end
57
   SILE.languageSupport.loadLanguage("und") -- preload for unlocalized fallbacks
155✔
58
   self:_declareBaseOptions()
155✔
59
   self:declareOptions()
155✔
60
   self:registerRawHandlers()
155✔
61
   self:_declareBaseSettings()
155✔
62
   self:declareSettings()
155✔
63
   self:_registerBaseCommands()
155✔
64
   self:registerCommands()
155✔
65
   self:setOptions(options)
155✔
66
   self:declareFrames(self.defaultFrameset)
155✔
67
   self:registerPostinit(function (self_)
310✔
68
      if type(self.firstContentFrame) == "string" then
155✔
69
         self_.pageTemplate.firstContentFrame = self_.pageTemplate.frames[self_.firstContentFrame]
155✔
70
      end
71
      local frame = self_:initialFrame()
155✔
72
      SILE.typesetter = SILE.typesetters.default(frame)
464✔
73
      SILE.typesetter:registerPageEndHook(function ()
310✔
74
         SU.debug("frames", function ()
396✔
75
            for _, v in pairs(SILE.frames) do
×
76
               SILE.outputter:debugFrame(v)
×
77
            end
78
            return "Drew debug outlines around frames"
×
79
         end)
80
      end)
81
   end)
82
end
83

84
function class:_post_init ()
306✔
85
   SILE.documentState.documentClass = self
155✔
86
   self._initialized = true
155✔
87
   for i, func in ipairs(self.deferredInit) do
387✔
88
      func(self)
232✔
89
      self.deferredInit[i] = nil
232✔
90
   end
91
   SILE.scratch.half_initialized_class = nil
155✔
92
end
93

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

111
function class:declareOption (option, setter)
306✔
112
   rawset(getmetatable(self.options)._opts, option, nil)
939✔
113
   self.options[option] = setter
939✔
114
end
115

116
function class.declareOptions (_) end
306✔
117

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

181
function class.declareSettings (_) end
306✔
182

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

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

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

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

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

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

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

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

349
function class.registerRawHandler (_, format, callback)
306✔
350
   SILE.rawHandlers[format] = callback
164✔
351
end
352

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

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

372
function class.registerCommands () end
306✔
373

374
-- These need refactoring probably somewhere outside of the document class system
375
function class:_registerBaseCommands ()
306✔
376
   local function replaceProcessBy (replacement, tree)
377
      if type(tree) ~= "table" then
167✔
378
         return tree
65✔
379
      end
380
      local ret = pl.tablex.deepcopy(tree)
102✔
381
      if tree.command == "process" then
102✔
382
         return replacement
7✔
383
      else
384
         for i, child in ipairs(tree) do
221✔
385
            ret[i] = replaceProcessBy(replacement, child)
252✔
386
         end
387
         return ret
95✔
388
      end
389
   end
390

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

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

430
   -- A utility function that allows SILE.call() to be used as a noop wrapper.
431
   self:registerCommand("noop", function (_, content)
310✔
432
      SILE.process(content)
9✔
433
   end)
434

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

447
   self:registerCommand("comment", function (_, _) end, "Ignores any text within this command's body.")
155✔
448

449
   self:registerCommand("process", function ()
310✔
450
      SU.error("Encountered unsubstituted \\process")
×
451
   end, "Within a macro definition, processes the contents of the macro body.")
155✔
452

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

469
               For this use case consider replacing:
470

471
               %s
472

473
               with:
474

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

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

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

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

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

552
   self:registerCommand(
310✔
553
      "use",
155✔
554
      function (options, content)
555
         local packopts = packOptions(options)
205✔
556
         if content[1] and string.len(content[1]) > 0 then
205✔
557
            local doc = SU.ast.contentToString(content)
×
558
            SILE.processString(doc, "lua", nil, packopts)
×
559
         else
560
            if options.src then
205✔
561
               SU.warn([[
×
562
                  Use of 'src' with \\use is discouraged.
563

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

577
   self:registerCommand("raw", function (options, content)
310✔
578
      local rawtype = SU.required(options, "type", "raw")
5✔
579
      local handler = SILE.rawHandlers[rawtype]
5✔
580
      if not handler then
5✔
581
         SU.error("No inline handler for '" .. rawtype .. "'")
×
582
      end
583
      handler(options, content)
5✔
584
   end, "Invoke a raw passthrough handler")
160✔
585

586
   self:registerCommand("pagetemplate", function (options, content)
310✔
587
      SILE.typesetter:pushState()
8✔
588
      SILE.documentState.thisPageTemplate = { frames = {} }
8✔
589
      SILE.process(content)
8✔
590
      SILE.documentState.thisPageTemplate.firstContentFrame = SILE.getFrame(options["first-content-frame"])
16✔
591
      SILE.typesetter:initFrame(SILE.documentState.thisPageTemplate.firstContentFrame)
8✔
592
      SILE.typesetter:popState()
8✔
593
   end, "Defines a new page template for the current page and sets the typesetter to use it.")
163✔
594

595
   self:registerCommand("frame", function (options, _)
310✔
596
      SILE.documentState.thisPageTemplate.frames[options.id] = SILE.newFrame(options)
60✔
597
   end, "Declares (or re-declares) a frame on this page.")
185✔
598

599
   self:registerCommand("penalty", function (options, _)
310✔
600
      if SU.boolean(options.vertical, false) and not SILE.typesetter:vmode() then
1,485✔
601
         SILE.typesetter:leaveHmode()
113✔
602
      end
603
      if SILE.typesetter:vmode() then
1,144✔
604
         SILE.typesetter:pushVpenalty({ penalty = tonumber(options.penalty) })
1,034✔
605
      else
606
         SILE.typesetter:pushPenalty({ penalty = tonumber(options.penalty) })
55✔
607
      end
608
   end, "Inserts a penalty node. Option is penalty= for the size of the penalty.")
727✔
609

610
   self:registerCommand("discretionary", function (options, _)
310✔
611
      local discretionary = SILE.types.node.discretionary({})
2✔
612
      if options.prebreak then
2✔
613
         local hbox = SILE.typesetter:makeHbox({ options.prebreak })
2✔
614
         discretionary.prebreak = { hbox }
2✔
615
      end
616
      if options.postbreak then
2✔
UNCOV
617
         local hbox = SILE.typesetter:makeHbox({ options.postbreak })
×
UNCOV
618
         discretionary.postbreak = { hbox }
×
619
      end
620
      if options.replacement then
2✔
621
         local hbox = SILE.typesetter:makeHbox({ options.replacement })
2✔
622
         discretionary.replacement = { hbox }
2✔
623
      end
624
      table.insert(SILE.typesetter.state.nodes, discretionary)
2✔
625
   end, "Inserts a discretionary node.")
157✔
626

627
   self:registerCommand("glue", function (options, _)
310✔
628
      local width = SU.cast("length", options.width):absolute()
114✔
629
      SILE.typesetter:pushGlue(width)
57✔
630
   end, "Inserts a glue node. The width option denotes the glue dimension.")
212✔
631

632
   self:registerCommand("kern", function (options, _)
310✔
633
      local width = SU.cast("length", options.width):absolute()
160✔
634
      SILE.typesetter:pushHorizontal(SILE.types.node.kern(width))
160✔
635
   end, "Inserts a glue node. The width option denotes the glue dimension.")
235✔
636

637
   self:registerCommand("skip", function (options, _)
310✔
638
      options.discardable = SU.boolean(options.discardable, false)
48✔
639
      options.height = SILE.types.length(options.height):absolute()
72✔
640
      SILE.typesetter:leaveHmode()
24✔
641
      if options.discardable then
24✔
642
         SILE.typesetter:pushVglue(options)
×
643
      else
644
         SILE.typesetter:pushExplicitVglue(options)
24✔
645
      end
646
   end, "Inserts vertical skip. The height options denotes the skip dimension.")
179✔
647

648
   self:registerCommand("par", function (_, _)
310✔
649
      SILE.typesetter:endline()
204✔
650
   end, "Ends the current paragraph.")
359✔
651
end
652

653
function class:initialFrame ()
306✔
654
   SILE.documentState.thisPageTemplate = pl.tablex.deepcopy(self.pageTemplate)
404✔
655
   SILE.frames = { page = SILE.frames.page }
202✔
656
   for k, v in pairs(SILE.documentState.thisPageTemplate.frames) do
884✔
657
      SILE.frames[k] = v
682✔
658
   end
659
   if not SILE.documentState.thisPageTemplate.firstContentFrame then
202✔
660
      SILE.documentState.thisPageTemplate.firstContentFrame = SILE.frames[self.firstContentFrame]
×
661
   end
662
   SILE.documentState.thisPageTemplate.firstContentFrame:invalidate()
202✔
663
   return SILE.documentState.thisPageTemplate.firstContentFrame
202✔
664
end
665

666
function class:declareFrame (id, spec)
306✔
667
   spec.id = id
506✔
668
   if spec.solve then
506✔
669
      self.pageTemplate.frames[id] = spec
×
670
   else
671
      self.pageTemplate.frames[id] = SILE.newFrame(spec)
1,012✔
672
   end
673
   --   next = spec.next,
674
   --   left = spec.left and fW(spec.left),
675
   --   right = spec.right and fW(spec.right),
676
   --   top = spec.top and fH(spec.top),
677
   --   bottom = spec.bottom and fH(spec.bottom),
678
   --   height = spec.height and fH(spec.height),
679
   --   width = spec.width and fH(spec.width),
680
   --   id = id
681
   -- })
682
end
683

684
function class:declareFrames (specs)
306✔
685
   if specs then
155✔
686
      for k, v in pairs(specs) do
652✔
687
         self:declareFrame(k, v)
497✔
688
      end
689
   end
690
end
691

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

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

733
function class:newPage ()
306✔
734
   SILE.outputter:newPage()
47✔
735
   self:runHooks("newpage")
47✔
736
   -- Any other output-routiney things will be done here by inheritors
737
   return self:initialFrame()
47✔
738
end
739

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

748
function class:finish ()
306✔
749
   SILE.inputter:postamble()
155✔
750
   SILE.typesetter:endline()
155✔
751
   SILE.call("vfill")
155✔
752
   while not SILE.typesetter:isQueueEmpty() do
620✔
753
      SILE.call("supereject")
155✔
754
      SILE.typesetter:leaveHmode(true)
155✔
755
      SILE.typesetter:buildPage()
155✔
756
      if not SILE.typesetter:isQueueEmpty() then
310✔
757
         SILE.typesetter:initNextFrame()
3✔
758
      end
759
   end
760
   SILE.typesetter:runHooks("pageend") -- normally run by the typesetter
155✔
761
   self:endPage()
155✔
762
   if SILE.typesetter and not SILE.typesetter:isQueueEmpty() then
310✔
763
      SU.error("Queues are not empty as expected after ending last page", true)
×
764
   end
765
   SILE.outputter:finish()
155✔
766
   self:runHooks("finish")
155✔
767
end
768

769
return class
306✔
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