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

sile-typesetter / sile / 5934519125

22 Aug 2023 04:39AM UTC coverage: 74.32% (+0.7%) from 73.575%
5934519125

push

github

web-flow
Merge pull request #1835 from alerque/correct-master-filename

14 of 14 new or added lines in 6 files covered. (100.0%)

11724 of 15775 relevant lines covered (74.32%)

6977.74 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 verion to produce documents
63
-- programatically.
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 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
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
    status, lib = pcall(require, pl.path.join(pathprefix, dependency))
344✔
204
  end
205
  if not status then
192✔
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
192✔
214
  if not class and not deprecation_ack then
192✔
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
192✔
222
    if lib.type == "package" then
20✔
223
      lib(class)
40✔
224
    else
225
      class:initPackage(lib)
×
226
    end
227
  end
228
  return lib
192✔
229
end
230

231
SILE.process = function (ast)
172✔
232
  if not ast then return end
1,405✔
233
  if SU.debugging("ast") then
2,802✔
234
    SU.debugAST(ast, 0)
×
235
  end
236
  if type(ast) == "function" then return ast() end
1,401✔
237
  for _, content in ipairs(ast) do
4,841✔
238
    if type(content) == "string" then
3,803✔
239
      SILE.typesetter:typeset(content)
4,440✔
240
    elseif type(content) == "function" then
1,583✔
241
      content()
2✔
242
    elseif SILE.Commands[content.command] then
1,582✔
243
      SILE.call(content.command, content.options, content)
3,142✔
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" }
172✔
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
173✔
262
    for _, format in ipairs(preloadedinputters) do
692✔
263
      local _ = SILE.inputters[format]
519✔
264
    end
265
  end
266
  local contentDetectionOrder = {}
173✔
267
  for _, inputter in pairs(SILE.inputters) do
692✔
268
    if inputter.order then table.insert(contentDetectionOrder, inputter) end
519✔
269
  end
270
  table.sort(contentDetectionOrder, function (a, b) return a.order < b.order end)
692✔
271
  local initialround = filename and 1 or 2
173✔
272
  for round = initialround, 3 do
176✔
273
    for _, inputter in ipairs(contentDetectionOrder) do
351✔
274
      SU.debug("inputter", "Running content type detection round", round, "with", inputter._name)
348✔
275
      if inputter.appropriate(round, filename, doc) then
696✔
276
        return inputter._name
173✔
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)
344✔
284
  local cpf
285
  if not filename then
241✔
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.normcase(pl.path.normpath(filename)) == pl.path.normcase(SILE.input.filenames[1]) and SILE.inputter then
760✔
295
    inputter = SILE.inputter
×
296
  else
297
    format = format or detectFormat(doc, filename)
414✔
298
    if not SILE.quiet then
241✔
299
      io.stderr:write(("<%s> as %s\n"):format(SILE.currentlyProcessingFile, format))
241✔
300
    end
301
    inputter = SILE.inputters[format](options)
482✔
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.normcase(filename) == pl.path.normcase(SILE.input.filenames[1]) then
587✔
305
      SILE.inputter = inputter
172✔
306
    end
307
  end
308
  local pId = SILE.traceStack:pushDocument(SILE.currentlyProcessingFile, doc)
241✔
309
  inputter:process(doc)
241✔
310
  SILE.traceStack:pop(pId)
241✔
311
  if cpf then SILE.currentlyProcessingFile = cpf end
241✔
312
end
313

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

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

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

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

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

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

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

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

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

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

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

486
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