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

sile-typesetter / sile / 12313034533

13 Dec 2024 09:28AM UTC coverage: 60.234% (-0.7%) from 60.941%
12313034533

push

github

web-flow
Merge 5a7694dff into d737b2656

9 of 25 new or added lines in 5 files covered. (36.0%)

145 existing lines in 16 files now uncovered.

12801 of 21252 relevant lines covered (60.23%)

2545.46 hits per line

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

67.38
/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 = {}
385✔
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")
385✔
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")
385✔
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)
770✔
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"
385✔
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)
385✔
49

50
--- Default to verbose mode, can be changed from the CLI or by libraries
51
--- @boolean quiet
52
SILE.quiet = false
385✔
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
385✔
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")
385✔
65
SU = SILE.utilities -- regrettable global alias
385✔
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")
385✔
71

72
-- On demand loader, allows modules to be loaded into a specific scope but
73
-- only when/if accessed.
74
local function core_loader (scope)
75
   return setmetatable({}, {
3,080✔
76
      __index = function (self, key)
77
         -- local var = rawget(self, key)
78
         local m = require(("%s.%s"):format(scope, key))
3,306✔
79
         self[key] = m
3,304✔
80
         return m
3,304✔
81
      end,
82
   })
3,080✔
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 = {}
385✔
91

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

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

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

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

169
-- Internal libraries that don't try to use anything on load, only provide something
170
SILE.parserBits = require("core.parserbits")
385✔
171
SILE.frameParser = require("core.frameparser")
385✔
172
SILE.fontManager = require("core.fontmanager")
385✔
173
SILE.papersize = require("core.papersize")
385✔
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
386✔
182
      local pId = SILE.traceStack:pushText(snippet)
×
183
      local status, func = pcall(load, snippet)
×
184
      if status and type(func) == "function" then
×
185
         func()
×
186
      else
187
         SU.error(("Error parsing code provided in --%s snippet: %s"):format(arg, snippet))
×
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 ()
194✔
208
   if SILE.backend then
50✔
209
      SU.deprecated("SILE.backend", "SILE.input.backend", "0.15.7", "0.17.0")
×
210
      SILE.input.backend = SILE.backend
×
211
   end
212
   if not SILE.input.backend then
50✔
213
      SILE.input.backend = "libtexpdf"
50✔
214
   end
215
   if SILE.input.backend == "libtexpdf" then
50✔
216
      SILE.shaper = SILE.shapers.harfbuzz()
150✔
217
      SILE.outputter = SILE.outputters.libtexpdf()
150✔
218
   elseif SILE.input.backend == "cairo" then
×
219
      SILE.shaper = SILE.shapers.pango()
×
220
      SILE.outputter = SILE.outputters.cairo()
×
221
   elseif SILE.input.backend == "debug" then
×
222
      SILE.shaper = SILE.shapers.harfbuzz()
×
223
      SILE.outputter = SILE.outputters.debug()
×
224
   elseif SILE.input.backend == "text" then
×
225
      SILE.shaper = SILE.shapers.harfbuzz()
×
226
      SILE.outputter = SILE.outputters.text()
×
227
   elseif SILE.input.backend == "dummy" then
×
228
      SILE.shaper = SILE.shapers.harfbuzz()
×
229
      SILE.outputter = SILE.outputters.dummy()
×
230
   end
231
   SILE.pagebuilder = SILE.pagebuilders.base()
150✔
232
   io.stdout:setvbuf("no")
50✔
233
   if SU.debugging("profile") then
100✔
234
      ProFi = require("ProFi")
×
235
      ProFi:start()
×
236
   end
237
   if SILE.makeDeps then
50✔
238
      SILE.makeDeps:add(_G.executablePath)
×
239
   end
240
   runEvals(SILE.input.evaluates, "evaluate")
50✔
241
end
242

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

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

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

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

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

260
    Thereafter running SILE again should work as expected:
261

262
       sile %s
263

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

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

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

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

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

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

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

401
--- Process content.
402
-- This is the main 'action' SILE does. Once input files are parsed into an abstract syntax tree, then we recursively
403
-- iterate through the tree handling each item in the order encountered.
404
-- @tparam table ast SILE content in abstract syntax tree format (a table of strings, functions, or more AST trees).
405
function SILE.process (ast)
194✔
406
   if not ast then
214✔
UNCOV
407
      return
×
408
   end
409
   if SU.debugging("ast") then
428✔
410
      SU.debugAST(ast, 0)
×
411
   end
412
   if type(ast) == "function" then
214✔
413
      return ast()
27✔
414
   end
415
   for _, content in ipairs(ast) do
1,218✔
416
      if type(content) == "string" then
1,031✔
417
         SILE.typesetter:typeset(content)
1,156✔
418
      elseif type(content) == "function" then
453✔
UNCOV
419
         content()
×
420
      elseif SILE.Commands[content.command] then
453✔
421
         SILE.call(content.command, content.options, content)
906✔
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", "sil" }
97✔
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
50✔
439
      for _, format in ipairs(preloadedinputters) do
200✔
440
         local _ = SILE.inputters[format]
150✔
441
      end
442
   end
443
   local contentDetectionOrder = {}
50✔
444
   for _, inputter in pairs(SILE.inputters) do
200✔
445
      if inputter.order then
150✔
446
         table.insert(contentDetectionOrder, inputter)
150✔
447
      end
448
   end
449
   table.sort(contentDetectionOrder, function (a, b)
100✔
450
      return a.order < b.order
137✔
451
   end)
452
   local initialround = filename and 1 or 2
50✔
453
   for round = initialround, 3 do
50✔
454
      for _, inputter in ipairs(contentDetectionOrder) do
93✔
455
         SU.debug("inputter", "Running content type detection round", round, "with", inputter._name)
93✔
456
         if inputter.appropriate(round, filename, doc) then
186✔
457
            return inputter._name
50✔
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)
194✔
471
   local cpf
472
   if not filename then
64✔
473
      cpf = SILE.currentlyProcessingFile
14✔
474
      local caller = debug.getinfo(2, "Sl")
14✔
475
      SILE.currentlyProcessingFile = caller.short_src .. ":" .. caller.currentline
14✔
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])
214✔
484
      and SILE.inputter
50✔
485
   then
486
      inputter = SILE.inputter
×
487
   else
488
      format = format or detectFormat(doc, filename)
114✔
489
      if not SILE.quiet then
64✔
490
         io.stderr:write(("<%s> as %s\n"):format(SILE.currentlyProcessingFile, format))
64✔
491
      end
492
      inputter = SILE.inputters[format](options)
128✔
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
164✔
496
         SILE.inputter = inputter
50✔
497
      end
498
   end
499
   local pId = SILE.traceStack:pushDocument(SILE.currentlyProcessingFile, doc)
64✔
500
   inputter:process(doc)
64✔
501
   SILE.traceStack:pop(pId)
64✔
502
   if cpf then
64✔
503
      SILE.currentlyProcessingFile = cpf
14✔
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)
194✔
514
   local lfs = require("lfs")
50✔
515
   local doc
516
   if filename == "-" then
50✔
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("\\", "/")
50✔
522
      if not SILE.masterFilename then
50✔
523
         SILE.masterFilename = pl.path.splitext(pl.path.normpath(filename))
200✔
524
      end
525
      if SILE.input.filenames[1] and not SILE.masterDir then
50✔
526
         SILE.masterDir = pl.path.dirname(SILE.input.filenames[1])
100✔
527
      end
528
      if SILE.masterDir and SILE.masterDir:len() >= 1 then
100✔
529
         _G.extendSilePath(SILE.masterDir)
50✔
530
         _G.extendSilePathRocks(SILE.masterDir .. "/lua_modules")
50✔
531
      end
532
      filename = SILE.resolveFile(filename) or SU.error("Could not find file")
100✔
533
      local mode = lfs.attributes(filename).mode
50✔
534
      if mode ~= "file" and mode ~= "named pipe" then
50✔
535
         SU.error(filename .. " isn't a file or named pipe, it's a " .. mode .. "!")
×
536
      end
537
      if SILE.makeDeps then
50✔
538
         SILE.makeDeps:add(filename)
×
539
      end
540
      local file, err = io.open(filename)
50✔
541
      if not file then
50✔
542
         print("Could not open " .. filename .. ": " .. err)
×
543
         return
×
544
      end
545
      doc = file:read("*a")
50✔
546
   end
547
   local cpf = SILE.currentlyProcessingFile
50✔
548
   SILE.currentlyProcessingFile = filename
50✔
549
   local pId = SILE.traceStack:pushDocument(filename, doc)
50✔
550
   local ret = SILE.processString(doc, format, filename, options)
50✔
551
   SILE.traceStack:pop(pId)
50✔
552
   SILE.currentlyProcessingFile = cpf
50✔
553
   return ret
50✔
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)
194✔
559
   local saveTypesetter = SILE.typesetter
39✔
560
   if SILE.typesetter.frame then
39✔
561
      SILE.typesetter.frame:leave(SILE.typesetter)
39✔
562
   end
563
   SILE.typesetter = SILE.typesetters.base(frame)
78✔
564
   SILE.settings:temporarily(func)
39✔
565
   SILE.typesetter:leaveHmode()
39✔
566
   SILE.typesetter:chuck()
39✔
567
   SILE.typesetter.frame:leave(SILE.typesetter)
39✔
568
   SILE.typesetter = saveTypesetter
39✔
569
   if SILE.typesetter.frame then
39✔
570
      SILE.typesetter.frame:enter(SILE.typesetter)
39✔
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)
194✔
579
   local candidates = {}
51✔
580
   -- Start with the raw file name as given prefixed with a path if requested
581
   if pathprefix then
51✔
582
      candidates[#candidates + 1] = pl.path.join(pathprefix, "?")
×
583
   end
584
   -- Also check the raw file name without a path
585
   candidates[#candidates + 1] = "?"
51✔
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
51✔
589
      for path in SU.gtoke(SILE.masterDir .. ";" .. tostring(os.getenv("SILE_PATH")), ";") do
459✔
590
         if path.string and path.string ~= "nil" then
357✔
591
            if pathprefix then
204✔
592
               candidates[#candidates + 1] = pl.path.join(path.string, pathprefix, "?")
×
593
            end
594
            candidates[#candidates + 1] = pl.path.join(path.string, "?")
408✔
595
         end
596
      end
597
   end
598
   -- Return the first candidate that exists, also checking the .sil suffix
599
   local path = table.concat(candidates, ";")
51✔
600
   local resolved, err = package.searchpath(filename, path, "/")
51✔
601
   if resolved then
51✔
602
      if SILE.makeDeps then
51✔
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
51✔
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)
194✔
618
   options = options or {}
1,592✔
619
   content = content or {}
1,592✔
620
   if SILE.traceback and type(content) == "table" and not content.lno then
1,592✔
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)
1,592✔
627
   if not SILE.Commands[command] then
1,592✔
628
      SU.error("Unknown command " .. command)
×
629
   end
630
   local result = SILE.Commands[command](options, content)
1,592✔
631
   SILE.traceStack:pop(pId)
1,592✔
632
   return result
1,592✔
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)
194✔
649
   local class = SILE.documentState.documentClass
577✔
650
   if not cheat then
577✔
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
577✔
664
      return SILE.classes.base.registerCommand(nil, name, func, help, pack)
673✔
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)
194✔
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)
194✔
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
15✔
688
      SILE.types.unit[unit] = nil
×
689
   end
690
   SILE.types.unit[unit] = spec
15✔
691
end
692

693
function SILE.paperSizeParser (size)
194✔
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 ()
194✔
709
   SILE.documentState.documentClass:finish()
50✔
710
   SILE.font.finish()
50✔
711
   runEvals(SILE.input.evaluateAfters, "evaluate-after")
50✔
712
   if SILE.makeDeps then
50✔
713
      SILE.makeDeps:write()
×
714
   end
715
   if not SILE.quiet then
50✔
716
      io.stderr:write("\n")
50✔
717
   end
718
   if SU.debugging("profile") then
100✔
719
      ProFi:stop()
×
720
      ProFi:writeReport(pl.path.splitext(SILE.input.filenames[1]) .. ".profile.txt")
×
721
   end
722
   if SU.debugging("versions") then
100✔
723
      SILE.shaper:debugVersions()
50✔
724
   end
725
end
726

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

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

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