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

sile-typesetter / sile / 6473680551

10 Oct 2023 07:02PM UTC coverage: 74.295%. Remained the same
6473680551

push

github

web-flow
Merge pull request #1885 from sile-typesetter/spell

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

11723 of 15779 relevant lines covered (74.29%)

6975.7 hits per line

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

77.67
/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
SILE.lua_isjit = type(jit) == "table"
1✔
10
SILE.full_version = string.format("SILE %s (%s)", SILE.version, SILE.lua_isjit and jit.version or _VERSION)
1✔
11

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

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

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

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

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

29
-- Includes for _this_ scope
30
local lfs = require("lfs")
172✔
31

32
-- Developer tooling profiler
33
local ProFi
34

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

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

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

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

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

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

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

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

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

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

148
SILE.use = function (module, options)
172✔
149
  local pack
150
  if type(module) == "string" then
107✔
151
    pack = require(module)
106✔
152
  elseif type(module) == "table" then
1✔
153
    pack = module
1✔
154
  end
155
  local name = pack._name
107✔
156
  local class = SILE.documentState.documentClass
107✔
157
  if not pack.type then
107✔
158
    SU.error("Modules must declare their type")
×
159
  elseif pack.type == "class" then
107✔
160
    SILE.classes[name] = pack
×
161
    if class then
×
162
      SU.error("Cannot load a class after one is already instantiated")
×
163
    end
164
    SILE.scratch.class_from_uses = pack
×
165
  elseif pack.type == "inputter" then
107✔
166
    SILE.inputters[name] = pack
×
167
    SILE.inputter = pack(options)
×
168
  elseif pack.type == "outputter" then
107✔
169
    SILE.outputters[name] = pack
×
170
    SILE.outputter = pack(options)
×
171
  elseif pack.type == "shaper" then
107✔
172
    SILE.shapers[name] = pack
×
173
    SILE.shaper = pack(options)
×
174
  elseif pack.type == "typesetter" then
107✔
175
    SILE.typesetters[name] = pack
×
176
    SILE.typesetter = pack(options)
×
177
  elseif pack.type == "pagebuilder" then
107✔
178
    SILE.pagebuilders[name] = pack
×
179
    SILE.pagebuilder = pack(options)
×
180
  elseif pack.type == "package" then
107✔
181
    SILE.packages[name] = pack
107✔
182
    if class then
107✔
183
      pack(options)
214✔
184
    else
185
      table.insert(SILE.input.preambles, { pack = pack, options = options })
×
186
    end
187
  end
188
end
189

190
SILE.require = function (dependency, pathprefix, deprecation_ack)
172✔
191
  if pathprefix and not deprecation_ack then
192✔
192
    local notice = string.format([[
×
193
  Please don't use the path prefix mechanism; it was intended to provide
194
  alternate paths to override core components but never worked well and is
195
  causing portability problems. Just use Lua idiomatic module loading:
196
      SILE.require("%s", "%s") → SILE.require("%s.%s")]],
197
      dependency, pathprefix, pathprefix, dependency)
×
198
    SU.deprecated("SILE.require", "SILE.require", "0.13.0", nil, notice)
×
199
  end
200
  dependency = dependency:gsub(".lua$", "")
192✔
201
  local status, lib
202
  if pathprefix then
192✔
203
    -- Note this is not a *path*, it is a module identifier:
204
    -- https://github.com/sile-typesetter/sile/issues/1861
205
    status, lib = pcall(require, pl.stringx.join('.', { pathprefix, dependency }))
344✔
206
  end
207
  if not status then
192✔
208
    local prefixederror = lib
20✔
209
    status, lib = pcall(require, dependency)
20✔
210
    if not status then
20✔
211
      SU.error(("Unable to find module '%s'%s")
×
212
        :format(dependency, SILE.traceback and ((pathprefix and "\n  " .. prefixederror or "") .. "\n  " .. lib) or ""))
×
213
    end
214
  end
215
  local class = SILE.documentState.documentClass
192✔
216
  if not class and not deprecation_ack then
192✔
217
    SU.warn(string.format([[
×
218
  Use of SILE.require() is only supported in documents, packages, or class
219
  init functions. It will not function fully before the class is instantiated.
220
  Please just use the Lua require() function directly:
221
      SILE.require("%s") → require("%s")]], dependency, dependency))
×
222
  end
223
  if type(lib) == "table" and class then
192✔
224
    if lib.type == "package" then
20✔
225
      lib(class)
40✔
226
    else
227
      class:initPackage(lib)
×
228
    end
229
  end
230
  return lib
192✔
231
end
232

233
SILE.process = function (ast)
172✔
234
  if not ast then return end
1,405✔
235
  if SU.debugging("ast") then
2,802✔
236
    SU.debugAST(ast, 0)
×
237
  end
238
  if type(ast) == "function" then return ast() end
1,401✔
239
  for _, content in ipairs(ast) do
4,841✔
240
    if type(content) == "string" then
3,803✔
241
      SILE.typesetter:typeset(content)
4,440✔
242
    elseif type(content) == "function" then
1,583✔
243
      content()
2✔
244
    elseif SILE.Commands[content.command] then
1,582✔
245
      SILE.call(content.command, content.options, content)
3,142✔
246
    elseif content.id == "texlike_stuff"
11✔
247
      or (not content.command and not content.id) then
6✔
248
      local pId = SILE.traceStack:pushContent(content, "texlike_stuff")
11✔
249
      SILE.process(content)
11✔
250
      SILE.traceStack:pop(pId)
22✔
251
    elseif type(content) ~= "nil" then
×
252
      local pId = SILE.traceStack:pushContent(content)
×
253
      SU.error("Unknown command "..(tostring(content.command or content.id)))
×
254
      SILE.traceStack:pop(pId)
×
255
    end
256
  end
257
end
258

259
local preloadedinputters = { "xml", "lua", "sil" }
172✔
260

261
local function detectFormat (doc, filename)
262
  -- Preload default reader types so content detection has something to work with
263
  if #SILE.inputters == 0 then
173✔
264
    for _, format in ipairs(preloadedinputters) do
692✔
265
      local _ = SILE.inputters[format]
519✔
266
    end
267
  end
268
  local contentDetectionOrder = {}
173✔
269
  for _, inputter in pairs(SILE.inputters) do
692✔
270
    if inputter.order then table.insert(contentDetectionOrder, inputter) end
519✔
271
  end
272
  table.sort(contentDetectionOrder, function (a, b) return a.order < b.order end)
692✔
273
  local initialround = filename and 1 or 2
173✔
274
  for round = initialround, 3 do
176✔
275
    for _, inputter in ipairs(contentDetectionOrder) do
351✔
276
      SU.debug("inputter", "Running content type detection round", round, "with", inputter._name)
348✔
277
      if inputter.appropriate(round, filename, doc) then
696✔
278
        return inputter._name
173✔
279
      end
280
    end
281
  end
282
  SU.error(("Unable to pick inputter to process input from '%s'"):format(filename))
×
283
end
284

285
function SILE.processString (doc, format, filename, options)
344✔
286
  local cpf
287
  if not filename then
241✔
288
    cpf = SILE.currentlyProcessingFile
68✔
289
    local caller = debug.getinfo(2, "Sl")
68✔
290
    SILE.currentlyProcessingFile = caller.short_src..":"..caller.currentline
68✔
291
  end
292
  -- In the event we're processing the master file *and* the user gave us
293
  -- a specific inputter to use, use it at the exclusion of all content type
294
  -- detection
295
  local inputter
296
  if filename and pl.path.normcase(pl.path.normpath(filename)) == pl.path.normcase(SILE.input.filenames[1]) and SILE.inputter then
760✔
297
    inputter = SILE.inputter
×
298
  else
299
    format = format or detectFormat(doc, filename)
414✔
300
    if not SILE.quiet then
241✔
301
      io.stderr:write(("<%s> as %s\n"):format(SILE.currentlyProcessingFile, format))
241✔
302
    end
303
    inputter = SILE.inputters[format](options)
482✔
304
    -- If we did content detection *and* this is the master file, save the
305
    -- inputter for posterity and postambles
306
    if filename and pl.path.normcase(filename) == pl.path.normcase(SILE.input.filenames[1]:gsub("^-$", "STDIN")) then
587✔
307
      SILE.inputter = inputter
172✔
308
    end
309
  end
310
  local pId = SILE.traceStack:pushDocument(SILE.currentlyProcessingFile, doc)
241✔
311
  inputter:process(doc)
241✔
312
  SILE.traceStack:pop(pId)
241✔
313
  if cpf then SILE.currentlyProcessingFile = cpf end
241✔
314
end
315

316
function SILE.processFile (filename, format, options)
344✔
317
  local doc
318
  if filename == "-" then
173✔
319
    filename = "STDIN"
×
320
    doc = io.stdin:read("*a")
×
321
  else
322
    -- Turn slashes around in the event we get passed a path from a Windows shell
323
    filename = filename:gsub("\\", "/")
173✔
324
    if not SILE.masterFilename then
173✔
325
      SILE.masterFilename = pl.path.splitext(pl.path.normpath(filename))
688✔
326
    end
327
    if SILE.input.filenames[1] and not SILE.masterDir then
173✔
328
      SILE.masterDir = pl.path.dirname(SILE.input.filenames[1])
344✔
329
    end
330
    if SILE.masterDir and SILE.masterDir:len() >= 1 then
346✔
331
      _G.extendSilePath(SILE.masterDir)
173✔
332
    end
333
    filename = SILE.resolveFile(filename)
346✔
334
    if not filename then
173✔
335
      SU.error("Could not find file")
×
336
    end
337
    local mode = lfs.attributes(filename).mode
173✔
338
    if mode ~= "file" and mode ~= "named pipe" then
173✔
339
      SU.error(filename.." isn't a file or named pipe, it's a ".. mode .."!")
×
340
    end
341
    if SILE.makeDeps then
173✔
342
      SILE.makeDeps:add(filename)
173✔
343
    end
344
    local file, err = io.open(filename)
173✔
345
    if not file then
173✔
346
      print("Could not open "..filename..": "..err)
×
347
      return
×
348
    end
349
    doc = file:read("*a")
173✔
350
  end
351
  local cpf = SILE.currentlyProcessingFile
173✔
352
  SILE.currentlyProcessingFile = filename
173✔
353
  local pId = SILE.traceStack:pushDocument(filename, doc)
173✔
354
  local ret = SILE.processString(doc, format, filename, options)
173✔
355
  SILE.traceStack:pop(pId)
173✔
356
  SILE.currentlyProcessingFile = cpf
173✔
357
  return ret
173✔
358
end
359

360
-- TODO: this probably needs deprecating, moved here just to get out of the way so
361
-- typesetters classing works as expected
362
SILE.typesetNaturally = function (frame, func)
172✔
363
  local saveTypesetter = SILE.typesetter
95✔
364
  if SILE.typesetter.frame then SILE.typesetter.frame:leave(SILE.typesetter) end
95✔
365
  SILE.typesetter = SILE.typesetters.base(frame)
190✔
366
  SILE.settings:temporarily(func)
95✔
367
  SILE.typesetter:leaveHmode()
95✔
368
  SILE.typesetter:chuck()
95✔
369
  SILE.typesetter.frame:leave(SILE.typesetter)
95✔
370
  SILE.typesetter = saveTypesetter
95✔
371
  if SILE.typesetter.frame then SILE.typesetter.frame:enter(SILE.typesetter) end
95✔
372
end
373

374
-- Sort through possible places files could be
375
function SILE.resolveFile (filename, pathprefix)
344✔
376
  local candidates = {}
178✔
377
  -- Start with the raw file name as given prefixed with a path if requested
378
  if pathprefix then candidates[#candidates+1] = pl.path.join(pathprefix, "?") end
178✔
379
  -- Also check the raw file name without a path
380
  candidates[#candidates+1] = "?"
178✔
381
  -- Iterate through the directory of the master file, the SILE_PATH variable, and the current directory
382
  -- Check for prefixed paths first, then the plain path in that fails
383
  if SILE.masterDir then
178✔
384
    for path in SU.gtoke(SILE.masterDir..";"..tostring(os.getenv("SILE_PATH")), ";") do
890✔
385
      if path.string and path.string ~= "nil" then
534✔
386
        if pathprefix then candidates[#candidates+1] = pl.path.join(path.string, pathprefix, "?") end
178✔
387
        candidates[#candidates+1] = pl.path.join(path.string, "?")
356✔
388
      end
389
    end
390
  end
391
  -- Return the first candidate that exists, also checking the .sil suffix
392
  local path = table.concat(candidates, ";")
178✔
393
  local resolved, err = package.searchpath(filename, path, "/")
178✔
394
  if resolved then
178✔
395
    if SILE.makeDeps then SILE.makeDeps:add(resolved) end
356✔
396
  else
397
    SU.warn(("Unable to find file '%s': %s"):format(filename, err))
×
398
  end
399
  return resolved
178✔
400
end
401

402
function SILE.call (command, options, content)
344✔
403
  options = options or {}
4,553✔
404
  content = content or {}
4,553✔
405
  if SILE.traceback and type(content) == "table" and not content.lno then
4,553✔
406
    -- This call is from code (no content.lno) and we want to spend the time
407
    -- to determine everything we need about the caller
408
    local caller = debug.getinfo(2, "Sl")
×
409
    content.file, content.lno = caller.short_src, caller.currentline
×
410
  end
411
  local pId = SILE.traceStack:pushCommand(command, content, options)
4,553✔
412
  if not SILE.Commands[command] then SU.error("Unknown command " .. command) end
4,553✔
413
  local result = SILE.Commands[command](options, content)
4,553✔
414
  SILE.traceStack:pop(pId)
4,553✔
415
  return result
4,553✔
416
end
417

418
function SILE.registerCommand (name, func, help, pack, cheat)
344✔
419
  local class = SILE.documentState.documentClass
1,055✔
420
  if not cheat then
1,055✔
421
    SU.deprecated("SILE.registerCommand", "class:registerCommand", "0.14.0", "0.16.0",
24✔
422
    [[Commands are being scoped to the document classes they are loaded into rather than being globals.]])
12✔
423
  end
424
  -- Shimming until we have all scope cheating removed from core
425
  if not cheat or not class or class.type ~= "class" then
1,055✔
426
    return SILE.classes.base.registerCommand(nil, name, func, help, pack)
1,225✔
427
  end
428
  return class:registerCommand(name, func, help, pack)
2✔
429
end
430

431
function SILE.setCommandDefaults (command, defaults)
344✔
432
  local oldCommand = SILE.Commands[command]
×
433
  SILE.Commands[command] = function (options, content)
×
434
    for k, v in pairs(defaults) do
×
435
      options[k] = options[k] or v
×
436
    end
437
    return oldCommand(options, content)
×
438
  end
439
end
440

441
function SILE.registerUnit (unit, spec)
344✔
442
  -- If a unit exists already, clear it first so we get fresh meta table entries, see #1607
443
  if SILE.units[unit] then
12✔
444
    SILE.units[unit] = nil
×
445
  end
446
  SILE.units[unit] = spec
12✔
447
end
448

449
function SILE.paperSizeParser (size)
344✔
450
  -- SU.deprecated("SILE.paperSizeParser", "SILE.papersize", "0.10.0", nil)
451
  return SILE.papersize(size)
×
452
end
453

454
function SILE.finish ()
344✔
455
  if SILE.makeDeps then
172✔
456
    SILE.makeDeps:write()
172✔
457
  end
458
  SILE.documentState.documentClass:finish()
172✔
459
  SILE.font.finish()
172✔
460
  runEvals(SILE.input.evaluateAfters, "evaluate-after")
172✔
461
  if not SILE.quiet then
172✔
462
    io.stderr:write("\n")
172✔
463
  end
464
  if SU.debugging("profile") then
344✔
465
    ProFi:stop()
×
466
    ProFi:writeReport(pl.path.splitext(SILE.input.filenames[1]) .. '.profile.txt')
×
467
  end
468
  if SU.debugging("versions") then
344✔
469
    SILE.shaper:debugVersions()
172✔
470
  end
471
end
472

473
-- Internal libraries that run core SILE functions on load
474
SILE.settings = require("core.settings")()
344✔
475
require("core.hyphenator-liang")
172✔
476
require("core.languages")
172✔
477
require("core.packagemanager")
172✔
478
SILE.linebreak = require("core.break")
172✔
479
require("core.frame")
172✔
480
SILE.cli = require("core.cli")
172✔
481
SILE.repl = require("core.repl")
172✔
482
SILE.font = require("core.font")
172✔
483

484
-- For warnings and shims scheduled for removal that are easier to keep track
485
-- of when they are not spead across so many locations...
486
require("core/deprecations")
172✔
487

488
return SILE
172✔
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