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

sile-typesetter / sile / 9304060604

30 May 2024 02:07PM UTC coverage: 74.124% (-0.6%) from 74.707%
9304060604

push

github

alerque
style: Reformat Lua with stylua

8104 of 11995 new or added lines in 184 files covered. (67.56%)

15 existing lines in 11 files now uncovered.

12444 of 16788 relevant lines covered (74.12%)

7175.1 hits per line

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

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

5
local utilities = {}
181✔
6

7
local epsilon = 1E-12
181✔
8

9
utilities.required = function (options, name, context, required_type)
10
   if not options[name] then
9,385✔
NEW
11
      utilities.error(context .. " needs a " .. name .. " parameter")
×
12
   end
13
   if required_type then
9,385✔
14
      return utilities.cast(required_type, options[name])
1,710✔
15
   end
16
   return options[name]
7,675✔
17
end
18

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

23
utilities.boolean = function (value, default)
24
   if value == false then
6,628✔
25
      return false
6✔
26
   end
27
   if value == true then
6,622✔
28
      return true
3,199✔
29
   end
30
   if value == "false" then
3,423✔
31
      return false
8✔
32
   end
33
   if value == "true" then
3,415✔
34
      return true
8✔
35
   end
36
   if value == "no" then
3,407✔
NEW
37
      preferbool()
×
NEW
38
      return false
×
39
   end
40
   if value == "yes" then
3,407✔
NEW
41
      preferbool()
×
NEW
42
      return true
×
43
   end
44
   if value == nil then
3,407✔
45
      return default
3,407✔
46
   end
NEW
47
   SU.error("Expecting a boolean value but got '" .. value .. "'")
×
NEW
48
   return default
×
49
end
50

51
local _skip_traceback_levels = 2
181✔
52

53
utilities.error = function (message, isbug)
NEW
54
   _skip_traceback_levels = 3
×
NEW
55
   utilities.warn(message, isbug)
×
NEW
56
   _skip_traceback_levels = 2
×
NEW
57
   io.stderr:flush()
×
NEW
58
   SILE.outputter:finish() -- Only really useful from the REPL but no harm in trying
×
NEW
59
   SILE.scratch.caughterror = true
×
NEW
60
   error("", 2)
×
61
end
62

63
utilities.warn = function (message, isbug)
64
   if SILE.quiet then
102✔
NEW
65
      return
×
66
   end
67
   io.stderr:write("\n! " .. message)
102✔
68
   if SILE.traceback or isbug then
102✔
69
      io.stderr:write(" at:\n" .. SILE.traceStack:locationTrace())
2✔
70
      if _skip_traceback_levels == 2 then
1✔
71
         io.stderr:write(
2✔
72
            debug.traceback("", _skip_traceback_levels) or "\t! debug.traceback() did not identify code location"
1✔
73
         )
74
      end
75
   else
76
      io.stderr:write(" at " .. SILE.traceStack:locationHead())
202✔
77
   end
78
   io.stderr:write("\n")
102✔
79
end
80

81
utilities.msg = function (message)
NEW
82
   if SILE.quiet then
×
NEW
83
      return
×
84
   end
NEW
85
   io.stderr:write("\n! " .. message .. "\n")
×
86
end
87

88
utilities.debugging = function (category)
89
   return SILE.debugFlags.all and category ~= "profile" or SILE.debugFlags[category]
170,457✔
90
end
91

92
utilities.feq = function (lhs, rhs) -- Float point equal
93
   lhs = SU.cast("number", lhs)
178✔
94
   rhs = SU.cast("number", rhs)
178✔
95
   local abs = math.abs
89✔
96
   return abs(lhs - rhs) <= epsilon * (abs(lhs) + abs(rhs))
89✔
97
end
98

99
utilities.gtoke = function (string, pattern)
100
   string = string and tostring(string) or ""
3,638✔
101
   pattern = pattern and tostring(pattern) or "%s+"
3,638✔
102
   local length = #string
3,638✔
103
   return coroutine.wrap(function ()
3,638✔
104
      local index = 1
3,638✔
105
      repeat
106
         local first, last = string:find(pattern, index)
4,022✔
107
         if last then
4,022✔
108
            if index < first then
689✔
109
               coroutine.yield({ string = string:sub(index, first - 1) })
824✔
110
            end
111
            coroutine.yield({ separator = string:sub(first, last) })
1,378✔
112
            index = last + 1
689✔
113
         else
114
            if index <= length then
3,333✔
115
               coroutine.yield({ string = string:sub(index) })
6,344✔
116
            end
117
            break
3,172✔
118
         end
119
      until index > length
689✔
120
   end)
121
end
122

123
utilities.deprecated = function (old, new, warnat, errorat, extra)
124
   warnat, errorat = semver(warnat or 0), semver(errorat or 0)
96✔
125
   local current = SILE.version and semver(SILE.version:match("v([0-9]*.[0-9]*.[0-9]*)")) or warnat
64✔
126
   -- SILE.version is defined *after* most of SILE loads. It’s available at
127
   -- runtime but not useful if we encounter deprecated code in core code. Users
128
   -- will never encounter this failure, but as a developer it’s hard to test a
129
   -- deprecation when core code refactoring is an all-or-nothing proposition.
130
   -- Hence we fake it ‘till we make it, all deprecations internally are warnings.
131
   local brackets = old:sub(1, 1) == "\\" and "" or "()"
64✔
132
   local _new = new and "Please use " .. (new .. brackets) .. " instead." or "Plase don't use it."
32✔
133
   local msg = (old .. brackets)
32✔
NEW
134
      .. " was deprecated in SILE v"
×
135
      .. tostring(warnat)
32✔
NEW
136
      .. ". "
×
137
      .. _new
32✔
138
      .. (extra and "\n" .. extra .. "\n\n" or "")
32✔
139
   if errorat and current >= errorat then
32✔
NEW
140
      SU.error(msg)
×
141
   elseif warnat and current >= warnat then
32✔
142
      SU.warn(msg)
32✔
143
   end
144
end
145

146
utilities.debug = function (category, ...)
147
   if SILE.quiet then
163,120✔
NEW
148
      return
×
149
   end
150
   if utilities.debugging(category) then
326,240✔
NEW
151
      local inputs = pl.utils.pack(...)
×
NEW
152
      for i, input in ipairs(inputs) do
×
NEW
153
         if type(input) == "function" then
×
NEW
154
            local status, output = pcall(input)
×
NEW
155
            inputs[i] = status and output
×
NEW
156
               or SU.warn(("Output of %s debug function was an error: %s"):format(category, output))
×
157
         end
158
      end
NEW
159
      local message = utilities.concat(inputs, " ")
×
NEW
160
      if message then
×
NEW
161
         io.stderr:write(("\n[%s] %s"):format(category, message))
×
162
      end
163
   end
164
end
165

166
utilities.debugAST = function (ast, level)
NEW
167
   if not ast then
×
NEW
168
      SU.error("debugAST called with nil", true)
×
169
   end
NEW
170
   local out = string.rep("  ", 1 + level)
×
NEW
171
   if level == 0 then
×
NEW
172
      SU.debug("ast", function ()
×
NEW
173
         return "[" .. SILE.currentlyProcessingFile
×
174
      end)
175
   end
NEW
176
   if type(ast) == "function" then
×
NEW
177
      SU.debug("ast", function ()
×
NEW
178
         return out .. tostring(ast)
×
179
      end)
NEW
180
   elseif type(ast) == "table" then
×
NEW
181
      for _, content in ipairs(ast) do
×
NEW
182
         if type(content) == "string" then
×
NEW
183
            SU.debug("ast", function ()
×
NEW
184
               return out .. "[" .. content .. "]"
×
185
            end)
NEW
186
         elseif type(content) == "table" then
×
NEW
187
            if SILE.Commands[content.command] then
×
NEW
188
               SU.debug("ast", function ()
×
NEW
189
                  return out .. "\\" .. content.command .. " " .. pl.pretty.write(content.options, "")
×
190
               end)
NEW
191
               if #content >= 1 then
×
NEW
192
                  utilities.debugAST(content, level + 1)
×
193
               end
NEW
194
            elseif content.id == "texlike_stuff" or (not content.command and not content.id) then
×
NEW
195
               utilities.debugAST(content, level + 1)
×
196
            else
NEW
197
               SU.debug("ast", function ()
×
NEW
198
                  return out .. "?\\" .. (content.command or content.id)
×
199
               end)
200
            end
201
         end
202
      end
203
   end
NEW
204
   if level == 0 then
×
NEW
205
      SU.debug("ast", "]")
×
206
   end
207
end
208

209
utilities.dump = function (...)
210
   local arg = { ... } -- Avoid things that Lua stuffs in arg like args to self()
1✔
211
   pl.pretty.dump(#arg == 1 and arg[1] or arg, "/dev/stderr")
2✔
212
end
213

214
utilities.concat = function (array, separator)
215
   return table.concat(utilities.map(tostring, array), separator)
120✔
216
end
217

218
utilities.inherit = function (orig, spec)
NEW
219
   local new = pl.tablex.deepcopy(orig)
×
NEW
220
   if spec then
×
NEW
221
      for k, v in pairs(spec) do
×
NEW
222
         new[k] = v
×
223
      end
224
   end
NEW
225
   if new.init then
×
NEW
226
      new:init()
×
227
   end
NEW
228
   return new
×
229
end
230

231
utilities.map = function (func, array)
232
   local new_array = {}
69,442✔
233
   local last = #array
69,442✔
234
   for i = 1, last do
222,616✔
235
      new_array[i] = func(array[i])
306,053✔
236
   end
237
   return new_array
69,442✔
238
end
239

240
utilities.sortedpairs = function (input)
241
   local keys = {}
8✔
242
   for k, _ in pairs(input) do
64✔
243
      keys[#keys + 1] = k
56✔
244
   end
245
   table.sort(keys, function (a, b)
16✔
246
      if type(a) == type(b) then
136✔
247
         return a < b
112✔
248
      elseif type(a) == "number" then
24✔
NEW
249
         return true
×
250
      else
251
         return false
24✔
252
      end
253
   end)
254
   return coroutine.wrap(function ()
8✔
255
      for i = 1, #keys do
64✔
256
         coroutine.yield(keys[i], input[keys[i]])
56✔
257
      end
258
   end)
259
end
260

261
utilities.splice = function (array, start, stop, replacement)
262
   local ptr = start
566✔
263
   local room = stop - start + 1
566✔
264
   local last = replacement and #replacement or 0
566✔
265
   for i = 1, last do
4,014✔
266
      if room > 0 then
3,448✔
267
         room = room - 1
512✔
268
         array[ptr] = replacement[i]
512✔
269
      else
270
         table.insert(array, ptr, replacement[i])
2,936✔
271
      end
272
      ptr = ptr + 1
3,448✔
273
   end
274

275
   for _ = 1, room do
620✔
276
      table.remove(array, ptr)
54✔
277
   end
278
   return array
566✔
279
end
280

281
utilities.sum = function (array)
282
   local total = 0
20,341✔
283
   local last = #array
20,341✔
284
   for i = 1, last do
39,004✔
285
      total = total + array[i]
18,663✔
286
   end
287
   return total
20,341✔
288
end
289

290
-- Lua <= 5.2 can't handle objects in math functions
291
utilities.max = function (...)
292
   local input = pl.utils.pack(...)
44,665✔
293
   local max = table.remove(input, 1)
44,665✔
294
   for _, val in ipairs(input) do
146,663✔
295
      if val > max then
101,998✔
296
         max = val
36,525✔
297
      end
298
   end
299
   return max
44,665✔
300
end
301

302
utilities.min = function (...)
303
   local input = pl.utils.pack(...)
12✔
304
   local min = input[1]
12✔
305
   for _, val in ipairs(input) do
36✔
306
      if val < min then
24✔
NEW
307
         min = val
×
308
      end
309
   end
310
   return min
12✔
311
end
312

313
utilities.compress = function (items)
314
   local rv = {}
1,462✔
315
   local max = math.max(pl.utils.unpack(pl.tablex.keys(items)))
4,386✔
316
   for i = 1, max do
33,192✔
317
      if items[i] then
31,730✔
318
         rv[#rv + 1] = items[i]
31,730✔
319
      end
320
   end
321
   return rv
1,462✔
322
end
323

324
utilities.flip_in_place = function (tbl)
325
   local tmp, j
326
   for i = 1, math.floor(#tbl / 2) do
1,108✔
327
      tmp = tbl[i]
676✔
328
      j = #tbl - i + 1
676✔
329
      tbl[i] = tbl[j]
676✔
330
      tbl[j] = tmp
676✔
331
   end
332
end
333

334
utilities.allCombinations = function (options)
NEW
335
   local count = 1
×
NEW
336
   for i = 1, #options do
×
NEW
337
      count = count * options[i]
×
338
   end
NEW
339
   return coroutine.wrap(function ()
×
NEW
340
      for i = 0, count - 1 do
×
NEW
341
         local this = i
×
NEW
342
         local rv = {}
×
NEW
343
         for j = 1, #options do
×
NEW
344
            local base = options[j]
×
NEW
345
            rv[#rv + 1] = this % base + 1
×
NEW
346
            this = (this - this % base) / base
×
347
         end
NEW
348
         coroutine.yield(rv)
×
349
      end
350
   end)
351
end
352

353
utilities.type = function (value)
354
   if type(value) == "number" then
3,066,249✔
355
      return math.floor(value) == value and "integer" or "number"
724,863✔
356
   elseif type(value) == "table" and value.prototype then
2,341,386✔
NEW
357
      return value:prototype()
×
358
   elseif type(value) == "table" and value.is_a then
2,341,386✔
359
      return value.type
1,099,719✔
360
   else
361
      return type(value)
1,241,667✔
362
   end
363
end
364

365
utilities.cast = function (wantedType, value)
366
   local actualType = SU.type(value)
681,314✔
367
   wantedType = string.lower(wantedType)
1,362,628✔
368
   if wantedType:match(actualType) then
681,314✔
369
      return value
198,176✔
370
   elseif actualType == "nil" and wantedType:match("nil") then
483,138✔
NEW
371
      return nil
×
372
   elseif wantedType:match("length") then
483,138✔
373
      return SILE.length(value)
67,011✔
374
   elseif wantedType:match("measurement") then
416,127✔
375
      return SILE.measurement(value)
7,872✔
376
   elseif wantedType:match("vglue") then
408,255✔
377
      return SILE.nodefactory.vglue(value)
15✔
378
   elseif wantedType:match("glue") then
408,240✔
379
      return SILE.nodefactory.glue(value)
279✔
380
   elseif wantedType:match("kern") then
407,961✔
NEW
381
      return SILE.nodefactory.kern(value)
×
382
   elseif actualType == "nil" then
407,961✔
NEW
383
      SU.error("Cannot cast nil to " .. wantedType)
×
384
   elseif wantedType:match("boolean") then
407,961✔
385
      return SU.boolean(value)
4✔
386
   elseif wantedType:match("string") then
407,957✔
NEW
387
      return tostring(value)
×
388
   elseif wantedType:match("number") then
407,957✔
389
      if type(value) == "table" and type(value.tonumber) == "function" then
407,927✔
390
         return value:tonumber()
402,219✔
391
      end
392
      local num = tonumber(value)
5,708✔
393
      if not num then
5,708✔
NEW
394
         SU.error("Cannot cast '" .. value .. "'' to " .. wantedType)
×
395
      end
396
      return num
5,708✔
397
   elseif wantedType:match("integer") then
30✔
398
      local num
399
      if type(value) == "table" and type(value.tonumber) == "function" then
30✔
NEW
400
         num = value:tonumber()
×
401
      else
402
         num = tonumber(value)
30✔
403
      end
404
      if not num then
30✔
NEW
405
         SU.error("Cannot cast '" .. value .. "'' to " .. wantedType)
×
406
      end
407
      if not wantedType:match("number") and num % 1 ~= 0 then
30✔
408
         -- Could be an error but since it wasn't checked before, let's just warn:
409
         -- Some packages might have wrongly typed settings, for instance.
NEW
410
         SU.warn("Casting an integer but got a float number " .. num)
×
411
      end
412
      return num
30✔
413
   else
NEW
414
      SU.error("Cannot cast to unrecognized type " .. wantedType)
×
415
   end
416
end
417

418
utilities.hasContent = function (content)
419
   return type(content) == "function" or type(content) == "table" and #content > 0
1,985✔
420
end
421

422
-- Flatten content trees into just the string components (allows passing
423
-- objects with complex structures to functions that need plain strings)
424
utilities.contentToString = function (content)
425
   local string = ""
28✔
426
   for i = 1, #content do
56✔
427
      if type(content[i]) == "table" and type(content[i][1]) == "string" then
31✔
NEW
428
         string = string .. content[i][1]
×
429
      elseif type(content[i]) == "string" then
31✔
430
         -- Work around PEG parser returning env tags as content
431
         -- TODO: refactor capture groups in PEG parser
432
         if content.command == content[i] and content[i] == content[i + 1] then
31✔
433
            break
3✔
434
         end
435
         string = string .. content[i]
28✔
436
      end
437
   end
438
   return string
28✔
439
end
440

441
-- Strip the top level command off a content object and keep only the child
442
-- items — assuming that the current command is taking care of itself
443
utilities.subContent = function (content)
444
   local out = { id = "stuff" }
6✔
445
   for key, val in utilities.sortedpairs(content) do
54✔
446
      if type(key) == "number" then
42✔
447
         out[#out + 1] = val
6✔
448
      end
449
   end
450
   return out
6✔
451
end
452

453
-- Call `action` on each content AST node, recursively, including `content` itself.
454
-- Not called on leaves, i.e. strings.
455
utilities.walkContent = function (content, action)
NEW
456
   if type(content) ~= "table" then
×
NEW
457
      return
×
458
   end
NEW
459
   action(content)
×
NEW
460
   for i = 1, #content do
×
NEW
461
      utilities.walkContent(content[i], action)
×
462
   end
463
end
464

465
--- Strip position, line and column recursively from a content tree.
466
-- This can be used to remove position details where we do not want them,
467
-- e.g. in table of contents entries (referring to the original content,
468
-- regardless where it was exactly, for the purpose of checking whether
469
-- the table of contents changed.)
470
--
471
utilities.stripContentPos = function (content)
472
   if type(content) ~= "table" then
6✔
NEW
473
      return content
×
474
   end
475
   local stripped = {}
6✔
476
   for k, v in pairs(content) do
18✔
477
      if type(v) == "table" then
12✔
NEW
478
         v = SU.stripContentPos(v)
×
479
      end
480
      stripped[k] = v
12✔
481
   end
482
   if content.id or content.command then
6✔
483
      stripped.pos, stripped.col, stripped.lno = nil, nil, nil
6✔
484
   end
485
   return stripped
6✔
486
end
487

488
utilities.rateBadness = function (inf_bad, shortfall, spring)
489
   if spring == 0 then
22,885✔
490
      return inf_bad
865✔
491
   end
492
   local bad = math.floor(100 * math.abs(shortfall / spring) ^ 3)
22,020✔
493
   return math.min(inf_bad, bad)
22,020✔
494
end
495

496
utilities.rationWidth = function (target, width, ratio)
497
   if ratio < 0 and width.shrink:tonumber() > 0 then
31,792✔
498
      target:___add(width.shrink:tonumber() * ratio)
5,637✔
499
   elseif ratio > 0 and width.stretch:tonumber() > 0 then
45,030✔
500
      target:___add(width.stretch:tonumber() * ratio)
11,246✔
501
   end
502
   return target
26,236✔
503
end
504

505
-- Unicode-related utilities
506
utilities.utf8char = function (c)
NEW
507
   utilities.deprecated("SU.utf8char", "luautf8.char", "0.11.0", "0.12.0")
×
NEW
508
   return luautf8.char(c)
×
509
end
510

511
utilities.codepoint = function (uchar)
512
   local seq = 0
288,783✔
513
   local val = -1
288,783✔
514
   for i = 1, #uchar do
587,192✔
515
      local c = string.byte(uchar, i)
300,999✔
516
      if seq == 0 then
300,999✔
517
         if val > -1 then
289,985✔
518
            return val
2,590✔
519
         end
520
         seq = c < 0x80 and 1
287,395✔
521
            or c < 0xE0 and 2
287,395✔
522
            or c < 0xF0 and 3
8,707✔
523
            or c < 0xF8 and 4 --c < 0xFC and 5 or c < 0xFE and 6 or
2,290✔
524
            or error("invalid UTF-8 character sequence")
17✔
525
         val = bitshim.band(c, 2 ^ (8 - seq) - 1)
287,395✔
526
      else
527
         val = bitshim.bor(bitshim.lshift(val, 6), bitshim.band(c, 0x3F))
11,014✔
528
      end
529
      seq = seq - 1
298,409✔
530
   end
531
   return val
286,193✔
532
end
533

534
utilities.utf8charfromcodepoint = function (codepoint)
535
   local val = codepoint
667✔
536
   local cp = val
667✔
537
   local hex = (cp:match("[Uu]%+(%x+)") or cp:match("0[xX](%x+)"))
667✔
538
   if hex then
667✔
539
      cp = tonumber("0x" .. hex)
6✔
540
   elseif tonumber(cp) then
661✔
NEW
541
      cp = tonumber(cp)
×
542
   end
543

544
   if type(cp) == "number" then
667✔
545
      val = luautf8.char(cp)
6✔
546
   end
547
   return val
667✔
548
end
549

550
utilities.utf8codes = function (ustr)
NEW
551
   utilities.deprecated("SU.utf8codes", "luautf8.codes", "0.11.0", "0.12.0")
×
NEW
552
   return luautf8.codes(ustr)
×
553
end
554

555
utilities.utf16codes = function (ustr, endian)
556
   local pos = 1
64,844✔
557
   return function ()
558
      if pos > #ustr then
1,981,686✔
559
         return nil
64,844✔
560
      else
561
         local c1, c2, c3, c4, wchar, lowchar
562
         c1 = string.byte(ustr, pos, pos + 1)
1,916,842✔
563
         pos = pos + 1
1,916,842✔
564
         c2 = string.byte(ustr, pos, pos + 1)
1,916,842✔
565
         pos = pos + 1
1,916,842✔
566
         if endian == "be" then
1,916,842✔
567
            wchar = c1 * 256 + c2
1,916,800✔
568
         else
569
            wchar = c2 * 256 + c1
42✔
570
         end
571
         if not (wchar >= 0xD800 and wchar <= 0xDBFF) then
1,916,842✔
572
            return wchar
1,916,836✔
573
         end
574
         c3 = string.byte(ustr, pos, pos + 1)
6✔
575
         pos = pos + 1
6✔
576
         c4 = string.byte(ustr, pos, pos + 1)
6✔
577
         pos = pos + 1
6✔
578
         if endian == "be" then
6✔
579
            lowchar = c3 * 256 + c4
3✔
580
         else
581
            lowchar = c4 * 256 + c3
3✔
582
         end
583
         return 0x10000 + bitshim.lshift(bitshim.band(wchar, 0x03FF), 10) + bitshim.band(lowchar, 0x03FF)
6✔
584
      end
585
   end
586
end
587

588
utilities.splitUtf8 = function (str) -- Return an array of UTF8 strings each representing a Unicode char
589
   local rv = {}
169,510✔
590
   for _, cp in luautf8.next, str do
1,094,177✔
591
      table.insert(rv, luautf8.char(cp))
924,667✔
592
   end
593
   return rv
169,510✔
594
end
595

596
utilities.lastChar = function (str)
597
   local chars = utilities.splitUtf8(str)
4✔
598
   return chars[#chars]
4✔
599
end
600

601
utilities.firstChar = function (str)
602
   local chars = utilities.splitUtf8(str)
10✔
603
   return chars[1]
10✔
604
end
605

606
local byte, floor, reverse = string.byte, math.floor, string.reverse
181✔
607

608
utilities.utf8charat = function (str, index)
NEW
609
   return str:sub(index):match("([%z\1-\127\194-\244][\128-\191]*)")
×
610
end
611

612
local utf16bom = function (endianness)
613
   return endianness == "be" and "\254\255" or endianness == "le" and "\255\254" or SU.error("Unrecognized endianness")
64,854✔
614
end
615

616
utilities.hexencoded = function (str)
617
   local ustr = ""
8✔
618
   for i = 1, #str do
154✔
619
      ustr = ustr .. string.format("%02x", byte(str, i, i + 1))
146✔
620
   end
621
   return ustr
8✔
622
end
623

624
utilities.hexdecoded = function (str)
625
   if #str % 2 == 1 then
12✔
NEW
626
      SU.error("Cannot decode hex string with odd len")
×
627
   end
628
   local ustr = ""
12✔
629
   for i = 1, #str, 2 do
200✔
630
      ustr = ustr .. string.char(tonumber(string.sub(str, i, i + 1), 16))
564✔
631
   end
632
   return ustr
12✔
633
end
634

635
local uchr_to_surrogate_pair = function (uchr, endianness)
636
   local hi, lo = floor((uchr - 0x10000) / 0x400) + 0xd800, (uchr - 0x10000) % 0x400 + 0xdc00
3✔
637
   local s_hi, s_lo =
638
      string.char(floor(hi / 256)) .. string.char(hi % 256), string.char(floor(lo / 256)) .. string.char(lo % 256)
15✔
639
   return endianness == "le" and (reverse(s_hi) .. reverse(s_lo)) or s_hi .. s_lo
5✔
640
end
641

642
local uchr_to_utf16_double_byte = function (uchr, endianness)
643
   local ustr = string.char(floor(uchr / 256)) .. string.char(uchr % 256)
297✔
644
   return endianness == "le" and reverse(ustr) or ustr
112✔
645
end
646

647
local utf8_to_utf16 = function (str, endianness)
648
   local ustr = utf16bom(endianness)
10✔
649
   for _, uchr in luautf8.codes(str) do
112✔
NEW
650
      ustr = ustr
×
651
         .. (uchr < 0x10000 and uchr_to_utf16_double_byte(uchr, endianness) or uchr_to_surrogate_pair(uchr, endianness))
204✔
652
   end
653
   return ustr
10✔
654
end
655

656
utilities.utf8_to_utf16be = function (str)
657
   return utf8_to_utf16(str, "be")
8✔
658
end
659
utilities.utf8_to_utf16le = function (str)
660
   return utf8_to_utf16(str, "le")
2✔
661
end
662
utilities.utf8_to_utf16be_hexencoded = function (str)
663
   return utilities.hexencoded(utilities.utf8_to_utf16be(str))
12✔
664
end
665
utilities.utf8_to_utf16le_hexencoded = function (str)
666
   return utilities.hexencoded(utilities.utf8_to_utf16le(str))
4✔
667
end
668

669
local utf16_to_utf8 = function (str, endianness)
670
   local bom = utf16bom(endianness)
64,844✔
671

672
   if str:find(bom) == 1 then
64,844✔
673
      str = string.sub(str, 3, #str)
8✔
674
   end
675
   local ustr = ""
64,844✔
676
   for uchr in utilities.utf16codes(str, endianness) do
4,028,216✔
677
      ustr = ustr .. luautf8.char(uchr)
1,916,842✔
678
   end
679
   return ustr
64,844✔
680
end
681

682
utilities.utf16be_to_utf8 = function (str)
683
   return utf16_to_utf8(str, "be")
64,838✔
684
end
685
utilities.utf16le_to_utf8 = function (str)
686
   return utf16_to_utf8(str, "le")
6✔
687
end
688

689
utilities.breadcrumbs = function ()
690
   local breadcrumbs = {}
316✔
691

692
   setmetatable(breadcrumbs, {
632✔
693
      __index = function (_, key)
694
         local frame = SILE.traceStack[key]
1✔
695
         return frame and frame.command or nil
1✔
696
      end,
697
      __len = function (_)
NEW
698
         return #SILE.traceStack
×
699
      end,
700
      __tostring = function (self)
NEW
701
         return "B»" .. table.concat(self, "»")
×
702
      end,
703
   })
704

705
   function breadcrumbs:dump ()
316✔
NEW
706
      SU.dump(self)
×
707
   end
708

709
   function breadcrumbs:parent (count)
316✔
710
      -- Note LuaJIT does not support __len, so this has to work even when that metamethod doesn't fire...
711
      return self[#SILE.traceStack - (count or 1)]
2✔
712
   end
713

714
   function breadcrumbs:contains (needle, startdepth)
316✔
NEW
715
      startdepth = startdepth or 0
×
NEW
716
      for i = startdepth, #SILE.traceStack - 1 do
×
NEW
717
         local frame = SILE.traceStack[#SILE.traceStack - i]
×
NEW
718
         if frame.command == needle then
×
NEW
719
            return true, #self - i
×
720
         end
721
      end
NEW
722
      return false, -1
×
723
   end
724

725
   return breadcrumbs
316✔
726
end
727

728
utilities.formatNumber = require("core.utilities-numbers")
181✔
729

730
utilities.collatedSort = require("core.utilities-sorting")
181✔
731

732
return utilities
181✔
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