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

sile-typesetter / sile / 6915746301

18 Nov 2023 07:02PM UTC coverage: 56.433% (-12.3%) from 68.751%
6915746301

push

github

web-flow
Merge 8b3fdc301 into f64e235fa

8729 of 15468 relevant lines covered (56.43%)

932.75 hits per line

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

71.34
/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")
35✔
25

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

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

32
-- Developer tooling profiler
33
local ProFi
34

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

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

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

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

91
-- Internal libraries that assume globals, may be picky about load order
92
SILE.measurement = require("core.measurement")
34✔
93
SILE.length = require("core.length")
34✔
94
SILE.papersize = require("core.papersize")
34✔
95
SILE.nodefactory = require("core.nodefactory")
34✔
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
36✔
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 ()
34✔
116
  if not SILE.backend then
18✔
117
    SILE.backend = "libtexpdf"
18✔
118
  end
119
  if SILE.backend == "libtexpdf" then
18✔
120
    SILE.shaper = SILE.shapers.harfbuzz()
54✔
121
    SILE.outputter = SILE.outputters.libtexpdf()
54✔
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()
54✔
136
  io.stdout:setvbuf("no")
18✔
137
  if SU.debugging("profile") then
36✔
138
    ProFi = require("ProFi")
×
139
    ProFi:start()
×
140
  end
141
  if SILE.makeDeps then
18✔
142
    SILE.makeDeps:add(_G.executablePath)
×
143
  end
144
  runEvals(SILE.input.evaluates, "evaluate")
18✔
145
end
146

147
SILE.use = function (module, options)
34✔
148
  local pack
149
  if type(module) == "string" then
10✔
150
    pack = require(module)
10✔
151
  elseif type(module) == "table" then
×
152
    pack = module
×
153
  end
154
  local name = pack._name
10✔
155
  local class = SILE.documentState.documentClass
10✔
156
  if not pack.type then
10✔
157
    SU.error("Modules must declare their type")
×
158
  elseif pack.type == "class" then
10✔
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
10✔
165
    SILE.inputters[name] = pack
×
166
    SILE.inputter = pack(options)
×
167
  elseif pack.type == "outputter" then
10✔
168
    SILE.outputters[name] = pack
×
169
    SILE.outputter = pack(options)
×
170
  elseif pack.type == "shaper" then
10✔
171
    SILE.shapers[name] = pack
×
172
    SILE.shaper = pack(options)
×
173
  elseif pack.type == "typesetter" then
10✔
174
    SILE.typesetters[name] = pack
×
175
    SILE.typesetter = pack(options)
×
176
  elseif pack.type == "pagebuilder" then
10✔
177
    SILE.pagebuilders[name] = pack
×
178
    SILE.pagebuilder = pack(options)
×
179
  elseif pack.type == "package" then
10✔
180
    SILE.packages[name] = pack
10✔
181
    if class then
10✔
182
      pack(options)
20✔
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)
34✔
190
  if pathprefix and not deprecation_ack then
18✔
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$", "")
18✔
200
  local status, lib
201
  if pathprefix then
18✔
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 }))
54✔
205
  end
206
  if not status then
18✔
207
    local prefixederror = lib
×
208
    status, lib = pcall(require, dependency)
×
209
    if not status then
×
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
18✔
215
  if not class and not deprecation_ack then
18✔
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
18✔
223
    if lib.type == "package" then
×
224
      lib(class)
×
225
    else
226
      class:initPackage(lib)
×
227
    end
228
  end
229
  return lib
18✔
230
end
231

232
SILE.process = function (ast)
34✔
233
  if not ast then return end
108✔
234
  if SU.debugging("ast") then
216✔
235
    SU.debugAST(ast, 0)
×
236
  end
237
  if type(ast) == "function" then return ast() end
108✔
238
  for _, content in ipairs(ast) do
396✔
239
    if type(content) == "string" then
311✔
240
      SILE.typesetter:typeset(content)
374✔
241
    elseif type(content) == "function" then
124✔
242
      content()
×
243
    elseif SILE.Commands[content.command] then
124✔
244
      SILE.call(content.command, content.options, content)
248✔
245
    elseif content.id == "texlike_stuff"
×
246
      or (not content.command and not content.id) then
×
247
      local pId = SILE.traceStack:pushContent(content, "texlike_stuff")
×
248
      SILE.process(content)
×
249
      SILE.traceStack:pop(pId)
×
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" }
34✔
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
18✔
263
    for _, format in ipairs(preloadedinputters) do
72✔
264
      local _ = SILE.inputters[format]
54✔
265
    end
266
  end
267
  local contentDetectionOrder = {}
18✔
268
  for _, inputter in pairs(SILE.inputters) do
72✔
269
    if inputter.order then table.insert(contentDetectionOrder, inputter) end
54✔
270
  end
271
  table.sort(contentDetectionOrder, function (a, b) return a.order < b.order end)
68✔
272
  local initialround = filename and 1 or 2
18✔
273
  for round = initialround, 3 do
21✔
274
    for _, inputter in ipairs(contentDetectionOrder) do
41✔
275
      SU.debug("inputter", "Running content type detection round", round, "with", inputter._name)
38✔
276
      if inputter.appropriate(round, filename, doc) then
76✔
277
        return inputter._name
18✔
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)
68✔
285
  local cpf
286
  if not filename then
22✔
287
    cpf = SILE.currentlyProcessingFile
4✔
288
    local caller = debug.getinfo(2, "Sl")
4✔
289
    SILE.currentlyProcessingFile = caller.short_src..":"..caller.currentline
4✔
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
76✔
296
    inputter = SILE.inputter
×
297
  else
298
    format = format or detectFormat(doc, filename)
40✔
299
    if not SILE.quiet then
22✔
300
      io.stderr:write(("<%s> as %s\n"):format(SILE.currentlyProcessingFile, format))
22✔
301
    end
302
    inputter = SILE.inputters[format](options)
44✔
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
58✔
306
      SILE.inputter = inputter
18✔
307
    end
308
  end
309
  local pId = SILE.traceStack:pushDocument(SILE.currentlyProcessingFile, doc)
22✔
310
  inputter:process(doc)
22✔
311
  SILE.traceStack:pop(pId)
22✔
312
  if cpf then SILE.currentlyProcessingFile = cpf end
22✔
313
end
314

315
function SILE.processFile (filename, format, options)
68✔
316
  local doc
317
  if filename == "-" then
18✔
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("\\", "/")
18✔
323
    if not SILE.masterFilename then
18✔
324
      SILE.masterFilename = pl.path.splitext(pl.path.normpath(filename))
72✔
325
    end
326
    if SILE.input.filenames[1] and not SILE.masterDir then
18✔
327
      SILE.masterDir = pl.path.dirname(SILE.input.filenames[1])
36✔
328
    end
329
    if SILE.masterDir and SILE.masterDir:len() >= 1 then
36✔
330
      _G.extendSilePath(SILE.masterDir)
18✔
331
      _G.extendSilePathRocks(SILE.masterDir .. "/lua_modules")
18✔
332
    end
333
    filename = SILE.resolveFile(filename) or SU.error("Could not find file")
36✔
334
    local mode = lfs.attributes(filename).mode
18✔
335
    if mode ~= "file" and mode ~= "named pipe" then
18✔
336
      SU.error(filename.." isn't a file or named pipe, it's a ".. mode .."!")
×
337
    end
338
    if SILE.makeDeps then
18✔
339
      SILE.makeDeps:add(filename)
×
340
    end
341
    local file, err = io.open(filename)
18✔
342
    if not file then
18✔
343
      print("Could not open "..filename..": "..err)
×
344
      return
×
345
    end
346
    doc = file:read("*a")
18✔
347
  end
348
  local cpf = SILE.currentlyProcessingFile
18✔
349
  SILE.currentlyProcessingFile = filename
18✔
350
  local pId = SILE.traceStack:pushDocument(filename, doc)
18✔
351
  local ret = SILE.processString(doc, format, filename, options)
18✔
352
  SILE.traceStack:pop(pId)
18✔
353
  SILE.currentlyProcessingFile = cpf
18✔
354
  return ret
18✔
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)
34✔
360
  local saveTypesetter = SILE.typesetter
23✔
361
  if SILE.typesetter.frame then SILE.typesetter.frame:leave(SILE.typesetter) end
23✔
362
  SILE.typesetter = SILE.typesetters.base(frame)
46✔
363
  SILE.settings:temporarily(func)
23✔
364
  SILE.typesetter:leaveHmode()
23✔
365
  SILE.typesetter:chuck()
23✔
366
  SILE.typesetter.frame:leave(SILE.typesetter)
23✔
367
  SILE.typesetter = saveTypesetter
23✔
368
  if SILE.typesetter.frame then SILE.typesetter.frame:enter(SILE.typesetter) end
23✔
369
end
370

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

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

415
function SILE.registerCommand (name, func, help, pack, cheat)
68✔
416
  local class = SILE.documentState.documentClass
205✔
417
  if not cheat then
205✔
418
    SU.deprecated("SILE.registerCommand", "class:registerCommand", "0.14.0", "0.16.0",
×
419
    [[Commands are being scoped to the document classes they are loaded into rather than being globals.]])
×
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
205✔
423
    return SILE.classes.base.registerCommand(nil, name, func, help, pack)
239✔
424
  end
425
  return class:registerCommand(name, func, help, pack)
×
426
end
427

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

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

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

470
-- Internal libraries that run core SILE functions on load
471
SILE.settings = require("core.settings")()
68✔
472
require("core.hyphenator-liang")
34✔
473
require("core.languages")
34✔
474
SILE.linebreak = require("core.break")
34✔
475
require("core.frame")
34✔
476
SILE.cli = require("core.cli")
34✔
477
SILE.repl = require("core.repl")
34✔
478
SILE.font = require("core.font")
34✔
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")
34✔
483

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