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

sile-typesetter / sile / 8288578143

14 Mar 2024 09:39PM UTC coverage: 64.155% (-10.6%) from 74.718%
8288578143

Pull #1904

github

alerque
chore(core): Fixup ec6ed657 which didn't shim old pack styles properly
Pull Request #1904: Merge develop into master (commit to next release being breaking)

1648 of 2421 new or added lines in 107 files covered. (68.07%)

1843 existing lines in 77 files now uncovered.

10515 of 16390 relevant lines covered (64.15%)

3306.56 hits per line

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

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

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

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

47
class.packages = {}
365✔
48

49
function class:_init (options)
365✔
50
  SILE.scratch.half_initialized_class = self
182✔
51
  if self == options then options = {} end
182✔
52
  SILE.languageSupport.loadLanguage('und') -- preload for unlocalized fallbacks
182✔
53
  self:declareOptions()
182✔
54
  self:registerRawHandlers()
182✔
55
  self:declareSettings()
182✔
56
  self:registerCommands()
182✔
57
  self:setOptions(options)
182✔
58
  self:declareFrames(self.defaultFrameset)
182✔
59
  self:registerPostinit(function (self_)
364✔
60
      if type(self.firstContentFrame) == "string" then
182✔
61
        self_.pageTemplate.firstContentFrame = self_.pageTemplate.frames[self_.firstContentFrame]
182✔
62
      end
63
      local frame = self_:initialFrame()
182✔
64
      SILE.typesetter = SILE.typesetters.base(frame)
364✔
65
      SILE.typesetter:registerPageEndHook(function ()
364✔
66
        SU.debug("frames", function ()
458✔
67
          for _, v in pairs(SILE.frames) do SILE.outputter:debugFrame(v) end
×
68
          return "Drew debug outlines around frames"
×
69
        end)
70
      end)
71
    end)
72
end
73

74
function class:_post_init ()
365✔
75
  self._initialized = true
182✔
76
  for i, func in ipairs(self.deferredInit) do
446✔
77
    func(self)
264✔
78
    self.deferredInit[i] = nil
264✔
79
  end
80
  SILE.scratch.half_initialized_class = nil
182✔
81
end
82

83
function class:setOptions (options)
365✔
84
  options = options or {}
182✔
85
  -- Classes that add options with dependencies should explicitly handle them, then exempt them from furthur processing.
86
  -- The landscape and crop related options are handled explicitly before papersize, then the "rest" of options that are not interdependent.
87
  self.options.landscape = SU.boolean(options.landscape, false)
364✔
88
  options.landscape = nil
182✔
89
  self.options.papersize = options.papersize or "a4"
182✔
90
  options.papersize = nil
182✔
91
  self.options.bleed = options.bleed or "0"
182✔
92
  options.bleed = nil
182✔
93
  self.options.sheetsize = options.sheetsize or nil
182✔
94
  options.sheetsize = nil
182✔
95
  for option, value in pairs(options) do
200✔
96
    self.options[option] = value
18✔
97
  end
98
end
99

100
function class:declareOption (option, setter)
365✔
101
  rawset(getmetatable(self.options)._opts, option, nil)
1,103✔
102
  self.options[option] = setter
1,103✔
103
end
104

105
function class:declareOptions ()
365✔
106
  self:declareOption("class", function (_, name)
364✔
107
    if name then
×
108
      if self._legacy then
×
109
        self._name = name
×
110
      elseif name ~= self._name then
×
111
        SU.error("Cannot change class name after instantiation, derive a new class instead.")
×
112
      end
113
    end
114
    return self._name
×
115
  end)
116
  self:declareOption("landscape", function(_, landscape)
364✔
117
    if landscape then
365✔
118
      self.landscape = landscape
1✔
119
    end
120
    return self.landscape
365✔
121
  end)
122
  self:declareOption("papersize", function (_, size)
364✔
123
    if size then
182✔
124
      self.papersize = size
182✔
125
      SILE.documentState.paperSize = SILE.papersize(size, self.options.landscape)
546✔
126
      SILE.documentState.orgPaperSize = SILE.documentState.paperSize
182✔
127
      SILE.newFrame({
364✔
128
        id = "page",
129
        left = 0,
130
        top = 0,
131
        right = SILE.documentState.paperSize[1],
182✔
132
        bottom = SILE.documentState.paperSize[2]
182✔
133
      })
134
    end
135
    return self.papersize
182✔
136
  end)
137
  self:declareOption("sheetsize", function (_, size)
364✔
138
    if size then
182✔
139
      self.sheetsize = size
1✔
140
      SILE.documentState.sheetSize = SILE.papersize(size, self.options.landscape)
3✔
141
      if SILE.documentState.sheetSize[1] < SILE.documentState.paperSize[1]
1✔
142
        or SILE.documentState.sheetSize[2] < SILE.documentState.paperSize[2] then
1✔
NEW
143
        SU.error("Sheet size shall not be smaller than the paper size")
×
144
      end
145
      if SILE.documentState.sheetSize[1] < SILE.documentState.paperSize[1] + SILE.documentState.bleed then
1✔
NEW
146
        SU.debug("frames", "Sheet size width augmented to take page bleed into account")
×
NEW
147
        SILE.documentState.sheetSize[1] = SILE.documentState.paperSize[1] + SILE.documentState.bleed
×
148
      end
149
      if SILE.documentState.sheetSize[2] < SILE.documentState.paperSize[2] + SILE.documentState.bleed then
1✔
NEW
150
        SU.debug("frames", "Sheet size height augmented to take page bleed into account")
×
NEW
151
        SILE.documentState.sheetSize[2] = SILE.documentState.paperSize[2] + SILE.documentState.bleed
×
152
      end
153
    else
154
      return self.sheetsize
181✔
155
    end
156
   end)
157
  self:declareOption("bleed", function (_, dimen)
364✔
158
    if dimen then
182✔
159
      self.bleed = dimen
182✔
160
      SILE.documentState.bleed = SU.cast("measurement", dimen):tonumber()
546✔
161
    end
162
    return self.bleed
182✔
163
  end)
164
end
165

166
function class.declareSettings (_)
365✔
167
  SILE.settings:declare({
182✔
168
    parameter = "current.parindent",
169
    type = "glue or nil",
170
    default = nil,
171
    help = "Glue at start of paragraph"
×
172
  })
173
  SILE.settings:declare({
182✔
174
    parameter = "current.hangIndent",
175
    type = "measurement or nil",
176
    default = nil,
177
    help = "Size of hanging indent"
×
178
  })
179
  SILE.settings:declare({
182✔
180
    parameter = "current.hangAfter",
181
    type = "integer or nil",
182
    default = nil,
183
    help = "Number of lines affected by handIndent"
×
184
  })
185
end
186

187
function class:loadPackage (packname, options, reload)
365✔
188
  local pack
189
  -- Allow loading by injecting whole packages as-is, otherwise try to load it with the usual packages path.
190
  if type(packname) == "table" then
1,234✔
191
    pack, packname = packname, packname._name
239✔
192
  elseif type(packname) == "nil" or packname == "nil" or pl.stringx.strip(packname) == "" then
1,990✔
NEW
193
    SU.error(("Attempted to load package with an invalid packname '%s'"):format(packname))
×
194
  else
195
    pack = require(("packages.%s"):format(packname))
995✔
196
    if pack._name ~= packname then
995✔
NEW
197
      SU.error(("Loaded module name '%s' does not match requested name '%s'"):format(pack._name, packname))
×
198
    end
199
  end
200
  SILE.packages[packname] = pack
1,234✔
201
  if type(pack) == "table" and pack.type == "package" then -- current package api
1,234✔
202
    if self.packages[packname] then
1,234✔
203
      -- If the same package name has been loaded before, we might be loading a modified version of the same package or
204
      -- we might be re-loading the same package, or we might just be doubling up work because somebody called us twice.
205
      -- The package itself should take care of the difference between load and reload based on the reload flag here,
206
      -- but in addition to that we also want to avoid creating a new instance. We want to run the intitializer from the
207
      -- (possibly different) new module, but not create a new instance ID and loose any connections it made.
208
      -- To do this we add a create function that returns the current instance. This brings along the _initialized flag
209
      -- and of course anything else already setup and running.
210
      local current_instance = self.packages[packname]
98✔
211
      pack._create = function () return current_instance end
196✔
212
      pack(options, true)
196✔
213
    else
214
      self.packages[packname] = pack(options, reload)
2,272✔
215
    end
216
  else -- legacy package
217
    self:initPackage(pack, options)
×
218
  end
219
end
220

221
function class:reloadPackage (packname, options)
365✔
NEW
222
  return self:loadPackage(packname, options, true)
×
223
end
224

225
function class:initPackage (pack, options)
365✔
226
  SU.deprecated("class:initPackage(options)", "package(options)", "0.14.0", "0.16.0", [[
×
227
  This package appears to be a legacy format package. It returns a table
228
  an expects SILE to guess a bit about what to do. New packages inherit
229
  from the base class and have a constructor function (_init) that
230
  automatically handles setup.]])
×
231
  if type(pack) == "table" then
×
232
    if pack.exports then pl.tablex.update(self, pack.exports) end
×
233
    if type(pack.declareSettings) == "function" then
×
234
      pack.declareSettings(self)
×
235
    end
236
    if type(pack.registerRawHandlers) == "function" then
×
237
      pack.registerRawHandlers(self)
×
238
    end
239
    if type(pack.registerCommands) == "function" then
×
240
      pack.registerCommands(self)
×
241
    end
242
    if type(pack.init) == "function" then
×
243
      self:registerPostinit(pack.init, options)
×
244
    end
245
  end
246
end
247

248
function class:registerLegacyPostinit (func, options)
365✔
249
  if self._initialized then return func(self, options) end
×
250
  table.insert(self.deferredLegacyInit, function (_)
×
251
      func(self, options)
×
252
    end)
253
end
254

255
function class:registerPostinit (func, options)
365✔
256
  if self._initialized then return func(self, options) end
288✔
257
  table.insert(self.deferredInit, function (_)
528✔
258
      func(self, options)
264✔
259
    end)
260
end
261

262
function class:registerHook (category, func)
365✔
263
  for _, func_ in ipairs(self.hooks[category]) do
788✔
264
    if func_ == func then
249✔
265
      return
1✔
266
      --[[ See https://github.com/sile-typesetter/sile/issues/1531
267
      return SU.warn("Attempted to set the same function hook twice, probably unintended, skipping.")
268
      -- If the same function signature is already set a package is probably being
269
      -- re-initialized. Ditch the first instance of the hook so that it runs in
270
      -- the order of last initialization.
271
      self.hooks[category][_] = nil
272
      ]]
273
    end
274
  end
275
  table.insert(self.hooks[category], func)
539✔
276
end
277

278
function class:runHooks (category, options)
365✔
279
  for _, func in ipairs(self.hooks[category]) do
975✔
280
    SU.debug("classhooks", "Running hook from", category, options and "with options #" .. #options)
505✔
281
    func(self, options)
505✔
282
  end
283
end
284

285
--- Register a function as a SILE command.
286
-- Takes any Lua function and registers it for use as a SILE command (which will in turn be used to process any content
287
-- nodes identified with the command name.
288
--
289
-- Note that this should only be used to register commands supplied directly by a document class. A similar method is
290
-- available for packages, `packages:registerCommand`.
291
-- @tparam string name Name of cammand to register.
292
-- @tparam function func Callback function to use as command handler.
293
-- @tparam[opt] nil|string help User friendly short usage string for use in error messages, documentation, etc.
294
-- @tparam[opt] nil|string pack Information identifying the module registering the command for use in error and usage
295
-- messages. Usually auto-detected.
296
-- @see SILE.packages:registerCommand
297
function class.registerCommand (_, name, func, help, pack)
365✔
298
  SILE.Commands[name] = func
19,828✔
299
  if not pack then
19,828✔
300
    local where = debug.getinfo(2).source
19,815✔
301
    pack = where:match("(%w+).lua")
19,815✔
302
  end
303
  --if not help and not pack:match(".sil") then SU.error("Could not define command '"..name.."' (in package "..pack..") - no help text" ) end
304
  SILE.Help[name] = {
19,828✔
305
    description = help,
19,828✔
306
    where = pack
19,828✔
307
  }
19,828✔
308
end
309

310
function class.registerRawHandler (_, format, callback)
365✔
311
  SILE.rawHandlers[format] = callback
186✔
312
end
313

314
function class:registerRawHandlers ()
365✔
315

316
  self:registerRawHandler("text", function (_, content)
364✔
317
    SILE.settings:temporarily(function()
2✔
318
      SILE.settings:set("typesetter.parseppattern", "\n")
1✔
319
      SILE.settings:set("typesetter.obeyspaces", true)
1✔
320
      SILE.typesetter:typeset(content[1])
1✔
321
    end)
322
  end)
323

324
end
325

326
local function packOptions (options)
327
  local relevant = pl.tablex.copy(options)
277✔
328
  relevant.src = nil
277✔
329
  relevant.format = nil
277✔
330
  relevant.module = nil
277✔
331
  relevant.require = nil
277✔
332
  return relevant
277✔
333
end
334

335
function class:registerCommands ()
365✔
336

337
  local function replaceProcessBy(replacement, tree)
338
    if type(tree) ~= "table" then return tree end
160✔
339
    local ret = pl.tablex.deepcopy(tree)
95✔
340
    if tree.command == "process" then
95✔
341
      return replacement
2✔
342
    else
343
      for i, child in ipairs(tree) do
220✔
344
        ret[i] = replaceProcessBy(replacement, child)
254✔
345
      end
346
      return ret
93✔
347
    end
348
  end
349

350
  self:registerCommand("define", function (options, content)
364✔
351
    SU.required(options, "command", "defining command")
13✔
352
    if type(content) == "function" then
13✔
353
      -- A macro defined as a function can take no argument, so we register
354
      -- it as-is.
355
      self:registerCommand(options["command"], content)
×
356
      return
×
357
    elseif options.command == "process" then
13✔
358
      SU.warn("Did you mean to re-definine the `\\process` macro? That probably won't go well.")
×
359
    end
360
    self:registerCommand(options["command"], function (_, inner_content)
26✔
361
      SU.debug("macros", "Processing macro \\" .. options["command"])
33✔
362
      local macroArg
363
      if type(inner_content) == "function" then
33✔
UNCOV
364
        macroArg = inner_content
×
365
      elseif type(inner_content) == "table" then
33✔
366
        macroArg = pl.tablex.copy(inner_content)
66✔
367
        macroArg.command = nil
33✔
368
        macroArg.id = nil
33✔
UNCOV
369
      elseif inner_content == nil then
×
UNCOV
370
        macroArg = {}
×
371
      else
372
        SU.error("Unhandled content type " .. type(inner_content) .. " passed to macro \\" .. options["command"], true)
×
373
      end
374
      -- Replace every occurrence of \process in `content` (the macro
375
      -- body) with `macroArg`, then have SILE go through the new `content`.
376
      local newContent = replaceProcessBy(macroArg, content)
33✔
377
      SILE.process(newContent)
33✔
378
      SU.debug("macros", "Finished processing \\" .. options["command"])
33✔
379
    end, options.help, SILE.currentlyProcessingFile)
46✔
380
  end, "Define a new macro. \\define[command=example]{ ... \\process }")
195✔
381

382
  -- A utility function that allows SILE.call() to be used as a noop wrapper.
383
  self:registerCommand("noop", function (_, content)
364✔
384
    SILE.process(content)
7✔
385
  end)
386

387
  -- The document (SIL) or sile (XML) command is always the sigular leaf at the
388
  -- top level of our AST. The work you might expect to see happen here is
389
  -- actually handled by SILE.inputter:classInit() before we get here, so these
390
  -- are just pass through functions. Theoretically, this could be a useful
391
  -- point to hook into-especially for included documents.
392
  self:registerCommand("document", function (_, content)
364✔
393
    SILE.process(content)
×
394
  end)
395
  self:registerCommand("sile", function (_, content)
364✔
396
    SILE.process(content)
×
397
  end)
398

399
  self:registerCommand("comment", function (_, _)
364✔
400
  end, "Ignores any text within this command's body.")
182✔
401

402
  self:registerCommand("process", function ()
364✔
403
    SU.error("Encountered unsubstituted \\process.")
×
404
  end, "Within a macro definition, processes the contents of the macro body.")
182✔
405

406
  self:registerCommand("script", function (options, content)
364✔
407
    local packopts = packOptions(options)
32✔
408
    local function _deprecated (original, suggested)
409
      SU.deprecated("\\script", "\\lua or \\use", "0.15.0", "0.16.0", ([[
64✔
410
      The \script function has been deprecated. It was overloaded to mean
411
      too many different things and more targeted tools were introduced in
412
      SILE v0.14.0. To load 3rd party modules designed for use with SILE,
413
      replace \script[src=...] with \use[module=...]. To run arbitrary Lua
414
      code inline use \lua{}, optionally with a require= parameter to load
415
      a (non-SILE) Lua module using the Lua module path or src= to load a
416
      file by file path.
417

418
      For this use case consider replacing:
419

420
          %s
421

422
      with:
423

424
          %s
425
      ]]):format(original, suggested))
32✔
426
    end
427
    if SU.ast.hasContent(content) then
64✔
428
      _deprecated("\\script{...}", "\\lua{...}")
32✔
429
      return SILE.processString(content[1], options.format or "lua", nil, packopts)
32✔
UNCOV
430
    elseif options.src then
×
NEW
431
      local module = options.src:gsub("%/", ".")
×
NEW
432
      local original = (("\\script[src=%s]"):format(options.src))
×
NEW
433
      local result = SILE.require(options.src)
×
NEW
434
      local suggested = (result._name and "\\use[module=%s]" or "\\lua[require=%s]"):format(module)
×
NEW
435
      _deprecated(original, suggested)
×
NEW
436
      return result
×
437
    else
438
      SU.error("\\script function requires inline content or a src file path")
×
439
      return SILE.processString(content[1], options.format or "lua", nil, packopts)
×
440
    end
441
  end, "Runs lua code. The code may be supplied either inline or using src=...")
182✔
442

443
  self:registerCommand("include", function (options, content)
364✔
444
    local packopts = packOptions(options)
1✔
445
    if SU.ast.hasContent(content) then
2✔
NEW
446
      local doc = SU.ast.contentToString(content)
×
UNCOV
447
      return SILE.processString(doc, options.format, nil, packopts)
×
448
    elseif options.src then
1✔
449
      return SILE.processFile(options.src, options.format, packopts)
1✔
450
    else
451
      SU.error("\\include function requires inline content or a src file path")
×
452
    end
453
  end, "Includes a content file for processing.")
182✔
454

455
  self:registerCommand("lua", function (options, content)
364✔
456
    local packopts = packOptions(options)
6✔
457
    if SU.ast.hasContent(content) then
12✔
458
      local doc = SU.ast.contentToString(content)
6✔
459
      return SILE.processString(doc, "lua", nil, packopts)
6✔
460
    elseif options.src then
×
461
      return SILE.processFile(options.src, "lua", packopts)
×
462
    elseif options.require then
×
463
      local module = SU.required(options, "require", "lua")
×
464
      return require(module)
×
465
    else
466
      SU.error("\\lua function requires inline content or a src file path or a require module name")
×
467
    end
468
  end, "Run Lua code. The code may be supplied either inline, using require=... for a Lua module, or using src=... for a file path")
182✔
469

470
  self:registerCommand("sil", function (options, content)
364✔
471
    local packopts = packOptions(options)
×
NEW
472
    if SU.ast.hasContent(content) then
×
NEW
473
      local doc = SU.ast.contentToString(content)
×
474
      return SILE.processString(doc, "sil")
×
475
    elseif options.src then
×
476
      return SILE.processFile(options.src, "sil", packopts)
×
477
    else
478
      SU.error("\\sil function requires inline content or a src file path")
×
479
    end
480
  end, "Process sil content. The content may be supplied either inline or using src=...")
182✔
481

482
  self:registerCommand("xml", function (options, content)
364✔
483
    local packopts = packOptions(options)
×
NEW
484
    if SU.ast.hasContent(content) then
×
NEW
485
      local doc = SU.ast.contentToString(content)
×
486
      return SILE.processString(doc, "xml", nil, packopts)
×
487
    elseif options.src then
×
488
      return SILE.processFile(options.src, "xml", packopts)
×
489
    else
490
      SU.error("\\xml function requires inline content or a src file path")
×
491
    end
492
  end, "Process xml content. The content may be supplied either inline or using src=...")
182✔
493

494
  self:registerCommand("use", function (options, content)
364✔
495
    local packopts = packOptions(options)
238✔
496
    if content[1] and string.len(content[1]) > 0 then
238✔
NEW
497
      local doc = SU.ast.contentToString(content)
×
498
      SILE.processString(doc, "lua", nil, packopts)
×
499
    else
500
      if options.src then
238✔
501
        SU.warn("Use of 'src' with \\use is discouraged because some of it's path handling\n  will eventually be deprecated. Use 'module' instead when possible.")
×
502
        SILE.processFile(options.src, "lua", packopts)
×
503
      else
504
        local module = SU.required(options, "module", "use")
238✔
505
        SILE.use(module, packopts)
238✔
506
      end
507
    end
508
  end, "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.")
420✔
509

510
  self:registerCommand("raw", function (options, content)
364✔
511
    local rawtype = SU.required(options, "type", "raw")
4✔
512
    local handler = SILE.rawHandlers[rawtype]
4✔
513
    if not handler then SU.error("No inline handler for '"..rawtype.."'") end
4✔
514
    handler(options, content)
4✔
515
  end, "Invoke a raw passthrough handler")
186✔
516

517
  self:registerCommand("pagetemplate", function (options, content)
364✔
518
    SILE.typesetter:pushState()
8✔
519
    SILE.documentState.thisPageTemplate = { frames = {} }
8✔
520
    SILE.process(content)
8✔
521
    SILE.documentState.thisPageTemplate.firstContentFrame = SILE.getFrame(options["first-content-frame"])
16✔
522
    SILE.typesetter:initFrame(SILE.documentState.thisPageTemplate.firstContentFrame)
8✔
523
    SILE.typesetter:popState()
8✔
524
  end, "Defines a new page template for the current page and sets the typesetter to use it.")
190✔
525

526
  self:registerCommand("frame", function (options, _)
364✔
527
    SILE.documentState.thisPageTemplate.frames[options.id] = SILE.newFrame(options)
62✔
528
  end, "Declares (or re-declares) a frame on this page.")
213✔
529

530
  self:registerCommand("penalty", function (options, _)
364✔
531
    if SU.boolean(options.vertical, false) and not SILE.typesetter:vmode() then
592✔
532
      SILE.typesetter:leaveHmode()
4✔
533
    end
534
    if SILE.typesetter:vmode() then
570✔
535
      SILE.typesetter:pushVpenalty({ penalty = tonumber(options.penalty) })
454✔
536
    else
537
      SILE.typesetter:pushPenalty({ penalty = tonumber(options.penalty) })
58✔
538
    end
539
  end, "Inserts a penalty node. Option is penalty= for the size of the penalty.")
467✔
540

541
  self:registerCommand("discretionary", function (options, _)
364✔
542
    local discretionary = SILE.types.node.discretionary({})
74✔
543
    if options.prebreak then
74✔
544
      local hbox = SILE.typesetter:makeHbox({ options.prebreak })
74✔
545
      discretionary.prebreak = { hbox }
74✔
546
    end
547
    if options.postbreak then
74✔
548
      local hbox = SILE.typesetter:makeHbox({ options.postbreak })
48✔
549
      discretionary.postbreak = { hbox }
48✔
550
    end
551
    if options.replacement then
74✔
552
      local hbox = SILE.typesetter:makeHbox({ options.replacement })
50✔
553
      discretionary.replacement = { hbox }
50✔
554
    end
555
    table.insert(SILE.typesetter.state.nodes, discretionary)
74✔
556
  end, "Inserts a discretionary node.")
256✔
557

558
  self:registerCommand("glue", function (options, _)
364✔
559
    local width = SU.cast("length", options.width):absolute()
118✔
560
    SILE.typesetter:pushGlue(width)
59✔
561
  end, "Inserts a glue node. The width option denotes the glue dimension.")
241✔
562

563
  self:registerCommand("kern", function (options, _)
364✔
564
    local width = SU.cast("length", options.width):absolute()
180✔
565
    SILE.typesetter:pushHorizontal(SILE.types.node.kern(width))
180✔
566
  end, "Inserts a glue node. The width option denotes the glue dimension.")
272✔
567

568
  self:registerCommand("skip", function (options, _)
364✔
569
    options.discardable = SU.boolean(options.discardable, false)
58✔
570
    options.height = SILE.types.length(options.height):absolute()
87✔
571
    SILE.typesetter:leaveHmode()
29✔
572
    if options.discardable then
29✔
573
      SILE.typesetter:pushVglue(options)
×
574
    else
575
      SILE.typesetter:pushExplicitVglue(options)
29✔
576
    end
577
  end, "Inserts vertical skip. The height options denotes the skip dimension.")
211✔
578

579
  self:registerCommand("par", function (_, _)
364✔
580
    SILE.typesetter:endline()
285✔
581
  end, "Ends the current paragraph.")
467✔
582

583
end
584

585
function class:initialFrame ()
365✔
586
  SILE.documentState.thisPageTemplate = pl.tablex.deepcopy(self.pageTemplate)
470✔
587
  SILE.frames = { page = SILE.frames.page }
235✔
588
  for k, v in pairs(SILE.documentState.thisPageTemplate.frames) do
1,019✔
589
    SILE.frames[k] = v
784✔
590
  end
591
  if not SILE.documentState.thisPageTemplate.firstContentFrame then
235✔
592
    SILE.documentState.thisPageTemplate.firstContentFrame = SILE.frames[self.firstContentFrame]
×
593
  end
594
  SILE.documentState.thisPageTemplate.firstContentFrame:invalidate()
235✔
595
  return SILE.documentState.thisPageTemplate.firstContentFrame
235✔
596
end
597

598
function class:declareFrame (id, spec)
365✔
599
  spec.id = id
595✔
600
  if spec.solve then
595✔
601
    self.pageTemplate.frames[id] = spec
×
602
  else
603
    self.pageTemplate.frames[id] = SILE.newFrame(spec)
1,190✔
604
  end
605
  --   next = spec.next,
606
  --   left = spec.left and fW(spec.left),
607
  --   right = spec.right and fW(spec.right),
608
  --   top = spec.top and fH(spec.top),
609
  --   bottom = spec.bottom and fH(spec.bottom),
610
  --   height = spec.height and fH(spec.height),
611
  --   width = spec.width and fH(spec.width),
612
  --   id = id
613
  -- })
614
end
615

616
function class:declareFrames (specs)
365✔
617
  if specs then
182✔
618
    for k, v in pairs(specs) do self:declareFrame(k, v) end
1,340✔
619
  end
620
end
621

622
-- WARNING: not called as class method
623
function class.newPar (typesetter)
365✔
624
  local parindent = SILE.settings:get("current.parindent") or SILE.settings:get("document.parindent")
1,682✔
625
  -- See https://github.com/sile-typesetter/sile/issues/1361
626
  -- The parindent *cannot* be pushed non-absolutized, as it may be evaluated
627
  -- outside the (possibly temporary) setting scope where it was used for line
628
  -- breaking.
629
  -- Early absolutization can be problematic sometimes, but here we do not
630
  -- really have the choice.
631
  -- As of problematic cases, consider a parindent that would be defined in a
632
  -- frame-related unit (%lw, %fw, etc.). If a frame break occurs and the next
633
  -- frame has a different width, the parindent won't be re-evaluated in that
634
  -- new frame context. However, defining a parindent in such a unit is quite
635
  -- unlikely. And anyway pushback() has plenty of other issues.
636
  typesetter:pushGlue(parindent:absolute())
1,682✔
637
  SILE.settings:set("current.parindent", nil)
841✔
638
  local hangIndent = SILE.settings:get("current.hangIndent")
841✔
639
  if hangIndent then
841✔
640
    SILE.settings:set("linebreak.hangIndent", hangIndent)
11✔
641
  end
642
  local hangAfter = SILE.settings:get("current.hangAfter")
841✔
643
  if hangAfter then
841✔
644
    SILE.settings:set("linebreak.hangAfter", hangAfter)
11✔
645
  end
646
end
647

648
-- WARNING: not called as class method
649
function class.endPar (typesetter)
365✔
650
  typesetter:pushVglue(SILE.settings:get("document.parskip"))
1,636✔
651
  if SILE.settings:get("current.hangIndent") then
1,636✔
652
    SILE.settings:set("current.hangIndent", nil)
10✔
653
    SILE.settings:set("linebreak.hangIndent", nil)
10✔
654
  end
655
  if SILE.settings:get("current.hangAfter") then
1,636✔
656
    SILE.settings:set("current.hangAfter", nil)
10✔
657
    SILE.settings:set("linebreak.hangAfter", nil)
10✔
658
  end
659
end
660

661
function class:newPage ()
365✔
662
  SILE.outputter:newPage()
53✔
663
  self:runHooks("newpage")
53✔
664
  -- Any other output-routiney things will be done here by inheritors
665
  return self:initialFrame()
53✔
666
end
667

668
function class:endPage ()
365✔
669
  SILE.typesetter.frame:leave(SILE.typesetter)
235✔
670
  self:runHooks("endpage")
235✔
671
  -- I'm trying to call up a new frame here, don't cause a page break in the current one
672
  -- SILE.typesetter:leaveHmode()
673
  -- Any other output-routiney things will be done here by inheritors
674
end
675

676
function class:finish ()
365✔
677
  SILE.inputter:postamble()
182✔
678
  SILE.call("vfill")
182✔
679
  while not SILE.typesetter:isQueueEmpty() do
728✔
680
    SILE.call("supereject")
182✔
681
    SILE.typesetter:leaveHmode(true)
182✔
682
    SILE.typesetter:buildPage()
182✔
683
    if not SILE.typesetter:isQueueEmpty() then
364✔
684
      SILE.typesetter:initNextFrame()
5✔
685
    end
686
  end
687
  SILE.typesetter:runHooks("pageend") -- normally run by the typesetter
182✔
688
  self:endPage()
182✔
689
  if SILE.typesetter and not SILE.typesetter:isQueueEmpty() then
364✔
690
    SU.error("Queues are not empty as expected after ending last page", true)
×
691
  end
692
  SILE.outputter:finish()
182✔
693
  self:runHooks("finish")
182✔
694
end
695

696
return class
365✔
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