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

sile-typesetter / sile / 7232761547

16 Dec 2023 03:24PM UTC coverage: 74.605% (-0.03%) from 74.636%
7232761547

push

github

web-flow
Merge pull request #1929 from alerque/suggest-luarocks

Change module load error to include suggestions of how to install 3rd party modules

2 of 11 new or added lines in 2 files covered. (18.18%)

34 existing lines in 1 file now uncovered.

11816 of 15838 relevant lines covered (74.61%)

6996.16 hits per line

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

75.71
/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")
176✔
26

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

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

33
-- Developer tooling profiler
34
local ProFi
35

36
SILE.utilities = require("core.utilities")
175✔
37
SU = SILE.utilities -- regrettable global alias
175✔
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({}, {
1,225✔
43
    __index = function (self, key)
44
      -- local var = rawget(self, key)
45
      local m = require(("%s.%s"):format(scope, key))
1,414✔
46
      self[key] = m
1,414✔
47
      return m
1,414✔
48
    end
49
  })
1,225✔
50
end
51

52
SILE.Commands = {}
175✔
53
SILE.Help = {}
175✔
54
SILE.debugFlags = {}
175✔
55
SILE.nodeMakers = {}
175✔
56
SILE.tokenizers = {}
175✔
57
SILE.status = {}
175✔
58
SILE.scratch = {}
175✔
59
SILE.documentState = {}
175✔
60
SILE.rawHandlers = {}
175✔
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 = {
175✔
66
  filenames = {},
175✔
67
  evaluates = {},
175✔
68
  evaluateAfters = {},
175✔
69
  includes = {},
175✔
70
  uses = {},
175✔
71
  options = {},
175✔
72
  preambles = {},
175✔
73
  postambles = {},
175✔
74
}
175✔
75

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

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

93
-- Internal libraries that assume globals, may be picky about load order
94
SILE.measurement = require("core.measurement")
175✔
95
SILE.length = require("core.length")
175✔
96
SILE.papersize = require("core.papersize")
175✔
97
SILE.nodefactory = require("core.nodefactory")
175✔
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
350✔
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 ()
175✔
118
  if not SILE.backend then
175✔
119
    SILE.backend = "libtexpdf"
175✔
120
  end
121
  if SILE.backend == "libtexpdf" then
175✔
122
    SILE.shaper = SILE.shapers.harfbuzz()
525✔
123
    SILE.outputter = SILE.outputters.libtexpdf()
525✔
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()
525✔
138
  io.stdout:setvbuf("no")
175✔
139
  if SU.debugging("profile") then
350✔
140
    ProFi = require("ProFi")
×
141
    ProFi:start()
×
142
  end
143
  if SILE.makeDeps then
175✔
144
    SILE.makeDeps:add(_G.executablePath)
175✔
145
  end
146
  runEvals(SILE.input.evaluates, "evaluate")
175✔
147
end
148

149
local function suggest_luarocks (module)
NEW
150
  local guessed_module_name = module:gsub(".*%.", "") .. ".sile"
×
NEW
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

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

178
SILE.use = function (module, options)
175✔
179
  local status, pack
180
  if type(module) == "string" then
108✔
181
    status, pack = pcall(require, module)
107✔
182
    if not status then
107✔
NEW
183
      SU.error(("Unable to use '%s':\n%s%s")
×
NEW
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
108✔
190
  local class = SILE.documentState.documentClass
108✔
191
  if not pack.type then
108✔
192
    SU.error("Modules must declare their type")
×
193
  elseif pack.type == "class" then
108✔
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
108✔
200
    SILE.inputters[name] = pack
×
201
    SILE.inputter = pack(options)
×
202
  elseif pack.type == "outputter" then
108✔
203
    SILE.outputters[name] = pack
×
204
    SILE.outputter = pack(options)
×
205
  elseif pack.type == "shaper" then
108✔
206
    SILE.shapers[name] = pack
×
207
    SILE.shaper = pack(options)
×
208
  elseif pack.type == "typesetter" then
108✔
209
    SILE.typesetters[name] = pack
×
210
    SILE.typesetter = pack(options)
×
211
  elseif pack.type == "pagebuilder" then
108✔
212
    SILE.pagebuilders[name] = pack
×
213
    SILE.pagebuilder = pack(options)
×
214
  elseif pack.type == "package" then
108✔
215
    SILE.packages[name] = pack
108✔
216
    if class then
108✔
217
      pack(options)
216✔
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)
175✔
225
  if pathprefix and not deprecation_ack then
195✔
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$", "")
195✔
235
  local status, lib
236
  if pathprefix then
195✔
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 }))
350✔
240
  end
241
  if not status then
195✔
242
    local prefixederror = lib
20✔
243
    status, lib = pcall(require, dependency)
20✔
244
    if not status then
20✔
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
195✔
250
  if not class and not deprecation_ack then
195✔
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
195✔
258
    if lib.type == "package" then
20✔
259
      lib(class)
40✔
260
    else
261
      class:initPackage(lib)
×
262
    end
263
  end
264
  return lib
195✔
265
end
266

267
SILE.process = function (ast)
175✔
268
  if not ast then return end
1,426✔
269
  if SU.debugging("ast") then
2,844✔
270
    SU.debugAST(ast, 0)
×
271
  end
272
  if type(ast) == "function" then return ast() end
1,422✔
273
  for _, content in ipairs(ast) do
4,902✔
274
    if type(content) == "string" then
3,843✔
275
      SILE.typesetter:typeset(content)
4,506✔
276
    elseif type(content) == "function" then
1,590✔
277
      content()
2✔
278
    elseif SILE.Commands[content.command] then
1,589✔
279
      SILE.call(content.command, content.options, content)
3,156✔
280
    elseif content.id == "texlike_stuff"
11✔
281
      or (not content.command and not content.id) then
6✔
282
      local pId = SILE.traceStack:pushContent(content, "texlike_stuff")
11✔
283
      SILE.process(content)
11✔
284
      SILE.traceStack:pop(pId)
22✔
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" }
175✔
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
176✔
298
    for _, format in ipairs(preloadedinputters) do
704✔
299
      local _ = SILE.inputters[format]
528✔
300
    end
301
  end
302
  local contentDetectionOrder = {}
176✔
303
  for _, inputter in pairs(SILE.inputters) do
704✔
304
    if inputter.order then table.insert(contentDetectionOrder, inputter) end
528✔
305
  end
306
  table.sort(contentDetectionOrder, function (a, b) return a.order < b.order end)
704✔
307
  local initialround = filename and 1 or 2
176✔
308
  for round = initialround, 3 do
179✔
309
    for _, inputter in ipairs(contentDetectionOrder) do
357✔
310
      SU.debug("inputter", "Running content type detection round", round, "with", inputter._name)
354✔
311
      if inputter.appropriate(round, filename, doc) then
708✔
312
        return inputter._name
176✔
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)
350✔
320
  local cpf
321
  if not filename then
244✔
322
    cpf = SILE.currentlyProcessingFile
68✔
323
    local caller = debug.getinfo(2, "Sl")
68✔
324
    SILE.currentlyProcessingFile = caller.short_src..":"..caller.currentline
68✔
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
772✔
331
    inputter = SILE.inputter
×
332
  else
333
    format = format or detectFormat(doc, filename)
420✔
334
    if not SILE.quiet then
244✔
335
      io.stderr:write(("<%s> as %s\n"):format(SILE.currentlyProcessingFile, format))
244✔
336
    end
337
    inputter = SILE.inputters[format](options)
488✔
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
596✔
341
      SILE.inputter = inputter
175✔
342
    end
343
  end
344
  local pId = SILE.traceStack:pushDocument(SILE.currentlyProcessingFile, doc)
244✔
345
  inputter:process(doc)
244✔
346
  SILE.traceStack:pop(pId)
244✔
347
  if cpf then SILE.currentlyProcessingFile = cpf end
244✔
348
end
349

350
function SILE.processFile (filename, format, options)
350✔
351
  local doc
352
  if filename == "-" then
176✔
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("\\", "/")
176✔
358
    if not SILE.masterFilename then
176✔
359
      SILE.masterFilename = pl.path.splitext(pl.path.normpath(filename))
700✔
360
    end
361
    if SILE.input.filenames[1] and not SILE.masterDir then
176✔
362
      SILE.masterDir = pl.path.dirname(SILE.input.filenames[1])
350✔
363
    end
364
    if SILE.masterDir and SILE.masterDir:len() >= 1 then
352✔
365
      _G.extendSilePath(SILE.masterDir)
176✔
366
    end
367
    filename = SILE.resolveFile(filename) or SU.error("Could not find file")
352✔
368
    local mode = lfs.attributes(filename).mode
176✔
369
    if mode ~= "file" and mode ~= "named pipe" then
176✔
370
      SU.error(filename.." isn't a file or named pipe, it's a ".. mode .."!")
×
371
    end
372
    if SILE.makeDeps then
176✔
373
      SILE.makeDeps:add(filename)
176✔
374
    end
375
    local file, err = io.open(filename)
176✔
376
    if not file then
176✔
377
      print("Could not open "..filename..": "..err)
×
378
      return
×
379
    end
380
    doc = file:read("*a")
176✔
381
  end
382
  local cpf = SILE.currentlyProcessingFile
176✔
383
  SILE.currentlyProcessingFile = filename
176✔
384
  local pId = SILE.traceStack:pushDocument(filename, doc)
176✔
385
  local ret = SILE.processString(doc, format, filename, options)
176✔
386
  SILE.traceStack:pop(pId)
176✔
387
  SILE.currentlyProcessingFile = cpf
176✔
388
  return ret
176✔
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)
175✔
394
  local saveTypesetter = SILE.typesetter
97✔
395
  if SILE.typesetter.frame then SILE.typesetter.frame:leave(SILE.typesetter) end
97✔
396
  SILE.typesetter = SILE.typesetters.base(frame)
194✔
397
  SILE.settings:temporarily(func)
97✔
398
  SILE.typesetter:leaveHmode()
97✔
399
  SILE.typesetter:chuck()
97✔
400
  SILE.typesetter.frame:leave(SILE.typesetter)
97✔
401
  SILE.typesetter = saveTypesetter
97✔
402
  if SILE.typesetter.frame then SILE.typesetter.frame:enter(SILE.typesetter) end
97✔
403
end
404

405
-- Sort through possible places files could be
406
function SILE.resolveFile (filename, pathprefix)
350✔
407
  local candidates = {}
181✔
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
181✔
410
  -- Also check the raw file name without a path
411
  candidates[#candidates+1] = "?"
181✔
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
181✔
415
    for path in SU.gtoke(SILE.masterDir..";"..tostring(os.getenv("SILE_PATH")), ";") do
905✔
416
      if path.string and path.string ~= "nil" then
543✔
417
        if pathprefix then candidates[#candidates+1] = pl.path.join(path.string, pathprefix, "?") end
181✔
418
        candidates[#candidates+1] = pl.path.join(path.string, "?")
362✔
419
      end
420
    end
421
  end
422
  -- Return the first candidate that exists, also checking the .sil suffix
423
  local path = table.concat(candidates, ";")
181✔
424
  local resolved, err = package.searchpath(filename, path, "/")
181✔
425
  if resolved then
181✔
426
    if SILE.makeDeps then SILE.makeDeps:add(resolved) end
362✔
427
  elseif SU.debugging("paths") then
×
428
    SU.debug("paths", ("Unable to find file '%s': %s"):format(filename, err))
×
429
  end
430
  return resolved
181✔
431
end
432

433
function SILE.call (command, options, content)
350✔
434
  options = options or {}
4,580✔
435
  content = content or {}
4,580✔
436
  if SILE.traceback and type(content) == "table" and not content.lno then
4,580✔
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)
4,580✔
443
  if not SILE.Commands[command] then SU.error("Unknown command " .. command) end
4,580✔
444
  local result = SILE.Commands[command](options, content)
4,580✔
445
  SILE.traceStack:pop(pId)
4,580✔
446
  return result
4,580✔
447
end
448

449
function SILE.registerCommand (name, func, help, pack, cheat)
350✔
450
  local class = SILE.documentState.documentClass
1,073✔
451
  if not cheat then
1,073✔
452
    SU.deprecated("SILE.registerCommand", "class:registerCommand", "0.14.0", "0.16.0",
24✔
453
    [[Commands are being scoped to the document classes they are loaded into rather than being globals.]])
12✔
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
1,073✔
457
    return SILE.classes.base.registerCommand(nil, name, func, help, pack)
1,246✔
458
  end
459
  return class:registerCommand(name, func, help, pack)
2✔
460
end
461

462
function SILE.setCommandDefaults (command, defaults)
350✔
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)
350✔
473
  -- If a unit exists already, clear it first so we get fresh meta table entries, see #1607
474
  if SILE.units[unit] then
12✔
475
    SILE.units[unit] = nil
×
476
  end
477
  SILE.units[unit] = spec
12✔
478
end
479

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

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

504
-- Internal libraries that run core SILE functions on load
505
SILE.settings = require("core.settings")()
350✔
506
require("core.hyphenator-liang")
175✔
507
require("core.languages")
175✔
508
require("core.packagemanager")
175✔
509
SILE.linebreak = require("core.break")
175✔
510
require("core.frame")
175✔
511
SILE.cli = require("core.cli")
175✔
512
SILE.repl = require("core.repl")
175✔
513
SILE.font = require("core.font")
175✔
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")
175✔
518

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