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

sile-typesetter / sile / 11124789710

01 Oct 2024 11:57AM UTC coverage: 29.567% (-31.4%) from 60.926%
11124789710

push

github

web-flow
Merge pull request #2105 from Omikhleia/refactor-collated-sort

0 of 10 new or added lines in 1 file covered. (0.0%)

5252 existing lines in 53 files now uncovered.

5048 of 17073 relevant lines covered (29.57%)

1856.13 hits per line

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

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

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

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

50
class.packages = {}
135✔
51

52
function class:_init (options)
135✔
53
   SILE.scratch.half_initialized_class = self
68✔
54
   if self == options then
68✔
55
      options = {}
×
56
   end
57
   SILE.languageSupport.loadLanguage("und") -- preload for unlocalized fallbacks
68✔
58
   self:declareOptions()
68✔
59
   self:registerRawHandlers()
68✔
60
   self:declareSettings()
68✔
61
   self:registerCommands()
68✔
62
   self:setOptions(options)
68✔
63
   self:declareFrames(self.defaultFrameset)
68✔
64
   self:registerPostinit(function (self_)
136✔
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")
68✔
70
      SILE.languageSupport.loadLanguage(lang)
68✔
71
      if type(self.firstContentFrame) == "string" then
68✔
72
         self_.pageTemplate.firstContentFrame = self_.pageTemplate.frames[self_.firstContentFrame]
68✔
73
      end
74
      local frame = self_:initialFrame()
68✔
75
      SILE.typesetter = SILE.typesetters.base(frame)
136✔
76
      SILE.typesetter:registerPageEndHook(function ()
136✔
77
         SU.debug("frames", function ()
180✔
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 ()
135✔
88
   SILE.documentState.documentClass = self
68✔
89
   self._initialized = true
68✔
90
   for i, func in ipairs(self.deferredInit) do
160✔
91
      func(self)
92✔
92
      self.deferredInit[i] = nil
92✔
93
   end
94
   SILE.scratch.half_initialized_class = nil
68✔
95
end
96

97
function class:setOptions (options)
135✔
98
   options = options or {}
68✔
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)
136✔
102
   options.landscape = nil
68✔
103
   self.options.papersize = options.papersize or "a4"
68✔
104
   options.papersize = nil
68✔
105
   self.options.bleed = options.bleed or "0"
68✔
106
   options.bleed = nil
68✔
107
   self.options.sheetsize = options.sheetsize or nil
68✔
108
   options.sheetsize = nil
68✔
109
   for option, value in pairs(options) do
72✔
110
      self.options[option] = value
4✔
111
   end
112
end
113

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

119
function class:declareOptions ()
135✔
120
   self:declareOption("class", function (_, name)
136✔
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)
136✔
131
      if landscape then
136✔
132
         self.landscape = landscape
1✔
133
      end
134
      return self.landscape
136✔
135
   end)
136
   self:declareOption("papersize", function (_, size)
136✔
137
      if size then
68✔
138
         self.papersize = size
68✔
139
         SILE.documentState.paperSize = SILE.papersize(size, self.options.landscape)
204✔
140
         SILE.documentState.orgPaperSize = SILE.documentState.paperSize
68✔
141
         SILE.newFrame({
136✔
142
            id = "page",
143
            left = 0,
144
            top = 0,
145
            right = SILE.documentState.paperSize[1],
68✔
146
            bottom = SILE.documentState.paperSize[2],
68✔
147
         })
148
      end
149
      return self.papersize
68✔
150
   end)
151
   self:declareOption("sheetsize", function (_, size)
136✔
152
      if size then
68✔
UNCOV
153
         self.sheetsize = size
×
UNCOV
154
         SILE.documentState.sheetSize = SILE.papersize(size, self.options.landscape)
×
155
         if
UNCOV
156
            SILE.documentState.sheetSize[1] < SILE.documentState.paperSize[1]
×
UNCOV
157
            or SILE.documentState.sheetSize[2] < SILE.documentState.paperSize[2]
×
158
         then
159
            SU.error("Sheet size shall not be smaller than the paper size")
×
160
         end
UNCOV
161
         if SILE.documentState.sheetSize[1] < SILE.documentState.paperSize[1] + SILE.documentState.bleed then
×
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
UNCOV
165
         if SILE.documentState.sheetSize[2] < SILE.documentState.paperSize[2] + SILE.documentState.bleed then
×
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
68✔
171
      end
172
   end)
173
   self:declareOption("bleed", function (_, dimen)
136✔
174
      if dimen then
68✔
175
         self.bleed = dimen
68✔
176
         SILE.documentState.bleed = SU.cast("measurement", dimen):tonumber()
204✔
177
      end
178
      return self.bleed
68✔
179
   end)
180
end
181

182
function class.declareSettings (_)
135✔
183
   SILE.settings:declare({
68✔
184
      parameter = "current.parindent",
185
      type = "glue or nil",
186
      default = nil,
187
      help = "Glue at start of paragraph",
188
   })
189
   SILE.settings:declare({
68✔
190
      parameter = "current.hangIndent",
191
      type = "measurement or nil",
192
      default = nil,
193
      help = "Size of hanging indent",
194
   })
195
   SILE.settings:declare({
68✔
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)
135✔
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
442✔
207
      pack, packname = packname, packname._name
94✔
208
   elseif type(packname) == "nil" or packname == "nil" or pl.stringx.strip(packname) == "" then
696✔
209
      SU.error(("Attempted to load package with an invalid packname '%s'"):format(packname))
×
210
   else
211
      pack = require(("packages.%s"):format(packname))
348✔
212
      if pack._name ~= packname then
348✔
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
442✔
217
   if type(pack) == "table" and pack.type == "package" then -- current package api
442✔
218
      if self.packages[packname] then
442✔
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]
40✔
227
         pack._create = function ()
228
            return current_instance
40✔
229
         end
230
         pack(options, true)
80✔
231
      else
232
         self.packages[packname] = pack(options, reload)
804✔
233
      end
234
   else -- legacy package
235
      self:initPackage(pack, options)
×
236
   end
237
end
238

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

243
function class:initPackage (pack, options)
135✔
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
251
  and expects SILE to guess about what to do. New packages inherit
252
  from the base class and have a constructor function (_init) that
253
  automatically handles setup.]]
×
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)
135✔
291
   if self._initialized then
99✔
292
      return func(self, options)
7✔
293
   end
294
   table.insert(self.deferredInit, function (_)
184✔
295
      func(self, options)
92✔
296
   end)
297
end
298

299
function class:registerHook (category, func)
135✔
300
   for _, func_ in ipairs(self.hooks[category]) do
256✔
301
      if func_ == func then
70✔
UNCOV
302
         return
×
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)
186✔
313
end
314

315
function class:runHooks (category, options)
135✔
316
   for _, func in ipairs(self.hooks[category]) do
388✔
317
      SU.debug("classhooks", "Running hook from", category, options and "with options #" .. #options)
204✔
318
      func(self, options)
204✔
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)
135✔
335
   SILE.Commands[name] = func
7,317✔
336
   if not pack then
7,317✔
337
      local where = debug.getinfo(2).source
7,315✔
338
      pack = where:match("(%w+).lua")
7,315✔
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] = {
7,317✔
342
      description = help,
7,317✔
343
      where = pack,
7,317✔
344
   }
7,317✔
345
end
346

347
function class.registerRawHandler (_, format, callback)
135✔
348
   SILE.rawHandlers[format] = callback
70✔
349
end
350

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

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

370
function class:registerCommands ()
135✔
371
   local function replaceProcessBy (replacement, tree)
372
      if type(tree) ~= "table" then
24✔
373
         return tree
10✔
374
      end
375
      local ret = pl.tablex.deepcopy(tree)
14✔
376
      if tree.command == "process" then
14✔
377
         return replacement
1✔
378
      else
379
         for i, child in ipairs(tree) do
34✔
380
            ret[i] = replaceProcessBy(replacement, child)
42✔
381
         end
382
         return ret
13✔
383
      end
384
   end
385

386
   self:registerCommand("define", function (options, content)
136✔
387
      SU.required(options, "command", "defining command")
2✔
388
      if type(content) == "function" then
2✔
389
         -- A macro defined as a function can take no argument, so we register
390
         -- it as-is.
391
         self:registerCommand(options["command"], content)
×
392
         return
×
393
      elseif options.command == "process" then
2✔
394
         SU.warn("Did you mean to re-definine the `\\process` macro? That probably won't go well.")
×
395
      end
396
      self:registerCommand(options["command"], function (_, inner_content)
4✔
397
         SU.debug("macros", "Processing macro \\" .. options["command"])
3✔
398
         local macroArg
399
         if type(inner_content) == "function" then
3✔
UNCOV
400
            macroArg = inner_content
×
401
         elseif type(inner_content) == "table" then
3✔
402
            macroArg = pl.tablex.copy(inner_content)
6✔
403
            macroArg.command = nil
3✔
404
            macroArg.id = nil
3✔
UNCOV
405
         elseif inner_content == nil then
×
UNCOV
406
            macroArg = {}
×
407
         else
408
            SU.error(
×
409
               "Unhandled content type " .. type(inner_content) .. " passed to macro \\" .. options["command"],
×
410
               true
411
            )
412
         end
413
         -- Replace every occurrence of \process in `content` (the macro
414
         -- body) with `macroArg`, then have SILE go through the new `content`.
415
         local newContent = replaceProcessBy(macroArg, content)
3✔
416
         SILE.process(newContent)
3✔
417
         SU.debug("macros", "Finished processing \\" .. options["command"])
3✔
418
      end, options.help, SILE.currentlyProcessingFile)
5✔
419
   end, "Define a new macro. \\define[command=example]{ ... \\process }")
70✔
420

421
   -- A utility function that allows SILE.call() to be used as a noop wrapper.
422
   self:registerCommand("noop", function (_, content)
136✔
423
      SILE.process(content)
2✔
424
   end)
425

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

438
   self:registerCommand("comment", function (_, _) end, "Ignores any text within this command's body.")
68✔
439

440
   self:registerCommand("process", function ()
136✔
441
      SU.error("Encountered unsubstituted \\process.")
×
442
   end, "Within a macro definition, processes the contents of the macro body.")
68✔
443

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

461
      For this use case consider replacing:
462

463
          %s
464

465
      with:
466

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

487
   self:registerCommand("include", function (options, content)
136✔
UNCOV
488
      local packopts = packOptions(options)
×
UNCOV
489
      if SU.ast.hasContent(content) then
×
490
         local doc = SU.ast.contentToString(content)
×
491
         return SILE.processString(doc, options.format, nil, packopts)
×
UNCOV
492
      elseif options.src then
×
UNCOV
493
         return SILE.processFile(options.src, options.format, packopts)
×
494
      else
495
         SU.error("\\include function requires inline content or a src file path")
×
496
      end
497
   end, "Includes a content file for processing.")
68✔
498

499
   self:registerCommand(
136✔
500
      "lua",
68✔
501
      function (options, content)
502
         local packopts = packOptions(options)
22✔
503
         if SU.ast.hasContent(content) then
44✔
504
            local doc = SU.ast.contentToString(content)
22✔
505
            return SILE.processString(doc, "lua", nil, packopts)
22✔
506
         elseif options.src then
×
507
            return SILE.processFile(options.src, "lua", packopts)
×
508
         elseif options.require then
×
509
            local module = SU.required(options, "require", "lua")
×
510
            return require(module)
×
511
         else
512
            SU.error("\\lua function requires inline content or a src file path or a require module name")
×
513
         end
514
      end,
515
      "Run Lua code. The code may be supplied either inline, using require=... for a Lua module, or using src=... for a file path"
516
   )
68✔
517

518
   self:registerCommand("sil", function (options, content)
136✔
519
      local packopts = packOptions(options)
×
520
      if SU.ast.hasContent(content) then
×
521
         local doc = SU.ast.contentToString(content)
×
522
         return SILE.processString(doc, "sil")
×
523
      elseif options.src then
×
524
         return SILE.processFile(options.src, "sil", packopts)
×
525
      else
526
         SU.error("\\sil function requires inline content or a src file path")
×
527
      end
528
   end, "Process sil content. The content may be supplied either inline or using src=...")
68✔
529

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

542
   self:registerCommand(
136✔
543
      "use",
68✔
544
      function (options, content)
545
         local packopts = packOptions(options)
93✔
546
         if content[1] and string.len(content[1]) > 0 then
93✔
547
            local doc = SU.ast.contentToString(content)
×
548
            SILE.processString(doc, "lua", nil, packopts)
×
549
         else
550
            if options.src then
93✔
551
               SU.warn(
×
552
                  "Use of 'src' with \\use is discouraged because some of it's path handling\n  will eventually be deprecated. Use 'module' instead when possible."
553
               )
554
               SILE.processFile(options.src, "lua", packopts)
×
555
            else
556
               local module = SU.required(options, "module", "use")
93✔
557
               SILE.use(module, packopts)
93✔
558
            end
559
         end
560
      end,
561
      "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."
562
   )
68✔
563

564
   self:registerCommand("raw", function (options, content)
136✔
UNCOV
565
      local rawtype = SU.required(options, "type", "raw")
×
UNCOV
566
      local handler = SILE.rawHandlers[rawtype]
×
UNCOV
567
      if not handler then
×
568
         SU.error("No inline handler for '" .. rawtype .. "'")
×
569
      end
UNCOV
570
      handler(options, content)
×
571
   end, "Invoke a raw passthrough handler")
68✔
572

573
   self:registerCommand("pagetemplate", function (options, content)
136✔
574
      SILE.typesetter:pushState()
1✔
575
      SILE.documentState.thisPageTemplate = { frames = {} }
1✔
576
      SILE.process(content)
1✔
577
      SILE.documentState.thisPageTemplate.firstContentFrame = SILE.getFrame(options["first-content-frame"])
2✔
578
      SILE.typesetter:initFrame(SILE.documentState.thisPageTemplate.firstContentFrame)
1✔
579
      SILE.typesetter:popState()
1✔
580
   end, "Defines a new page template for the current page and sets the typesetter to use it.")
69✔
581

582
   self:registerCommand("frame", function (options, _)
136✔
583
      SILE.documentState.thisPageTemplate.frames[options.id] = SILE.newFrame(options)
22✔
584
   end, "Declares (or re-declares) a frame on this page.")
79✔
585

586
   self:registerCommand("penalty", function (options, _)
136✔
587
      if SU.boolean(options.vertical, false) and not SILE.typesetter:vmode() then
257✔
UNCOV
588
         SILE.typesetter:leaveHmode()
×
589
      end
590
      if SILE.typesetter:vmode() then
248✔
591
         SILE.typesetter:pushVpenalty({ penalty = tonumber(options.penalty) })
184✔
592
      else
593
         SILE.typesetter:pushPenalty({ penalty = tonumber(options.penalty) })
32✔
594
      end
595
   end, "Inserts a penalty node. Option is penalty= for the size of the penalty.")
192✔
596

597
   self:registerCommand("discretionary", function (options, _)
136✔
UNCOV
598
      local discretionary = SILE.types.node.discretionary({})
×
UNCOV
599
      if options.prebreak then
×
UNCOV
600
         local hbox = SILE.typesetter:makeHbox({ options.prebreak })
×
UNCOV
601
         discretionary.prebreak = { hbox }
×
602
      end
UNCOV
603
      if options.postbreak then
×
UNCOV
604
         local hbox = SILE.typesetter:makeHbox({ options.postbreak })
×
UNCOV
605
         discretionary.postbreak = { hbox }
×
606
      end
UNCOV
607
      if options.replacement then
×
UNCOV
608
         local hbox = SILE.typesetter:makeHbox({ options.replacement })
×
UNCOV
609
         discretionary.replacement = { hbox }
×
610
      end
UNCOV
611
      table.insert(SILE.typesetter.state.nodes, discretionary)
×
612
   end, "Inserts a discretionary node.")
68✔
613

614
   self:registerCommand("glue", function (options, _)
136✔
615
      local width = SU.cast("length", options.width):absolute()
24✔
616
      SILE.typesetter:pushGlue(width)
12✔
617
   end, "Inserts a glue node. The width option denotes the glue dimension.")
80✔
618

619
   self:registerCommand("kern", function (options, _)
136✔
620
      local width = SU.cast("length", options.width):absolute()
22✔
621
      SILE.typesetter:pushHorizontal(SILE.types.node.kern(width))
22✔
622
   end, "Inserts a glue node. The width option denotes the glue dimension.")
79✔
623

624
   self:registerCommand("skip", function (options, _)
136✔
625
      options.discardable = SU.boolean(options.discardable, false)
26✔
626
      options.height = SILE.types.length(options.height):absolute()
39✔
627
      SILE.typesetter:leaveHmode()
13✔
628
      if options.discardable then
13✔
629
         SILE.typesetter:pushVglue(options)
×
630
      else
631
         SILE.typesetter:pushExplicitVglue(options)
13✔
632
      end
633
   end, "Inserts vertical skip. The height options denotes the skip dimension.")
81✔
634

635
   self:registerCommand("par", function (_, _)
136✔
636
      SILE.typesetter:endline()
90✔
637
   end, "Ends the current paragraph.")
158✔
638
end
639

640
function class:initialFrame ()
135✔
641
   SILE.documentState.thisPageTemplate = pl.tablex.deepcopy(self.pageTemplate)
184✔
642
   SILE.frames = { page = SILE.frames.page }
92✔
643
   for k, v in pairs(SILE.documentState.thisPageTemplate.frames) do
400✔
644
      SILE.frames[k] = v
308✔
645
   end
646
   if not SILE.documentState.thisPageTemplate.firstContentFrame then
92✔
647
      SILE.documentState.thisPageTemplate.firstContentFrame = SILE.frames[self.firstContentFrame]
×
648
   end
649
   SILE.documentState.thisPageTemplate.firstContentFrame:invalidate()
92✔
650
   return SILE.documentState.thisPageTemplate.firstContentFrame
92✔
651
end
652

653
function class:declareFrame (id, spec)
135✔
654
   spec.id = id
217✔
655
   if spec.solve then
217✔
656
      self.pageTemplate.frames[id] = spec
×
657
   else
658
      self.pageTemplate.frames[id] = SILE.newFrame(spec)
434✔
659
   end
660
   --   next = spec.next,
661
   --   left = spec.left and fW(spec.left),
662
   --   right = spec.right and fW(spec.right),
663
   --   top = spec.top and fH(spec.top),
664
   --   bottom = spec.bottom and fH(spec.bottom),
665
   --   height = spec.height and fH(spec.height),
666
   --   width = spec.width and fH(spec.width),
667
   --   id = id
668
   -- })
669
end
670

671
function class:declareFrames (specs)
135✔
672
   if specs then
68✔
673
      for k, v in pairs(specs) do
280✔
674
         self:declareFrame(k, v)
212✔
675
      end
676
   end
677
end
678

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

704
-- WARNING: not called as class method
705
function class.endPar (typesetter)
135✔
706
   -- If we're already explicitly out of hmode don't do anything special in the way of skips or indents. Assume the user
707
   -- has handled that how they want, e.g. with a skip.
708
   local queue = typesetter.state.outputQueue
512✔
709
   local last_vbox = queue and queue[#queue]
512✔
710
   local last_is_vglue = last_vbox and last_vbox.is_vglue
512✔
711
   local last_is_vpenalty = last_vbox and last_vbox.is_penalty
512✔
712
   if typesetter:vmode() and (last_is_vglue or last_is_vpenalty) then
1,024✔
713
      return
115✔
714
   end
715
   SILE.settings:set("current.parindent", nil)
397✔
716
   typesetter:leaveHmode()
397✔
717
   typesetter:pushVglue(SILE.settings:get("document.parskip"))
794✔
718
end
719

720
function class:newPage ()
135✔
721
   SILE.outputter:newPage()
24✔
722
   self:runHooks("newpage")
24✔
723
   -- Any other output-routiney things will be done here by inheritors
724
   return self:initialFrame()
24✔
725
end
726

727
function class:endPage ()
135✔
728
   SILE.typesetter.frame:leave(SILE.typesetter)
92✔
729
   self:runHooks("endpage")
92✔
730
   -- I'm trying to call up a new frame here, don't cause a page break in the current one
731
   -- SILE.typesetter:leaveHmode()
732
   -- Any other output-routiney things will be done here by inheritors
733
end
734

735
function class:finish ()
135✔
736
   SILE.inputter:postamble()
68✔
737
   SILE.typesetter:endline()
68✔
738
   SILE.call("vfill")
68✔
739
   while not SILE.typesetter:isQueueEmpty() do
272✔
740
      SILE.call("supereject")
68✔
741
      SILE.typesetter:leaveHmode(true)
68✔
742
      SILE.typesetter:buildPage()
68✔
743
      if not SILE.typesetter:isQueueEmpty() then
136✔
744
         SILE.typesetter:initNextFrame()
2✔
745
      end
746
   end
747
   SILE.typesetter:runHooks("pageend") -- normally run by the typesetter
68✔
748
   self:endPage()
68✔
749
   if SILE.typesetter and not SILE.typesetter:isQueueEmpty() then
136✔
750
      SU.error("Queues are not empty as expected after ending last page", true)
×
751
   end
752
   SILE.outputter:finish()
68✔
753
   self:runHooks("finish")
68✔
754
end
755

756
return class
135✔
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