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

sile-typesetter / sile / 6713098919

31 Oct 2023 10:21PM UTC coverage: 52.831% (-21.8%) from 74.636%
6713098919

push

github

web-flow
Merge d0a2a1ee9 into b185d4972

45 of 45 new or added lines in 3 files covered. (100.0%)

8173 of 15470 relevant lines covered (52.83%)

6562.28 hits per line

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

76.62
/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")
347✔
25

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

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

32
-- Developer tooling profiler
33
local ProFi
34

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

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

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

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

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

148
SILE.use = function (module, options)
346✔
149
  local pack
150
  if type(module) == "string" then
108✔
151
    pack = require(module)
107✔
152
  elseif type(module) == "table" then
1✔
153
    pack = module
1✔
154
  end
155
  local name = pack._name
108✔
156
  local class = SILE.documentState.documentClass
108✔
157
  if not pack.type then
108✔
158
    SU.error("Modules must declare their type")
×
159
  elseif pack.type == "class" then
108✔
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
108✔
166
    SILE.inputters[name] = pack
×
167
    SILE.inputter = pack(options)
×
168
  elseif pack.type == "outputter" then
108✔
169
    SILE.outputters[name] = pack
×
170
    SILE.outputter = pack(options)
×
171
  elseif pack.type == "shaper" then
108✔
172
    SILE.shapers[name] = pack
×
173
    SILE.shaper = pack(options)
×
174
  elseif pack.type == "typesetter" then
108✔
175
    SILE.typesetters[name] = pack
×
176
    SILE.typesetter = pack(options)
×
177
  elseif pack.type == "pagebuilder" then
108✔
178
    SILE.pagebuilders[name] = pack
×
179
    SILE.pagebuilder = pack(options)
×
180
  elseif pack.type == "package" then
108✔
181
    SILE.packages[name] = pack
108✔
182
    if class then
108✔
183
      pack(options)
216✔
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)
346✔
191
  if pathprefix and not deprecation_ack then
193✔
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$", "")
193✔
201
  local status, lib
202
  if pathprefix then
193✔
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 }))
519✔
206
  end
207
  if not status then
193✔
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
193✔
216
  if not class and not deprecation_ack then
193✔
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
193✔
224
    if lib.type == "package" then
20✔
225
      lib(class)
40✔
226
    else
227
      class:initPackage(lib)
×
228
    end
229
  end
230
  return lib
193✔
231
end
232

233
SILE.process = function (ast)
346✔
234
  if not ast then return end
1,400✔
235
  if SU.debugging("ast") then
2,792✔
236
    SU.debugAST(ast, 0)
×
237
  end
238
  if type(ast) == "function" then return ast() end
1,396✔
239
  for _, content in ipairs(ast) do
4,826✔
240
    if type(content) == "string" then
3,793✔
241
      SILE.typesetter:typeset(content)
4,430✔
242
    elseif type(content) == "function" then
1,578✔
243
      content()
2✔
244
    elseif SILE.Commands[content.command] then
1,577✔
245
      SILE.call(content.command, content.options, content)
3,132✔
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" }
346✔
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
174✔
264
    for _, format in ipairs(preloadedinputters) do
696✔
265
      local _ = SILE.inputters[format]
522✔
266
    end
267
  end
268
  local contentDetectionOrder = {}
174✔
269
  for _, inputter in pairs(SILE.inputters) do
696✔
270
    if inputter.order then table.insert(contentDetectionOrder, inputter) end
522✔
271
  end
272
  table.sort(contentDetectionOrder, function (a, b) return a.order < b.order end)
657✔
273
  local initialround = filename and 1 or 2
174✔
274
  for round = initialround, 3 do
177✔
275
    for _, inputter in ipairs(contentDetectionOrder) do
353✔
276
      SU.debug("inputter", "Running content type detection round", round, "with", inputter._name)
350✔
277
      if inputter.appropriate(round, filename, doc) then
700✔
278
        return inputter._name
174✔
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)
692✔
286
  local cpf
287
  if not filename then
242✔
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
764✔
297
    inputter = SILE.inputter
×
298
  else
299
    format = format or detectFormat(doc, filename)
416✔
300
    if not SILE.quiet then
242✔
301
      io.stderr:write(("<%s> as %s\n"):format(SILE.currentlyProcessingFile, format))
242✔
302
    end
303
    inputter = SILE.inputters[format](options)
484✔
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
590✔
307
      SILE.inputter = inputter
173✔
308
    end
309
  end
310
  local pId = SILE.traceStack:pushDocument(SILE.currentlyProcessingFile, doc)
242✔
311
  inputter:process(doc)
242✔
312
  SILE.traceStack:pop(pId)
242✔
313
  if cpf then SILE.currentlyProcessingFile = cpf end
242✔
314
end
315

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

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

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

416
function SILE.registerCommand (name, func, help, pack, cheat)
692✔
417
  local class = SILE.documentState.documentClass
2,099✔
418
  if not cheat then
2,099✔
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
2,099✔
424
    return SILE.classes.base.registerCommand(nil, name, func, help, pack)
2,443✔
425
  end
426
  return class:registerCommand(name, func, help, pack)
2✔
427
end
428

429
function SILE.setCommandDefaults (command, defaults)
692✔
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)
692✔
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)
692✔
448
  -- SU.deprecated("SILE.paperSizeParser", "SILE.papersize", "0.10.0", nil)
449
  return SILE.papersize(size)
×
450
end
451

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

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

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

485
return SILE
346✔
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

© 2025 Coveralls, Inc