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

sile-typesetter / sile / 11170735472

03 Oct 2024 10:32PM UTC coverage: 58.612% (-4.5%) from 63.103%
11170735472

push

github

web-flow
Merge bcab25790 into 783083345

15 of 64 new or added lines in 5 files covered. (23.44%)

828 existing lines in 41 files now uncovered.

10478 of 17877 relevant lines covered (58.61%)

2029.7 hits per line

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

67.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 = {}
59✔
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")
59✔
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")
59✔
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)
118✔
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"
59✔
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)
59✔
49

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

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

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

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

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

169
-- Internal libraries that don't try to use anything on load, only provide something
170
SILE.parserBits = require("core.parserbits")
59✔
171
SILE.frameParser = require("core.frameparser")
59✔
172
SILE.fontManager = require("core.fontmanager")
59✔
173
SILE.papersize = require("core.papersize")
59✔
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
60✔
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 ()
118✔
208
   if not SILE.backend then
30✔
209
      SILE.backend = "libtexpdf"
30✔
210
   end
211
   if SILE.backend == "libtexpdf" then
30✔
212
      SILE.shaper = SILE.shapers.harfbuzz()
90✔
213
      SILE.outputter = SILE.outputters.libtexpdf()
90✔
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
×
NEW
221
      SILE.shaper = SILE.shapers.dummy()
×
222
      SILE.outputter = SILE.outputters.text()
×
223
   elseif SILE.backend == "dummy" then
×
NEW
224
      SILE.shaper = SILE.shapers.dummy()
×
225
      SILE.outputter = SILE.outputters.dummy()
×
NEW
226
   elseif SILE.backend == "ast" then
×
NEW
227
      SILE.shaper = SILE.shapers.dummy()
×
NEW
228
      SILE.outputter = SILE.outputters.ast()
×
229
   end
230
   SILE.pagebuilder = SILE.pagebuilders.base()
90✔
231
   io.stdout:setvbuf("no")
30✔
232
   if SU.debugging("profile") then
60✔
233
      ProFi = require("ProFi")
×
234
      ProFi:start()
×
235
   end
236
   if SILE.makeDeps then
30✔
237
      SILE.makeDeps:add(_G.executablePath)
×
238
   end
239
   runEvals(SILE.input.evaluates, "evaluate")
30✔
240
end
241

242
local function suggest_luarocks (module)
243
   local guessed_module_name = module:gsub(".*%.", "") .. ".sile"
×
244
   return ([[
×
245

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

251
        luarocks --lua-version %s --tree lua_modules install %s
252

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

257
        eval $(luarocks --lua-version %s --tree lua_modules path)
258

259
    Thereafter running SILE again should work as expected:
260

261
       sile %s
262

263
    ]]):format(SILE.lua_version, guessed_module_name, SILE.lua_version, pl.stringx.join(" ", _G.arg or {}))
×
264
end
265

266
--- Multi-purpose loader to load and initialize modules.
267
-- This is used to load and initialize core parts of SILE and also 3rd party modules.
268
-- Module types supported bay be an *inputter*, *outputer*, *shaper*, *typesetter*, *pagebuilder*, or *package*.
269
-- @tparam string|table module The module spec name to load (dot-separated, e.g. `"packages.lorem"`) or a table with
270
--   a module that has already been loaded.
271
-- @tparam[opt] table options Startup options as key/value pairs passed to the module when initialized.
272
-- @tparam[opt=false] boolean reload whether or not to reload a module that has been loaded and initialized before.
273
function SILE.use (module, options, reload)
118✔
274
   local status, pack
275
   if type(module) == "string" then
42✔
276
      if module:match("/") then
42✔
277
         SU.warn(([[
×
278
            Module names should not include platform-specific path separators
279

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

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

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

381
            It will not function fully before the class is instantiated. Please just use
382
            the Lua require() function directly:
383

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

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

434
local preloadedinputters = { "xml", "lua", "ast", "sil" }
59✔
435

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

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

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

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

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

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

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

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

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

693
function SILE.paperSizeParser (size)
118✔
694
   SU.deprecated("SILE.paperSizeParser", "SILE.papersize", "0.15.0", "0.16.0")
×
695
   return SILE.papersize(size)
×
696
end
697

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

727
-- Internal libraries that return classes, but we only ever use one instantiation
728
SILE.traceStack = require("core.tracestack")()
118✔
729
SILE.settings = require("core.settings")()
117✔
730

731
-- Internal libraries that run core SILE functions on load
732
require("core.hyphenator-liang")
58✔
733
require("core.languages")
58✔
734
SILE.linebreak = require("core.break")
58✔
735
require("core.frame")
58✔
736
SILE.font = require("core.font")
58✔
737

738
return SILE
58✔
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