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

sile-typesetter / sile / 6078546669

05 Sep 2023 12:03AM UTC coverage: 71.73% (-2.6%) from 74.32%
6078546669

push

github

web-flow
Merge pull request #1858 from alerque/toc-msg

8 of 8 new or added lines in 2 files covered. (100.0%)

11286 of 15734 relevant lines covered (71.73%)

6424.24 hits per line

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

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

5
local utilities = {}
132✔
6

7
local epsilon = 1E-12
132✔
8

9
utilities.required = function (options, name, context, required_type)
10
  if not options[name] then utilities.error(context.." needs a "..name.." parameter") end
7,226✔
11
  if required_type then
7,226✔
12
    return utilities.cast(required_type, options[name])
1,370✔
13
  end
14
  return options[name]
5,856✔
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
4,723✔
23
  if value == true then return true end
4,717✔
24
  if value == "false" then return false end
2,326✔
25
  if value == "true" then return true end
2,323✔
26
  if value == "no" then preferbool(); return false end
2,317✔
27
  if value == "yes" then preferbool(); return true end
2,317✔
28
  if value == nil then return default end
2,317✔
29
  SU.error("Expecting a boolean value but got '" .. value .. "'")
×
30
  return default
×
31
end
32

33
local _skip_traceback_levels = 2
132✔
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
82✔
47
  io.stderr:write("\n! " .. message)
82✔
48
  if SILE.traceback or isbug then
82✔
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())
162✔
55
  end
56
  io.stderr:write("\n")
82✔
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]
136,645✔
66
end
67

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

75
utilities.gtoke = function (string, pattern)
76
  string = string and tostring(string) or ''
1,359✔
77
  pattern = pattern and tostring(pattern) or "%s+"
1,359✔
78
  local length = #string
1,359✔
79
  return coroutine.wrap(function()
1,359✔
80
    local index = 1
1,359✔
81
    repeat
82
      local first, last = string:find(pattern, index)
1,639✔
83
      if last then
1,639✔
84
        if index < first then coroutine.yield({ string = string:sub(index, first - 1) }) end
775✔
85
        coroutine.yield({ separator = string:sub(first, last) })
1,004✔
86
        index = last + 1
502✔
87
      else
88
        if index <= length then
1,137✔
89
          coroutine.yield({ string = string:sub(index) })
2,086✔
90
        end
91
        break
1,043✔
92
      end
93
    until index > length
502✔
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)
75✔
99
  local current = SILE.version and semver(SILE.version:match("v([0-9]*.[0-9]*.[0-9]*)")) or warnat
50✔
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 warings.
105
  local brackets = old:sub(1,1) == '\\' and "" or "()"
50✔
106
  local _new = new and "Please use " .. (new .. brackets) .. " instead." or "Plase don't use it."
25✔
107
  local msg = (old .. brackets) .. " was deprecated in SILE v" .. tostring(warnat) .. ". " .. _new ..  (extra and "\n" .. extra .. "\n\n" or "")
50✔
108
  if errorat and current >= errorat then
25✔
109
    SU.error(msg)
×
110
  elseif warnat and current >= warnat then
25✔
111
    SU.warn(msg)
25✔
112
  end
113
end
114

115
utilities.debug = function (category, ...)
116
  if SILE.quiet then return end
130,735✔
117
  if utilities.debugging(category) then
261,470✔
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()
1✔
171
  pl.pretty.dump(#arg == 1 and arg[1] or arg, "/dev/stderr")
2✔
172
end
173

174
utilities.concat = function (array, separator)
175
  return table.concat(utilities.map(tostring, array), separator)
8✔
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 = {}
54,604✔
189
  local last = #array
54,604✔
190
  for i = 1, last do
188,498✔
191
    new_array[i] = func(array[i])
267,778✔
192
  end
193
  return new_array
54,604✔
194
end
195

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

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

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

234
utilities.sum = function (array)
235
  local total = 0
15,589✔
236
  local last = #array
15,589✔
237
  for i = 1, last do
30,099✔
238
    total = total + array[i]
14,510✔
239
  end
240
  return total
15,589✔
241
end
242

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

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

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

269
utilities.flip_in_place = function (tbl)
270
  local tmp, j
271
  for i = 1, math.floor(#tbl / 2) do
865✔
272
    tmp = tbl[i]
539✔
273
    j = #tbl - i + 1
539✔
274
    tbl[i] = tbl[j]
539✔
275
    tbl[j] = tmp
539✔
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
2,540,806✔
298
    return math.floor(value) == value and "integer" or "number"
606,294✔
299
  elseif type(value) == "table" and value.prototype then
1,934,512✔
300
    return value:prototype()
×
301
  elseif type(value) == "table" and value.is_a then
1,934,512✔
302
    return value.type
944,663✔
303
  else
304
    return type(value)
989,849✔
305
  end
306
end
307

308
utilities.cast = function (wantedType, value)
309
  local actualType = SU.type(value)
581,544✔
310
  wantedType = string.lower(wantedType)
1,163,088✔
311
  if wantedType:match(actualType)     then return value
581,544✔
312
  elseif actualType == "nil" and wantedType:match("nil") then return nil
415,336✔
313
  elseif wantedType:match("length")      then return SILE.length(value)
415,336✔
314
  elseif wantedType:match("measurement") then return SILE.measurement(value)
362,579✔
315
  elseif wantedType:match("vglue")       then return SILE.nodefactory.vglue(value)
355,762✔
316
  elseif wantedType:match("glue")        then return SILE.nodefactory.glue(value)
355,752✔
317
  elseif wantedType:match("kern")        then return SILE.nodefactory.kern(value)
355,492✔
318
  elseif actualType == "nil" then SU.error("Cannot cast nil to " .. wantedType)
355,492✔
319
  elseif wantedType:match("boolean")     then return SU.boolean(value)
355,492✔
320
  elseif wantedType:match("string")      then return tostring(value)
355,491✔
321
  elseif wantedType:match("number") then
355,491✔
322
    if type(value) == "table" and type(value.tonumber) == "function" then
355,467✔
323
      return value:tonumber()
350,633✔
324
    end
325
    local num = tonumber(value)
4,834✔
326
    if not num then SU.error("Cannot cast '" .. value .. "'' to " .. wantedType) end
4,834✔
327
    return num
4,834✔
328
  elseif wantedType:match("integer") then
24✔
329
    local num
330
    if type(value) == "table" and type(value.tonumber) == "function" then
24✔
331
      num = value:tonumber()
×
332
    else
333
      num = tonumber(value)
24✔
334
    end
335
    if not num then SU.error("Cannot cast '" .. value .. "'' to " .. wantedType) end
24✔
336
    if not wantedType:match("number") and num % 1 ~= 0 then
24✔
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
24✔
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
1,811✔
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 = ""
1✔
354
  for i = 1, #content do
2✔
355
    if type(content[i]) == "table" and type(content[i][1]) == "string" then
2✔
356
      string = string .. content[i][1]
×
357
    elseif type(content[i]) == "string" then
2✔
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
2✔
361
        break
1✔
362
      end
363
      string = string .. content[i]
1✔
364
    end
365
  end
366
  return string
1✔
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" }
6✔
373
  for key, val in utilities.sortedpairs(content) do
54✔
374
    if type(key) == "number" then
42✔
375
      out[#out+1] = val
6✔
376
    end
377
  end
378
  return out
6✔
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
6✔
401
    return content
×
402
  end
403
  local stripped = {}
6✔
404
  for k, v in pairs(content) do
18✔
405
    if type(v) == "table" then
12✔
406
      v = SU.stripContentPos(v)
×
407
    end
408
    stripped[k] = v
12✔
409
  end
410
  if content.id or content.command then
6✔
411
    stripped.pos, stripped.col, stripped.lno = nil, nil, nil
6✔
412
  end
413
  return stripped
6✔
414
end
415

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

422
utilities.rationWidth = function (target, width, ratio)
423
  if ratio < 0 and width.shrink:tonumber() > 0 then
26,929✔
424
    target:___add(width.shrink:tonumber() * ratio)
5,157✔
425
  elseif ratio > 0 and width.stretch:tonumber() > 0 then
36,865✔
426
    target:___add(width.stretch:tonumber() * ratio)
9,470✔
427
  end
428
  return target
21,840✔
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
203,344✔
439
  local val = -1
203,344✔
440
  for i = 1, #uchar do
414,055✔
441
    local c = string.byte(uchar, i)
211,917✔
442
    if seq == 0 then
211,917✔
443
      if val > -1 then return val end
203,560✔
444
      seq = c < 0x80 and 1 or c < 0xE0 and 2 or c < 0xF0 and 3 or
202,354✔
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)
202,354✔
448
    else
449
      val = bitshim.bor(bitshim.lshift(val, 6), bitshim.band(c, 0x3F))
8,357✔
450
    end
451
    seq = seq - 1
210,711✔
452
  end
453
  return val
202,138✔
454
end
455

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

466
  if type(cp) == "number" then
667✔
467
    val = luautf8.char(cp)
6✔
468
  end
469
  return val
667✔
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
51,399✔
479
  return function()
480
    if pos > #ustr then
1,550,813✔
481
      return nil
51,399✔
482
    else
483
      local c1, c2, c3, c4, wchar, lowchar
484
      c1 = string.byte(ustr, pos, pos+1)
1,499,414✔
485
      pos = pos + 1
1,499,414✔
486
      c2 = string.byte(ustr, pos, pos+1)
1,499,414✔
487
      pos = pos + 1
1,499,414✔
488
      if endian == "be" then
1,499,414✔
489
        wchar = c1 * 256 + c2
1,499,414✔
490
      else
491
        wchar = c2 * 256 + c1
×
492
      end
493
      if not (wchar >= 0xD800 and wchar <= 0xDBFF) then
1,499,414✔
494
        return wchar
1,499,414✔
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 = {}
125,034✔
512
  for _, cp in luautf8.next, str do
815,108✔
513
    table.insert(rv, luautf8.char(cp))
690,074✔
514
  end
515
  return rv
125,034✔
516
end
517

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

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

528
local byte, floor, reverse = string.byte, math.floor, string.reverse
132✔
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")
51,403✔
536
end
537

538
utilities.hexencoded = function (str)
539
  local ustr = ""
4✔
540
  for i = 1, #str do
82✔
541
    ustr = ustr..string.format("%02x", byte(str, i, i+1))
78✔
542
  end
543
  return ustr
4✔
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)
105✔
563
  return endianness == "le" and reverse(ustr) or ustr
35✔
564
end
565

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

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

580
local utf16_to_utf8 = function (str, endianness)
581
  local bom = utf16bom(endianness)
51,399✔
582

583
  if str:find(bom) == 1 then str = string.sub(str, 3, #str) end
51,399✔
584
  local ustr = ""
51,399✔
585
  for uchr in utilities.utf16codes(str, endianness) do
3,153,025✔
586
    ustr = ustr..luautf8.char(uchr)
1,499,414✔
587
  end
588
  return ustr
51,399✔
589
end
590

591
utilities.utf16be_to_utf8 = function (str) return utf16_to_utf8(str, "be") end
51,531✔
592
utilities.utf16le_to_utf8 = function (str) return utf16_to_utf8(str, "le") end
132✔
593

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

597
  setmetatable (breadcrumbs, {
502✔
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 ()
251✔
611
    SU.dump(self)
×
612
  end
613

614
  function breadcrumbs:parent (count)
251✔
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)
251✔
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
251✔
627
end
628

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

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

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