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

sile-typesetter / sile / 14844751993

05 May 2025 05:31PM UTC coverage: 59.865% (+27.8%) from 32.062%
14844751993

push

github

alerque
fix(packages): Fix package reload to not call for command reregistration unless asked

1 of 1 new or added line in 1 file covered. (100.0%)

88 existing lines in 14 files now uncovered.

12962 of 21652 relevant lines covered (59.87%)

4076.32 hits per line

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

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

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

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

50
class.packages = {}
305✔
51

52
function class:_init (options)
305✔
53
   SILE.scratch.half_initialized_class = self
153✔
54
   if self == options then
153✔
55
      options = {}
×
56
   end
57
   SILE.languageSupport.loadLanguage("und") -- preload for unlocalized fallbacks
153✔
58
   self:_declareBaseOptions()
153✔
59
   self:declareOptions()
153✔
60
   self:registerRawHandlers()
153✔
61
   self:_declareBaseSettings()
153✔
62
   self:declareSettings()
153✔
63
   self:_registerBaseCommands()
153✔
64
   self:registerCommands()
153✔
65
   self:setOptions(options)
153✔
66
   self:declareFrames(self.defaultFrameset)
153✔
67
   self:registerPostinit(function (self_)
306✔
68
      if type(self.firstContentFrame) == "string" then
153✔
69
         self_.pageTemplate.firstContentFrame = self_.pageTemplate.frames[self_.firstContentFrame]
153✔
70
      end
71
      local frame = self_:initialFrame()
153✔
72
      SILE.typesetter = SILE.typesetters.default(frame)
457✔
73
      SILE.typesetter:registerPageEndHook(function ()
306✔
74
         SU.debug("frames", function ()
388✔
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 ()
305✔
85
   SILE.documentState.documentClass = self
153✔
86
   self._initialized = true
153✔
87
   for i, func in ipairs(self.deferredInit) do
379✔
88
      func(self)
226✔
89
      self.deferredInit[i] = nil
226✔
90
   end
91
   SILE.scratch.half_initialized_class = nil
153✔
92
end
93

94
function class:setOptions (options)
305✔
95
   options = options or {}
153✔
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)
306✔
99
   options.landscape = nil
153✔
100
   self.options.papersize = options.papersize or "a4"
153✔
101
   options.papersize = nil
153✔
102
   self.options.bleed = options.bleed or "0"
153✔
103
   options.bleed = nil
153✔
104
   self.options.sheetsize = options.sheetsize or nil
153✔
105
   options.sheetsize = nil
153✔
106
   for option, value in pairs(options) do
165✔
107
      self.options[option] = value
12✔
108
   end
109
end
110

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

116
function class:declareOptions () end
305✔
117

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

179
function class:declareSettings () end
305✔
180

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

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

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

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

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

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

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

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

347
function class:registerRawHandler (format, callback)
305✔
348
   SILE.rawHandlers[format] = callback
159✔
349
end
350

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

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

370
function class.registerCommands () end
305✔
371

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

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

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

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

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

445
   self:registerCommand("comment", function (_, _) end, "Ignores any text within this command's body.")
153✔
446

447
   self:registerCommand("process", function ()
306✔
448
      SU.error("Encountered unsubstituted \\process")
×
449
   end, "Within a macro definition, processes the contents of the macro body.")
153✔
450

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

467
               For this use case consider replacing:
468

469
               %s
470

471
               with:
472

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

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

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

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

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

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

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

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

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

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

597
   self:registerCommand("penalty", function (options, _)
306✔
598
      if SU.boolean(options.vertical, false) and not SILE.typesetter:vmode() then
1,443✔
599
         SILE.typesetter:leaveHmode()
109✔
600
      end
601
      if SILE.typesetter:vmode() then
1,114✔
602
         SILE.typesetter:pushVpenalty({ penalty = tonumber(options.penalty) })
1,006✔
603
      else
604
         SILE.typesetter:pushPenalty({ penalty = tonumber(options.penalty) })
54✔
605
      end
606
   end, "Inserts a penalty node. Option is penalty= for the size of the penalty.")
710✔
607

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

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

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

635
   self:registerCommand("skip", function (options, _)
306✔
636
      options.discardable = SU.boolean(options.discardable, false)
44✔
637
      options.height = SILE.types.length(options.height):absolute()
66✔
638
      SILE.typesetter:leaveHmode()
22✔
639
      if options.discardable then
22✔
640
         SILE.typesetter:pushVglue(options)
×
641
      else
642
         SILE.typesetter:pushExplicitVglue(options)
22✔
643
      end
644
   end, "Inserts vertical skip. The height options denotes the skip dimension.")
175✔
645

646
   self:registerCommand("par", function (_, _)
306✔
647
      SILE.typesetter:endline()
192✔
648
   end, "Ends the current paragraph.")
345✔
649
end
650

651
function class:initialFrame ()
305✔
652
   SILE.documentState.thisPageTemplate = pl.tablex.deepcopy(self.pageTemplate)
400✔
653
   SILE.frames = { page = SILE.frames.page }
200✔
654
   for k, v in pairs(SILE.documentState.thisPageTemplate.frames) do
877✔
655
      SILE.frames[k] = v
677✔
656
   end
657
   if not SILE.documentState.thisPageTemplate.firstContentFrame then
200✔
658
      SILE.documentState.thisPageTemplate.firstContentFrame = SILE.frames[self.firstContentFrame]
×
659
   end
660
   SILE.documentState.thisPageTemplate.firstContentFrame:invalidate()
200✔
661
   return SILE.documentState.thisPageTemplate.firstContentFrame
200✔
662
end
663

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

682
function class:declareFrames (specs)
305✔
683
   if specs then
153✔
684
      for k, v in pairs(specs) do
642✔
685
         self:declareFrame(k, v)
489✔
686
      end
687
   end
688
end
689

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

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

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

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

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

767
return class
305✔
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

© 2025 Coveralls, Inc