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

sile-typesetter / sile / 6932773445

20 Nov 2023 04:11PM UTC coverage: 60.703% (-1.6%) from 62.266%
6932773445

Pull #1904

github

alerque
feat(utilities): Add Greek alphabetical (non-arithmetic) numbering

Useful in some context such as biblical annotations etc. where greek
characters are used orderly for numbering.
Pull Request #1904: Merge develop into master (commit to next release being breaking)

66 of 193 new or added lines in 19 files covered. (34.2%)

321 existing lines in 26 files now uncovered.

9452 of 15571 relevant lines covered (60.7%)

2104.43 hits per line

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

72.96
/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")
54✔
25

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

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

32
-- Developer tooling profiler
33
local ProFi
34

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

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

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

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

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

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

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

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

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

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

415
function SILE.registerCommand (name, func, help, pack, cheat)
106✔
416
  local class = SILE.documentState.documentClass
319✔
417
  if not cheat then
319✔
UNCOV
418
    SU.deprecated("SILE.registerCommand", "class:registerCommand", "0.14.0", "0.16.0",
×
UNCOV
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
319✔
423
    return SILE.classes.base.registerCommand(nil, name, func, help, pack)
372✔
424
  end
UNCOV
425
  return class:registerCommand(name, func, help, pack)
×
426
end
427

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

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

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

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

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