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

sile-typesetter / sile / 6941442205

21 Nov 2023 08:56AM UTC coverage: 63.58% (+1.3%) from 62.266%
6941442205

Pull #1904

github

web-flow
Merge pull request #1891 from sile-typesetter/ot-tate
Pull Request #1904: Merge develop into master (commit to next release being breaking)

67 of 198 new or added lines in 20 files covered. (33.84%)

171 existing lines in 13 files now uncovered.

9907 of 15582 relevant lines covered (63.58%)

6710.82 hits per line

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

76.55
/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")
346✔
25

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

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

32
-- Developer tooling profiler
33
local ProFi
34

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

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

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

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

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

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

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

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

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

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

232
SILE.process = function (ast)
345✔
233
  if not ast then return end
1,441✔
234
  if SU.debugging("ast") then
2,874✔
235
    SU.debugAST(ast, 0)
×
236
  end
237
  if type(ast) == "function" then return ast() end
1,437✔
238
  for _, content in ipairs(ast) do
5,091✔
239
    if type(content) == "string" then
4,014✔
240
      SILE.typesetter:typeset(content)
4,722✔
241
    elseif type(content) == "function" then
1,653✔
242
      content()
2✔
243
    elseif SILE.Commands[content.command] then
1,652✔
244
      SILE.call(content.command, content.options, content)
3,282✔
245
    elseif content.id == "texlike_stuff"
11✔
246
      or (not content.command and not content.id) then
6✔
247
      local pId = SILE.traceStack:pushContent(content, "texlike_stuff")
11✔
248
      SILE.process(content)
11✔
249
      SILE.traceStack:pop(pId)
22✔
250
    elseif type(content) ~= "nil" then
×
251
      local pId = SILE.traceStack:pushContent(content)
×
252
      SU.error("Unknown command "..(tostring(content.command or content.id)))
×
253
      SILE.traceStack:pop(pId)
×
254
    end
255
  end
256
end
257

258
local preloadedinputters = { "xml", "lua", "sil" }
345✔
259

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

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

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

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

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

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

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

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

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

446
function SILE.paperSizeParser (size)
690✔
447
  -- SU.deprecated("SILE.paperSizeParser", "SILE.papersize", "0.10.0", nil)
448
  return SILE.papersize(size)
×
449
end
450

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

470
-- Internal libraries that run core SILE functions on load
471
SILE.settings = require("core.settings")()
690✔
472
require("core.hyphenator-liang")
345✔
473
require("core.languages")
345✔
474
SILE.linebreak = require("core.break")
345✔
475
require("core.frame")
345✔
476
SILE.cli = require("core.cli")
345✔
477
SILE.repl = require("core.repl")
345✔
478
SILE.font = require("core.font")
345✔
479

480
-- For warnings and shims scheduled for removal that are easier to keep track
481
-- of when they are not spread across so many locations...
482
require("core/deprecations")
345✔
483

484
return SILE
345✔
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