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

sile-typesetter / sile / 11169993653

03 Oct 2024 09:26PM UTC coverage: 63.103% (+29.9%) from 33.23%
11169993653

push

github

web-flow
Merge pull request #2113 from Omikhleia/fix-ast-differences

46 of 57 new or added lines in 5 files covered. (80.7%)

88 existing lines in 10 files now uncovered.

11286 of 17885 relevant lines covered (63.1%)

3626.41 hits per line

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

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

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

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

50
class.packages = {}
190✔
51

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

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

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

119
function class:declareOptions ()
190✔
120
   self:declareOption("class", function (_, name)
194✔
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)
194✔
131
      if landscape then
194✔
132
         self.landscape = landscape
1✔
133
      end
134
      return self.landscape
194✔
135
   end)
136
   self:declareOption("papersize", function (_, size)
194✔
137
      if size then
97✔
138
         self.papersize = size
97✔
139
         SILE.documentState.paperSize = SILE.papersize(size, self.options.landscape)
291✔
140
         SILE.documentState.orgPaperSize = SILE.documentState.paperSize
97✔
141
         SILE.newFrame({
194✔
142
            id = "page",
143
            left = 0,
144
            top = 0,
145
            right = SILE.documentState.paperSize[1],
97✔
146
            bottom = SILE.documentState.paperSize[2],
97✔
147
         })
148
      end
149
      return self.papersize
97✔
150
   end)
151
   self:declareOption("sheetsize", function (_, size)
194✔
152
      if size then
97✔
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
97✔
171
      end
172
   end)
173
   self:declareOption("bleed", function (_, dimen)
194✔
174
      if dimen then
97✔
175
         self.bleed = dimen
97✔
176
         SILE.documentState.bleed = SU.cast("measurement", dimen):tonumber()
291✔
177
      end
178
      return self.bleed
97✔
179
   end)
180
end
181

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

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

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

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

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

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

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

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

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

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

371
function class:registerCommands ()
190✔
372
   local function replaceProcessBy (replacement, tree)
373
      if type(tree) ~= "table" then
45✔
374
         return tree
12✔
375
      end
376
      local ret = pl.tablex.deepcopy(tree)
33✔
377
      if tree.command == "process" then
33✔
378
         return replacement
6✔
379
      else
380
         for i, child in ipairs(tree) do
53✔
381
            ret[i] = replaceProcessBy(replacement, child)
52✔
382
         end
383
         return ret
27✔
384
      end
385
   end
386

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

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

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

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

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

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

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

465
               For this use case consider replacing:
466

467
               %s
468

469
               with:
470

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

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

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

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

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

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

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

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

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

589
   self:registerCommand("frame", function (options, _)
194✔
590
      SILE.documentState.thisPageTemplate.frames[options.id] = SILE.newFrame(options)
28✔
591
   end, "Declares (or re-declares) a frame on this page.")
111✔
592

593
   self:registerCommand("penalty", function (options, _)
194✔
594
      if SU.boolean(options.vertical, false) and not SILE.typesetter:vmode() then
370✔
595
         SILE.typesetter:leaveHmode()
5✔
596
      end
597
      if SILE.typesetter:vmode() then
344✔
598
         SILE.typesetter:pushVpenalty({ penalty = tonumber(options.penalty) })
278✔
599
      else
600
         SILE.typesetter:pushPenalty({ penalty = tonumber(options.penalty) })
33✔
601
      end
602
   end, "Inserts a penalty node. Option is penalty= for the size of the penalty.")
269✔
603

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

621
   self:registerCommand("glue", function (options, _)
194✔
622
      local width = SU.cast("length", options.width):absolute()
42✔
623
      SILE.typesetter:pushGlue(width)
21✔
624
   end, "Inserts a glue node. The width option denotes the glue dimension.")
118✔
625

626
   self:registerCommand("kern", function (options, _)
194✔
627
      local width = SU.cast("length", options.width):absolute()
122✔
628
      SILE.typesetter:pushHorizontal(SILE.types.node.kern(width))
122✔
629
   end, "Inserts a glue node. The width option denotes the glue dimension.")
158✔
630

631
   self:registerCommand("skip", function (options, _)
194✔
632
      options.discardable = SU.boolean(options.discardable, false)
28✔
633
      options.height = SILE.types.length(options.height):absolute()
42✔
634
      SILE.typesetter:leaveHmode()
14✔
635
      if options.discardable then
14✔
636
         SILE.typesetter:pushVglue(options)
×
637
      else
638
         SILE.typesetter:pushExplicitVglue(options)
14✔
639
      end
640
   end, "Inserts vertical skip. The height options denotes the skip dimension.")
111✔
641

642
   self:registerCommand("par", function (_, _)
194✔
643
      SILE.typesetter:endline()
130✔
644
   end, "Ends the current paragraph.")
227✔
645
end
646

647
function class:initialFrame ()
190✔
648
   SILE.documentState.thisPageTemplate = pl.tablex.deepcopy(self.pageTemplate)
250✔
649
   SILE.frames = { page = SILE.frames.page }
125✔
650
   for k, v in pairs(SILE.documentState.thisPageTemplate.frames) do
541✔
651
      SILE.frames[k] = v
416✔
652
   end
653
   if not SILE.documentState.thisPageTemplate.firstContentFrame then
125✔
654
      SILE.documentState.thisPageTemplate.firstContentFrame = SILE.frames[self.firstContentFrame]
×
655
   end
656
   SILE.documentState.thisPageTemplate.firstContentFrame:invalidate()
125✔
657
   return SILE.documentState.thisPageTemplate.firstContentFrame
125✔
658
end
659

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

678
function class:declareFrames (specs)
190✔
679
   if specs then
97✔
680
      for k, v in pairs(specs) do
403✔
681
         self:declareFrame(k, v)
306✔
682
      end
683
   end
684
end
685

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

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

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

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

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

763
return class
190✔
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