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

sile-typesetter / sile / 7234699031

16 Dec 2023 10:48PM UTC coverage: 71.471% (-3.2%) from 74.643%
7234699031

push

github

web-flow
Merge 8fd8be51a into ad5f09a68

155 of 235 new or added lines in 10 files covered. (65.96%)

463 existing lines in 23 files now uncovered.

11316 of 15833 relevant lines covered (71.47%)

6905.9 hits per line

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

80.56
/classes/base.lua
1
local class = pl.class()
345✔
2
class.type = "class"
345✔
3
class._name = "base"
345✔
4

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

44
class.packages = {}
345✔
45

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

71
function class:_post_init ()
345✔
72
  self._initialized = true
173✔
73
  for i, func in ipairs(self.deferredInit) do
426✔
74
    func(self)
253✔
75
    self.deferredInit[i] = nil
253✔
76
  end
77
  SILE.scratch.half_initialized_class = nil
173✔
78
end
79

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

97
function class:declareOption (option, setter)
345✔
98
  rawset(getmetatable(self.options)._opts, option, nil)
1,049✔
99
  self.options[option] = setter
1,049✔
100
end
101

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

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

184
function class:loadPackage (packname, options, reload)
345✔
185
  local pack = require(("packages.%s"):format(packname))
954✔
186
  if type(pack) == "table" and pack.type == "package" then -- new package
954✔
187
    self.packages[pack._name] = pack(options, reload)
1,908✔
188
  else -- legacy package
189
    self:initPackage(pack, options)
×
190
  end
191
end
192

193
function class:reloadPackage (packname, options)
345✔
194
  return self:loadPackage(packname, options, true)
×
195
end
196

197
function class:initPackage (pack, options)
345✔
198
  SU.deprecated("class:initPackage(options)", "package(options)", "0.14.0", "0.16.0", [[
×
199
  This package appears to be a legacy format package. It returns a table
200
  an expects SILE to guess a bit about what to do. New packages inherit
201
  from the base class and have a constructor function (_init) that
202
  automatically handles setup.]])
×
203
  if type(pack) == "table" then
×
204
    if pack.exports then pl.tablex.update(self, pack.exports) end
×
205
    if type(pack.declareSettings) == "function" then
×
206
      pack.declareSettings(self)
×
207
    end
208
    if type(pack.registerRawHandlers) == "function" then
×
209
      pack.registerRawHandlers(self)
×
210
    end
211
    if type(pack.registerCommands) == "function" then
×
212
      pack.registerCommands(self)
×
213
    end
214
    if type(pack.init) == "function" then
×
215
      self:registerPostinit(pack.init, options)
×
216
    end
217
  end
218
end
219

220
function class:registerLegacyPostinit (func, options)
345✔
221
  if self._initialized then return func(self, options) end
×
222
  table.insert(self.deferredLegacyInit, function (_)
×
223
      func(self, options)
×
224
    end)
225
end
226

227
function class:registerPostinit (func, options)
345✔
228
  if self._initialized then return func(self, options) end
276✔
229
  table.insert(self.deferredInit, function (_)
506✔
230
      func(self, options)
253✔
231
    end)
232
end
233

234
function class:registerHook (category, func)
345✔
235
  for _, func_ in ipairs(self.hooks[category]) do
758✔
236
    if func_ == func then
242✔
237
      return
1✔
238
      --[[ See https://github.com/sile-typesetter/sile/issues/1531
239
      return SU.warn("Attempted to set the same function hook twice, probably unintended, skipping.")
240
      -- If the same function signature is already set a package is probably being
241
      -- re-initialized. Ditch the first instance of the hook so that it runs in
242
      -- the order of last initialization.
243
      self.hooks[category][_] = nil
244
      ]]
245
    end
246
  end
247
  table.insert(self.hooks[category], func)
516✔
248
end
249

250
function class:runHooks (category, options)
345✔
251
  for _, func in ipairs(self.hooks[category]) do
950✔
252
    SU.debug("classhooks", "Running hook from", category, options and "with options " .. #options)
494✔
253
    func(self, options)
494✔
254
  end
255
end
256

257
function class.registerCommand (_, name, func, help, pack)
345✔
258
  SILE.Commands[name] = func
18,460✔
259
  if not pack then
18,460✔
260
    local where = debug.getinfo(2).source
18,446✔
261
    pack = where:match("(%w+).lua")
18,446✔
262
  end
263
  --if not help and not pack:match(".sil") then SU.error("Could not define command '"..name.."' (in package "..pack..") - no help text" ) end
264
  SILE.Help[name] = {
18,460✔
265
    description = help,
18,460✔
266
    where = pack
18,460✔
267
  }
18,460✔
268
end
269

270
function class.registerRawHandler (_, format, callback)
345✔
271
  SILE.rawHandlers[format] = callback
177✔
272
end
273

274
function class:registerRawHandlers ()
345✔
275

276
  self:registerRawHandler("text", function (_, content)
346✔
277
    SILE.settings:temporarily(function()
2✔
278
      SILE.settings:set("typesetter.parseppattern", "\n")
1✔
279
      SILE.settings:set("typesetter.obeyspaces", true)
1✔
280
      SILE.typesetter:typeset(content[1])
1✔
281
    end)
282
  end)
283

284
end
285

286
local function packOptions (options)
287
  local relevant = pl.tablex.copy(options)
262✔
288
  relevant.src = nil
262✔
289
  relevant.format = nil
262✔
290
  relevant.module = nil
262✔
291
  relevant.require = nil
262✔
292
  return relevant
262✔
293
end
294

295
function class:registerCommands ()
345✔
296

297
  local function replaceProcessBy(replacement, tree)
298
    if type(tree) ~= "table" then return tree end
157✔
299
    local ret = pl.tablex.deepcopy(tree)
94✔
300
    if tree.command == "process" then
94✔
301
      return replacement
7✔
302
    else
303
      for i, child in ipairs(tree) do
207✔
304
        ret[i] = replaceProcessBy(replacement, child)
240✔
305
      end
306
      return ret
87✔
307
    end
308
  end
309

310
  self:registerCommand("define", function (options, content)
346✔
311
    SU.required(options, "command", "defining command")
14✔
312
    if type(content) == "function" then
14✔
313
      -- A macro defined as a function can take no argument, so we register
314
      -- it as-is.
315
      self:registerCommand(options["command"], content)
×
316
      return
×
317
    elseif options.command == "process" then
14✔
318
      SU.warn("Did you mean to re-definine the `\\process` macro? That probably won't go well.")
×
319
    end
320
    self:registerCommand(options["command"], function (_, inner_content)
28✔
321
      SU.debug("macros", "Processing macro \\" .. options["command"])
37✔
322
      local macroArg
323
      if type(inner_content) == "function" then
37✔
324
        macroArg = inner_content
2✔
325
      elseif type(inner_content) == "table" then
35✔
326
        macroArg = pl.tablex.copy(inner_content)
66✔
327
        macroArg.command = nil
33✔
328
        macroArg.id = nil
33✔
329
      elseif inner_content == nil then
2✔
330
        macroArg = {}
2✔
331
      else
332
        SU.error("Unhandled content type " .. type(inner_content) .. " passed to macro \\" .. options["command"], true)
×
333
      end
334
      -- Replace every occurrence of \process in `content` (the macro
335
      -- body) with `macroArg`, then have SILE go through the new `content`.
336
      local newContent = replaceProcessBy(macroArg, content)
37✔
337
      SILE.process(newContent)
37✔
338
      SU.debug("macros", "Finished processing \\" .. options["command"])
37✔
339
    end, options.help, SILE.currentlyProcessingFile)
51✔
340
  end, "Define a new macro. \\define[command=example]{ ... \\process }")
187✔
341

342
  -- A utility function that allows SILE.call() to be used as a noop wrapper.
343
  self:registerCommand("noop", function (_, content)
346✔
344
    SILE.process(content)
1✔
345
  end)
346

347
  -- The document (SIL) or sile (XML) command is always the sigular leaf at the
348
  -- top level of our AST. The work you might expect to see happen here is
349
  -- actually handled by SILE.inputter:classInit() before we get here, so these
350
  -- are just pass through functions. Theoretically, this could be a useful
351
  -- point to hook into-especially for included documents.
352
  self:registerCommand("document", function (_, content)
346✔
353
    SILE.process(content)
×
354
  end)
355
  self:registerCommand("sile", function (_, content)
346✔
356
    SILE.process(content)
×
357
  end)
358

359
  self:registerCommand("comment", function (_, _)
346✔
360
  end, "Ignores any text within this command's body.")
173✔
361

362
  self:registerCommand("process", function ()
346✔
363
    SU.error("Encountered unsubstituted \\process.")
×
364
  end, "Within a macro definition, processes the contents of the macro body.")
173✔
365

366
  self:registerCommand("script", function (options, content)
346✔
367
    local packopts = packOptions(options)
37✔
368
    if SU.ast.hasContent(content) then
74✔
369
      return SILE.processString(content[1], options.format or "lua", nil, packopts)
32✔
370
    elseif options.src then
5✔
371
      return SILE.require(options.src)
5✔
372
    else
373
      SU.error("\\script function requires inline content or a src file path")
×
374
      return SILE.processString(content[1], options.format or "lua", nil, packopts)
×
375
    end
376
  end, "Runs lua code. The code may be supplied either inline or using src=...")
173✔
377

378
  self:registerCommand("include", function (options, content)
346✔
379
    local packopts = packOptions(options)
1✔
380
    if SU.ast.hasContent(content) then
2✔
381
      local doc = SU.ast.contentToString(content)
×
382
      return SILE.processString(doc, options.format, nil, packopts)
×
383
    elseif options.src then
1✔
384
      return SILE.processFile(options.src, options.format, packopts)
1✔
385
    else
386
      SU.error("\\include function requires inline content or a src file path")
×
387
    end
388
  end, "Includes a content file for processing.")
173✔
389

390
  self:registerCommand("lua", function (options, content)
346✔
391
    local packopts = packOptions(options)
3✔
392
    if SU.ast.hasContent(content) then
6✔
393
      local doc = SU.ast.contentToString(content)
3✔
394
      return SILE.processString(doc, "lua", nil, packopts)
3✔
395
    elseif options.src then
×
396
      return SILE.processFile(options.src, "lua", packopts)
×
397
    elseif options.require then
×
398
      local module = SU.required(options, "require", "lua")
×
399
      return require(module)
×
400
    else
401
      SU.error("\\lua function requires inline content or a src file path or a require module name")
×
402
    end
403
  end, "Run Lua code. The code may be supplied either inline, using require=... for a Lua module, or using src=... for a file path")
173✔
404

405
  self:registerCommand("sil", function (options, content)
346✔
406
    local packopts = packOptions(options)
×
407
    if SU.ast.hasContent(content) then
×
408
      local doc = SU.ast.contentToString(content)
×
409
      return SILE.processString(doc, "sil")
×
410
    elseif options.src then
×
411
      return SILE.processFile(options.src, "sil", packopts)
×
412
    else
413
      SU.error("\\sil function requires inline content or a src file path")
×
414
    end
415
  end, "Process sil content. The content may be supplied either inline or using src=...")
173✔
416

417
  self:registerCommand("xml", function (options, content)
346✔
418
    local packopts = packOptions(options)
×
419
    if SU.ast.hasContent(content) then
×
420
      local doc = SU.ast.contentToString(content)
×
421
      return SILE.processString(doc, "xml", nil, packopts)
×
422
    elseif options.src then
×
423
      return SILE.processFile(options.src, "xml", packopts)
×
424
    else
425
      SU.error("\\xml function requires inline content or a src file path")
×
426
    end
427
  end, "Process xml content. The content may be supplied either inline or using src=...")
173✔
428

429
  self:registerCommand("use", function (options, content)
346✔
430
    local packopts = packOptions(options)
221✔
431
    if content[1] and string.len(content[1]) > 0 then
221✔
432
      local doc = SU.ast.contentToString(content)
×
433
      SILE.processString(doc, "lua", nil, packopts)
×
434
    else
435
      if options.src then
221✔
436
        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.")
×
437
        SILE.processFile(options.src, "lua", packopts)
×
438
      else
439
        local module = SU.required(options, "module", "use")
221✔
440
        SILE.use(module, packopts)
221✔
441
      end
442
    end
443
  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.")
394✔
444

445
  self:registerCommand("raw", function (options, content)
346✔
446
    local rawtype = SU.required(options, "type", "raw")
4✔
447
    local handler = SILE.rawHandlers[rawtype]
4✔
448
    if not handler then SU.error("No inline handler for '"..rawtype.."'") end
4✔
449
    handler(options, content)
4✔
450
  end, "Invoke a raw passthrough handler")
177✔
451

452
  self:registerCommand("pagetemplate", function (options, content)
346✔
453
    SILE.typesetter:pushState()
8✔
454
    SILE.documentState.thisPageTemplate = { frames = {} }
8✔
455
    SILE.process(content)
8✔
456
    SILE.documentState.thisPageTemplate.firstContentFrame = SILE.getFrame(options["first-content-frame"])
16✔
457
    SILE.typesetter:initFrame(SILE.documentState.thisPageTemplate.firstContentFrame)
8✔
458
    SILE.typesetter:popState()
8✔
459
  end, "Defines a new page template for the current page and sets the typesetter to use it.")
181✔
460

461
  self:registerCommand("frame", function (options, _)
346✔
462
    SILE.documentState.thisPageTemplate.frames[options.id] = SILE.newFrame(options)
62✔
463
  end, "Declares (or re-declares) a frame on this page.")
204✔
464

465
  self:registerCommand("penalty", function (options, _)
346✔
466
    if SU.boolean(options.vertical, false) and not SILE.typesetter:vmode() then
577✔
467
      SILE.typesetter:leaveHmode()
4✔
468
    end
469
    if SILE.typesetter:vmode() then
552✔
470
      SILE.typesetter:pushVpenalty({ penalty = tonumber(options.penalty) })
442✔
471
    else
472
      SILE.typesetter:pushPenalty({ penalty = tonumber(options.penalty) })
55✔
473
    end
474
  end, "Inserts a penalty node. Option is penalty= for the size of the penalty.")
449✔
475

476
  self:registerCommand("discretionary", function (options, _)
346✔
477
    local discretionary = SILE.nodefactory.discretionary({})
74✔
478
    if options.prebreak then
74✔
479
      local hbox = SILE.typesetter:makeHbox({ options.prebreak })
74✔
480
      discretionary.prebreak = { hbox }
74✔
481
    end
482
    if options.postbreak then
74✔
483
      local hbox = SILE.typesetter:makeHbox({ options.postbreak })
48✔
484
      discretionary.postbreak = { hbox }
48✔
485
    end
486
    if options.replacement then
74✔
487
      local hbox = SILE.typesetter:makeHbox({ options.replacement })
50✔
488
      discretionary.replacement = { hbox }
50✔
489
    end
490
    table.insert(SILE.typesetter.state.nodes, discretionary)
74✔
491
  end, "Inserts a discretionary node.")
247✔
492

493
  self:registerCommand("glue", function (options, _)
346✔
494
    local width = SU.cast("length", options.width):absolute()
106✔
495
    SILE.typesetter:pushGlue(width)
53✔
496
  end, "Inserts a glue node. The width option denotes the glue dimension.")
226✔
497

498
  self:registerCommand("kern", function (options, _)
346✔
499
    local width = SU.cast("length", options.width):absolute()
150✔
500
    SILE.typesetter:pushHorizontal(SILE.nodefactory.kern(width))
150✔
501
  end, "Inserts a glue node. The width option denotes the glue dimension.")
248✔
502

503
  self:registerCommand("skip", function (options, _)
346✔
504
    options.discardable = SU.boolean(options.discardable, false)
52✔
505
    options.height = SILE.length(options.height):absolute()
78✔
506
    SILE.typesetter:leaveHmode()
26✔
507
    if options.discardable then
26✔
508
      SILE.typesetter:pushVglue(options)
×
509
    else
510
      SILE.typesetter:pushExplicitVglue(options)
26✔
511
    end
512
  end, "Inserts vertical skip. The height options denotes the skip dimension.")
199✔
513

514
  self:registerCommand("par", function (_, _)
346✔
515
    SILE.typesetter:endline()
291✔
516
  end, "Ends the current paragraph.")
464✔
517

518
end
519

520
function class:initialFrame ()
345✔
521
  SILE.documentState.thisPageTemplate = pl.tablex.deepcopy(self.pageTemplate)
456✔
522
  SILE.frames = { page = SILE.frames.page }
228✔
523
  for k, v in pairs(SILE.documentState.thisPageTemplate.frames) do
989✔
524
    SILE.frames[k] = v
761✔
525
  end
526
  if not SILE.documentState.thisPageTemplate.firstContentFrame then
228✔
527
    SILE.documentState.thisPageTemplate.firstContentFrame = SILE.frames[self.firstContentFrame]
×
528
  end
529
  SILE.documentState.thisPageTemplate.firstContentFrame:invalidate()
228✔
530
  return SILE.documentState.thisPageTemplate.firstContentFrame
228✔
531
end
532

533
function class:declareFrame (id, spec)
345✔
534
  spec.id = id
567✔
535
  if spec.solve then
567✔
536
    self.pageTemplate.frames[id] = spec
×
537
  else
538
    self.pageTemplate.frames[id] = SILE.newFrame(spec)
1,134✔
539
  end
540
  --   next = spec.next,
541
  --   left = spec.left and fW(spec.left),
542
  --   right = spec.right and fW(spec.right),
543
  --   top = spec.top and fH(spec.top),
544
  --   bottom = spec.bottom and fH(spec.bottom),
545
  --   height = spec.height and fH(spec.height),
546
  --   width = spec.width and fH(spec.width),
547
  --   id = id
548
  -- })
549
end
550

551
function class:declareFrames (specs)
345✔
552
  if specs then
173✔
553
    for k, v in pairs(specs) do self:declareFrame(k, v) end
1,275✔
554
  end
555
end
556

557
-- WARNING: not called as class method
558
function class.newPar (typesetter)
345✔
559
  local parindent = SILE.settings:get("current.parindent") or SILE.settings:get("document.parindent")
1,664✔
560
  -- See https://github.com/sile-typesetter/sile/issues/1361
561
  -- The parindent *cannot* be pushed non-absolutized, as it may be evaluated
562
  -- outside the (possibly temporary) setting scope where it was used for line
563
  -- breaking.
564
  -- Early absolutization can be problematic sometimes, but here we do not
565
  -- really have the choice.
566
  -- As of problematic cases, consider a parindent that would be defined in a
567
  -- frame-related unit (%lw, %fw, etc.). If a frame break occurs and the next
568
  -- frame has a different width, the parindent won't be re-evaluated in that
569
  -- new frame context. However, defining a parindent in such a unit is quite
570
  -- unlikely. And anyway pushback() has plenty of other issues.
571
  typesetter:pushGlue(parindent:absolute())
1,664✔
572
  SILE.settings:set("current.parindent", nil)
832✔
573
  local hangIndent = SILE.settings:get("current.hangIndent")
832✔
574
  if hangIndent then
832✔
575
    SILE.settings:set("linebreak.hangIndent", hangIndent)
5✔
576
  end
577
  local hangAfter = SILE.settings:get("current.hangAfter")
832✔
578
  if hangAfter then
832✔
579
    SILE.settings:set("linebreak.hangAfter", hangAfter)
5✔
580
  end
581
end
582

583
-- WARNING: not called as class method
584
function class.endPar (typesetter)
345✔
585
  typesetter:pushVglue(SILE.settings:get("document.parskip"))
1,636✔
586
  if SILE.settings:get("current.hangIndent") then
1,636✔
587
    SILE.settings:set("current.hangIndent", nil)
4✔
588
    SILE.settings:set("linebreak.hangIndent", nil)
4✔
589
  end
590
  if SILE.settings:get("current.hangAfter") then
1,636✔
591
    SILE.settings:set("current.hangAfter", nil)
4✔
592
    SILE.settings:set("linebreak.hangAfter", nil)
4✔
593
  end
594
end
595

596
function class:newPage ()
345✔
597
  SILE.outputter:newPage()
55✔
598
  self:runHooks("newpage")
55✔
599
  -- Any other output-routiney things will be done here by inheritors
600
  return self:initialFrame()
55✔
601
end
602

603
function class:endPage ()
345✔
604
  SILE.typesetter.frame:leave(SILE.typesetter)
228✔
605
  self:runHooks("endpage")
228✔
606
  -- I'm trying to call up a new frame here, don't cause a page break in the current one
607
  -- SILE.typesetter:leaveHmode()
608
  -- Any other output-routiney things will be done here by inheritors
609
end
610

611
function class:finish ()
345✔
612
  SILE.inputter:postamble()
173✔
613
  SILE.call("vfill")
173✔
614
  while not SILE.typesetter:isQueueEmpty() do
692✔
615
    SILE.call("supereject")
173✔
616
    SILE.typesetter:leaveHmode(true)
173✔
617
    SILE.typesetter:buildPage()
173✔
618
    if not SILE.typesetter:isQueueEmpty() then
346✔
619
      SILE.typesetter:initNextFrame()
5✔
620
    end
621
  end
622
  SILE.typesetter:runHooks("pageend") -- normally run by the typesetter
173✔
623
  self:endPage()
173✔
624
  if SILE.typesetter and not SILE.typesetter:isQueueEmpty() then
346✔
625
    SU.error("Queues are not empty as expected after ending last page", true)
×
626
  end
627
  SILE.outputter:finish()
173✔
628
  self:runHooks("finish")
173✔
629
end
630

631
return class
345✔
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