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

sile-typesetter / sile / 5913454052

19 Aug 2023 08:49PM UTC coverage: 54.288% (-20.1%) from 74.359%
5913454052

push

github

web-flow
Merge bb71e7fce into c8a15fb85

8469 of 15600 relevant lines covered (54.29%)

6468.28 hits per line

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

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

5
local utilities = {}
171✔
6

7
local epsilon = 1E-12
171✔
8

9
utilities.required = function (options, name, context, required_type)
10
  if not options[name] then utilities.error(context.." needs a "..name.." parameter") end
8,946✔
11
  if required_type then
8,946✔
12
    return utilities.cast(required_type, options[name])
1,639✔
13
  end
14
  return options[name]
7,307✔
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
5,931✔
23
  if value == true then return true end
5,925✔
24
  if value == "false" then return false end
2,896✔
25
  if value == "true" then return true end
2,893✔
26
  if value == "no" then preferbool(); return false end
2,887✔
27
  if value == "yes" then preferbool(); return true end
2,887✔
28
  if value == nil then return default end
2,887✔
29
  SU.error("Expecting a boolean value but got '" .. value .. "'")
×
30
  return default
×
31
end
32

33
local _skip_traceback_levels = 2
171✔
34

35
utilities.error = function(message, bug)
36
  _skip_traceback_levels = 3
×
37
  utilities.warn(message, bug)
×
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, bug)
46
  if SILE.quiet then return end
102✔
47
  io.stderr:write("\n! " .. message)
102✔
48
  if SILE.traceback or bug then
102✔
49
    io.stderr:write(" at:\n" .. SILE.traceStack:locationTrace())
2✔
50
    if _skip_traceback_levels == 2 then
1✔
51
      io.stderr:write(debug.traceback("", _skip_traceback_levels) or "\t! debug.traceback() did not identify code location")
1✔
52
    end
53
  else
54
    io.stderr:write(" at " .. SILE.traceStack:locationHead())
202✔
55
  end
56
  io.stderr:write("\n")
102✔
57
end
58

59
utilities.debugging = function (category)
60
  return SILE.debugFlags.all and category ~= "profile" or SILE.debugFlags[category]
161,485✔
61
end
62

63
utilities.feq = function (lhs, rhs) -- Float point equal
64
  lhs = SU.cast("number", lhs)
182✔
65
  rhs = SU.cast("number", rhs)
182✔
66
  local abs = math.abs
91✔
67
  return abs(lhs - rhs) <= epsilon * (abs(lhs) + abs(rhs))
91✔
68
end
69

70
utilities.gtoke = function (string, pattern)
71
  string = string and tostring(string) or ''
1,933✔
72
  pattern = pattern and tostring(pattern) or "%s+"
1,933✔
73
  local length = #string
1,933✔
74
  return coroutine.wrap(function()
1,933✔
75
    local index = 1
1,933✔
76
    repeat
77
      local first, last = string:find(pattern, index)
2,288✔
78
      if last then
2,288✔
79
        if index < first then coroutine.yield({ string = string:sub(index, first - 1) }) end
996✔
80
        coroutine.yield({ separator = string:sub(first, last) })
1,266✔
81
        index = last + 1
633✔
82
      else
83
        if index <= length then
1,655✔
84
          coroutine.yield({ string = string:sub(index) })
3,056✔
85
        end
86
        break
1,528✔
87
      end
88
    until index > length
633✔
89
  end)
90
end
91

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

110
utilities.debug = function (category, ...)
111
  if SILE.quiet then return end
154,364✔
112
  if utilities.debugging(category) then
308,728✔
113
    local inputs = table.pack(...)
×
114
    for i, input in ipairs(inputs) do
×
115
      if type(input) == "function" then
×
116
        local status, output = pcall(input)
×
117
        inputs[i] = status and output or SU.warn(("Output of %s debug function was an error: %s"):format(category, output))
×
118
      end
119
    end
120
    local message = utilities.concat(inputs, " ")
×
121
    if message then io.stderr:write(("\n[%s] %s"):format(category, message)) end
×
122
  end
123
end
124

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

164
utilities.dump = function (...)
165
  local arg = { ... } -- Avoid things that Lua stuffs in arg like args to self()
1✔
166
  pl.pretty.dump(#arg == 1 and arg[1] or arg, "/dev/stderr")
2✔
167
end
168

169
utilities.concat = function (array, separator)
170
  return table.concat(utilities.map(tostring, array), separator)
120✔
171
end
172

173
utilities.inherit = function (orig, spec)
174
  local new = pl.tablex.deepcopy(orig)
×
175
  if spec then
×
176
    for k,v in pairs(spec) do new[k] = v end
×
177
  end
178
  if new.init then new:init() end
×
179
  return new
×
180
end
181

182
utilities.map = function (func, array)
183
  local new_array = {}
63,276✔
184
  local last = #array
63,276✔
185
  for i = 1, last do
215,119✔
186
    new_array[i] = func(array[i])
303,391✔
187
  end
188
  return new_array
63,276✔
189
end
190

191
utilities.sortedpairs = function (input)
192
  local keys = {}
8✔
193
  for k, _ in pairs(input) do
64✔
194
    keys[#keys+1] = k
56✔
195
  end
196
  table.sort(keys, function(a, b)
16✔
197
    if type(a) == type(b) then return a < b
136✔
198
    elseif type(a) == "number" then return true
24✔
199
    else return false
24✔
200
    end
201
  end)
202
  return coroutine.wrap(function()
8✔
203
    for i = 1, #keys do
64✔
204
      coroutine.yield(keys[i], input[keys[i]])
56✔
205
    end
206
  end)
207
end
208

209
utilities.splice = function (array, start, stop, replacement)
210
  local ptr = start
550✔
211
  local room = stop - start + 1
550✔
212
  local last = replacement and #replacement or 0
550✔
213
  for i = 1, last do
3,830✔
214
    if room > 0 then
3,280✔
215
      room = room - 1
496✔
216
      array[ptr] = replacement[i]
496✔
217
    else
218
      table.insert(array, ptr, replacement[i])
2,784✔
219
    end
220
    ptr = ptr + 1
3,280✔
221
  end
222

223
  for _ = 1, room do
604✔
224
      table.remove(array, ptr)
54✔
225
  end
226
  return array
550✔
227
end
228

229
utilities.sum = function (array)
230
  local total = 0
18,151✔
231
  local last = #array
18,151✔
232
  for i = 1, last do
34,705✔
233
    total = total + array[i]
16,554✔
234
  end
235
  return total
18,151✔
236
end
237

238
-- Lua <= 5.2 can't handle objects in math functions
239
utilities.max = function (...)
240
  local input = pl.utils.pack(...)
40,451✔
241
  local max = table.remove(input, 1)
40,451✔
242
  for _, val in ipairs(input) do
139,807✔
243
    if val > max then max = val end
134,232✔
244
  end
245
  return max
40,451✔
246
end
247

248
utilities.min = function (...)
249
  local input = pl.utils.pack(...)
12✔
250
  local min = input[1]
12✔
251
  for _, val in ipairs(input) do
36✔
252
    if val < min then min = val end
24✔
253
  end
254
  return min
12✔
255
end
256

257
utilities.compress = function (items)
258
  local rv = {}
1,445✔
259
  local max = math.max(table.unpack(pl.tablex.keys(items)))
2,890✔
260
  for i = 1, max do if items[i] then rv[#rv+1] = items[i] end end
32,503✔
261
  return rv
1,445✔
262
end
263

264
utilities.flip_in_place = function (tbl)
265
  local tmp, j
266
  for i = 1, math.floor(#tbl / 2) do
1,122✔
267
    tmp = tbl[i]
676✔
268
    j = #tbl - i + 1
676✔
269
    tbl[i] = tbl[j]
676✔
270
    tbl[j] = tmp
676✔
271
  end
272
end
273

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

291
utilities.type = function(value)
292
  if type(value) == "number" then
2,902,572✔
293
    return math.floor(value) == value and "integer" or "number"
689,150✔
294
  elseif type(value) == "table" and value.prototype then
2,213,422✔
295
    return value:prototype()
×
296
  elseif type(value) == "table" and value.is_a then
2,213,422✔
297
    return value.type
1,075,266✔
298
  else
299
    return type(value)
1,138,156✔
300
  end
301
end
302

303
utilities.cast = function (wantedType, value)
304
  local actualType = SU.type(value)
663,103✔
305
  wantedType = string.lower(wantedType)
1,326,206✔
306
  if wantedType:match(actualType)     then return value
663,103✔
307
  elseif actualType == "nil" and wantedType:match("nil") then return nil
472,741✔
308
  elseif wantedType:match("length")      then return SILE.length(value)
472,741✔
309
  elseif wantedType:match("measurement") then return SILE.measurement(value)
412,535✔
310
  elseif wantedType:match("vglue")       then return SILE.nodefactory.vglue(value)
404,753✔
311
  elseif wantedType:match("glue")        then return SILE.nodefactory.glue(value)
404,740✔
312
  elseif wantedType:match("kern")        then return SILE.nodefactory.kern(value)
404,467✔
313
  elseif actualType == "nil" then SU.error("Cannot cast nil to " .. wantedType)
404,467✔
314
  elseif wantedType:match("boolean")     then return SU.boolean(value)
404,467✔
315
  elseif wantedType:match("string")      then return tostring(value)
404,466✔
316
  elseif wantedType:match("number") then
404,466✔
317
    if type(value) == "table" and type(value.tonumber) == "function" then
404,442✔
318
      return value:tonumber()
398,533✔
319
    end
320
    local num = tonumber(value)
5,909✔
321
    if not num then SU.error("Cannot cast '" .. value .. "'' to " .. wantedType) end
5,909✔
322
    return num
5,909✔
323
  elseif wantedType:match("integer") then
24✔
324
    local num
325
    if type(value) == "table" and type(value.tonumber) == "function" then
24✔
326
      num = value:tonumber()
×
327
    else
328
      num = tonumber(value)
24✔
329
    end
330
    if not num then SU.error("Cannot cast '" .. value .. "'' to " .. wantedType) end
24✔
331
    if not wantedType:match("number") and num % 1 ~= 0 then
24✔
332
      -- Could be an error but since it wasn't checked before, let's just warn:
333
      -- Some packages might have wrongly typed settings, for instance.
334
      SU.warn("Casting an integer but got a float number " .. num)
×
335
    end
336
    return num
24✔
337
  else SU.error("Cannot cast to unrecognized type " .. wantedType)
×
338
  end
339
end
340

341
utilities.hasContent = function(content)
342
  return type(content) == "function" or type(content) == "table" and #content > 0
1,941✔
343
end
344

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

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

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

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

411
utilities.rateBadness = function(inf_bad, shortfall, spring)
412
  if spring == 0 then return inf_bad end
22,887✔
413
  local bad = math.floor(100 * math.abs(shortfall / spring) ^ 3)
22,052✔
414
  return math.min(inf_bad, bad)
22,052✔
415
end
416

417
utilities.rationWidth = function (target, width, ratio)
418
  if ratio < 0 and width.shrink:tonumber() > 0 then
31,914✔
419
    target:___add(width.shrink:tonumber() * ratio)
5,790✔
420
  elseif ratio > 0 and width.stretch:tonumber() > 0 then
44,458✔
421
    target:___add(width.stretch:tonumber() * ratio)
11,036✔
422
  end
423
  return target
26,103✔
424
end
425

426
-- Unicode-related utilities
427
utilities.utf8char = function (c)
428
  utilities.deprecated("SU.utf8char", "luautf8.char", "0.11.0", "0.12.0")
×
429
  return luautf8.char(c)
×
430
end
431

432
utilities.codepoint = function (uchar)
433
  local seq = 0
228,399✔
434
  local val = -1
228,399✔
435
  for i = 1, #uchar do
467,082✔
436
    local c = string.byte(uchar, i)
240,529✔
437
    if seq == 0 then
240,529✔
438
      if val > -1 then return val end
229,152✔
439
      seq = c < 0x80 and 1 or c < 0xE0 and 2 or c < 0xF0 and 3 or
227,306✔
440
            c < 0xF8 and 4 or --c < 0xFC and 5 or c < 0xFE and 6 or
12✔
441
          error("invalid UTF-8 character sequence")
×
442
      val = bitshim.band(c, 2^(8-seq) - 1)
227,306✔
443
    else
444
      val = bitshim.bor(bitshim.lshift(val, 6), bitshim.band(c, 0x3F))
11,377✔
445
    end
446
    seq = seq - 1
238,683✔
447
  end
448
  return val
226,553✔
449
end
450

451
utilities.utf8charfromcodepoint = function (codepoint)
452
  local val = codepoint
667✔
453
  local cp = val
667✔
454
  local hex = (cp:match("[Uu]%+(%x+)") or cp:match("0[xX](%x+)"))
667✔
455
  if hex then
667✔
456
    cp = tonumber("0x"..hex)
6✔
457
  elseif tonumber(cp) then
661✔
458
    cp = tonumber(cp)
×
459
  end
460

461
  if type(cp) == "number" then
667✔
462
    val = luautf8.char(cp)
6✔
463
  end
464
  return val
667✔
465
end
466

467
utilities.utf8codes = function (ustr)
468
  utilities.deprecated("SU.utf8codes", "luautf8.codes", "0.11.0", "0.12.0")
×
469
  return luautf8.codes(ustr)
×
470
end
471

472
utilities.utf16codes = function (ustr, endian)
473
  local pos = 1
61,664✔
474
  return function()
475
    if pos > #ustr then
1,886,910✔
476
      return nil
61,664✔
477
    else
478
      local c1, c2, c3, c4, wchar, lowchar
479
      c1 = string.byte(ustr, pos, pos+1)
1,825,246✔
480
      pos = pos + 1
1,825,246✔
481
      c2 = string.byte(ustr, pos, pos+1)
1,825,246✔
482
      pos = pos + 1
1,825,246✔
483
      if endian == "be" then
1,825,246✔
484
        wchar = c1 * 256 + c2
1,825,204✔
485
      else
486
        wchar = c2 * 256 + c1
42✔
487
      end
488
      if not (wchar >= 0xD800 and wchar <= 0xDBFF) then
1,825,246✔
489
        return wchar
1,825,240✔
490
      end
491
      c3 = string.byte(ustr, pos, pos+1)
6✔
492
      pos = pos + 1
6✔
493
      c4 = string.byte(ustr, pos, pos+1)
6✔
494
      pos = pos + 1
6✔
495
      if endian == "be" then
6✔
496
        lowchar = c3 * 256 + c4
3✔
497
      else
498
        lowchar = c4 * 256 + c3
3✔
499
      end
500
      return 0x10000 + bitshim.lshift(bitshim.band(wchar, 0x03FF), 10) + bitshim.band(lowchar, 0x03FF)
6✔
501
    end
502
  end
503
end
504

505
utilities.splitUtf8 = function (str) -- Return an array of UTF8 strings each representing a Unicode char
506
  local rv = {}
155,024✔
507
  for _, cp in luautf8.next, str do
1,005,053✔
508
    table.insert(rv, luautf8.char(cp))
850,029✔
509
  end
510
  return rv
155,024✔
511
end
512

513
utilities.lastChar = function (str)
514
  local chars = utilities.splitUtf8(str)
4✔
515
  return chars[#chars]
4✔
516
end
517

518
utilities.firstChar = function (str)
519
  local chars = utilities.splitUtf8(str)
10✔
520
  return chars[1]
10✔
521
end
522

523
local byte, floor, reverse = string.byte, math.floor, string.reverse
171✔
524

525
utilities.utf8charat = function (str, index)
526
  return str:sub(index):match("([%z\1-\127\194-\244][\128-\191]*)")
×
527
end
528

529
local utf16bom = function(endianness)
530
  return endianness == "be" and "\254\255" or endianness == "le" and "\255\254" or SU.error("Unrecognized endianness")
61,674✔
531
end
532

533
utilities.hexencoded = function (str)
534
  local ustr = ""
8✔
535
  for i = 1, #str do
154✔
536
    ustr = ustr..string.format("%02x", byte(str, i, i+1))
146✔
537
  end
538
  return ustr
8✔
539
end
540

541
utilities.hexdecoded = function (str)
542
  if #str % 2 == 1 then SU.error("Cannot decode hex string with odd len") end
12✔
543
  local ustr = ""
12✔
544
  for i = 1, #str, 2 do
200✔
545
    ustr = ustr..string.char(tonumber(string.sub(str, i, i+1), 16))
564✔
546
  end
547
  return ustr
12✔
548
end
549

550
local uchr_to_surrogate_pair = function(uchr, endianness)
551
  local hi, lo = floor((uchr - 0x10000) / 0x400) + 0xd800, (uchr - 0x10000) % 0x400 + 0xdc00
3✔
552
  local s_hi, s_lo = string.char(floor(hi / 256)) .. string.char(hi % 256), string.char(floor(lo / 256)) .. string.char(lo % 256)
15✔
553
  return endianness == "le" and (reverse(s_hi) .. reverse(s_lo)) or s_hi .. s_lo
5✔
554
end
555

556
local uchr_to_utf16_double_byte = function(uchr, endianness)
557
  local ustr = string.char(floor(uchr / 256)) .. string.char(uchr % 256)
297✔
558
  return endianness == "le" and reverse(ustr) or ustr
112✔
559
end
560

561
local utf8_to_utf16 = function(str, endianness)
562
  local ustr = utf16bom(endianness)
10✔
563
  for _, uchr in luautf8.codes(str) do
112✔
564
    ustr = ustr..(uchr < 0x10000 and uchr_to_utf16_double_byte(uchr, endianness)
102✔
565
                  or uchr_to_surrogate_pair(uchr, endianness))
105✔
566
  end
567
  return ustr
10✔
568
end
569

570
utilities.utf8_to_utf16be = function (str) return utf8_to_utf16(str, "be") end
179✔
571
utilities.utf8_to_utf16le = function (str) return utf8_to_utf16(str, "le") end
173✔
572
utilities.utf8_to_utf16be_hexencoded = function (str) return utilities.hexencoded(utilities.utf8_to_utf16be(str)) end
183✔
573
utilities.utf8_to_utf16le_hexencoded = function (str) return utilities.hexencoded(utilities.utf8_to_utf16le(str)) end
175✔
574

575
local utf16_to_utf8 = function (str, endianness)
576
  local bom = utf16bom(endianness)
61,664✔
577

578
  if str:find(bom) == 1 then str = string.sub(str, 3, #str) end
61,668✔
579
  local ustr = ""
61,664✔
580
  for uchr in utilities.utf16codes(str, endianness) do
3,835,484✔
581
    ustr = ustr..luautf8.char(uchr)
1,825,246✔
582
  end
583
  return ustr
61,664✔
584
end
585

586
utilities.utf16be_to_utf8 = function (str) return utf16_to_utf8(str, "be") end
61,829✔
587
utilities.utf16le_to_utf8 = function (str) return utf16_to_utf8(str, "le") end
177✔
588

589
utilities.breadcrumbs = function ()
590
  local breadcrumbs = {}
303✔
591

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

605
  function breadcrumbs:dump ()
303✔
606
    SU.dump(self)
×
607
  end
608

609
  function breadcrumbs:parent (count)
303✔
610
    -- Note LuaJIT does not support __len, so this has to work even when that metamethod doesn't fire...
611
    return self[#SILE.traceStack-(count or 1)]
2✔
612
  end
613

614
  function breadcrumbs:contains (needle)
303✔
615
    for i, command in ipairs(self) do
×
616
      if command == needle then return true, #self - i end
×
617
    end
618
    return false, -1
×
619
  end
620

621
  return breadcrumbs
303✔
622
end
623

624
utilities.formatNumber = require("core.utilities-numbers")
171✔
625

626
utilities.collatedSort = require("core.utilities-sorting")
171✔
627

628
return utilities
171✔
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