• 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

51.73
/core/utilities.lua
1
local bitshim = require("bitshim")
34✔
2
local luautf8 = require("lua-utf8")
34✔
3
local semver = require("semver")
34✔
4

5
local utilities = {}
34✔
6

7
local epsilon = 1E-12
34✔
8

9
utilities.required = function (options, name, context, required_type)
10
  if not options[name] then utilities.error(context.." needs a "..name.." parameter") end
1,466✔
11
  if required_type then
1,466✔
12
    return utilities.cast(required_type, options[name])
261✔
13
  end
14
  return options[name]
1,205✔
15
end
16

17
local function preferbool ()
18
  utilities.warn("Please use boolean values or strings such as 'true' and 'false' instead of 'yes' and 'no'.")
×
19
end
20

21
utilities.boolean = function (value, default)
22
  if value == false then return false end
1,125✔
23
  if value == true then return true end
1,119✔
24
  if value == "false" then return false end
481✔
25
  if value == "true" then return true end
479✔
26
  if value == "no" then preferbool(); return false end
476✔
27
  if value == "yes" then preferbool(); return true end
476✔
28
  if value == nil then return default end
476✔
29
  SU.error("Expecting a boolean value but got '" .. value .. "'")
×
30
  return default
×
31
end
32

33
local _skip_traceback_levels = 2
34✔
34

35
utilities.error = function (message, isbug)
36
  _skip_traceback_levels = 3
×
37
  utilities.warn(message, isbug)
×
38
  _skip_traceback_levels = 2
×
39
  io.stderr:flush()
×
40
  SILE.outputter:finish() -- Only really useful from the REPL but no harm in trying
×
41
  SILE.scratch.caughterror = true
×
42
  error(message, 2)
×
43
end
44

45
utilities.warn = function (message, isbug)
46
  if SILE.quiet then return end
4✔
47
  io.stderr:write("\n! " .. message)
4✔
48
  if SILE.traceback or isbug then
4✔
49
    io.stderr:write(" at:\n" .. SILE.traceStack:locationTrace())
×
50
    if _skip_traceback_levels == 2 then
×
51
      io.stderr:write(debug.traceback("", _skip_traceback_levels) or "\t! debug.traceback() did not identify code location")
×
52
    end
53
  else
54
    io.stderr:write(" at " .. SILE.traceStack:locationHead())
8✔
55
  end
56
  io.stderr:write("\n")
4✔
57
end
58

59
utilities.msg = function (message)
60
  if SILE.quiet then return end
×
61
  io.stderr:write("\n! " .. message .. "\n")
×
62
end
63

64
utilities.debugging = function (category)
65
  return SILE.debugFlags.all and category ~= "profile" or SILE.debugFlags[category]
19,754✔
66
end
67

68
utilities.feq = function (lhs, rhs) -- Float point equal
69
  lhs = SU.cast("number", lhs)
56✔
70
  rhs = SU.cast("number", rhs)
56✔
71
  local abs = math.abs
28✔
72
  return abs(lhs - rhs) <= epsilon * (abs(lhs) + abs(rhs))
28✔
73
end
74

75
utilities.gtoke = function (string, pattern)
76
  string = string and tostring(string) or ''
166✔
77
  pattern = pattern and tostring(pattern) or "%s+"
166✔
78
  local length = #string
166✔
79
  return coroutine.wrap(function()
166✔
80
    local index = 1
166✔
81
    repeat
82
      local first, last = string:find(pattern, index)
258✔
83
      if last then
258✔
84
        if index < first then coroutine.yield({ string = string:sub(index, first - 1) }) end
170✔
85
        coroutine.yield({ separator = string:sub(first, last) })
218✔
86
        index = last + 1
109✔
87
      else
88
        if index <= length then
149✔
89
          coroutine.yield({ string = string:sub(index) })
270✔
90
        end
91
        break
135✔
92
      end
93
    until index > length
109✔
94
  end)
95
end
96

97
utilities.deprecated = function (old, new, warnat, errorat, extra)
98
  warnat, errorat = semver(warnat or 0), semver(errorat or 0)
6✔
99
  local current = SILE.version and semver(SILE.version:match("v([0-9]*.[0-9]*.[0-9]*)")) or warnat
4✔
100
  -- SILE.version is defined *after* most of SILE loads. It’s available at
101
  -- runtime but not useful if we encounter deprecated code in core code. Users
102
  -- will never encounter this failure, but as a developer it’s hard to test a
103
  -- deprecation when core code refactoring is an all-or-nothing proposition.
104
  -- Hence we fake it ‘till we make it, all deprecations internally are warnings.
105
  local brackets = old:sub(1,1) == '\\' and "" or "()"
4✔
106
  local _new = new and "Please use " .. (new .. brackets) .. " instead." or "Plase don't use it."
2✔
107
  local msg = (old .. brackets) .. " was deprecated in SILE v" .. tostring(warnat) .. ". " .. _new ..  (extra and "\n" .. extra .. "\n\n" or "")
4✔
108
  if errorat and current >= errorat then
2✔
109
    SU.error(msg)
×
110
  elseif warnat and current >= warnat then
2✔
111
    SU.warn(msg)
2✔
112
  end
113
end
114

115
utilities.debug = function (category, ...)
116
  if SILE.quiet then return end
18,746✔
117
  if utilities.debugging(category) then
37,492✔
118
    local inputs = table.pack(...)
×
119
    for i, input in ipairs(inputs) do
×
120
      if type(input) == "function" then
×
121
        local status, output = pcall(input)
×
122
        inputs[i] = status and output or SU.warn(("Output of %s debug function was an error: %s"):format(category, output))
×
123
      end
124
    end
125
    local message = utilities.concat(inputs, " ")
×
126
    if message then io.stderr:write(("\n[%s] %s"):format(category, message)) end
×
127
  end
128
end
129

130
utilities.debugAST = function (ast, level)
131
  if not ast then
×
132
    SU.error("debugAST called with nil", true)
×
133
  end
134
  local out = string.rep("  ", 1+level)
×
135
  if level == 0 then
×
136
    SU.debug("ast", function ()
×
137
      return "[" .. SILE.currentlyProcessingFile
×
138
    end)
139
  end
140
  if type(ast) == "function" then
×
141
    SU.debug("ast", function ()
×
142
      return out .. tostring(ast)
×
143
    end)
144
  elseif type(ast) == "table" then
×
145
    for _, content in ipairs(ast) do
×
146
      if type(content) == "string" then
×
147
        SU.debug("ast", function ()
×
148
          return out .. "[" .. content .. "]"
×
149
        end)
150
      elseif type(content) == "table" then
×
151
        if SILE.Commands[content.command] then
×
152
          SU.debug("ast", function ()
×
153
            return out .. "\\" .. content.command .. " " .. pl.pretty.write(content.options, "")
×
154
          end)
155
          if (#content>=1) then utilities.debugAST(content, level+1) end
×
156
        elseif content.id == "texlike_stuff" or (not content.command and not content.id) then
×
157
          utilities.debugAST(content, level+1)
×
158
        else
159
          SU.debug("ast", function ()
×
160
            return out .. "?\\" .. (content.command or content.id)
×
161
          end)
162
        end
163
      end
164
    end
165
  end
166
  if level == 0 then SU.debug("ast", "]") end
×
167
end
168

169
utilities.dump = function (...)
170
  local arg = { ... } -- Avoid things that Lua stuffs in arg like args to self()
×
171
  pl.pretty.dump(#arg == 1 and arg[1] or arg, "/dev/stderr")
×
172
end
173

174
utilities.concat = function (array, separator)
175
  return table.concat(utilities.map(tostring, array), separator)
×
176
end
177

178
utilities.inherit = function (orig, spec)
179
  local new = pl.tablex.deepcopy(orig)
×
180
  if spec then
×
181
    for k,v in pairs(spec) do new[k] = v end
×
182
  end
183
  if new.init then new:init() end
×
184
  return new
×
185
end
186

187
utilities.map = function (func, array)
188
  local new_array = {}
5,676✔
189
  local last = #array
5,676✔
190
  for i = 1, last do
19,938✔
191
    new_array[i] = func(array[i])
28,524✔
192
  end
193
  return new_array
5,676✔
194
end
195

196
utilities.sortedpairs = function (input)
197
  local keys = {}
×
198
  for k, _ in pairs(input) do
×
199
    keys[#keys+1] = k
×
200
  end
201
  table.sort(keys, function(a, b)
×
202
    if type(a) == type(b) then return a < b
×
203
    elseif type(a) == "number" then return true
×
204
    else return false
×
205
    end
206
  end)
207
  return coroutine.wrap(function()
×
208
    for i = 1, #keys do
×
209
      coroutine.yield(keys[i], input[keys[i]])
×
210
    end
211
  end)
212
end
213

214
utilities.splice = function (array, start, stop, replacement)
215
  local ptr = start
32✔
216
  local room = stop - start + 1
32✔
217
  local last = replacement and #replacement or 0
32✔
218
  for i = 1, last do
203✔
219
    if room > 0 then
171✔
220
      room = room - 1
32✔
221
      array[ptr] = replacement[i]
32✔
222
    else
223
      table.insert(array, ptr, replacement[i])
139✔
224
    end
225
    ptr = ptr + 1
171✔
226
  end
227

228
  for _ = 1, room do
32✔
229
      table.remove(array, ptr)
×
230
  end
231
  return array
32✔
232
end
233

234
utilities.sum = function (array)
235
  local total = 0
1,619✔
236
  local last = #array
1,619✔
237
  for i = 1, last do
3,090✔
238
    total = total + array[i]
1,471✔
239
  end
240
  return total
1,619✔
241
end
242

243
-- Lua <= 5.2 can't handle objects in math functions
244
utilities.max = function (...)
245
  local input = pl.utils.pack(...)
3,617✔
246
  local max = table.remove(input, 1)
3,617✔
247
  for _, val in ipairs(input) do
13,302✔
248
    if val > max then max = val end
12,932✔
249
  end
250
  return max
3,617✔
251
end
252

253
utilities.min = function (...)
254
  local input = pl.utils.pack(...)
1✔
255
  local min = input[1]
1✔
256
  for _, val in ipairs(input) do
3✔
257
    if val < min then min = val end
2✔
258
  end
259
  return min
1✔
260
end
261

262
utilities.compress = function (items)
263
  local rv = {}
172✔
264
  local max = math.max(table.unpack(pl.tablex.keys(items)))
344✔
265
  for i = 1, max do if items[i] then rv[#rv+1] = items[i] end end
3,295✔
266
  return rv
172✔
267
end
268

269
utilities.flip_in_place = function (tbl)
270
  local tmp, j
271
  for i = 1, math.floor(#tbl / 2) do
532✔
272
    tmp = tbl[i]
374✔
273
    j = #tbl - i + 1
374✔
274
    tbl[i] = tbl[j]
374✔
275
    tbl[j] = tmp
374✔
276
  end
277
end
278

279
utilities.allCombinations = function (options)
280
  local count = 1
×
281
  for i=1,#options do count = count * options[i] end
×
282
  return coroutine.wrap(function()
×
283
    for i=0,count-1 do
×
284
      local this = i
×
285
      local rv = {}
×
286
      for j = 1,#options do
×
287
        local base = options[j]
×
288
        rv[#rv+1] = this % base + 1
×
289
        this = (this - this % base )/ base
×
290
      end
291
      coroutine.yield(rv)
×
292
    end
293
  end)
294
end
295

296
utilities.type = function(value)
297
  if type(value) == "number" then
416,077✔
298
    return math.floor(value) == value and "integer" or "number"
111,556✔
299
  elseif type(value) == "table" and value.prototype then
304,521✔
300
    return value:prototype()
×
301
  elseif type(value) == "table" and value.is_a then
304,521✔
302
    return value.type
155,447✔
303
  else
304
    return type(value)
149,074✔
305
  end
306
end
307

308
utilities.cast = function (wantedType, value)
309
  local actualType = SU.type(value)
81,851✔
310
  wantedType = string.lower(wantedType)
163,702✔
311
  if wantedType:match(actualType)     then return value
81,851✔
312
  elseif actualType == "nil" and wantedType:match("nil") then return nil
57,924✔
313
  elseif wantedType:match("length")      then return SILE.length(value)
57,924✔
314
  elseif wantedType:match("measurement") then return SILE.measurement(value)
50,740✔
315
  elseif wantedType:match("vglue")       then return SILE.nodefactory.vglue(value)
49,967✔
316
  elseif wantedType:match("glue")        then return SILE.nodefactory.glue(value)
49,966✔
317
  elseif wantedType:match("kern")        then return SILE.nodefactory.kern(value)
49,882✔
318
  elseif actualType == "nil" then SU.error("Cannot cast nil to " .. wantedType)
49,882✔
319
  elseif wantedType:match("boolean")     then return SU.boolean(value)
49,882✔
320
  elseif wantedType:match("string")      then return tostring(value)
49,882✔
321
  elseif wantedType:match("number") then
49,882✔
322
    if type(value) == "table" and type(value.tonumber) == "function" then
49,876✔
323
      return value:tonumber()
48,566✔
324
    end
325
    local num = tonumber(value)
1,310✔
326
    if not num then SU.error("Cannot cast '" .. value .. "'' to " .. wantedType) end
1,310✔
327
    return num
1,310✔
328
  elseif wantedType:match("integer") then
6✔
329
    local num
330
    if type(value) == "table" and type(value.tonumber) == "function" then
6✔
331
      num = value:tonumber()
×
332
    else
333
      num = tonumber(value)
6✔
334
    end
335
    if not num then SU.error("Cannot cast '" .. value .. "'' to " .. wantedType) end
6✔
336
    if not wantedType:match("number") and num % 1 ~= 0 then
6✔
337
      -- Could be an error but since it wasn't checked before, let's just warn:
338
      -- Some packages might have wrongly typed settings, for instance.
339
      SU.warn("Casting an integer but got a float number " .. num)
×
340
    end
341
    return num
6✔
342
  else SU.error("Cannot cast to unrecognized type " .. wantedType)
×
343
  end
344
end
345

346
utilities.hasContent = function(content)
347
  return type(content) == "function" or type(content) == "table" and #content > 0
36✔
348
end
349

350
-- Flatten content trees into just the string components (allows passing
351
-- objects with complex structures to functions that need plain strings)
352
utilities.contentToString = function (content)
353
  local string = ""
×
354
  for i = 1, #content do
×
355
    if type(content[i]) == "table" and type(content[i][1]) == "string" then
×
356
      string = string .. content[i][1]
×
357
    elseif type(content[i]) == "string" then
×
358
      -- Work around PEG parser returning env tags as content
359
      -- TODO: refactor capture groups in PEG parser
360
      if content.command == content[i] and content[i] == content[i+1] then
×
361
        break
362
      end
363
      string = string .. content[i]
×
364
    end
365
  end
366
  return string
×
367
end
368

369
-- Strip the top level command off a content object and keep only the child
370
-- items — assuming that the current command is taking care of itself
371
utilities.subContent = function (content)
372
  local out = { id="stuff" }
×
373
  for key, val in utilities.sortedpairs(content) do
×
374
    if type(key) == "number" then
×
375
      out[#out+1] = val
×
376
    end
377
  end
378
  return out
×
379
end
380

381
-- Call `action` on each content AST node, recursively, including `content` itself.
382
-- Not called on leaves, i.e. strings.
383
utilities.walkContent = function (content, action)
384
  if type(content) ~= "table" then
×
385
    return
×
386
  end
387
  action(content)
×
388
  for i = 1, #content do
×
389
    utilities.walkContent(content[i], action)
×
390
  end
391
end
392

393
--- Strip position, line and column recursively from a content tree.
394
-- This can be used to remove position details where we do not want them,
395
-- e.g. in table of contents entries (referring to the original content,
396
-- regardless where it was exactly, for the purpose of checking whether
397
-- the table of contents changed.)
398
--
399
utilities.stripContentPos = function (content)
400
  if type(content) ~= "table" then
×
401
    return content
×
402
  end
403
  local stripped = {}
×
404
  for k, v in pairs(content) do
×
405
    if type(v) == "table" then
×
406
      v = SU.stripContentPos(v)
×
407
    end
408
    stripped[k] = v
×
409
  end
410
  if content.id or content.command then
×
411
    stripped.pos, stripped.col, stripped.lno = nil, nil, nil
×
412
  end
413
  return stripped
×
414
end
415

416
utilities.rateBadness = function(inf_bad, shortfall, spring)
417
  if spring == 0 then return inf_bad end
4,525✔
418
  local bad = math.floor(100 * math.abs(shortfall / spring) ^ 3)
4,468✔
419
  return math.min(inf_bad, bad)
4,468✔
420
end
421

422
utilities.rationWidth = function (target, width, ratio)
423
  if ratio < 0 and width.shrink:tonumber() > 0 then
3,010✔
424
    target:___add(width.shrink:tonumber() * ratio)
345✔
425
  elseif ratio > 0 and width.stretch:tonumber() > 0 then
4,981✔
426
    target:___add(width.stretch:tonumber() * ratio)
1,438✔
427
  end
428
  return target
2,702✔
429
end
430

431
-- Unicode-related utilities
432
utilities.utf8char = function (c)
433
  utilities.deprecated("SU.utf8char", "luautf8.char", "0.11.0", "0.12.0")
×
434
  return luautf8.char(c)
×
435
end
436

437
utilities.codepoint = function (uchar)
438
  local seq = 0
21,917✔
439
  local val = -1
21,917✔
440
  for i = 1, #uchar do
44,450✔
441
    local c = string.byte(uchar, i)
23,246✔
442
    if seq == 0 then
23,246✔
443
      if val > -1 then return val end
21,955✔
444
      seq = c < 0x80 and 1 or c < 0xE0 and 2 or c < 0xF0 and 3 or
21,242✔
445
            c < 0xF8 and 4 or --c < 0xFC and 5 or c < 0xFE and 6 or
×
446
          error("invalid UTF-8 character sequence")
×
447
      val = bitshim.band(c, 2^(8-seq) - 1)
21,242✔
448
    else
449
      val = bitshim.bor(bitshim.lshift(val, 6), bitshim.band(c, 0x3F))
1,291✔
450
    end
451
    seq = seq - 1
22,533✔
452
  end
453
  return val
21,204✔
454
end
455

456
utilities.utf8charfromcodepoint = function (codepoint)
457
  local val = codepoint
×
458
  local cp = val
×
459
  local hex = (cp:match("[Uu]%+(%x+)") or cp:match("0[xX](%x+)"))
×
460
  if hex then
×
461
    cp = tonumber("0x"..hex)
×
462
  elseif tonumber(cp) then
×
463
    cp = tonumber(cp)
×
464
  end
465

466
  if type(cp) == "number" then
×
467
    val = luautf8.char(cp)
×
468
  end
469
  return val
×
470
end
471

472
utilities.utf8codes = function (ustr)
473
  utilities.deprecated("SU.utf8codes", "luautf8.codes", "0.11.0", "0.12.0")
×
474
  return luautf8.codes(ustr)
×
475
end
476

477
utilities.utf16codes = function (ustr, endian)
478
  local pos = 1
5,707✔
479
  return function()
480
    if pos > #ustr then
174,431✔
481
      return nil
5,707✔
482
    else
483
      local c1, c2, c3, c4, wchar, lowchar
484
      c1 = string.byte(ustr, pos, pos+1)
168,724✔
485
      pos = pos + 1
168,724✔
486
      c2 = string.byte(ustr, pos, pos+1)
168,724✔
487
      pos = pos + 1
168,724✔
488
      if endian == "be" then
168,724✔
489
        wchar = c1 * 256 + c2
168,724✔
490
      else
491
        wchar = c2 * 256 + c1
×
492
      end
493
      if not (wchar >= 0xD800 and wchar <= 0xDBFF) then
168,724✔
494
        return wchar
168,724✔
495
      end
496
      c3 = string.byte(ustr, pos, pos+1)
×
497
      pos = pos + 1
×
498
      c4 = string.byte(ustr, pos, pos+1)
×
499
      pos = pos + 1
×
500
      if endian == "be" then
×
501
        lowchar = c3 * 256 + c4
×
502
      else
503
        lowchar = c4 * 256 + c3
×
504
      end
505
      return 0x10000 + bitshim.lshift(bitshim.band(wchar, 0x03FF), 10) + bitshim.band(lowchar, 0x03FF)
×
506
    end
507
  end
508
end
509

510
utilities.splitUtf8 = function (str) -- Return an array of UTF8 strings each representing a Unicode char
511
  local rv = {}
14,072✔
512
  for _, cp in luautf8.next, str do
94,331✔
513
    table.insert(rv, luautf8.char(cp))
80,259✔
514
  end
515
  return rv
14,072✔
516
end
517

518
utilities.lastChar = function (str)
519
  local chars = utilities.splitUtf8(str)
×
520
  return chars[#chars]
×
521
end
522

523
utilities.firstChar = function (str)
524
  local chars = utilities.splitUtf8(str)
×
525
  return chars[1]
×
526
end
527

528
local byte, floor, reverse = string.byte, math.floor, string.reverse
34✔
529

530
utilities.utf8charat = function (str, index)
531
  return str:sub(index):match("([%z\1-\127\194-\244][\128-\191]*)")
×
532
end
533

534
local utf16bom = function(endianness)
535
  return endianness == "be" and "\254\255" or endianness == "le" and "\255\254" or SU.error("Unrecognized endianness")
5,707✔
536
end
537

538
utilities.hexencoded = function (str)
539
  local ustr = ""
×
540
  for i = 1, #str do
×
541
    ustr = ustr..string.format("%02x", byte(str, i, i+1))
×
542
  end
543
  return ustr
×
544
end
545

546
utilities.hexdecoded = function (str)
547
  if #str % 2 == 1 then SU.error("Cannot decode hex string with odd len") end
×
548
  local ustr = ""
×
549
  for i = 1, #str, 2 do
×
550
    ustr = ustr..string.char(tonumber(string.sub(str, i, i+1), 16))
×
551
  end
552
  return ustr
×
553
end
554

555
local uchr_to_surrogate_pair = function(uchr, endianness)
556
  local hi, lo = floor((uchr - 0x10000) / 0x400) + 0xd800, (uchr - 0x10000) % 0x400 + 0xdc00
×
557
  local s_hi, s_lo = string.char(floor(hi / 256)) .. string.char(hi % 256), string.char(floor(lo / 256)) .. string.char(lo % 256)
×
558
  return endianness == "le" and (reverse(s_hi) .. reverse(s_lo)) or s_hi .. s_lo
×
559
end
560

561
local uchr_to_utf16_double_byte = function(uchr, endianness)
562
  local ustr = string.char(floor(uchr / 256)) .. string.char(uchr % 256)
×
563
  return endianness == "le" and reverse(ustr) or ustr
×
564
end
565

566
local utf8_to_utf16 = function(str, endianness)
567
  local ustr = utf16bom(endianness)
×
568
  for _, uchr in luautf8.codes(str) do
×
569
    ustr = ustr..(uchr < 0x10000 and uchr_to_utf16_double_byte(uchr, endianness)
×
570
                  or uchr_to_surrogate_pair(uchr, endianness))
×
571
  end
572
  return ustr
×
573
end
574

575
utilities.utf8_to_utf16be = function (str) return utf8_to_utf16(str, "be") end
34✔
576
utilities.utf8_to_utf16le = function (str) return utf8_to_utf16(str, "le") end
34✔
577
utilities.utf8_to_utf16be_hexencoded = function (str) return utilities.hexencoded(utilities.utf8_to_utf16be(str)) end
34✔
578
utilities.utf8_to_utf16le_hexencoded = function (str) return utilities.hexencoded(utilities.utf8_to_utf16le(str)) end
34✔
579

580
local utf16_to_utf8 = function (str, endianness)
581
  local bom = utf16bom(endianness)
5,707✔
582

583
  if str:find(bom) == 1 then str = string.sub(str, 3, #str) end
5,707✔
584
  local ustr = ""
5,707✔
585
  for uchr in utilities.utf16codes(str, endianness) do
354,569✔
586
    ustr = ustr..luautf8.char(uchr)
168,724✔
587
  end
588
  return ustr
5,707✔
589
end
590

591
utilities.utf16be_to_utf8 = function (str) return utf16_to_utf8(str, "be") end
5,741✔
592
utilities.utf16le_to_utf8 = function (str) return utf16_to_utf8(str, "le") end
34✔
593

594
utilities.breadcrumbs = function ()
595
  local breadcrumbs = {}
43✔
596

597
  setmetatable (breadcrumbs, {
86✔
598
      __index = function(_, key)
599
        local frame = SILE.traceStack[key]
×
600
        return frame and frame.command or nil
×
601
      end,
602
      __len = function(_)
603
        return #SILE.traceStack
×
604
      end,
605
      __tostring = function (self)
606
        return "B»" .. table.concat(self, "»")
×
607
      end
608
    })
609

610
  function breadcrumbs:dump ()
43✔
611
    SU.dump(self)
×
612
  end
613

614
  function breadcrumbs:parent (count)
43✔
615
    -- Note LuaJIT does not support __len, so this has to work even when that metamethod doesn't fire...
616
    return self[#SILE.traceStack-(count or 1)]
×
617
  end
618

619
  function breadcrumbs:contains (needle)
43✔
620
    for i, command in ipairs(self) do
×
621
      if command == needle then return true, #self - i end
×
622
    end
623
    return false, -1
×
624
  end
625

626
  return breadcrumbs
43✔
627
end
628

629
utilities.formatNumber = require("core.utilities-numbers")
34✔
630

631
utilities.collatedSort = require("core.utilities-sorting")
34✔
632

633
return utilities
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