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

sile-typesetter / sile / 9409557472

07 Jun 2024 12:09AM UTC coverage: 69.448% (-4.5%) from 73.988%
9409557472

push

github

alerque
fix(build): Distribute vendored compat-5.3.c source file

12025 of 17315 relevant lines covered (69.45%)

6023.46 hits per line

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

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

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

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

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

50
-- Backport of lots of Lua 5.3 features to Lua 5.[12]
51
if not SILE.lua_isjit and SILE.lua_version < "5.3" then
375✔
52
   require("compat53")
×
53
end
54

55
--- Modules
56
-- @section modules
57

58
--- Utilities module, typically accessed via `SU` alias.
59
-- @see SU
60
SILE.utilities = require("core.utilities")
375✔
61
SU = SILE.utilities -- regrettable global alias
375✔
62

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

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

81
--- Data tables
82
--- @section data
83

84
--- Stash of all Lua functions used to power typesetter commands.
85
-- @table Commands
86
SILE.Commands = {}
375✔
87

88
--- Short usage messages corresponding to typesetter commands.
89
-- @table Help
90
SILE.Help = {}
375✔
91

92
--- List of currently enabled debug flags.
93
-- E.g. `{ typesetter = true, frames, true }`.
94
-- @table debugFlags
95
SILE.debugFlags = {}
375✔
96

97
SILE.nodeMakers = {}
375✔
98
SILE.tokenizers = {}
375✔
99
SILE.status = {}
375✔
100

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

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

120
--- Callback functions for handling types of raw content.
121
-- All registered handlers for raw content blocks have an entry in this table with the type as the key and the
122
-- processing function as the value.
123
-- @ table rawHandlers
124
SILE.rawHandlers = {}
375✔
125

126
--- User input
127
-- @section input
128

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

155
-- Internal libraries that are idempotent and return classes that need instantiation
156
SILE.inputters = core_loader("inputters")
750✔
157
SILE.shapers = core_loader("shapers")
750✔
158
SILE.outputters = core_loader("outputters")
750✔
159
SILE.classes = core_loader("classes")
750✔
160
SILE.packages = core_loader("packages")
750✔
161
SILE.typesetters = core_loader("typesetters")
750✔
162
SILE.pagebuilders = core_loader("pagebuilders")
750✔
163
SILE.types = core_loader("types")
750✔
164

165
-- Internal libraries that don't try to use anything on load, only provide something
166
SILE.parserBits = require("core.parserbits")
375✔
167
SILE.frameParser = require("core.frameparser")
375✔
168
SILE.fontManager = require("core.fontmanager")
375✔
169
SILE.papersize = require("core.papersize")
375✔
170

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

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

189
--- Core functions
190
-- @section functions
191

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

235
local function suggest_luarocks (module)
236
   local guessed_module_name = module:gsub(".*%.", "") .. ".sile"
×
237
   return ([[
×
238

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

244
        luarocks --lua-version %s --tree lua_modules install %s
245

246
    This will install the LuaRocks to your project, then you need to tell your
247
    shell to pass along that info about available LuaRocks paths to SILE. This
248
    only needs to be done once in each shell.
249

250
        eval $(luarocks --lua-version %s --tree lua_modules path)
251

252
    Thereafter running SILE again should work as expected:
253

254
       sile %s
255

256
    ]]):format(SILE.lua_version, guessed_module_name, SILE.lua_version, pl.stringx.join(" ", _G.arg or {}))
×
257
end
258

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

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

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

409
local preloadedinputters = { "xml", "lua", "sil" }
375✔
410

411
local function detectFormat (doc, filename)
412
   -- Preload default reader types so content detection has something to work with
413
   if #SILE.inputters == 0 then
189✔
414
      for _, format in ipairs(preloadedinputters) do
756✔
415
         local _ = SILE.inputters[format]
567✔
416
      end
417
   end
418
   local contentDetectionOrder = {}
189✔
419
   for _, inputter in pairs(SILE.inputters) do
756✔
420
      if inputter.order then
567✔
421
         table.insert(contentDetectionOrder, inputter)
567✔
422
      end
423
   end
424
   table.sort(contentDetectionOrder, function (a, b)
378✔
425
      return a.order < b.order
516✔
426
   end)
427
   local initialround = filename and 1 or 2
189✔
428
   for round = initialround, 3 do
192✔
429
      for _, inputter in ipairs(contentDetectionOrder) do
383✔
430
         SU.debug("inputter", "Running content type detection round", round, "with", inputter._name)
380✔
431
         if inputter.appropriate(round, filename, doc) then
760✔
432
            return inputter._name
189✔
433
         end
434
      end
435
   end
436
   SU.error(("Unable to pick inputter to process input from '%s'"):format(filename))
×
437
end
438

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

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

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

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

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

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

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

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

665
function SILE.paperSizeParser (size)
750✔
666
   SU.deprecated("SILE.paperSizeParser", "SILE.papersize", "0.15.0", "0.16.0")
×
667
   return SILE.papersize(size)
×
668
end
669

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

699
-- Internal libraries that return classes, but we only ever use one instantiation
700
SILE.traceStack = require("core.tracestack")()
750✔
701
SILE.settings = require("core.settings")()
750✔
702

703
-- Internal libraries that run core SILE functions on load
704
require("core.hyphenator-liang")
375✔
705
require("core.languages")
375✔
706
SILE.linebreak = require("core.break")
375✔
707
require("core.frame")
375✔
708
SILE.font = require("core.font")
375✔
709

710
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

© 2025 Coveralls, Inc