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

sile-typesetter / sile / 5017842917

pending completion
5017842917

push

github

GitHub
Merge pull request #1738 from alerque/multi-inputs

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

9954 of 15641 relevant lines covered (63.64%)

6575.94 hits per line

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

77.42
/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")
172✔
25

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

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

32
-- Developer tooling profiler
33
local ProFi
34

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

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

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

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

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

98
-- NOTE:
99
-- See remainaing internal libraries loaded at the end of this file because
100
-- they run core SILE functions on load istead 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
342✔
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 ()
171✔
117
  if not SILE.backend then
171✔
118
    SILE.backend = "libtexpdf"
171✔
119
  end
120
  if SILE.backend == "libtexpdf" then
171✔
121
    SILE.shaper = SILE.shapers.harfbuzz()
513✔
122
    SILE.outputter = SILE.outputters.libtexpdf()
513✔
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()
513✔
137
  io.stdout:setvbuf("no")
171✔
138
  if SU.debugging("profile") then
342✔
139
    ProFi = require("ProFi")
×
140
    ProFi:start()
×
141
  end
142
  if SILE.makeDeps then
171✔
143
    SILE.makeDeps:add(_G.executablePath)
171✔
144
  end
145
  runEvals(SILE.input.evaluates, "evaluate")
171✔
146
end
147

148
SILE.use = function (module, options)
171✔
149
  local pack
150
  if type(module) == "string" then
106✔
151
    pack = require(module)
105✔
152
  elseif type(module) == "table" then
1✔
153
    pack = module
1✔
154
  end
155
  local name = pack._name
106✔
156
  local class = SILE.documentState.documentClass
106✔
157
  if not pack.type then
106✔
158
    SU.error("Modules must declare their type")
×
159
  elseif pack.type == "class" then
106✔
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
106✔
166
    SILE.inputters[name] = pack
×
167
    SILE.inputter = pack(options)
×
168
  elseif pack.type == "outputter" then
106✔
169
    SILE.outputters[name] = pack
×
170
    SILE.outputter = pack(options)
×
171
  elseif pack.type == "shaper" then
106✔
172
    SILE.shapers[name] = pack
×
173
    SILE.shaper = pack(options)
×
174
  elseif pack.type == "typesetter" then
106✔
175
    SILE.typesetters[name] = pack
×
176
    SILE.typesetter = pack(options)
×
177
  elseif pack.type == "pagebuilder" then
106✔
178
    SILE.pagebuilders[name] = pack
×
179
    SILE.pagebuilder = pack(options)
×
180
  elseif pack.type == "package" then
106✔
181
    SILE.packages[name] = pack
106✔
182
    if class then
106✔
183
      pack(options)
212✔
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)
171✔
191
  if pathprefix and not deprecation_ack then
191✔
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$", "")
191✔
201
  local status, lib
202
  if pathprefix then
191✔
203
    status, lib = pcall(require, pl.path.join(pathprefix, dependency))
342✔
204
  end
205
  if not status then
191✔
206
    local prefixederror = lib
20✔
207
    status, lib = pcall(require, dependency)
20✔
208
    if not status then
20✔
209
      SU.error(("Unable to find module '%s'%s")
×
210
        :format(dependency, SILE.traceback and ((pathprefix and "\n  " .. prefixederror or "") .. "\n  " .. lib) or ""))
×
211
    end
212
  end
213
  local class = SILE.documentState.documentClass
191✔
214
  if not class and not deprecation_ack then
191✔
215
    SU.warn(string.format([[
×
216
  Use of SILE.require() is only supported in documents, packages, or class
217
  init functions. It will not function fully before the class is instantiated.
218
  Please just use the Lua require() function directly:
219
      SILE.require("%s") → require("%s")]], dependency, dependency))
×
220
  end
221
  if type(lib) == "table" and class then
191✔
222
    if lib.type == "package" then
20✔
223
      lib(class)
40✔
224
    else
225
      class:initPackage(lib)
×
226
    end
227
  end
228
  return lib
191✔
229
end
230

231
SILE.process = function (ast)
171✔
232
  if not ast then return end
1,389✔
233
  if SU.debugging("ast") then
2,770✔
234
    SU.debugAST(ast, 0)
×
235
  end
236
  if type(ast) == "function" then return ast() end
1,385✔
237
  for _, content in ipairs(ast) do
4,817✔
238
    if type(content) == "string" then
3,785✔
239
      SILE.typesetter:typeset(content)
4,418✔
240
    elseif type(content) == "function" then
1,576✔
241
      content()
2✔
242
    elseif SILE.Commands[content.command] then
1,575✔
243
      SILE.call(content.command, content.options, content)
3,128✔
244
    elseif content.id == "texlike_stuff"
11✔
245
      or (not content.command and not content.id) then
6✔
246
      local pId = SILE.traceStack:pushContent(content, "texlike_stuff")
11✔
247
      SILE.process(content)
11✔
248
      SILE.traceStack:pop(pId)
22✔
249
    elseif type(content) ~= "nil" then
×
250
      local pId = SILE.traceStack:pushContent(content)
×
251
      SU.error("Unknown command "..(tostring(content.command or content.id)))
×
252
      SILE.traceStack:pop(pId)
×
253
    end
254
  end
255
end
256

257
local preloadedinputters = { "xml", "lua", "sil" }
171✔
258

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

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

314
function SILE.processFile (filename, format, options)
342✔
315
  local doc
316
  if filename == "-" then
172✔
317
    filename = "STDIN"
×
318
    SILE.masterFilename = "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("\\", "/")
172✔
323
    if not SILE.masterFilename then
172✔
324
      -- Strip extension
325
      SILE.masterFilename = string.match(filename, "(.+)%..-$") or filename
171✔
326
    end
327
    if SILE.masterFilename and not SILE.masterDir then
172✔
328
      SILE.masterDir = SILE.masterFilename:match("(.-)[^%/]+$")
171✔
329
    end
330
    if SILE.masterDir and SILE.masterDir:len() >= 1 then
344✔
331
      _G.extendSilePath(SILE.masterDir)
172✔
332
    end
333
    filename = SILE.resolveFile(filename)
344✔
334
    if not filename then
172✔
335
      SU.error("Could not find file")
×
336
    end
337
    local mode = lfs.attributes(filename).mode
172✔
338
    if mode ~= "file" and mode ~= "named pipe" then
172✔
339
      SU.error(filename.." isn't a file or named pipe, it's a ".. mode .."!")
×
340
    end
341
    if SILE.makeDeps then
172✔
342
      SILE.makeDeps:add(filename)
172✔
343
    end
344
    local file, err = io.open(filename)
172✔
345
    if not file then
172✔
346
      print("Could not open "..filename..": "..err)
×
347
      return
×
348
    end
349
    doc = file:read("*a")
172✔
350
  end
351
  local cpf = SILE.currentlyProcessingFile
172✔
352
  SILE.currentlyProcessingFile = filename
172✔
353
  local pId = SILE.traceStack:pushDocument(filename, doc)
172✔
354
  local ret = SILE.processString(doc, format, filename, options)
172✔
355
  SILE.traceStack:pop(pId)
172✔
356
  SILE.currentlyProcessingFile = cpf
172✔
357
  return ret
172✔
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)
171✔
363
  local saveTypesetter = SILE.typesetter
94✔
364
  if SILE.typesetter.frame then SILE.typesetter.frame:leave(SILE.typesetter) end
94✔
365
  SILE.typesetter = SILE.typesetters.base(frame)
188✔
366
  SILE.settings:temporarily(func)
94✔
367
  SILE.typesetter:leaveHmode()
94✔
368
  SILE.typesetter:chuck()
94✔
369
  SILE.typesetter.frame:leave(SILE.typesetter)
94✔
370
  SILE.typesetter = saveTypesetter
94✔
371
  if SILE.typesetter.frame then SILE.typesetter.frame:enter(SILE.typesetter) end
94✔
372
end
373

374
-- Sort through possible places files could be
375
function SILE.resolveFile (filename, pathprefix)
342✔
376
  local candidates = {}
177✔
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
177✔
379
  -- Also check the raw file name without a path
380
  candidates[#candidates+1] = "?"
177✔
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
177✔
384
    for path in SU.gtoke(SILE.masterDir..";"..tostring(os.getenv("SILE_PATH")), ";") do
885✔
385
      if path.string and path.string ~= "nil" then
531✔
386
        if pathprefix then candidates[#candidates+1] = pl.path.join(path.string, pathprefix, "?") end
177✔
387
        candidates[#candidates+1] = pl.path.join(path.string, "?")
525✔
388
      end
389
    end
390
  end
391
  -- Return the first candidate that exists, also checking the .sil suffix
392
  local path = table.concat(candidates, ";")
177✔
393
  local resolved, err = package.searchpath(filename, path, "/")
177✔
394
  if resolved then
177✔
395
    if SILE.makeDeps then SILE.makeDeps:add(resolved) end
354✔
396
  else
397
    SU.warn(("Unable to find file '%s': %s"):format(filename, err))
×
398
  end
399
  return resolved
177✔
400
end
401

402
function SILE.call (command, options, content)
342✔
403
  options = options or {}
4,516✔
404
  content = content or {}
4,516✔
405
  if SILE.traceback and type(content) == "table" and not content.lno then
4,516✔
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,516✔
412
  if not SILE.Commands[command] then SU.error("Unknown command " .. command) end
4,516✔
413
  local result = SILE.Commands[command](options, content)
4,516✔
414
  SILE.traceStack:pop(pId)
4,516✔
415
  return result
4,516✔
416
end
417

418
function SILE.registerCommand (name, func, help, pack, cheat)
342✔
419
  local class = SILE.documentState.documentClass
1,049✔
420
  if not cheat then
1,049✔
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,049✔
426
    return SILE.classes.base.registerCommand(nil, name, func, help, pack)
1,218✔
427
  end
428
  return class:registerCommand(name, func, help, pack)
2✔
429
end
430

431
function SILE.setCommandDefaults (command, defaults)
342✔
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)
342✔
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)
342✔
450
  -- SU.deprecated("SILE.paperSizeParser", "SILE.papersize", "0.10.0", nil)
451
  return SILE.papersize(size)
×
452
end
453

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

473
-- Internal libraries that run core SILE functions on load
474
SILE.settings = require("core.settings")()
342✔
475
require("core.hyphenator-liang")
171✔
476
require("core.languages")
171✔
477
require("core.packagemanager")
171✔
478
SILE.linebreak = require("core.break")
171✔
479
require("core.frame")
171✔
480
SILE.cli = require("core.cli")
171✔
481
SILE.repl = require("core.repl")
171✔
482
SILE.font = require("core.font")
171✔
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")
171✔
487

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