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

sile-typesetter / sile / 6484677185

11 Oct 2023 03:25PM UTC coverage: 74.293% (-0.002%) from 74.295%
6484677185

push

github

web-flow
Merge pull request #1888 from sile-typesetter/quieter

7 of 7 new or added lines in 2 files covered. (100.0%)

11722 of 15778 relevant lines covered (74.29%)

6976.14 hits per line

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

77.6
/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) or SU.error("Could not find file")
346✔
334
    local mode = lfs.attributes(filename).mode
173✔
335
    if mode ~= "file" and mode ~= "named pipe" then
173✔
336
      SU.error(filename.." isn't a file or named pipe, it's a ".. mode .."!")
×
337
    end
338
    if SILE.makeDeps then
173✔
339
      SILE.makeDeps:add(filename)
173✔
340
    end
341
    local file, err = io.open(filename)
173✔
342
    if not file then
173✔
343
      print("Could not open "..filename..": "..err)
×
344
      return
×
345
    end
346
    doc = file:read("*a")
173✔
347
  end
348
  local cpf = SILE.currentlyProcessingFile
173✔
349
  SILE.currentlyProcessingFile = filename
173✔
350
  local pId = SILE.traceStack:pushDocument(filename, doc)
173✔
351
  local ret = SILE.processString(doc, format, filename, options)
173✔
352
  SILE.traceStack:pop(pId)
173✔
353
  SILE.currentlyProcessingFile = cpf
173✔
354
  return ret
173✔
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)
172✔
360
  local saveTypesetter = SILE.typesetter
95✔
361
  if SILE.typesetter.frame then SILE.typesetter.frame:leave(SILE.typesetter) end
95✔
362
  SILE.typesetter = SILE.typesetters.base(frame)
190✔
363
  SILE.settings:temporarily(func)
95✔
364
  SILE.typesetter:leaveHmode()
95✔
365
  SILE.typesetter:chuck()
95✔
366
  SILE.typesetter.frame:leave(SILE.typesetter)
95✔
367
  SILE.typesetter = saveTypesetter
95✔
368
  if SILE.typesetter.frame then SILE.typesetter.frame:enter(SILE.typesetter) end
95✔
369
end
370

371
-- Sort through possible places files could be
372
function SILE.resolveFile (filename, pathprefix)
344✔
373
  local candidates = {}
178✔
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
178✔
376
  -- Also check the raw file name without a path
377
  candidates[#candidates+1] = "?"
178✔
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
178✔
381
    for path in SU.gtoke(SILE.masterDir..";"..tostring(os.getenv("SILE_PATH")), ";") do
890✔
382
      if path.string and path.string ~= "nil" then
534✔
383
        if pathprefix then candidates[#candidates+1] = pl.path.join(path.string, pathprefix, "?") end
178✔
384
        candidates[#candidates+1] = pl.path.join(path.string, "?")
356✔
385
      end
386
    end
387
  end
388
  -- Return the first candidate that exists, also checking the .sil suffix
389
  local path = table.concat(candidates, ";")
178✔
390
  local resolved, err = package.searchpath(filename, path, "/")
178✔
391
  if resolved then
178✔
392
    if SILE.makeDeps then SILE.makeDeps:add(resolved) end
356✔
393
  elseif SU.debugging("paths") then
×
394
    SU.debug("paths", ("Unable to find file '%s': %s"):format(filename, err))
×
395
  end
396
  return resolved
178✔
397
end
398

399
function SILE.call (command, options, content)
344✔
400
  options = options or {}
4,553✔
401
  content = content or {}
4,553✔
402
  if SILE.traceback and type(content) == "table" and not content.lno then
4,553✔
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,553✔
409
  if not SILE.Commands[command] then SU.error("Unknown command " .. command) end
4,553✔
410
  local result = SILE.Commands[command](options, content)
4,553✔
411
  SILE.traceStack:pop(pId)
4,553✔
412
  return result
4,553✔
413
end
414

415
function SILE.registerCommand (name, func, help, pack, cheat)
344✔
416
  local class = SILE.documentState.documentClass
1,055✔
417
  if not cheat then
1,055✔
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
1,055✔
423
    return SILE.classes.base.registerCommand(nil, name, func, help, pack)
1,225✔
424
  end
425
  return class:registerCommand(name, func, help, pack)
2✔
426
end
427

428
function SILE.setCommandDefaults (command, defaults)
344✔
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)
344✔
439
  -- If a unit exists already, clear it first so we get fresh meta table entries, see #1607
440
  if SILE.units[unit] then
12✔
441
    SILE.units[unit] = nil
×
442
  end
443
  SILE.units[unit] = spec
12✔
444
end
445

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

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

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

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

485
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