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

sile-typesetter / sile / 11770252752

11 Nov 2024 01:09AM UTC coverage: 32.853% (-27.5%) from 60.402%
11770252752

Pull #2164

github

web-flow
chore(deps): Bump DeterminateSystems/nix-installer-action from 14 to 15

Bumps [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action) from 14 to 15.
- [Release notes](https://github.com/determinatesystems/nix-installer-action/releases)
- [Commits](https://github.com/determinatesystems/nix-installer-action/compare/v14...v15)

---
updated-dependencies:
- dependency-name: DeterminateSystems/nix-installer-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #2164: chore(deps): Bump DeterminateSystems/nix-installer-action from 14 to 15

5855 of 17822 relevant lines covered (32.85%)

2493.73 hits per line

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

70.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")
×
9

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

13
-- Placeholder for SILE internals table
14
SILE = {}
162✔
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")
162✔
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")
162✔
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)
324✔
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"
162✔
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)
162✔
49

50
--- Default to verbose mode, can be changed from the CLI or by libraries
51
--- @boolean quiet
52
SILE.quiet = false
162✔
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
162✔
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")
162✔
65
SU = SILE.utilities -- regrettable global alias
162✔
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")
162✔
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,296✔
76
      __index = function (self, key)
77
         -- local var = rawget(self, key)
78
         local m = require(("%s.%s"):format(scope, key))
1,400✔
79
         self[key] = m
1,400✔
80
         return m
1,400✔
81
      end,
82
   })
1,296✔
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 = {}
162✔
91

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

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

101
SILE.nodeMakers = {}
162✔
102
SILE.tokenizers = {}
162✔
103
SILE.status = {}
162✔
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 = {}
162✔
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 = {}
162✔
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 = {}
162✔
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 evaluateAfters 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 accommodates 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 = {
162✔
150
   filenames = {},
162✔
151
   evaluates = {},
162✔
152
   evaluateAfters = {},
162✔
153
   uses = {},
162✔
154
   options = {},
162✔
155
   preambles = {}, -- deprecated, undocumented
162✔
156
   postambles = {}, -- deprecated, undocumented
162✔
157
}
162✔
158

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

169
-- Internal libraries that don't try to use anything on load, only provide something
170
SILE.parserBits = require("core.parserbits")
162✔
171
SILE.frameParser = require("core.frameparser")
162✔
172
SILE.fontManager = require("core.fontmanager")
162✔
173
SILE.papersize = require("core.papersize")
162✔
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
166✔
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 ()
324✔
208
   if not SILE.backend then
83✔
209
      SILE.backend = "libtexpdf"
83✔
210
   end
211
   if SILE.backend == "libtexpdf" then
83✔
212
      SILE.shaper = SILE.shapers.harfbuzz()
249✔
213
      SILE.outputter = SILE.outputters.libtexpdf()
249✔
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()
249✔
228
   io.stdout:setvbuf("no")
83✔
229
   if SU.debugging("profile") then
166✔
230
      ProFi = require("ProFi")
×
231
      ProFi:start()
×
232
   end
233
   if SILE.makeDeps then
83✔
234
      SILE.makeDeps:add(_G.executablePath)
×
235
   end
236
   runEvals(SILE.input.evaluates, "evaluate")
83✔
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 initialize 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)
324✔
271
   local status, pack
272
   if type(module) == "string" then
116✔
273
      if module:match("/") then
115✔
274
         SU.warn(([[
×
275
            Module names should not include platform-specific path separators
276

277
            Using slashes like '/' or '\' in a module name looks like a path segment. This
278
            may appear to work in some cases, but breaks cross platform compatibility.
279
            Even on the platform with the matching separator, this can lead to packages
280
            getting loaded more than once because Lua will cache one each of the different
281
            formats. Please use '.' separators which are automatically translated to the
282
            correct platform one. For example a correct use statement would be:
283

284
              \use[module=%s] instead of \use[module=%s].
285
         ]]):format(module:gsub("/", "."), module))
×
286
      end
287
      status, pack = pcall(require, module)
115✔
288
      if not status then
115✔
289
         SU.error(
×
290
            ("Unable to use '%s':\n%s%s"):format(
×
291
               module,
292
               SILE.traceback and ("    Lua " .. pack) or "",
×
293
               suggest_luarocks(module)
×
294
            )
295
         )
296
      end
297
   elseif type(module) == "table" then
1✔
298
      pack = module
1✔
299
   end
300
   local name = pack._name
116✔
301
   local class = SILE.documentState.documentClass
116✔
302
   if not pack.type then
116✔
303
      SU.error("Modules must declare their type")
×
304
   elseif pack.type == "class" then
116✔
305
      SILE.classes[name] = pack
×
306
      if class then
×
307
         SU.error("Cannot load a class after one is already instantiated")
×
308
      end
309
      SILE.scratch.class_from_uses = pack
×
310
   elseif pack.type == "inputter" then
116✔
311
      SILE.inputters[name] = pack
×
312
      SILE.inputter = pack(options)
×
313
   elseif pack.type == "outputter" then
116✔
314
      SILE.outputters[name] = pack
×
315
      SILE.outputter = pack(options)
×
316
   elseif pack.type == "shaper" then
116✔
317
      SILE.shapers[name] = pack
×
318
      SILE.shaper = pack(options)
×
319
   elseif pack.type == "typesetter" then
116✔
320
      SILE.typesetters[name] = pack
×
321
      SILE.typesetter = pack(options)
×
322
   elseif pack.type == "pagebuilder" then
116✔
323
      SILE.pagebuilders[name] = pack
×
324
      SILE.pagebuilder = pack(options)
×
325
   elseif pack.type == "package" then
116✔
326
      SILE.packages[pack._name] = pack
116✔
327
      if class then
116✔
328
         class:loadPackage(pack, options, reload)
232✔
329
      else
330
         table.insert(SILE.input.preambles, { pack = pack, options = options })
×
331
      end
332
   end
333
end
334

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

378
            It will not function fully before the class is instantiated. Please just use
379
            the Lua require() function directly:
380

381
              SILE.require("%s") → require("%s")
382
         ]],
383
         dependency,
384
         dependency
385
      ))
386
   end
387
   if type(lib) == "table" and class then
83✔
388
      if lib.type == "package" then
×
389
         lib(class)
×
390
      else
391
         class:initPackage(lib)
×
392
      end
393
   end
394
   return lib
83✔
395
end
396

397
--- Process content.
398
-- This is the main 'action' SILE does. Once input files are parsed into an abstract syntax tree, then we recursively
399
-- iterate through the tree handling each item in the order encountered.
400
-- @tparam table ast SILE content in abstract syntax tree format (a table of strings, functions, or more AST trees).
401
function SILE.process (ast)
324✔
402
   if not ast then
637✔
403
      return
×
404
   end
405
   if SU.debugging("ast") then
1,274✔
406
      SU.debugAST(ast, 0)
×
407
   end
408
   if type(ast) == "function" then
637✔
409
      return ast()
146✔
410
   end
411
   for _, content in ipairs(ast) do
2,633✔
412
      if type(content) == "string" then
2,142✔
413
         SILE.typesetter:typeset(content)
2,426✔
414
      elseif type(content) == "function" then
929✔
415
         content()
×
416
      elseif SILE.Commands[content.command] then
929✔
417
         SILE.call(content.command, content.options, content)
1,856✔
418
      elseif not content.command and not content.id then
1✔
419
         local pId = SILE.traceStack:pushContent(content, "content")
1✔
420
         SILE.process(content)
1✔
421
         SILE.traceStack:pop(pId)
2✔
422
      elseif type(content) ~= "nil" then
×
423
         local pId = SILE.traceStack:pushContent(content)
×
424
         SU.error("Unknown command " .. (tostring(content.command or content.id)))
×
425
         SILE.traceStack:pop(pId)
×
426
      end
427
   end
428
end
429

430
local preloadedinputters = { "xml", "lua", "sil" }
162✔
431

432
local function detectFormat (doc, filename)
433
   -- Preload default reader types so content detection has something to work with
434
   if #SILE.inputters == 0 then
83✔
435
      for _, format in ipairs(preloadedinputters) do
332✔
436
         local _ = SILE.inputters[format]
249✔
437
      end
438
   end
439
   local contentDetectionOrder = {}
83✔
440
   for _, inputter in pairs(SILE.inputters) do
332✔
441
      if inputter.order then
249✔
442
         table.insert(contentDetectionOrder, inputter)
249✔
443
      end
444
   end
445
   table.sort(contentDetectionOrder, function (a, b)
166✔
446
      return a.order < b.order
232✔
447
   end)
448
   local initialround = filename and 1 or 2
83✔
449
   for round = initialround, 3 do
84✔
450
      for _, inputter in ipairs(contentDetectionOrder) do
162✔
451
         SU.debug("inputter", "Running content type detection round", round, "with", inputter._name)
161✔
452
         if inputter.appropriate(round, filename, doc) then
322✔
453
            return inputter._name
83✔
454
         end
455
      end
456
   end
457
   SU.error(("Unable to pick inputter to process input from '%s'"):format(filename))
×
458
end
459

460
--- Process an input string.
461
-- First converts the string to an AST, then runs `process` on it.
462
-- @tparam string doc Input string to be converted to SILE content.
463
-- @tparam[opt] nil|string format The name of the formatter. If nil, defaults to using each intputter's auto detection.
464
-- @tparam[opt] nil|string filename Pseudo filename to identify the content with, useful for error messages stack traces.
465
-- @tparam[opt] nil|table options Options to pass to the inputter instance when instantiated.
466
function SILE.processString (doc, format, filename, options)
324✔
467
   local cpf
468
   if not filename then
109✔
469
      cpf = SILE.currentlyProcessingFile
26✔
470
      local caller = debug.getinfo(2, "Sl")
26✔
471
      SILE.currentlyProcessingFile = caller.short_src .. ":" .. caller.currentline
26✔
472
   end
473
   -- In the event we're processing the master file *and* the user gave us
474
   -- a specific inputter to use, use it at the exclusion of all content type
475
   -- detection
476
   local inputter
477
   if
478
      filename
479
      and pl.path.normcase(pl.path.normpath(filename)) == pl.path.normcase(SILE.input.filenames[1])
358✔
480
      and SILE.inputter
83✔
481
   then
482
      inputter = SILE.inputter
×
483
   else
484
      format = format or detectFormat(doc, filename)
192✔
485
      if not SILE.quiet then
109✔
486
         io.stderr:write(("<%s> as %s\n"):format(SILE.currentlyProcessingFile, format))
109✔
487
      end
488
      inputter = SILE.inputters[format](options)
218✔
489
      -- If we did content detection *and* this is the master file, save the
490
      -- inputter for posterity and postambles
491
      if filename and pl.path.normcase(filename) == pl.path.normcase(SILE.input.filenames[1]:gsub("^-$", "STDIN")) then
275✔
492
         SILE.inputter = inputter
83✔
493
      end
494
   end
495
   local pId = SILE.traceStack:pushDocument(SILE.currentlyProcessingFile, doc)
109✔
496
   inputter:process(doc)
109✔
497
   SILE.traceStack:pop(pId)
109✔
498
   if cpf then
109✔
499
      SILE.currentlyProcessingFile = cpf
26✔
500
   end
501
end
502

503
--- Process an input file
504
-- Opens a file, converts the contents to an AST, then runs `process` on it.
505
-- Roughly equivalent to listing the file as an input, but easier to embed in code.
506
-- @tparam string filename Path of file to open string to be converted to SILE content.
507
-- @tparam[opt] nil|string format The name of the formatter. If nil, defaults to using each intputter's auto detection.
508
-- @tparam[opt] nil|table options Options to pass to the inputter instance when instantiated.
509
function SILE.processFile (filename, format, options)
324✔
510
   local lfs = require("lfs")
83✔
511
   local doc
512
   if filename == "-" then
83✔
513
      filename = "STDIN"
×
514
      doc = io.stdin:read("*a")
×
515
   else
516
      -- Turn slashes around in the event we get passed a path from a Windows shell
517
      filename = filename:gsub("\\", "/")
83✔
518
      if not SILE.masterFilename then
83✔
519
         SILE.masterFilename = pl.path.splitext(pl.path.normpath(filename))
332✔
520
      end
521
      if SILE.input.filenames[1] and not SILE.masterDir then
83✔
522
         SILE.masterDir = pl.path.dirname(SILE.input.filenames[1])
166✔
523
      end
524
      if SILE.masterDir and SILE.masterDir:len() >= 1 then
166✔
525
         _G.extendSilePath(SILE.masterDir)
83✔
526
         _G.extendSilePathRocks(SILE.masterDir .. "/lua_modules")
83✔
527
      end
528
      filename = SILE.resolveFile(filename) or SU.error("Could not find file")
166✔
529
      local mode = lfs.attributes(filename).mode
83✔
530
      if mode ~= "file" and mode ~= "named pipe" then
83✔
531
         SU.error(filename .. " isn't a file or named pipe, it's a " .. mode .. "!")
×
532
      end
533
      if SILE.makeDeps then
83✔
534
         SILE.makeDeps:add(filename)
×
535
      end
536
      local file, err = io.open(filename)
83✔
537
      if not file then
83✔
538
         print("Could not open " .. filename .. ": " .. err)
×
539
         return
×
540
      end
541
      doc = file:read("*a")
83✔
542
   end
543
   local cpf = SILE.currentlyProcessingFile
83✔
544
   SILE.currentlyProcessingFile = filename
83✔
545
   local pId = SILE.traceStack:pushDocument(filename, doc)
83✔
546
   local ret = SILE.processString(doc, format, filename, options)
83✔
547
   SILE.traceStack:pop(pId)
83✔
548
   SILE.currentlyProcessingFile = cpf
83✔
549
   return ret
83✔
550
end
551

552
-- TODO: this probably needs deprecating, moved here just to get out of the way so
553
-- typesetters classing works as expected
554
function SILE.typesetNaturally (frame, func)
324✔
555
   local saveTypesetter = SILE.typesetter
53✔
556
   if SILE.typesetter.frame then
53✔
557
      SILE.typesetter.frame:leave(SILE.typesetter)
53✔
558
   end
559
   SILE.typesetter = SILE.typesetters.base(frame)
106✔
560
   SILE.settings:temporarily(func)
53✔
561
   SILE.typesetter:leaveHmode()
53✔
562
   SILE.typesetter:chuck()
53✔
563
   SILE.typesetter.frame:leave(SILE.typesetter)
53✔
564
   SILE.typesetter = saveTypesetter
53✔
565
   if SILE.typesetter.frame then
53✔
566
      SILE.typesetter.frame:enter(SILE.typesetter)
53✔
567
   end
568
end
569

570
--- Resolve relative file paths to identify absolute resources locations.
571
-- Makes it possible to load resources from relative paths, relative to a document or project or SILE itself.
572
-- @tparam string filename Name of file to find using the same order of precedence logic in `require()`.
573
-- @tparam[opt] nil|string pathprefix Optional prefix in which to look for if the file isn't found otherwise.
574
function SILE.resolveFile (filename, pathprefix)
324✔
575
   local candidates = {}
86✔
576
   -- Start with the raw file name as given prefixed with a path if requested
577
   if pathprefix then
86✔
578
      candidates[#candidates + 1] = pl.path.join(pathprefix, "?")
×
579
   end
580
   -- Also check the raw file name without a path
581
   candidates[#candidates + 1] = "?"
86✔
582
   -- Iterate through the directory of the master file, the SILE_PATH variable, and the current directory
583
   -- Check for prefixed paths first, then the plain path in that fails
584
   if SILE.masterDir then
86✔
585
      for path in SU.gtoke(SILE.masterDir .. ";" .. tostring(os.getenv("SILE_PATH")), ";") do
774✔
586
         if path.string and path.string ~= "nil" then
602✔
587
            if pathprefix then
344✔
588
               candidates[#candidates + 1] = pl.path.join(path.string, pathprefix, "?")
×
589
            end
590
            candidates[#candidates + 1] = pl.path.join(path.string, "?")
688✔
591
         end
592
      end
593
   end
594
   -- Return the first candidate that exists, also checking the .sil suffix
595
   local path = table.concat(candidates, ";")
86✔
596
   local resolved, err = package.searchpath(filename, path, "/")
86✔
597
   if resolved then
86✔
598
      if SILE.makeDeps then
86✔
599
         SILE.makeDeps:add(resolved)
×
600
      end
601
   elseif SU.debugging("paths") then
×
602
      SU.debug("paths", ("Unable to find file '%s': %s"):format(filename, err))
×
603
   end
604
   return resolved
86✔
605
end
606

607
--- Execute a registered SILE command.
608
-- Uses a function previously registered by any modules explicitly loaded by the user at runtime via `--use`, the SILE
609
-- core, the document class, or any loaded package.
610
-- @tparam string command Command name.
611
-- @tparam[opt={}] nil|table options Options to pass to the command.
612
-- @tparam[opt] nil|table content Any valid AST node to be processed by the command.
613
function SILE.call (command, options, content)
324✔
614
   options = options or {}
2,324✔
615
   content = content or {}
2,324✔
616
   if SILE.traceback and type(content) == "table" and not content.lno then
2,324✔
617
      -- This call is from code (no content.lno) and we want to spend the time
618
      -- to determine everything we need about the caller
619
      local caller = debug.getinfo(2, "Sl")
×
620
      content.file, content.lno = caller.short_src, caller.currentline
×
621
   end
622
   local pId = SILE.traceStack:pushCommand(command, content, options)
2,324✔
623
   if not SILE.Commands[command] then
2,324✔
624
      SU.error("Unknown command " .. command)
×
625
   end
626
   local result = SILE.Commands[command](options, content)
2,324✔
627
   SILE.traceStack:pop(pId)
2,324✔
628
   return result
2,324✔
629
end
630

631
--- (Deprecated) Register a function as a SILE command.
632
-- Takes any Lua function and registers it for use as a SILE command (which will in turn be used to process any content
633
-- nodes identified with the command name.
634
--
635
-- Note that alternative versions of this action are available as methods on document classes and packages. Those
636
-- interfaces should be preferred to this global one.
637
-- @tparam string name Name of cammand to register.
638
-- @tparam function func Callback function to use as command handler.
639
-- @tparam[opt] nil|string help User friendly short usage string for use in error messages, documentation, etc.
640
-- @tparam[opt] nil|string pack Information identifying the module registering the command for use in error and usage
641
-- messages. Usually auto-detected.
642
-- @see SILE.classes:registerCommand
643
-- @see SILE.packages:registerCommand
644
function SILE.registerCommand (name, func, help, pack, cheat)
324✔
645
   local class = SILE.documentState.documentClass
977✔
646
   if not cheat then
977✔
647
      SU.deprecated(
6✔
648
         "SILE.registerCommand",
3✔
649
         "class:registerCommand",
3✔
650
         "0.14.0",
3✔
651
         "0.16.0",
3✔
652
         [[
×
653
            Commands are being scoped to the document classes they are loaded into rather
654
            than being globals.
655
         ]]
×
656
      )
3✔
657
   end
658
   -- Shimming until we have all scope cheating removed from core
659
   if not cheat or not class or class.type ~= "class" then
977✔
660
      return SILE.classes.base.registerCommand(nil, name, func, help, pack)
1,139✔
661
   end
662
   return class:registerCommand(name, func, help, pack)
×
663
end
664

665
--- Wrap an existing command with new default options.
666
-- Modifies an already registered SILE command with a new table of options to be used as default values any time it is
667
-- called. Calling options still take precedence.
668
-- @tparam string command Name of command to overwrite.
669
-- @tparam table options Options to set as updated defaults.
670
function SILE.setCommandDefaults (command, options)
324✔
671
   local oldCommand = SILE.Commands[command]
×
672
   SILE.Commands[command] = function (defaults, content)
×
673
      for k, v in pairs(options) do
×
674
         defaults[k] = defaults[k] or v
×
675
      end
676
      return oldCommand(defaults, content)
×
677
   end
678
end
679

680
-- TODO: Move to new table entry handler in types.unit
681
function SILE.registerUnit (unit, spec)
324✔
682
   -- If a unit exists already, clear it first so we get fresh meta table entries, see #1607
683
   if SILE.types.unit[unit] then
13✔
684
      SILE.types.unit[unit] = nil
×
685
   end
686
   SILE.types.unit[unit] = spec
13✔
687
end
688

689
function SILE.paperSizeParser (size)
324✔
690
   SU.deprecated("SILE.paperSizeParser", "SILE.papersize", "0.15.0", "0.16.0")
×
691
   return SILE.papersize(size)
×
692
end
693

694
--- Finalize document processing
695
-- Signals that all the `SILE.process()` calls have been made and SILE should move on to finish up the output
696
--
697
-- 1. Tells the document class to run its `:finish()` method. This method is typically responsible for calling the
698
-- `:finish()` method of the outputter module in the appropriate sequence.
699
-- 2. Closes out anything in active memory we don't need like font instances.
700
-- 3. Evaluate any snippets in SILE.input.evalAfter table.
701
-- 4. Stops logging dependencies and writes them to a makedepends file if requested.
702
-- 5. Close out the Lua profiler if it was running.
703
-- 6. Output version information if versions debug flag is set.
704
function SILE.finish ()
324✔
705
   SILE.documentState.documentClass:finish()
83✔
706
   SILE.font.finish()
83✔
707
   runEvals(SILE.input.evaluateAfters, "evaluate-after")
83✔
708
   if SILE.makeDeps then
83✔
709
      SILE.makeDeps:write()
×
710
   end
711
   if not SILE.quiet then
83✔
712
      io.stderr:write("\n")
83✔
713
   end
714
   if SU.debugging("profile") then
166✔
715
      ProFi:stop()
×
716
      ProFi:writeReport(pl.path.splitext(SILE.input.filenames[1]) .. ".profile.txt")
×
717
   end
718
   if SU.debugging("versions") then
166✔
719
      SILE.shaper:debugVersions()
83✔
720
   end
721
end
722

723
-- Internal libraries that return classes, but we only ever use one instantiation
724
SILE.traceStack = require("core.tracestack")()
324✔
725
SILE.settings = require("core.settings")()
324✔
726

727
-- Internal libraries that run core SILE functions on load
728
require("core.hyphenator-liang")
162✔
729
require("core.languages")
162✔
730
SILE.linebreak = require("core.break")
162✔
731
require("core.frame")
162✔
732
SILE.font = require("core.font")
162✔
733

734
return SILE
162✔
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