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

sile-typesetter / sile / 10616097075

29 Aug 2024 01:29PM UTC coverage: 59.298% (-5.5%) from 64.818%
10616097075

push

github

alerque
ci(actions): Switch to Lua actions forks that fix current LuaJIT issues

10338 of 17434 relevant lines covered (59.3%)

3374.78 hits per line

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

72.0
/core/sile.lua
1
--- The core SILE library.
2
-- Depending on how SILE was loaded, everything in here will probably be available under a top level `SILE` global. Note
3
-- that an additional global `SU` is typically available as an alias to `SILE.utilities`. Also some 3rd party Lua
4
-- libraries are always made available in the global scope, see `globals`.
5
-- @module SILE
6

7
-- Placeholder for 3rd party Lua libraries SILE always provides as globals
8
require("core.globals")
1✔
9

10
-- Reserve scope placeholder for profiler (developer tooling)
11
local ProFi
12

13
-- Placeholder for SILE internals table
14
SILE = {}
198✔
15

16
--- Fields
17
-- @section fields
18

19
--- Machine friendly short-form version.
20
-- Semver, prefixed with "v", possible postfixed with ".r" followed by VCS version information.
21
-- @string version
22
SILE.version = require("core.version")
198✔
23

24
--- Status information about what options SILE was compiled with.
25
-- @table SILE.features
26
-- @tfield boolean appkit
27
-- @tfield boolean font_variations
28
-- @tfield boolean fontconfig
29
-- @tfield boolean harfbuzz
30
-- @tfield boolean icu
31
SILE.features = require("core.features")
198✔
32

33
-- Initialize Lua environment and global utilities
34

35
--- ABI version of Lua VM.
36
-- For example may be `"5.1"` or `"5.4"` or others. Note that the ABI version for most LuaJIT implementations is 5.1.
37
-- @string lua_version
38
SILE.lua_version = _VERSION:sub(-3)
396✔
39

40
--- Whether or not Lua VM is a JIT compiler.
41
-- @boolean lua_isjit
42
-- luacheck: ignore jit
43
SILE.lua_isjit = type(jit) == "table"
198✔
44

45
--- User friendly long-form version string.
46
-- For example may be "SILE v0.14.17 (Lua 5.4)".
47
-- @string full_version
48
SILE.full_version = string.format("SILE %s (%s)", SILE.version, SILE.lua_isjit and jit.version or _VERSION)
198✔
49

50
--- Default to verbose mode, can be changed from the CLI or by libraries
51
--- @boolean quiet
52
SILE.quiet = false
198✔
53

54
-- Backport of lots of Lua 5.3 features to Lua 5.[12]
55
if not SILE.lua_isjit and SILE.lua_version < "5.3" then
198✔
56
   require("compat53")
×
57
end
58

59
--- Modules
60
-- @section modules
61

62
--- Utilities module, typically accessed via `SU` alias.
63
-- @see SU
64
SILE.utilities = require("core.utilities")
198✔
65
SU = SILE.utilities -- regrettable global alias
198✔
66

67
-- For warnings and shims scheduled for removal that are easier to keep track
68
-- of when they are not spread across so many locations...
69
-- Loaded early to make it easier to manage migrations in core code.
70
require("core/deprecations")
198✔
71

72
-- On demand loader, allows modules to be loaded into a specific scope but
73
-- only when/if accessed.
74
local function core_loader (scope)
75
   return setmetatable({}, {
1,584✔
76
      __index = function (self, key)
77
         -- local var = rawget(self, key)
78
         local m = require(("%s.%s"):format(scope, key))
1,705✔
79
         self[key] = m
1,705✔
80
         return m
1,705✔
81
      end,
82
   })
1,584✔
83
end
84

85
--- Data tables
86
--- @section data
87

88
--- Stash of all Lua functions used to power typesetter commands.
89
-- @table Commands
90
SILE.Commands = {}
198✔
91

92
--- Short usage messages corresponding to typesetter commands.
93
-- @table Help
94
SILE.Help = {}
198✔
95

96
--- List of currently enabled debug flags.
97
-- E.g. `{ typesetter = true, frames, true }`.
98
-- @table debugFlags
99
SILE.debugFlags = {}
198✔
100

101
SILE.nodeMakers = {}
198✔
102
SILE.tokenizers = {}
198✔
103
SILE.status = {}
198✔
104

105
--- The wild-west of stash stuff.
106
-- No rules, just right (or usually wrong). Everything in here *should* be somewhere else, but lots of early SILE code
107
-- relied on this as a global key/value store for various class, document, and package values. Since v0.14.0 with many
108
-- core SILE components being instances of classes –and especially with each package having it's own variable namespace–
109
-- there are almost always better places for things. This scratch space will eventually be completely deprecated, so
110
-- don't put anything new in here and only use things in it if there are no other current alternatives.
111
-- @table scratch
112
SILE.scratch = {}
198✔
113

114
--- Data storage for typesetter, frame, and class information.
115
-- Current document class instances, node queues, and other "hot" data can be found here. As with `SILE.scratch`
116
-- everything in here probably belongs elsewhere, but for now it is what it is.
117
-- @table documentState
118
-- @tfield table documentClass The instantiated document processing class.
119
-- @tfield table thisPageTemplate The frameset used for the current page.
120
-- @tfield table paperSize The current paper size.
121
-- @tfield table orgPaperSize The original paper size if the current one is modified via packages.
122
SILE.documentState = {}
198✔
123

124
--- Callback functions for handling types of raw content.
125
-- All registered handlers for raw content blocks have an entry in this table with the type as the key and the
126
-- processing function as the value.
127
-- @ table rawHandlers
128
SILE.rawHandlers = {}
198✔
129

130
--- User input
131
-- @section input
132

133
--- All user-provided input collected before beginning document processing.
134
-- User input values, currently from CLI options, potentially all the inuts
135
-- needed for a user to use a SILE-as-a-library version to produce documents
136
-- programmatically.
137
-- @table input
138
-- @tfield table filenames Path names of file(s) intended for processing. Files are processed in the order provided.
139
-- File types may be mixed of any formaat for which SILE has an inputter module.
140
-- @tfield table evaluates List of strings to be evaluated as Lua code snippets *before* processing inputs.
141
-- @tfield table evaluteAfters List of strings to be evaluated as Lua code snippets *after* processing inputs.
142
-- @tfield table uses List of strings specifying module names (and optionally optionns) for modules to load *before*
143
-- processing inputs. For example this accomodates loading inputter modules before any input of that type is encountered.
144
-- Additionally it can be used to process a document using a document class *other than* the one specified in the
145
-- document itself. Document class modules loaded here are instantiated after load, meaning the document will not be
146
-- queried for a class at all.
147
-- @tfield table options Extra document class options to set or override in addition to ones found in the first input
148
-- document.
149
SILE.input = {
198✔
150
   filenames = {},
198✔
151
   evaluates = {},
198✔
152
   evaluateAfters = {},
198✔
153
   uses = {},
198✔
154
   options = {},
198✔
155
   preambles = {}, -- deprecated, undocumented
198✔
156
   postambles = {}, -- deprecated, undocumented
198✔
157
}
198✔
158

159
-- Internal libraries that are idempotent and return classes that need instantiation
160
SILE.inputters = core_loader("inputters")
396✔
161
SILE.shapers = core_loader("shapers")
396✔
162
SILE.outputters = core_loader("outputters")
396✔
163
SILE.classes = core_loader("classes")
396✔
164
SILE.packages = core_loader("packages")
396✔
165
SILE.typesetters = core_loader("typesetters")
396✔
166
SILE.pagebuilders = core_loader("pagebuilders")
396✔
167
SILE.types = core_loader("types")
396✔
168

169
-- Internal libraries that don't try to use anything on load, only provide something
170
SILE.parserBits = require("core.parserbits")
198✔
171
SILE.frameParser = require("core.frameparser")
198✔
172
SILE.fontManager = require("core.fontmanager")
197✔
173
SILE.papersize = require("core.papersize")
197✔
174

175
-- NOTE:
176
-- See remainaing internal libraries loaded at the end of this file because
177
-- they run core SILE functions on load instead of waiting to be called (or
178
-- depend on others that do).
179

180
local function runEvals (evals, arg)
181
   for _, snippet in ipairs(evals) do
202✔
182
      local pId = SILE.traceStack:pushText(snippet)
×
183
      local status, func = pcall(load, snippet)
×
184
      if status then
×
185
         func()
×
186
      else
187
         SU.error(("Error parsing code provided in --%s snippet: %s"):format(arg, func))
×
188
      end
189
      SILE.traceStack:pop(pId)
×
190
   end
191
end
192

193
--- Core functions
194
-- @section functions
195

196
--- Initialize a SILE instance.
197
-- Presumes CLI args have already been processed and/or library inputs are set.
198
--
199
-- 1. If no backend has been loaded already (e.g. via `--use`) then assumes *libtexpdf*.
200
-- 2. Loads and instantiates a shaper and outputter module appropriate for the chosen backend.
201
-- 3. Instantiates a pagebuilder.
202
-- 4. Starts a Lua profiler if the profile debug flag is set.
203
-- 5. Instantiates a dependency tracker if we've been asked to write make dependencies.
204
-- 6. Runs any code snippents passed with `--eval`.
205
--
206
-- Does not move on to processing input document(s).
207
function SILE.init ()
394✔
208
   if not SILE.backend then
101✔
209
      SILE.backend = "libtexpdf"
101✔
210
   end
211
   if SILE.backend == "libtexpdf" then
101✔
212
      SILE.shaper = SILE.shapers.harfbuzz()
303✔
213
      SILE.outputter = SILE.outputters.libtexpdf()
303✔
214
   elseif SILE.backend == "cairo" then
×
215
      SILE.shaper = SILE.shapers.pango()
×
216
      SILE.outputter = SILE.outputters.cairo()
×
217
   elseif SILE.backend == "debug" then
×
218
      SILE.shaper = SILE.shapers.harfbuzz()
×
219
      SILE.outputter = SILE.outputters.debug()
×
220
   elseif SILE.backend == "text" then
×
221
      SILE.shaper = SILE.shapers.harfbuzz()
×
222
      SILE.outputter = SILE.outputters.text()
×
223
   elseif SILE.backend == "dummy" then
×
224
      SILE.shaper = SILE.shapers.harfbuzz()
×
225
      SILE.outputter = SILE.outputters.dummy()
×
226
   end
227
   SILE.pagebuilder = SILE.pagebuilders.base()
303✔
228
   io.stdout:setvbuf("no")
101✔
229
   if SU.debugging("profile") then
202✔
230
      ProFi = require("ProFi")
×
231
      ProFi:start()
×
232
   end
233
   if SILE.makeDeps then
101✔
234
      SILE.makeDeps:add(_G.executablePath)
×
235
   end
236
   runEvals(SILE.input.evaluates, "evaluate")
101✔
237
end
238

239
local function suggest_luarocks (module)
240
   local guessed_module_name = module:gsub(".*%.", "") .. ".sile"
×
241
   return ([[
×
242

243
    If the expected module is a 3rd party extension you may need to install it
244
    using LuaRocks. The details of how to do this are highly dependent on
245
    your system and preferred installation method, but as an example installing
246
    a 3rd party SILE module to a project-local tree where might look like this:
247

248
        luarocks --lua-version %s --tree lua_modules install %s
249

250
    This will install the LuaRocks to your project, then you need to tell your
251
    shell to pass along that info about available LuaRocks paths to SILE. This
252
    only needs to be done once in each shell.
253

254
        eval $(luarocks --lua-version %s --tree lua_modules path)
255

256
    Thereafter running SILE again should work as expected:
257

258
       sile %s
259

260
    ]]):format(SILE.lua_version, guessed_module_name, SILE.lua_version, pl.stringx.join(" ", _G.arg or {}))
×
261
end
262

263
--- Multi-purpose loader to load and initialize modules.
264
-- This is used to load and intitialize core parts of SILE and also 3rd party modules.
265
-- Module types supported bay be an *inputter*, *outputer*, *shaper*, *typesetter*, *pagebuilder*, or *package*.
266
-- @tparam string|table module The module spec name to load (dot-separated, e.g. `"packages.lorem"`) or a table with
267
--   a module that has already been loaded.
268
-- @tparam[opt] table options Startup options as key/value pairs passed to the module when initialized.
269
-- @tparam[opt=false] boolean reload whether or not to reload a module that has been loaded and initialized before.
270
function SILE.use (module, options, reload)
394✔
271
   local status, pack
272
   if type(module) == "string" then
135✔
273
      status, pack = pcall(require, module)
134✔
274
      if not status then
134✔
275
         SU.error(
×
276
            ("Unable to use '%s':\n%s%s"):format(
×
277
               module,
278
               SILE.traceback and ("    Lua " .. pack) or "",
×
279
               suggest_luarocks(module)
×
280
            )
281
         )
282
      end
283
   elseif type(module) == "table" then
1✔
284
      pack = module
1✔
285
   end
286
   local name = pack._name
135✔
287
   local class = SILE.documentState.documentClass
135✔
288
   if not pack.type then
135✔
289
      SU.error("Modules must declare their type")
×
290
   elseif pack.type == "class" then
135✔
291
      SILE.classes[name] = pack
×
292
      if class then
×
293
         SU.error("Cannot load a class after one is already instantiated")
×
294
      end
295
      SILE.scratch.class_from_uses = pack
×
296
   elseif pack.type == "inputter" then
135✔
297
      SILE.inputters[name] = pack
×
298
      SILE.inputter = pack(options)
×
299
   elseif pack.type == "outputter" then
135✔
300
      SILE.outputters[name] = pack
×
301
      SILE.outputter = pack(options)
×
302
   elseif pack.type == "shaper" then
135✔
303
      SILE.shapers[name] = pack
×
304
      SILE.shaper = pack(options)
×
305
   elseif pack.type == "typesetter" then
135✔
306
      SILE.typesetters[name] = pack
×
307
      SILE.typesetter = pack(options)
×
308
   elseif pack.type == "pagebuilder" then
135✔
309
      SILE.pagebuilders[name] = pack
×
310
      SILE.pagebuilder = pack(options)
×
311
   elseif pack.type == "package" then
135✔
312
      SILE.packages[pack._name] = pack
135✔
313
      if class then
135✔
314
         class:loadPackage(pack, options, reload)
270✔
315
      else
316
         table.insert(SILE.input.preambles, { pack = pack, options = options })
×
317
      end
318
   end
319
end
320

321
-- --- Content loader like Lua's `require()` but whith special path handling for loading SILE resource files.
322
-- -- Used for example by commands that load data via a `src=file.name` option.
323
-- -- @tparam string dependency Lua spec
324
function SILE.require (dependency, pathprefix, deprecation_ack)
394✔
325
   if pathprefix and not deprecation_ack then
101✔
326
      local notice = string.format(
×
327
         [[
×
328
  Please don't use the path prefix mechanism; it was intended to provide
329
  alternate paths to override core components but never worked well and is
330
  causing portability problems. Just use Lua idiomatic module loading:
331
      SILE.require("%s", "%s") → SILE.require("%s.%s")]],
332
         dependency,
333
         pathprefix,
334
         pathprefix,
335
         dependency
336
      )
337
      SU.deprecated("SILE.require", "SILE.require", "0.13.0", nil, notice)
×
338
   end
339
   dependency = dependency:gsub(".lua$", "")
101✔
340
   local status, lib
341
   if pathprefix then
101✔
342
      -- Note this is not a *path*, it is a module identifier:
343
      -- https://github.com/sile-typesetter/sile/issues/1861
344
      status, lib = pcall(require, pl.stringx.join(".", { pathprefix, dependency }))
202✔
345
   end
346
   if not status then
101✔
347
      local prefixederror = lib
×
348
      status, lib = pcall(require, dependency)
×
349
      if not status then
×
350
         SU.error(
×
351
            ("Unable to find module '%s'%s"):format(
×
352
               dependency,
353
               SILE.traceback and ((pathprefix and "\n  " .. prefixederror or "") .. "\n  " .. lib) or ""
×
354
            )
355
         )
356
      end
357
   end
358
   local class = SILE.documentState.documentClass
101✔
359
   if not class and not deprecation_ack then
101✔
360
      SU.warn(string.format(
×
361
         [[
×
362
  Use of SILE.require() is only supported in documents, packages, or class
363
  init functions. It will not function fully before the class is instantiated.
364
  Please just use the Lua require() function directly:
365
      SILE.require("%s") → require("%s")]],
366
         dependency,
367
         dependency
368
      ))
369
   end
370
   if type(lib) == "table" and class then
101✔
371
      if lib.type == "package" then
×
372
         lib(class)
×
373
      else
374
         class:initPackage(lib)
×
375
      end
376
   end
377
   return lib
101✔
378
end
379

380
--- Process content.
381
-- This is the main 'action' SILE does. Once input files are parsed into an abstract syntax tree, then we recursively
382
-- iterate through the tree handling each item in the order encountered.
383
-- @tparam table ast SILE content in abstract syntax tree format (a table of strings, functions, or more AST trees).
384
function SILE.process (ast)
394✔
385
   if not ast then
905✔
386
      return
4✔
387
   end
388
   if SU.debugging("ast") then
1,802✔
389
      SU.debugAST(ast, 0)
×
390
   end
391
   if type(ast) == "function" then
901✔
392
      return ast()
174✔
393
   end
394
   for _, content in ipairs(ast) do
3,311✔
395
      if type(content) == "string" then
2,584✔
396
         SILE.typesetter:typeset(content)
2,768✔
397
      elseif type(content) == "function" then
1,200✔
398
         content()
2✔
399
      elseif SILE.Commands[content.command] then
1,199✔
400
         SILE.call(content.command, content.options, content)
2,134✔
401
      elseif content.id == "content" or (not content.command and not content.id) then
132✔
402
         local pId = SILE.traceStack:pushContent(content, "content")
132✔
403
         SILE.process(content)
132✔
404
         SILE.traceStack:pop(pId)
264✔
405
      elseif type(content) ~= "nil" then
×
406
         local pId = SILE.traceStack:pushContent(content)
×
407
         SU.error("Unknown command " .. (tostring(content.command or content.id)))
×
408
         SILE.traceStack:pop(pId)
×
409
      end
410
   end
411
end
412

413
local preloadedinputters = { "xml", "lua", "sil" }
197✔
414

415
local function detectFormat (doc, filename)
416
   -- Preload default reader types so content detection has something to work with
417
   if #SILE.inputters == 0 then
101✔
418
      for _, format in ipairs(preloadedinputters) do
404✔
419
         local _ = SILE.inputters[format]
303✔
420
      end
421
   end
422
   local contentDetectionOrder = {}
101✔
423
   for _, inputter in pairs(SILE.inputters) do
404✔
424
      if inputter.order then
303✔
425
         table.insert(contentDetectionOrder, inputter)
303✔
426
      end
427
   end
428
   table.sort(contentDetectionOrder, function (a, b)
202✔
429
      return a.order < b.order
279✔
430
   end)
431
   local initialround = filename and 1 or 2
101✔
432
   for round = initialround, 3 do
104✔
433
      for _, inputter in ipairs(contentDetectionOrder) do
207✔
434
         SU.debug("inputter", "Running content type detection round", round, "with", inputter._name)
204✔
435
         if inputter.appropriate(round, filename, doc) then
408✔
436
            return inputter._name
101✔
437
         end
438
      end
439
   end
440
   SU.error(("Unable to pick inputter to process input from '%s'"):format(filename))
×
441
end
442

443
--- Process an input string.
444
-- First converts the string to an AST, then runs `process` on it.
445
-- @tparam string doc Input string to be coverted to SILE content.
446
-- @tparam[opt] nil|string format The name of the formatter. If nil, defaults to using each intputter's auto detection.
447
-- @tparam[opt] nil|string filename Pseudo filename to identify the content with, useful for error messages stack traces.
448
-- @tparam[opt] nil|table options Options to pass to the inputter instance when instantiated.
449
function SILE.processString (doc, format, filename, options)
394✔
450
   local cpf
451
   if not filename then
134✔
452
      cpf = SILE.currentlyProcessingFile
33✔
453
      local caller = debug.getinfo(2, "Sl")
33✔
454
      SILE.currentlyProcessingFile = caller.short_src .. ":" .. caller.currentline
33✔
455
   end
456
   -- In the event we're processing the master file *and* the user gave us
457
   -- a specific inputter to use, use it at the exclusion of all content type
458
   -- detection
459
   local inputter
460
   if
461
      filename
462
      and pl.path.normcase(pl.path.normpath(filename)) == pl.path.normcase(SILE.input.filenames[1])
437✔
463
      and SILE.inputter
101✔
464
   then
465
      inputter = SILE.inputter
×
466
   else
467
      format = format or detectFormat(doc, filename)
235✔
468
      if not SILE.quiet then
134✔
469
         io.stderr:write(("<%s> as %s\n"):format(SILE.currentlyProcessingFile, format))
134✔
470
      end
471
      inputter = SILE.inputters[format](options)
268✔
472
      -- If we did content detection *and* this is the master file, save the
473
      -- inputter for posterity and postambles
474
      if filename and pl.path.normcase(filename) == pl.path.normcase(SILE.input.filenames[1]:gsub("^-$", "STDIN")) then
336✔
475
         SILE.inputter = inputter
101✔
476
      end
477
   end
478
   local pId = SILE.traceStack:pushDocument(SILE.currentlyProcessingFile, doc)
134✔
479
   inputter:process(doc)
134✔
480
   SILE.traceStack:pop(pId)
134✔
481
   if cpf then
134✔
482
      SILE.currentlyProcessingFile = cpf
33✔
483
   end
484
end
485

486
--- Process an input file
487
-- Opens a file, converts the contents to an AST, then runs `process` on it.
488
-- Roughly equivalent to listing the file as an input, but easier to embed in code.
489
-- @tparam string filename Path of file to open string to be coverted to SILE content.
490
-- @tparam[opt] nil|string format The name of the formatter. If nil, defaults to using each intputter's auto detection.
491
-- @tparam[opt] nil|table options Options to pass to the inputter instance when instantiated.
492
function SILE.processFile (filename, format, options)
394✔
493
   local lfs = require("lfs")
101✔
494
   local doc
495
   if filename == "-" then
101✔
496
      filename = "STDIN"
×
497
      doc = io.stdin:read("*a")
×
498
   else
499
      -- Turn slashes around in the event we get passed a path from a Windows shell
500
      filename = filename:gsub("\\", "/")
101✔
501
      if not SILE.masterFilename then
101✔
502
         SILE.masterFilename = pl.path.splitext(pl.path.normpath(filename))
404✔
503
      end
504
      if SILE.input.filenames[1] and not SILE.masterDir then
101✔
505
         SILE.masterDir = pl.path.dirname(SILE.input.filenames[1])
202✔
506
      end
507
      if SILE.masterDir and SILE.masterDir:len() >= 1 then
202✔
508
         _G.extendSilePath(SILE.masterDir)
101✔
509
         _G.extendSilePathRocks(SILE.masterDir .. "/lua_modules")
101✔
510
      end
511
      filename = SILE.resolveFile(filename) or SU.error("Could not find file")
202✔
512
      local mode = lfs.attributes(filename).mode
101✔
513
      if mode ~= "file" and mode ~= "named pipe" then
101✔
514
         SU.error(filename .. " isn't a file or named pipe, it's a " .. mode .. "!")
×
515
      end
516
      if SILE.makeDeps then
101✔
517
         SILE.makeDeps:add(filename)
×
518
      end
519
      local file, err = io.open(filename)
101✔
520
      if not file then
101✔
521
         print("Could not open " .. filename .. ": " .. err)
×
522
         return
×
523
      end
524
      doc = file:read("*a")
101✔
525
   end
526
   local cpf = SILE.currentlyProcessingFile
101✔
527
   SILE.currentlyProcessingFile = filename
101✔
528
   local pId = SILE.traceStack:pushDocument(filename, doc)
101✔
529
   local ret = SILE.processString(doc, format, filename, options)
101✔
530
   SILE.traceStack:pop(pId)
101✔
531
   SILE.currentlyProcessingFile = cpf
101✔
532
   return ret
101✔
533
end
534

535
-- TODO: this probably needs deprecating, moved here just to get out of the way so
536
-- typesetters classing works as expected
537
function SILE.typesetNaturally (frame, func)
394✔
538
   local saveTypesetter = SILE.typesetter
59✔
539
   if SILE.typesetter.frame then
59✔
540
      SILE.typesetter.frame:leave(SILE.typesetter)
59✔
541
   end
542
   SILE.typesetter = SILE.typesetters.base(frame)
118✔
543
   SILE.settings:temporarily(func)
59✔
544
   SILE.typesetter:leaveHmode()
59✔
545
   SILE.typesetter:chuck()
59✔
546
   SILE.typesetter.frame:leave(SILE.typesetter)
59✔
547
   SILE.typesetter = saveTypesetter
59✔
548
   if SILE.typesetter.frame then
59✔
549
      SILE.typesetter.frame:enter(SILE.typesetter)
59✔
550
   end
551
end
552

553
--- Resolve relative file paths to identify absolute resources locations.
554
-- Makes it possible to load resources from relative paths, relative to a document or project or SILE itself.
555
-- @tparam string filename Name of file to find using the same order of precidence logic in `require()`.
556
-- @tparam[opt] nil|string pathprefix Optional prefix in which to look for if the file isn't found otherwise.
557
function SILE.resolveFile (filename, pathprefix)
394✔
558
   local candidates = {}
103✔
559
   -- Start with the raw file name as given prefixed with a path if requested
560
   if pathprefix then
103✔
561
      candidates[#candidates + 1] = pl.path.join(pathprefix, "?")
×
562
   end
563
   -- Also check the raw file name without a path
564
   candidates[#candidates + 1] = "?"
103✔
565
   -- Iterate through the directory of the master file, the SILE_PATH variable, and the current directory
566
   -- Check for prefixed paths first, then the plain path in that fails
567
   if SILE.masterDir then
103✔
568
      for path in SU.gtoke(SILE.masterDir .. ";" .. tostring(os.getenv("SILE_PATH")), ";") do
927✔
569
         if path.string and path.string ~= "nil" then
721✔
570
            if pathprefix then
412✔
571
               candidates[#candidates + 1] = pl.path.join(path.string, pathprefix, "?")
×
572
            end
573
            candidates[#candidates + 1] = pl.path.join(path.string, "?")
824✔
574
         end
575
      end
576
   end
577
   -- Return the first candidate that exists, also checking the .sil suffix
578
   local path = table.concat(candidates, ";")
103✔
579
   local resolved, err = package.searchpath(filename, path, "/")
103✔
580
   if resolved then
103✔
581
      if SILE.makeDeps then
103✔
582
         SILE.makeDeps:add(resolved)
×
583
      end
584
   elseif SU.debugging("paths") then
×
585
      SU.debug("paths", ("Unable to find file '%s': %s"):format(filename, err))
×
586
   end
587
   return resolved
103✔
588
end
589

590
--- Execute a registered SILE command.
591
-- Uses a function previously registered by any modules explicitly loaded by the user at runtime via `--use`, the SILE
592
-- core, the document class, or any loaded package.
593
-- @tparam string command Command name.
594
-- @tparam[opt={}] nil|table options Options to pass to the command.
595
-- @tparam[opt] nil|table content Any valid AST node to be processed by the command.
596
function SILE.call (command, options, content)
394✔
597
   options = options or {}
2,723✔
598
   content = content or {}
2,723✔
599
   if SILE.traceback and type(content) == "table" and not content.lno then
2,723✔
600
      -- This call is from code (no content.lno) and we want to spend the time
601
      -- to determine everything we need about the caller
602
      local caller = debug.getinfo(2, "Sl")
×
603
      content.file, content.lno = caller.short_src, caller.currentline
×
604
   end
605
   local pId = SILE.traceStack:pushCommand(command, content, options)
2,723✔
606
   if not SILE.Commands[command] then
2,723✔
607
      SU.error("Unknown command " .. command)
×
608
   end
609
   local result = SILE.Commands[command](options, content)
2,723✔
610
   SILE.traceStack:pop(pId)
2,723✔
611
   return result
2,723✔
612
end
613

614
--- (Deprecated) Register a function as a SILE command.
615
-- Takes any Lua function and registers it for use as a SILE command (which will in turn be used to process any content
616
-- nodes identified with the command name.
617
--
618
-- Note that alternative versions of this action are available as methods on document classes and packages. Those
619
-- interfaces should be prefered to this global one.
620
-- @tparam string name Name of cammand to register.
621
-- @tparam function func Callback function to use as command handler.
622
-- @tparam[opt] nil|string help User friendly short usage string for use in error messages, documentation, etc.
623
-- @tparam[opt] nil|string pack Information identifying the module registering the command for use in error and usage
624
-- messages. Usually auto-detected.
625
-- @see SILE.classes:registerCommand
626
-- @see SILE.packages:registerCommand
627
function SILE.registerCommand (name, func, help, pack, cheat)
394✔
628
   local class = SILE.documentState.documentClass
1,194✔
629
   if not cheat then
1,194✔
630
      SU.deprecated(
12✔
631
         "SILE.registerCommand",
6✔
632
         "class:registerCommand",
6✔
633
         "0.14.0",
6✔
634
         "0.16.0",
6✔
635
         [[Commands are being scoped to the document classes they are loaded into rather than being globals.]]
636
      )
6✔
637
   end
638
   -- Shimming until we have all scope cheating removed from core
639
   if not cheat or not class or class.type ~= "class" then
1,194✔
640
      return SILE.classes.base.registerCommand(nil, name, func, help, pack)
1,389✔
641
   end
642
   return class:registerCommand(name, func, help, pack)
2✔
643
end
644

645
--- Wrap an existing command with new default options.
646
-- Modifies an already registered SILE command with a new table of options to be used as default values any time it is
647
-- called. Calling options still take precidence.
648
-- @tparam string command Name of command to overwride.
649
-- @tparam table options Options to set as updated defaults.
650
function SILE.setCommandDefaults (command, options)
394✔
651
   local oldCommand = SILE.Commands[command]
×
652
   SILE.Commands[command] = function (defaults, content)
×
653
      for k, v in pairs(options) do
×
654
         defaults[k] = defaults[k] or v
×
655
      end
656
      return oldCommand(defaults, content)
×
657
   end
658
end
659

660
-- TODO: Move to new table entry handler in types.unit
661
function SILE.registerUnit (unit, spec)
394✔
662
   -- If a unit exists already, clear it first so we get fresh meta table entries, see #1607
663
   if SILE.types.unit[unit] then
13✔
664
      SILE.types.unit[unit] = nil
×
665
   end
666
   SILE.types.unit[unit] = spec
13✔
667
end
668

669
function SILE.paperSizeParser (size)
394✔
670
   SU.deprecated("SILE.paperSizeParser", "SILE.papersize", "0.15.0", "0.16.0")
×
671
   return SILE.papersize(size)
×
672
end
673

674
--- Finalize document processing
675
-- Signals that all the `SILE.process()` calls have been made and SILE should move on to finish up the output
676
--
677
-- 1. Tells the document class to run its `:finish()` method. This method is typically responsible for calling the
678
-- `:finish()` method of the outputter module in the appropriate sequence.
679
-- 2. Closes out anything in active memory we don't need like font instances.
680
-- 3. Evaluate any snippets in SILE.input.evalAfter table.
681
-- 4. Stops logging dependecies and writes them to a makedepends file if requested.
682
-- 5. Close out the Lua profiler if it was running.
683
-- 6. Output version information if versions debug flag is set.
684
function SILE.finish ()
394✔
685
   SILE.documentState.documentClass:finish()
101✔
686
   SILE.font.finish()
101✔
687
   runEvals(SILE.input.evaluateAfters, "evaluate-after")
101✔
688
   if SILE.makeDeps then
101✔
689
      SILE.makeDeps:write()
×
690
   end
691
   if not SILE.quiet then
101✔
692
      io.stderr:write("\n")
101✔
693
   end
694
   if SU.debugging("profile") then
202✔
695
      ProFi:stop()
×
696
      ProFi:writeReport(pl.path.splitext(SILE.input.filenames[1]) .. ".profile.txt")
×
697
   end
698
   if SU.debugging("versions") then
202✔
699
      SILE.shaper:debugVersions()
101✔
700
   end
701
end
702

703
-- Internal libraries that return classes, but we only ever use one instantiation
704
SILE.traceStack = require("core.tracestack")()
394✔
705
SILE.settings = require("core.settings")()
394✔
706

707
-- Internal libraries that run core SILE functions on load
708
require("core.hyphenator-liang")
197✔
709
require("core.languages")
197✔
710
SILE.linebreak = require("core.break")
197✔
711
require("core.frame")
197✔
712
SILE.font = require("core.font")
197✔
713

714
return SILE
197✔
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