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

sile-typesetter / sile / 7246678005

18 Dec 2023 10:19AM UTC coverage: 67.096% (-7.5%) from 74.62%
7246678005

push

github

web-flow
chore(deps): Bump actions/upload-artifact from 3 to 4 (#1940)

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

10583 of 15773 relevant lines covered (67.1%)

3150.6 hits per line

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

75.08
/core/sile.lua
1
-- Initialize SILE internals
2
SILE = {}
1✔
3

4
SILE.version = require("core.version")
1✔
5
SILE.features = require("core.features")
1✔
6

7
-- Initialize Lua environment and global utilities
8
SILE.lua_version = _VERSION:sub(-3)
2✔
9
-- luacheck: ignore jit
10
SILE.lua_isjit = type(jit) == "table"
1✔
11
SILE.full_version = string.format("SILE %s (%s)", SILE.version, SILE.lua_isjit and jit.version or _VERSION)
1✔
12

13
-- Backport of lots of Lua 5.3 features to Lua 5.[12]
14
if not SILE.lua_isjit and SILE.lua_version < "5.3" then require("compat53") end
1✔
15

16
-- Penlight on-demand module loader, provided for SILE and document usage
17
pl = require("pl.import_into")()
2✔
18

19
-- For developer testing only, usually in CI
20
if os.getenv("SILE_COVERAGE") then require("luacov") end
1✔
21

22
-- Lua 5.3+ has a UTF-8 safe string function module but it is somewhat
23
-- underwhelming. This module includes more functions and supports older Lua
24
-- versions. Docs: https://github.com/starwing/luautf8
25
luautf8 = require("lua-utf8")
65✔
26

27
-- Localization library, provided as global
28
fluent = require("fluent")()
129✔
29

30
-- Includes for _this_ scope
31
local lfs = require("lfs")
64✔
32

33
-- Developer tooling profiler
34
local ProFi
35

36
SILE.utilities = require("core.utilities")
64✔
37
SU = SILE.utilities -- regrettable global alias
64✔
38

39
-- On demand loader, allows modules to be loaded into a specific scope but
40
-- only when/if accessed.
41
local core_loader = function (scope)
42
  return setmetatable({}, {
448✔
43
    __index = function (self, key)
44
      -- local var = rawget(self, key)
45
      local m = require(("%s.%s"):format(scope, key))
516✔
46
      self[key] = m
516✔
47
      return m
516✔
48
    end
49
  })
448✔
50
end
51

52
SILE.Commands = {}
64✔
53
SILE.Help = {}
64✔
54
SILE.debugFlags = {}
64✔
55
SILE.nodeMakers = {}
64✔
56
SILE.tokenizers = {}
64✔
57
SILE.status = {}
64✔
58
SILE.scratch = {}
64✔
59
SILE.documentState = {}
64✔
60
SILE.rawHandlers = {}
64✔
61

62
-- User input values, currently from CLI options, potentially all the inuts
63
-- needed for a user to use a SILE-as-a-library version to produce documents
64
-- programmatically.
65
SILE.input = {
64✔
66
  filenames = {},
64✔
67
  evaluates = {},
64✔
68
  evaluateAfters = {},
64✔
69
  includes = {},
64✔
70
  uses = {},
64✔
71
  options = {},
64✔
72
  preambles = {},
64✔
73
  postambles = {},
64✔
74
}
64✔
75

76
-- Internal libraries that are idempotent and return classes that need instantiation
77
SILE.inputters = core_loader("inputters")
128✔
78
SILE.shapers = core_loader("shapers")
128✔
79
SILE.outputters = core_loader("outputters")
128✔
80
SILE.classes = core_loader("classes")
128✔
81
SILE.packages = core_loader("packages")
128✔
82
SILE.typesetters = core_loader("typesetters")
128✔
83
SILE.pagebuilders = core_loader("pagebuilders")
128✔
84

85
-- Internal libraries that don't make assumptions on load, only use
86
SILE.traceStack = require("core.tracestack")()
128✔
87
SILE.parserBits = require("core.parserbits")
64✔
88
SILE.frameParser = require("core.frameparser")
64✔
89
SILE.color = require("core.color")
64✔
90
SILE.units = require("core.units")
64✔
91
SILE.fontManager = require("core.fontmanager")
64✔
92

93
-- Internal libraries that assume globals, may be picky about load order
94
SILE.measurement = require("core.measurement")
64✔
95
SILE.length = require("core.length")
64✔
96
SILE.papersize = require("core.papersize")
64✔
97
SILE.nodefactory = require("core.nodefactory")
64✔
98

99
-- NOTE:
100
-- See remainaing internal libraries loaded at the end of this file because
101
-- they run core SILE functions on load instead of waiting to be called (or
102
-- depend on others that do).
103

104
local function runEvals (evals, arg)
105
  for _, snippet in ipairs(evals) do
128✔
106
    local pId = SILE.traceStack:pushText(snippet)
×
107
    local status, func = pcall(load, snippet)
×
108
    if status then
×
109
      func()
×
110
    else
111
      SU.error(("Error parsing code provided in --%s snippet: %s"):format(arg, func))
×
112
    end
113
    SILE.traceStack:pop(pId)
×
114
  end
115
end
116

117
SILE.init = function ()
64✔
118
  if not SILE.backend then
64✔
119
    SILE.backend = "libtexpdf"
64✔
120
  end
121
  if SILE.backend == "libtexpdf" then
64✔
122
    SILE.shaper = SILE.shapers.harfbuzz()
192✔
123
    SILE.outputter = SILE.outputters.libtexpdf()
192✔
124
  elseif SILE.backend == "cairo" then
×
125
    SILE.shaper = SILE.shapers.pango()
×
126
    SILE.outputter = SILE.outputters.cairo()
×
127
  elseif SILE.backend == "debug" then
×
128
    SILE.shaper = SILE.shapers.harfbuzz()
×
129
    SILE.outputter = SILE.outputters.debug()
×
130
  elseif SILE.backend == "text" then
×
131
    SILE.shaper = SILE.shapers.harfbuzz()
×
132
    SILE.outputter = SILE.outputters.text()
×
133
  elseif SILE.backend == "dummy" then
×
134
    SILE.shaper = SILE.shapers.harfbuzz()
×
135
    SILE.outputter = SILE.outputters.dummy()
×
136
  end
137
  SILE.pagebuilder = SILE.pagebuilders.base()
192✔
138
  io.stdout:setvbuf("no")
64✔
139
  if SU.debugging("profile") then
128✔
140
    ProFi = require("ProFi")
×
141
    ProFi:start()
×
142
  end
143
  if SILE.makeDeps then
64✔
144
    SILE.makeDeps:add(_G.executablePath)
64✔
145
  end
146
  runEvals(SILE.input.evaluates, "evaluate")
64✔
147
end
148

149
local function suggest_luarocks (module)
150
  local guessed_module_name = module:gsub(".*%.", "") .. ".sile"
×
151
  return ([[
×
152

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

158
        luarocks --lua-version %s --tree lua_modules install %s
159

160
    This will install the LuaRocks to your project, then you need to tell your
161
    shell to pass along that info about available LuaRocks paths to SILE. This
162
    only needs to be done once in each shell.
163

164
        eval $(luarocks --lua-version %s --tree lua_modules path)
165

166
    Thereafter running SILE again should work as expected:
167

168
       sile %s
169

170
    ]]):format(
×
171
        SILE.lua_version,
×
172
        guessed_module_name,
173
        SILE.lua_version,
×
174
        pl.stringx.join(" ", _G.arg)
×
175
        )
176
end
177

178
SILE.use = function (module, options)
64✔
179
  local status, pack
180
  if type(module) == "string" then
45✔
181
    status, pack = pcall(require, module)
44✔
182
    if not status then
44✔
183
      SU.error(("Unable to use '%s':\n%s%s")
×
184
        :format(module, SILE.traceback and ("    Lua ".. pack) or "", suggest_luarocks(module)))
×
185
    end
186
  elseif type(module) == "table" then
1✔
187
    pack = module
1✔
188
  end
189
  local name = pack._name
45✔
190
  local class = SILE.documentState.documentClass
45✔
191
  if not pack.type then
45✔
192
    SU.error("Modules must declare their type")
×
193
  elseif pack.type == "class" then
45✔
194
    SILE.classes[name] = pack
×
195
    if class then
×
196
      SU.error("Cannot load a class after one is already instantiated")
×
197
    end
198
    SILE.scratch.class_from_uses = pack
×
199
  elseif pack.type == "inputter" then
45✔
200
    SILE.inputters[name] = pack
×
201
    SILE.inputter = pack(options)
×
202
  elseif pack.type == "outputter" then
45✔
203
    SILE.outputters[name] = pack
×
204
    SILE.outputter = pack(options)
×
205
  elseif pack.type == "shaper" then
45✔
206
    SILE.shapers[name] = pack
×
207
    SILE.shaper = pack(options)
×
208
  elseif pack.type == "typesetter" then
45✔
209
    SILE.typesetters[name] = pack
×
210
    SILE.typesetter = pack(options)
×
211
  elseif pack.type == "pagebuilder" then
45✔
212
    SILE.pagebuilders[name] = pack
×
213
    SILE.pagebuilder = pack(options)
×
214
  elseif pack.type == "package" then
45✔
215
    SILE.packages[name] = pack
45✔
216
    if class then
45✔
217
      pack(options)
90✔
218
    else
219
      table.insert(SILE.input.preambles, { pack = pack, options = options })
×
220
    end
221
  end
222
end
223

224
SILE.require = function (dependency, pathprefix, deprecation_ack)
64✔
225
  if pathprefix and not deprecation_ack then
66✔
226
    local notice = string.format([[
×
227
  Please don't use the path prefix mechanism; it was intended to provide
228
  alternate paths to override core components but never worked well and is
229
  causing portability problems. Just use Lua idiomatic module loading:
230
      SILE.require("%s", "%s") → SILE.require("%s.%s")]],
231
      dependency, pathprefix, pathprefix, dependency)
×
232
    SU.deprecated("SILE.require", "SILE.require", "0.13.0", nil, notice)
×
233
  end
234
  dependency = dependency:gsub(".lua$", "")
66✔
235
  local status, lib
236
  if pathprefix then
66✔
237
    -- Note this is not a *path*, it is a module identifier:
238
    -- https://github.com/sile-typesetter/sile/issues/1861
239
    status, lib = pcall(require, pl.stringx.join('.', { pathprefix, dependency }))
128✔
240
  end
241
  if not status then
66✔
242
    local prefixederror = lib
2✔
243
    status, lib = pcall(require, dependency)
2✔
244
    if not status then
2✔
245
      SU.error(("Unable to find module '%s'%s")
×
246
        :format(dependency, SILE.traceback and ((pathprefix and "\n  " .. prefixederror or "") .. "\n  " .. lib) or ""))
×
247
    end
248
  end
249
  local class = SILE.documentState.documentClass
66✔
250
  if not class and not deprecation_ack then
66✔
251
    SU.warn(string.format([[
×
252
  Use of SILE.require() is only supported in documents, packages, or class
253
  init functions. It will not function fully before the class is instantiated.
254
  Please just use the Lua require() function directly:
255
      SILE.require("%s") → require("%s")]], dependency, dependency))
×
256
  end
257
  if type(lib) == "table" and class then
66✔
258
    if lib.type == "package" then
2✔
259
      lib(class)
4✔
260
    else
261
      class:initPackage(lib)
×
262
    end
263
  end
264
  return lib
66✔
265
end
266

267
SILE.process = function (ast)
64✔
268
  if not ast then return end
387✔
269
  if SU.debugging("ast") then
774✔
270
    SU.debugAST(ast, 0)
×
271
  end
272
  if type(ast) == "function" then return ast() end
387✔
273
  for _, content in ipairs(ast) do
1,664✔
274
    if type(content) == "string" then
1,375✔
275
      SILE.typesetter:typeset(content)
1,588✔
276
    elseif type(content) == "function" then
581✔
277
      content()
×
278
    elseif SILE.Commands[content.command] then
581✔
279
      SILE.call(content.command, content.options, content)
1,160✔
280
    elseif content.id == "texlike_stuff"
1✔
281
      or (not content.command and not content.id) then
1✔
282
      local pId = SILE.traceStack:pushContent(content, "texlike_stuff")
1✔
283
      SILE.process(content)
1✔
284
      SILE.traceStack:pop(pId)
2✔
285
    elseif type(content) ~= "nil" then
×
286
      local pId = SILE.traceStack:pushContent(content)
×
287
      SU.error("Unknown command "..(tostring(content.command or content.id)))
×
288
      SILE.traceStack:pop(pId)
×
289
    end
290
  end
291
end
292

293
local preloadedinputters = { "xml", "lua", "sil" }
64✔
294

295
local function detectFormat (doc, filename)
296
  -- Preload default reader types so content detection has something to work with
297
  if #SILE.inputters == 0 then
64✔
298
    for _, format in ipairs(preloadedinputters) do
256✔
299
      local _ = SILE.inputters[format]
192✔
300
    end
301
  end
302
  local contentDetectionOrder = {}
64✔
303
  for _, inputter in pairs(SILE.inputters) do
256✔
304
    if inputter.order then table.insert(contentDetectionOrder, inputter) end
192✔
305
  end
306
  table.sort(contentDetectionOrder, function (a, b) return a.order < b.order end)
256✔
307
  local initialround = filename and 1 or 2
64✔
308
  for round = initialround, 3 do
67✔
309
    for _, inputter in ipairs(contentDetectionOrder) do
133✔
310
      SU.debug("inputter", "Running content type detection round", round, "with", inputter._name)
130✔
311
      if inputter.appropriate(round, filename, doc) then
260✔
312
        return inputter._name
64✔
313
      end
314
    end
315
  end
316
  SU.error(("Unable to pick inputter to process input from '%s'"):format(filename))
×
317
end
318

319
function SILE.processString (doc, format, filename, options)
128✔
320
  local cpf
321
  if not filename then
87✔
322
    cpf = SILE.currentlyProcessingFile
23✔
323
    local caller = debug.getinfo(2, "Sl")
23✔
324
    SILE.currentlyProcessingFile = caller.short_src..":"..caller.currentline
23✔
325
  end
326
  -- In the event we're processing the master file *and* the user gave us
327
  -- a specific inputter to use, use it at the exclusion of all content type
328
  -- detection
329
  local inputter
330
  if filename and pl.path.normcase(pl.path.normpath(filename)) == pl.path.normcase(SILE.input.filenames[1]) and SILE.inputter then
279✔
331
    inputter = SILE.inputter
×
332
  else
333
    format = format or detectFormat(doc, filename)
151✔
334
    if not SILE.quiet then
87✔
335
      io.stderr:write(("<%s> as %s\n"):format(SILE.currentlyProcessingFile, format))
87✔
336
    end
337
    inputter = SILE.inputters[format](options)
174✔
338
    -- If we did content detection *and* this is the master file, save the
339
    -- inputter for posterity and postambles
340
    if filename and pl.path.normcase(filename) == pl.path.normcase(SILE.input.filenames[1]:gsub("^-$", "STDIN")) then
215✔
341
      SILE.inputter = inputter
64✔
342
    end
343
  end
344
  local pId = SILE.traceStack:pushDocument(SILE.currentlyProcessingFile, doc)
87✔
345
  inputter:process(doc)
87✔
346
  SILE.traceStack:pop(pId)
87✔
347
  if cpf then SILE.currentlyProcessingFile = cpf end
87✔
348
end
349

350
function SILE.processFile (filename, format, options)
128✔
351
  local doc
352
  if filename == "-" then
64✔
353
    filename = "STDIN"
×
354
    doc = io.stdin:read("*a")
×
355
  else
356
    -- Turn slashes around in the event we get passed a path from a Windows shell
357
    filename = filename:gsub("\\", "/")
64✔
358
    if not SILE.masterFilename then
64✔
359
      SILE.masterFilename = pl.path.splitext(pl.path.normpath(filename))
256✔
360
    end
361
    if SILE.input.filenames[1] and not SILE.masterDir then
64✔
362
      SILE.masterDir = pl.path.dirname(SILE.input.filenames[1])
128✔
363
    end
364
    if SILE.masterDir and SILE.masterDir:len() >= 1 then
128✔
365
      _G.extendSilePath(SILE.masterDir)
64✔
366
    end
367
    filename = SILE.resolveFile(filename) or SU.error("Could not find file")
128✔
368
    local mode = lfs.attributes(filename).mode
64✔
369
    if mode ~= "file" and mode ~= "named pipe" then
64✔
370
      SU.error(filename.." isn't a file or named pipe, it's a ".. mode .."!")
×
371
    end
372
    if SILE.makeDeps then
64✔
373
      SILE.makeDeps:add(filename)
64✔
374
    end
375
    local file, err = io.open(filename)
64✔
376
    if not file then
64✔
377
      print("Could not open "..filename..": "..err)
×
378
      return
×
379
    end
380
    doc = file:read("*a")
64✔
381
  end
382
  local cpf = SILE.currentlyProcessingFile
64✔
383
  SILE.currentlyProcessingFile = filename
64✔
384
  local pId = SILE.traceStack:pushDocument(filename, doc)
64✔
385
  local ret = SILE.processString(doc, format, filename, options)
64✔
386
  SILE.traceStack:pop(pId)
64✔
387
  SILE.currentlyProcessingFile = cpf
64✔
388
  return ret
64✔
389
end
390

391
-- TODO: this probably needs deprecating, moved here just to get out of the way so
392
-- typesetters classing works as expected
393
SILE.typesetNaturally = function (frame, func)
64✔
394
  local saveTypesetter = SILE.typesetter
48✔
395
  if SILE.typesetter.frame then SILE.typesetter.frame:leave(SILE.typesetter) end
48✔
396
  SILE.typesetter = SILE.typesetters.base(frame)
96✔
397
  SILE.settings:temporarily(func)
48✔
398
  SILE.typesetter:leaveHmode()
48✔
399
  SILE.typesetter:chuck()
48✔
400
  SILE.typesetter.frame:leave(SILE.typesetter)
48✔
401
  SILE.typesetter = saveTypesetter
48✔
402
  if SILE.typesetter.frame then SILE.typesetter.frame:enter(SILE.typesetter) end
48✔
403
end
404

405
-- Sort through possible places files could be
406
function SILE.resolveFile (filename, pathprefix)
128✔
407
  local candidates = {}
67✔
408
  -- Start with the raw file name as given prefixed with a path if requested
409
  if pathprefix then candidates[#candidates+1] = pl.path.join(pathprefix, "?") end
67✔
410
  -- Also check the raw file name without a path
411
  candidates[#candidates+1] = "?"
67✔
412
  -- Iterate through the directory of the master file, the SILE_PATH variable, and the current directory
413
  -- Check for prefixed paths first, then the plain path in that fails
414
  if SILE.masterDir then
67✔
415
    for path in SU.gtoke(SILE.masterDir..";"..tostring(os.getenv("SILE_PATH")), ";") do
335✔
416
      if path.string and path.string ~= "nil" then
201✔
417
        if pathprefix then candidates[#candidates+1] = pl.path.join(path.string, pathprefix, "?") end
67✔
418
        candidates[#candidates+1] = pl.path.join(path.string, "?")
134✔
419
      end
420
    end
421
  end
422
  -- Return the first candidate that exists, also checking the .sil suffix
423
  local path = table.concat(candidates, ";")
67✔
424
  local resolved, err = package.searchpath(filename, path, "/")
67✔
425
  if resolved then
67✔
426
    if SILE.makeDeps then SILE.makeDeps:add(resolved) end
134✔
427
  elseif SU.debugging("paths") then
×
428
    SU.debug("paths", ("Unable to find file '%s': %s"):format(filename, err))
×
429
  end
430
  return resolved
67✔
431
end
432

433
function SILE.call (command, options, content)
128✔
434
  options = options or {}
1,800✔
435
  content = content or {}
1,800✔
436
  if SILE.traceback and type(content) == "table" and not content.lno then
1,800✔
437
    -- This call is from code (no content.lno) and we want to spend the time
438
    -- to determine everything we need about the caller
439
    local caller = debug.getinfo(2, "Sl")
×
440
    content.file, content.lno = caller.short_src, caller.currentline
×
441
  end
442
  local pId = SILE.traceStack:pushCommand(command, content, options)
1,800✔
443
  if not SILE.Commands[command] then SU.error("Unknown command " .. command) end
1,800✔
444
  local result = SILE.Commands[command](options, content)
1,800✔
445
  SILE.traceStack:pop(pId)
1,800✔
446
  return result
1,800✔
447
end
448

449
function SILE.registerCommand (name, func, help, pack, cheat)
128✔
450
  local class = SILE.documentState.documentClass
387✔
451
  if not cheat then
387✔
452
    SU.deprecated("SILE.registerCommand", "class:registerCommand", "0.14.0", "0.16.0",
2✔
453
    [[Commands are being scoped to the document classes they are loaded into rather than being globals.]])
1✔
454
  end
455
  -- Shimming until we have all scope cheating removed from core
456
  if not cheat or not class or class.type ~= "class" then
387✔
457
    return SILE.classes.base.registerCommand(nil, name, func, help, pack)
451✔
458
  end
459
  return class:registerCommand(name, func, help, pack)
×
460
end
461

462
function SILE.setCommandDefaults (command, defaults)
128✔
463
  local oldCommand = SILE.Commands[command]
×
464
  SILE.Commands[command] = function (options, content)
×
465
    for k, v in pairs(defaults) do
×
466
      options[k] = options[k] or v
×
467
    end
468
    return oldCommand(options, content)
×
469
  end
470
end
471

472
function SILE.registerUnit (unit, spec)
128✔
473
  -- If a unit exists already, clear it first so we get fresh meta table entries, see #1607
474
  if SILE.units[unit] then
10✔
475
    SILE.units[unit] = nil
×
476
  end
477
  SILE.units[unit] = spec
10✔
478
end
479

480
function SILE.paperSizeParser (size)
128✔
481
  -- SU.deprecated("SILE.paperSizeParser", "SILE.papersize", "0.10.0", nil)
482
  return SILE.papersize(size)
×
483
end
484

485
function SILE.finish ()
128✔
486
  if SILE.makeDeps then
64✔
487
    SILE.makeDeps:write()
64✔
488
  end
489
  SILE.documentState.documentClass:finish()
64✔
490
  SILE.font.finish()
64✔
491
  runEvals(SILE.input.evaluateAfters, "evaluate-after")
64✔
492
  if not SILE.quiet then
64✔
493
    io.stderr:write("\n")
64✔
494
  end
495
  if SU.debugging("profile") then
128✔
496
    ProFi:stop()
×
497
    ProFi:writeReport(pl.path.splitext(SILE.input.filenames[1]) .. '.profile.txt')
×
498
  end
499
  if SU.debugging("versions") then
128✔
500
    SILE.shaper:debugVersions()
64✔
501
  end
502
end
503

504
-- Internal libraries that run core SILE functions on load
505
SILE.settings = require("core.settings")()
128✔
506
require("core.hyphenator-liang")
64✔
507
require("core.languages")
64✔
508
require("core.packagemanager")
64✔
509
SILE.linebreak = require("core.break")
64✔
510
require("core.frame")
64✔
511
SILE.cli = require("core.cli")
64✔
512
SILE.repl = require("core.repl")
64✔
513
SILE.font = require("core.font")
64✔
514

515
-- For warnings and shims scheduled for removal that are easier to keep track
516
-- of when they are not spead across so many locations...
517
require("core/deprecations")
64✔
518

519
return SILE
64✔
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