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

sile-typesetter / sile / 13989129417

21 Mar 2025 09:42AM UTC coverage: 30.742% (-29.5%) from 60.236%
13989129417

push

github

alerque
ci(actions): Use ICU 77 for macOS builds

6144 of 19986 relevant lines covered (30.74%)

1461.24 hits per line

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

66.77
/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 = {}
180✔
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")
180✔
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")
180✔
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)
360✔
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"
18✔
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)
18✔
49

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

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

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

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

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

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

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

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

194
--- Core functions
195
-- @section functions
196

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

247
local function suggest_luarocks (module)
248
   local guessed_module_name = module:gsub(".*%.", "") .. ".sile"
×
249
   return ([[
×
250

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

257
        luarocks --lua-version %s --tree lua_modules install %s
258

259
      This will install the LuaRock(s) to your project. Note this takes
260
      advantage of the fact that SILE checks for modules in the path
261
      'lua_modules' relative to the current input file by default. SILE also
262
      automatically checks the default system Lua path by default, so using
263
      `--global` will also work.
264

265
      In the event you use a different path to the LuaRocks tree, you must also
266
      set an environment variable to teach SILE about how to find the tree
267
      *before* it runs. This can be aided by asking LuaRocks to come up with a
268
      path and evaling the result in the shell before running SILE. This only
269
      needs to be done once in each shell, (obviously substituting 'path' for
270
      your actual path):
271

272
        eval $(luarocks --lua-version %s --tree path)
273

274
      Thereafter running `sile` as normal in the same shell should work as
275
      expected. This code can be used in your shell's initialization script
276
      to avoid having to do it manually in each new shell. This is true for
277
      user home directory installations using `--local` or any specific values
278
      for `--tree` other than 'lua_modules'.
279

280
      As an anternative to setting up environment variables when using a
281
      non-default tree location, you can use the `--luarocks-tree` option to
282
      add path(s) at runtime. This is simpler to type, but must be used on each
283
      and every invocation. The value for tree should be the same as used when
284
      installing the LuaRock(s), or an appropriate full path to the location
285
      used by `--local` (generally "$HOME/.luarocks"):
286

287
        sile --luarocks-tree path %s
288

289
    ]]):format(SILE.lua_version, guessed_module_name, SILE.lua_version, pl.stringx.join(" ", _G.arg or {}))
×
290
end
291

292
--- Multi-purpose loader to load and initialize modules.
293
-- This is used to load and initialize core parts of SILE and also 3rd party modules.
294
-- Module types supported bay be an *inputter*, *outputer*, *shaper*, *typesetter*, *pagebuilder*, or *package*.
295
-- @tparam string|table module The module spec name to load (dot-separated, e.g. `"packages.lorem"`) or a table with
296
--   a module that has already been loaded.
297
-- @tparam[opt] table options Startup options as key/value pairs passed to the module when initialized.
298
-- @tparam[opt=false] boolean reload whether or not to reload a module that has been loaded and initialized before.
299
function SILE.use (module, options, reload)
34✔
300
   local status, pack
301
   if type(module) == "string" then
10✔
302
      if module:match("/") then
10✔
303
         SU.warn(([[
×
304
            Module names should not include platform-specific path separators
305

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

313
              \use[module=%s] instead of \use[module=%s].
314
         ]]):format(module:gsub("/", "."), module))
×
315
      end
316
      status, pack = pcall(require, module)
10✔
317
      if not status then
10✔
318
         SU.error(
×
319
            ("Unable to use '%s':\n%s%s"):format(
×
320
               module,
321
               SILE.traceback and ("    Lua " .. pack) or "",
×
322
               suggest_luarocks(module)
×
323
            )
324
         )
325
      end
326
   elseif type(module) == "table" then
×
327
      pack = module
×
328
   end
329
   local name = pack._name
10✔
330
   local class = SILE.documentState.documentClass
10✔
331
   if not pack.type then
10✔
332
      SU.error("Modules must declare their type")
×
333
   elseif pack.type == "class" then
10✔
334
      SILE.classes[name] = pack
×
335
      if class then
×
336
         SU.error("Cannot load a class after one is already instantiated")
×
337
      end
338
      SILE.scratch.class_from_uses = pack
×
339
   elseif pack.type == "inputter" then
10✔
340
      SILE.inputters[name] = pack
×
341
      SILE.inputter = pack(options)
×
342
   elseif pack.type == "outputter" then
10✔
343
      SILE.outputters[name] = pack
×
344
      SILE.outputter = pack(options)
×
345
   elseif pack.type == "shaper" then
10✔
346
      SILE.shapers[name] = pack
×
347
      SILE.shaper = pack(options)
×
348
   elseif pack.type == "typesetter" then
10✔
349
      SILE.typesetters[name] = pack
×
350
      SILE.typesetter = pack(options)
×
351
   elseif pack.type == "pagebuilder" then
10✔
352
      SILE.pagebuilders[name] = pack
×
353
      SILE.pagebuilder = pack(options)
×
354
   elseif pack.type == "package" then
10✔
355
      SILE.packages[pack._name] = pack
10✔
356
      if class then
10✔
357
         class:loadPackage(pack, options, reload)
20✔
358
      else
359
         table.insert(SILE.input.preambles, { pack = pack, options = options })
×
360
      end
361
   end
362
end
363

364
-- --- Content loader like Lua's `require()` but with special path handling for loading SILE resource files.
365
-- -- Used for example by commands that load data via a `src=file.name` option.
366
-- -- @tparam string dependency Lua spec
367
function SILE.require (dependency, pathprefix, deprecation_ack)
34✔
368
   if pathprefix and not deprecation_ack then
10✔
369
      local notice = string.format(
×
370
         [[
×
371
  Please don't use the path prefix mechanism; it was intended to provide
372
  alternate paths to override core components but never worked well and is
373
  causing portability problems. Just use Lua idiomatic module loading:
374
      SILE.require("%s", "%s") → SILE.require("%s.%s")]],
375
         dependency,
376
         pathprefix,
377
         pathprefix,
378
         dependency
379
      )
380
      SU.deprecated("SILE.require", "SILE.require", "0.13.0", nil, notice)
×
381
   end
382
   dependency = dependency:gsub(".lua$", "")
10✔
383
   local status, lib
384
   if pathprefix then
10✔
385
      -- Note this is not a *path*, it is a module identifier:
386
      -- https://github.com/sile-typesetter/sile/issues/1861
387
      status, lib = pcall(require, pl.stringx.join(".", { pathprefix, dependency }))
20✔
388
   end
389
   if not status then
10✔
390
      local prefixederror = lib
×
391
      status, lib = pcall(require, dependency)
×
392
      if not status then
×
393
         SU.error(
×
394
            ("Unable to find module '%s'%s"):format(
×
395
               dependency,
396
               SILE.traceback and ((pathprefix and "\n  " .. prefixederror or "") .. "\n  " .. lib) or ""
×
397
            )
398
         )
399
      end
400
   end
401
   local class = SILE.documentState.documentClass
10✔
402
   if not class and not deprecation_ack then
10✔
403
      SU.warn(string.format(
×
404
         [[
×
405
            SILE.require() is only supported in documents, packages, or class init
406

407
            It will not function fully before the class is instantiated. Please just use
408
            the Lua require() function directly:
409

410
              SILE.require("%s") → require("%s")
411
         ]],
412
         dependency,
413
         dependency
414
      ))
415
   end
416
   if type(lib) == "table" and class then
10✔
417
      if lib.type == "package" then
×
418
         lib(class)
×
419
      else
420
         class:initPackage(lib)
×
421
      end
422
   end
423
   return lib
10✔
424
end
425

426
--- Process content.
427
-- This is the main 'action' SILE does. Once input files are parsed into an abstract syntax tree, then we recursively
428
-- iterate through the tree handling each item in the order encountered.
429
-- @tparam table ast SILE content in abstract syntax tree format (a table of strings, functions, or more AST trees).
430
function SILE.process (ast)
34✔
431
   if not ast then
72✔
432
      return
×
433
   end
434
   if SU.debugging("ast") then
144✔
435
      SU.debugAST(ast, 0)
×
436
   end
437
   if type(ast) == "function" then
72✔
438
      return ast()
13✔
439
   end
440
   for _, content in ipairs(ast) do
237✔
441
      if type(content) == "string" then
178✔
442
         SILE.typesetter:typeset(content)
210✔
443
      elseif type(content) == "function" then
73✔
444
         content()
×
445
      elseif SILE.Commands[content.command] then
73✔
446
         SILE.call(content.command, content.options, content)
146✔
447
      elseif not content.command and not content.id then
×
448
         local pId = SILE.traceStack:pushContent(content, "content")
×
449
         SILE.process(content)
×
450
         SILE.traceStack:pop(pId)
×
451
      elseif type(content) ~= "nil" then
×
452
         local pId = SILE.traceStack:pushContent(content)
×
453
         SU.error("Unknown command " .. (tostring(content.command or content.id)))
×
454
         SILE.traceStack:pop(pId)
×
455
      end
456
   end
457
end
458

459
local preloadedinputters = { "xml", "lua", "sil" }
17✔
460

461
local function detectFormat (doc, filename)
462
   -- Preload default reader types so content detection has something to work with
463
   if #SILE.inputters == 0 then
10✔
464
      for _, format in ipairs(preloadedinputters) do
40✔
465
         local _ = SILE.inputters[format]
30✔
466
      end
467
   end
468
   local contentDetectionOrder = {}
10✔
469
   for _, inputter in pairs(SILE.inputters) do
40✔
470
      if inputter.order then
30✔
471
         table.insert(contentDetectionOrder, inputter)
30✔
472
      end
473
   end
474
   table.sort(contentDetectionOrder, function (a, b)
20✔
475
      return a.order < b.order
29✔
476
   end)
477
   local initialround = filename and 1 or 2
10✔
478
   for round = initialround, 3 do
10✔
479
      for _, inputter in ipairs(contentDetectionOrder) do
19✔
480
         SU.debug("inputter", "Running content type detection round", round, "with", inputter._name)
19✔
481
         if inputter.appropriate(round, filename, doc) then
38✔
482
            return inputter._name
10✔
483
         end
484
      end
485
   end
486
   SU.error(("Unable to pick inputter to process input from '%s'"):format(filename))
×
487
end
488

489
--- Process an input string.
490
-- First converts the string to an AST, then runs `process` on it.
491
-- @tparam string doc Input string to be converted to SILE content.
492
-- @tparam[opt] nil|string format The name of the formatter. If nil, defaults to using each intputter's auto detection.
493
-- @tparam[opt] nil|string filename Pseudo filename to identify the content with, useful for error messages stack traces.
494
-- @tparam[opt] nil|table options Options to pass to the inputter instance when instantiated.
495
function SILE.processString (doc, format, filename, options)
34✔
496
   local cpf
497
   if not filename then
11✔
498
      cpf = SILE.currentlyProcessingFile
1✔
499
      local caller = debug.getinfo(2, "Sl")
1✔
500
      SILE.currentlyProcessingFile = caller.short_src .. ":" .. caller.currentline
1✔
501
   end
502
   -- In the event we're processing the master file *and* the user gave us
503
   -- a specific inputter to use, use it at the exclusion of all content type
504
   -- detection
505
   local inputter
506
   if
507
      filename
508
      and pl.path.normcase(pl.path.normpath(filename)) == pl.path.normcase(SILE.input.filenames[1])
41✔
509
      and SILE.inputter
10✔
510
   then
511
      inputter = SILE.inputter
×
512
   else
513
      format = format or detectFormat(doc, filename)
21✔
514
      if not SILE.quiet then
11✔
515
         io.stderr:write(("<%s> as %s\n"):format(SILE.currentlyProcessingFile, format))
11✔
516
      end
517
      inputter = SILE.inputters[format](options)
22✔
518
      -- If we did content detection *and* this is the master file, save the
519
      -- inputter for posterity and postambles
520
      if filename and pl.path.normcase(filename) == pl.path.normcase(SILE.input.filenames[1]:gsub("^-$", "STDIN")) then
31✔
521
         SILE.inputter = inputter
10✔
522
      end
523
   end
524
   local pId = SILE.traceStack:pushDocument(SILE.currentlyProcessingFile, doc)
11✔
525
   inputter:process(doc)
11✔
526
   SILE.traceStack:pop(pId)
11✔
527
   if cpf then
11✔
528
      SILE.currentlyProcessingFile = cpf
1✔
529
   end
530
end
531

532
--- Process an input file
533
-- Opens a file, converts the contents to an AST, then runs `process` on it.
534
-- Roughly equivalent to listing the file as an input, but easier to embed in code.
535
-- @tparam string filename Path of file to open string to be converted to SILE content.
536
-- @tparam[opt] nil|string format The name of the formatter. If nil, defaults to using each intputter's auto detection.
537
-- @tparam[opt] nil|table options Options to pass to the inputter instance when instantiated.
538
function SILE.processFile (filename, format, options)
34✔
539
   local lfs = require("lfs")
10✔
540
   local doc
541
   if filename == "-" then
10✔
542
      filename = "STDIN"
×
543
      doc = io.stdin:read("*a")
×
544
   else
545
      -- Turn slashes around in the event we get passed a path from a Windows shell
546
      filename = filename:gsub("\\", "/")
10✔
547
      if not SILE.masterFilename then
10✔
548
         SILE.masterFilename = pl.path.splitext(pl.path.normpath(filename))
40✔
549
      end
550
      if SILE.input.filenames[1] and not SILE.masterDir then
10✔
551
         SILE.masterDir = pl.path.dirname(SILE.input.filenames[1])
20✔
552
      end
553
      if SILE.masterDir and SILE.masterDir:len() >= 1 then
20✔
554
         _G.extendSilePath(SILE.masterDir)
10✔
555
         _G.extendSilePathRocks(SILE.masterDir .. "/lua_modules")
10✔
556
      end
557
      filename = SILE.resolveFile(filename) or SU.error("Could not find file")
20✔
558
      local mode = lfs.attributes(filename).mode
10✔
559
      if mode ~= "file" and mode ~= "named pipe" then
10✔
560
         SU.error(filename .. " isn't a file or named pipe, it's a " .. mode .. "!")
×
561
      end
562
      if SILE.makeDeps then
10✔
563
         SILE.makeDeps:add(filename)
×
564
      end
565
      local file, err = io.open(filename)
10✔
566
      if not file then
10✔
567
         print("Could not open " .. filename .. ": " .. err)
×
568
         return
×
569
      end
570
      doc = file:read("*a")
10✔
571
   end
572
   local cpf = SILE.currentlyProcessingFile
10✔
573
   SILE.currentlyProcessingFile = filename
10✔
574
   local pId = SILE.traceStack:pushDocument(filename, doc)
10✔
575
   local ret = SILE.processString(doc, format, filename, options)
10✔
576
   SILE.traceStack:pop(pId)
10✔
577
   SILE.currentlyProcessingFile = cpf
10✔
578
   return ret
10✔
579
end
580

581
-- TODO: this probably needs deprecating, moved here just to get out of the way so
582
-- typesetters classing works as expected
583
function SILE.typesetNaturally (frame, func)
34✔
584
   local saveTypesetter = SILE.typesetter
14✔
585
   if SILE.typesetter.frame then
14✔
586
      SILE.typesetter.frame:leave(SILE.typesetter)
14✔
587
   end
588
   SILE.typesetter = SILE.typesetters.base(frame)
28✔
589
   SILE.settings:temporarily(func)
14✔
590
   SILE.typesetter:leaveHmode()
14✔
591
   SILE.typesetter:chuck()
14✔
592
   SILE.typesetter.frame:leave(SILE.typesetter)
14✔
593
   SILE.typesetter = saveTypesetter
14✔
594
   if SILE.typesetter.frame then
14✔
595
      SILE.typesetter.frame:enter(SILE.typesetter)
14✔
596
   end
597
end
598

599
--- Resolve relative file paths to identify absolute resources locations.
600
-- Makes it possible to load resources from relative paths, relative to a document or project or SILE itself.
601
-- @tparam string filename Name of file to find using the same order of precedence logic in `require()`.
602
-- @tparam[opt] nil|string pathprefix Optional prefix in which to look for if the file isn't found otherwise.
603
function SILE.resolveFile (filename, pathprefix)
34✔
604
   local candidates = {}
10✔
605
   -- Start with the raw file name as given prefixed with a path if requested
606
   if pathprefix then
10✔
607
      candidates[#candidates + 1] = pl.path.join(pathprefix, "?")
×
608
   end
609
   -- Also check the raw file name without a path
610
   candidates[#candidates + 1] = "?"
10✔
611
   -- Iterate through the directory of the master file, the SILE_PATH variable, and the current directory
612
   -- Check for prefixed paths first, then the plain path in that fails
613
   if SILE.masterDir then
10✔
614
      for path in SU.gtoke(SILE.masterDir .. ";" .. tostring(os.getenv("SILE_PATH")), ";") do
90✔
615
         if path.string and path.string ~= "nil" then
70✔
616
            if pathprefix then
40✔
617
               candidates[#candidates + 1] = pl.path.join(path.string, pathprefix, "?")
×
618
            end
619
            candidates[#candidates + 1] = pl.path.join(path.string, "?")
80✔
620
         end
621
      end
622
   end
623
   -- Return the first candidate that exists, also checking the .sil suffix
624
   local path = table.concat(candidates, ";")
10✔
625
   local resolved, err = package.searchpath(filename, path, "/")
10✔
626
   if resolved then
10✔
627
      if SILE.makeDeps then
10✔
628
         SILE.makeDeps:add(resolved)
×
629
      end
630
   elseif SU.debugging("paths") then
×
631
      SU.debug("paths", ("Unable to find file '%s': %s"):format(filename, err))
×
632
   end
633
   return resolved
10✔
634
end
635

636
--- Execute a registered SILE command.
637
-- Uses a function previously registered by any modules explicitly loaded by the user at runtime via `--use`, the SILE
638
-- core, the document class, or any loaded package.
639
-- @tparam string command Command name.
640
-- @tparam[opt={}] nil|table options Options to pass to the command.
641
-- @tparam[opt] nil|table content Any valid AST node to be processed by the command.
642
function SILE.call (command, options, content)
34✔
643
   options = options or {}
250✔
644
   content = content or {}
250✔
645
   if SILE.traceback and type(content) == "table" and not content.lno then
250✔
646
      -- This call is from code (no content.lno) and we want to spend the time
647
      -- to determine everything we need about the caller
648
      local caller = debug.getinfo(2, "Sl")
×
649
      content.file, content.lno = caller.short_src, caller.currentline
×
650
   end
651
   local pId = SILE.traceStack:pushCommand(command, content, options)
250✔
652
   if not SILE.Commands[command] then
250✔
653
      SU.error("Unknown command " .. command)
×
654
   end
655
   local result = SILE.Commands[command](options, content)
250✔
656
   SILE.traceStack:pop(pId)
250✔
657
   return result
250✔
658
end
659

660
--- (Deprecated) Register a function as a SILE command.
661
-- Takes any Lua function and registers it for use as a SILE command (which will in turn be used to process any content
662
-- nodes identified with the command name.
663
--
664
-- Note that alternative versions of this action are available as methods on document classes and packages. Those
665
-- interfaces should be preferred to this global one.
666
-- @tparam string name Name of cammand to register.
667
-- @tparam function func Callback function to use as command handler.
668
-- @tparam[opt] nil|string help User friendly short usage string for use in error messages, documentation, etc.
669
-- @tparam[opt] nil|string pack Information identifying the module registering the command for use in error and usage
670
-- messages. Usually auto-detected.
671
-- @see SILE.classes:registerCommand
672
-- @see SILE.packages:registerCommand
673
function SILE.registerCommand (name, func, help, pack, cheat)
34✔
674
   local class = SILE.documentState.documentClass
103✔
675
   if not cheat then
103✔
676
      SU.deprecated(
×
677
         "SILE.registerCommand",
678
         "class:registerCommand",
679
         "0.14.0",
680
         "0.16.0",
681
         [[
×
682
            Commands are being scoped to the document classes they are loaded into rather
683
            than being globals.
684
         ]]
×
685
      )
686
   end
687
   -- Shimming until we have all scope cheating removed from core
688
   if not cheat or not class or class.type ~= "class" then
103✔
689
      return SILE.classes.base.registerCommand(nil, name, func, help, pack)
120✔
690
   end
691
   return class:registerCommand(name, func, help, pack)
×
692
end
693

694
--- Wrap an existing command with new default options.
695
-- Modifies an already registered SILE command with a new table of options to be used as default values any time it is
696
-- called. Calling options still take precedence.
697
-- @tparam string command Name of command to overwrite.
698
-- @tparam table options Options to set as updated defaults.
699
function SILE.setCommandDefaults (command, options)
34✔
700
   local oldCommand = SILE.Commands[command]
×
701
   SILE.Commands[command] = function (defaults, content)
×
702
      for k, v in pairs(options) do
×
703
         defaults[k] = defaults[k] or v
×
704
      end
705
      return oldCommand(defaults, content)
×
706
   end
707
end
708

709
-- TODO: Move to new table entry handler in types.unit
710
function SILE.registerUnit (unit, spec)
34✔
711
   -- If a unit exists already, clear it first so we get fresh meta table entries, see #1607
712
   if SILE.types.unit[unit] then
×
713
      SILE.types.unit[unit] = nil
×
714
   end
715
   SILE.types.unit[unit] = spec
×
716
end
717

718
function SILE.paperSizeParser (size)
34✔
719
   SU.deprecated("SILE.paperSizeParser", "SILE.papersize", "0.15.0", "0.16.0")
×
720
   return SILE.papersize(size)
×
721
end
722

723
--- Finalize document processing
724
-- Signals that all the `SILE.process()` calls have been made and SILE should move on to finish up the output
725
--
726
-- 1. Tells the document class to run its `:finish()` method. This method is typically responsible for calling the
727
-- `:finish()` method of the outputter module in the appropriate sequence.
728
-- 2. Closes out anything in active memory we don't need like font instances.
729
-- 3. Evaluate any snippets in SILE.input.evalAfter table.
730
-- 4. Stops logging dependencies and writes them to a makedepends file if requested.
731
-- 5. Close out the Lua profiler if it was running.
732
-- 6. Output version information if versions debug flag is set.
733
function SILE.finish ()
34✔
734
   SILE.documentState.documentClass:finish()
10✔
735
   SILE.font.finish()
10✔
736
   runEvals(SILE.input.evaluateAfters, "evaluate-after")
10✔
737
   if SILE.makeDeps then
10✔
738
      SILE.makeDeps:write()
×
739
   end
740
   if not SILE.quiet then
10✔
741
      io.stderr:write("\n")
10✔
742
   end
743
   if SU.debugging("profile") then
20✔
744
      ProFi:stop()
×
745
      ProFi:writeReport(pl.path.splitext(SILE.input.filenames[1]) .. ".profile.txt")
×
746
   end
747
   if SU.debugging("versions") then
20✔
748
      SILE.shaper:debugVersions()
10✔
749
   end
750
end
751

752
-- Internal libraries that return classes, but we only ever use one instantiation
753
SILE.traceStack = require("core.tracestack")()
34✔
754
SILE.settings = require("core.settings")()
34✔
755

756
-- Internal libraries that run core SILE functions on load
757
require("core.hyphenator-liang")
17✔
758
require("core.languages")
17✔
759
SILE.linebreak = require("core.break")
17✔
760
require("core.frame")
17✔
761
SILE.font = require("core.font")
17✔
762

763
return SILE
17✔
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