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

sile-typesetter / sile / 10621606353

29 Aug 2024 07:43PM UTC coverage: 66.23% (+3.6%) from 62.644%
10621606353

push

github

alerque
Merge tag 'v0.15.5' into develop

13 of 289 new or added lines in 17 files covered. (4.5%)

403 existing lines in 59 files now uncovered.

11585 of 17492 relevant lines covered (66.23%)

5713.06 hits per line

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

73.17
/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 = {}
376✔
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")
376✔
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")
376✔
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)
752✔
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"
376✔
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)
376✔
49

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

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

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

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

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

169
-- Internal libraries that don't try to use anything on load, only provide something
170
SILE.parserBits = require("core.parserbits")
376✔
171
SILE.frameParser = require("core.frameparser")
376✔
172
SILE.fontManager = require("core.fontmanager")
375✔
173
SILE.papersize = require("core.papersize")
375✔
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
376✔
UNCOV
182
      local pId = SILE.traceStack:pushText(snippet)
×
UNCOV
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 ()
750✔
208
   if not SILE.backend then
188✔
209
      SILE.backend = "libtexpdf"
188✔
210
   end
211
   if SILE.backend == "libtexpdf" then
188✔
212
      SILE.shaper = SILE.shapers.harfbuzz()
564✔
213
      SILE.outputter = SILE.outputters.libtexpdf()
564✔
UNCOV
214
   elseif SILE.backend == "cairo" then
×
UNCOV
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()
564✔
228
   io.stdout:setvbuf("no")
188✔
229
   if SU.debugging("profile") then
376✔
UNCOV
230
      ProFi = require("ProFi")
×
UNCOV
231
      ProFi:start()
×
232
   end
233
   if SILE.makeDeps then
188✔
UNCOV
234
      SILE.makeDeps:add(_G.executablePath)
×
235
   end
236
   runEvals(SILE.input.evaluates, "evaluate")
188✔
237
end
238

239
local function suggest_luarocks (module)
UNCOV
240
   local guessed_module_name = module:gsub(".*%.", "") .. ".sile"
×
UNCOV
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

UNCOV
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)
750✔
271
   local status, pack
272
   if type(module) == "string" then
243✔
273
      if module:match("/") then
242✔
NEW
274
         SU.warn(([[Module names should not include platform-specific path separators
×
275

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

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

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

393
--- Process content.
394
-- This is the main 'action' SILE does. Once input files are parsed into an abstract syntax tree, then we recursively
395
-- iterate through the tree handling each item in the order encountered.
396
-- @tparam table ast SILE content in abstract syntax tree format (a table of strings, functions, or more AST trees).
397
function SILE.process (ast)
750✔
398
   if not ast then
1,699✔
399
      return
4✔
400
   end
401
   if SU.debugging("ast") then
3,390✔
UNCOV
402
      SU.debugAST(ast, 0)
×
403
   end
404
   if type(ast) == "function" then
1,695✔
405
      return ast()
355✔
406
   end
407
   for _, content in ipairs(ast) do
6,000✔
408
      if type(content) == "string" then
4,660✔
409
         SILE.typesetter:typeset(content)
5,162✔
410
      elseif type(content) == "function" then
2,079✔
411
         content()
2✔
412
      elseif SILE.Commands[content.command] then
2,078✔
413
         SILE.call(content.command, content.options, content)
3,756✔
414
      elseif content.id == "content" or (not content.command and not content.id) then
200✔
415
         local pId = SILE.traceStack:pushContent(content, "content")
200✔
416
         SILE.process(content)
200✔
417
         SILE.traceStack:pop(pId)
400✔
UNCOV
418
      elseif type(content) ~= "nil" then
×
UNCOV
419
         local pId = SILE.traceStack:pushContent(content)
×
UNCOV
420
         SU.error("Unknown command " .. (tostring(content.command or content.id)))
×
UNCOV
421
         SILE.traceStack:pop(pId)
×
422
      end
423
   end
424
end
425

426
local preloadedinputters = { "xml", "lua", "sil" }
375✔
427

428
local function detectFormat (doc, filename)
429
   -- Preload default reader types so content detection has something to work with
430
   if #SILE.inputters == 0 then
189✔
431
      for _, format in ipairs(preloadedinputters) do
756✔
432
         local _ = SILE.inputters[format]
567✔
433
      end
434
   end
435
   local contentDetectionOrder = {}
189✔
436
   for _, inputter in pairs(SILE.inputters) do
756✔
437
      if inputter.order then
567✔
438
         table.insert(contentDetectionOrder, inputter)
567✔
439
      end
440
   end
441
   table.sort(contentDetectionOrder, function (a, b)
378✔
442
      return a.order < b.order
513✔
443
   end)
444
   local initialround = filename and 1 or 2
189✔
445
   for round = initialround, 3 do
192✔
446
      for _, inputter in ipairs(contentDetectionOrder) do
383✔
447
         SU.debug("inputter", "Running content type detection round", round, "with", inputter._name)
380✔
448
         if inputter.appropriate(round, filename, doc) then
760✔
449
            return inputter._name
189✔
450
         end
451
      end
452
   end
UNCOV
453
   SU.error(("Unable to pick inputter to process input from '%s'"):format(filename))
×
454
end
455

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

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

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

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

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

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

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

673
-- TODO: Move to new table entry handler in types.unit
674
function SILE.registerUnit (unit, spec)
750✔
675
   -- If a unit exists already, clear it first so we get fresh meta table entries, see #1607
676
   if SILE.types.unit[unit] then
15✔
677
      SILE.types.unit[unit] = nil
×
678
   end
679
   SILE.types.unit[unit] = spec
15✔
680
end
681

682
function SILE.paperSizeParser (size)
750✔
UNCOV
683
   SU.deprecated("SILE.paperSizeParser", "SILE.papersize", "0.15.0", "0.16.0")
×
UNCOV
684
   return SILE.papersize(size)
×
685
end
686

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

716
-- Internal libraries that return classes, but we only ever use one instantiation
717
SILE.traceStack = require("core.tracestack")()
750✔
718
SILE.settings = require("core.settings")()
750✔
719

720
-- Internal libraries that run core SILE functions on load
721
require("core.hyphenator-liang")
375✔
722
require("core.languages")
375✔
723
SILE.linebreak = require("core.break")
375✔
724
require("core.frame")
375✔
725
SILE.font = require("core.font")
375✔
726

727
return SILE
375✔
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