local bitshim = require("bitshim")
local luautf8 = require("lua-utf8")
local semver = require("semver")
local utilities = {}
local epsilon = 1E-12
function utilities.concat (array, separator)
return table.concat(utilities.map(tostring, array), separator)
end
function utilities.map (func, array)
local new_array = {}
local last = #array
for i = 1, last do
new_array[i] = func(array[i])
end
return new_array
end
function utilities.required (options, name, context, required_type)
if not options[name] then
utilities.error(context .. " needs a " .. name .. " parameter")
end
if required_type then
return utilities.cast(required_type, options[name])
end
return options[name]
end
function utilities.sortedpairs (input)
local keys = {}
for k, _ in pairs(input) do
keys[#keys + 1] = k
end
table.sort(keys, function (a, b)
if type(a) == type(b) then
return a < b
elseif type(a) == "number" then
return true
else
return false
end
end)
return coroutine.wrap(function ()
for i = 1, #keys do
coroutine.yield(keys[i], input[keys[i]])
end
end)
end
function utilities.splice (array, start, stop, replacement)
local ptr = start
local room = stop - start + 1
local last = replacement and #replacement or 0
for i = 1, last do
if room > 0 then
room = room - 1
array[ptr] = replacement[i]
else
table.insert(array, ptr, replacement[i])
end
ptr = ptr + 1
end
for _ = 1, room do
table.remove(array, ptr)
end
return array
end
function utilities.inherit (orig, spec)
local new = pl.tablex.deepcopy(orig)
if spec then
for k, v in pairs(spec) do
new[k] = v
end
end
if new.init then
new:init()
end
return new
end
local function preferbool ()
utilities.warn("Please use boolean values or strings such as 'true' and 'false' instead of 'yes' and 'no'.")
end
function utilities.boolean (value, default)
if value == false then
return false
end
if value == true then
return true
end
if value == "false" then
return false
end
if value == "true" then
return true
end
if value == "no" then
preferbool()
return false
end
if value == "yes" then
preferbool()
return true
end
if value == nil then
return default == true
end
if value == "" then
return default == true
end
SU.error("Expecting a boolean value but got '" .. value .. "'")
return default == true
end
function utilities.cast (wantedType, value)
local actualType = SU.type(value)
wantedType = string.lower(wantedType)
if wantedType:match(actualType) then
return value
elseif actualType == "nil" and wantedType:match("nil") then
return nil
elseif wantedType:match("length") then
return SILE.types.length(value)
elseif wantedType:match("measurement") then
return SILE.types.measurement(value)
elseif wantedType:match("vglue") then
return SILE.types.node.vglue(value)
elseif wantedType:match("glue") then
return SILE.types.node.glue(value)
elseif wantedType:match("kern") then
return SILE.types.node.kern(value)
elseif actualType == "nil" then
SU.error("Cannot cast nil to " .. wantedType)
elseif wantedType:match("boolean") then
return SU.boolean(value)
elseif wantedType:match("string") then
return tostring(value)
elseif wantedType:match("number") then
if type(value) == "table" and type(value.tonumber) == "function" then
return value:tonumber()
end
local num = tonumber(value)
if not num then
SU.error("Cannot cast '" .. value .. "'' to " .. wantedType)
end
return num
elseif wantedType:match("integer") then
local num
if type(value) == "table" and type(value.tonumber) == "function" then
num = value:tonumber()
else
num = tonumber(value)
end
if not num then
SU.error("Cannot cast '" .. value .. "'' to " .. wantedType)
end
if not wantedType:match("number") and num % 1 ~= 0 then
SU.warn("Casting an integer but got a float number " .. num)
end
return num
else
SU.error("Cannot cast to unrecognized type " .. wantedType)
end
end
function utilities.type (value)
if type(value) == "number" then
return math.floor(value) == value and "integer" or "number"
elseif type(value) == "table" and value.prototype then
return value:prototype()
elseif type(value) == "table" and value.is_a then
return value.type
else
return type(value)
end
end
function utilities.debug (category, ...)
if SILE.quiet then
return
end
if utilities.debugging(category) then
local inputs = pl.utils.pack(...)
for i, input in ipairs(inputs) do
if type(input) == "function" then
local status, output = pcall(input)
inputs[i] = status and output
or SU.warn(("Output of %s debug function was an error: %s"):format(category, output))
elseif type(input) ~= "string" then
inputs[i] = tostring(input)
end
end
local message = utilities.concat(inputs, " ")
if message then
io.stderr:write(("\n[%s] %s"):format(category, message))
end
end
end
function utilities.debugging (category)
return SILE.debugFlags.all and category ~= "profile" or SILE.debugFlags[category]
end
function utilities.deprecated (old, new, warnat, errorat, extra)
warnat, errorat = semver(warnat or 0), semver(errorat or 0)
local current = SILE.version and semver(SILE.version:match("v([0-9]*.[0-9]*.[0-9]*)")) or warnat
local brackets = old:sub(1, 1) == "\\" and "" or "()"
local _new = new and "Please use " .. (new .. brackets) .. " instead." or "Plase don't use it."
local msg = (old .. brackets)
.. " was deprecated in SILE v"
.. tostring(warnat)
.. ". "
.. _new
.. (extra and ("\n\n" .. extra .. "\n") or "")
if errorat and current >= errorat then
SU.error(msg)
elseif warnat and current >= warnat then
SU.warn(msg)
end
end
function utilities.dump (...)
local arg = { ... } pl.pretty.dump(#arg == 1 and arg[1] or arg, "/dev/stderr")
end
local _skip_traceback_levels = 2
function utilities.error (message, isbug)
_skip_traceback_levels = 3
utilities.warn(message, isbug)
_skip_traceback_levels = 2
io.stderr:flush()
SILE.outputter:finish() SILE.scratch.caughterror = true
error("", 2)
end
function utilities.msg (message)
if SILE.quiet then
return
end
io.stderr:write("\n! " .. message .. "\n")
end
function utilities.warn (message, isbug)
utilities.msg(message)
if SILE.traceback or isbug then
io.stderr:write(" at:\n" .. SILE.traceStack:locationTrace())
if _skip_traceback_levels == 2 then
io.stderr:write(
debug.traceback("", _skip_traceback_levels) or "\t! debug.traceback() did not identify code location"
)
end
else
io.stderr:write(" at " .. SILE.traceStack:locationHead())
end
io.stderr:write("\n")
end
function utilities.feq (lhs, rhs)
lhs = SU.cast("number", lhs)
rhs = SU.cast("number", rhs)
local abs = math.abs
return abs(lhs - rhs) <= epsilon * (abs(lhs) + abs(rhs))
end
function utilities.sum (array)
local total = 0
local last = #array
for i = 1, last do
total = total + array[i]
end
return total
end
function utilities.max (...)
local input = pl.utils.pack(...)
local max = table.remove(input, 1)
for _, val in ipairs(input) do
if val > max then
max = val
end
end
return max
end
function utilities.min (...)
local input = pl.utils.pack(...)
local min = input[1]
for _, val in ipairs(input) do
if val < min then
min = val
end
end
return min
end
function utilities.debug_round (input)
if input > 0 then
input = input + 0.00000000000001
end
if input < 0 then
input = input - 0.00000000000001
end
return string.format("%.4f", input)
end
function utilities.compress (items)
local rv = {}
local max = math.max(pl.utils.unpack(pl.tablex.keys(items)))
for i = 1, max do
if items[i] then
rv[#rv + 1] = items[i]
end
end
return rv
end
function utilities.flip_in_place (tbl)
local tmp, j
for i = 1, math.floor(#tbl / 2) do
tmp = tbl[i]
j = #tbl - i + 1
tbl[i] = tbl[j]
tbl[j] = tmp
end
end
function utilities.allCombinations (options)
local count = 1
for i = 1, #options do
count = count * options[i]
end
return coroutine.wrap(function ()
for i = 0, count - 1 do
local this = i
local rv = {}
for j = 1, #options do
local base = options[j]
rv[#rv + 1] = this % base + 1
this = (this - this % base) / base
end
coroutine.yield(rv)
end
end)
end
function utilities.rateBadness (inf_bad, shortfall, spring)
if spring == 0 then
return inf_bad
end
local bad = math.floor(100 * math.abs(shortfall / spring) ^ 3)
return math.min(inf_bad, bad)
end
function utilities.rationWidth (target, width, ratio)
if ratio < 0 and width.shrink:tonumber() > 0 then
target:___add(width.shrink:tonumber() * ratio)
elseif ratio > 0 and width.stretch:tonumber() > 0 then
target:___add(width.stretch:tonumber() * ratio)
end
return target
end
function utilities.gtoke (string, pattern)
string = string and tostring(string) or ""
pattern = pattern and tostring(pattern) or "%s+"
local length = #string
return coroutine.wrap(function ()
local index = 1
repeat
local first, last = string:find(pattern, index)
if last then
if index < first then
coroutine.yield({ string = string:sub(index, first - 1) })
end
coroutine.yield({ separator = string:sub(first, last) })
index = last + 1
else
if index <= length then
coroutine.yield({ string = string:sub(index) })
end
break
end
until index > length
end)
end
function utilities.codepoint (uchar)
local seq = 0
local val = -1
for i = 1, #uchar do
local c = string.byte(uchar, i)
if seq == 0 then
if val > -1 then
return val
end
seq = c < 0x80 and 1
or c < 0xE0 and 2
or c < 0xF0 and 3
or c < 0xF8 and 4 or error("invalid UTF-8 character sequence")
val = bitshim.band(c, 2 ^ (8 - seq) - 1)
else
val = bitshim.bor(bitshim.lshift(val, 6), bitshim.band(c, 0x3F))
end
seq = seq - 1
end
return val
end
function utilities.utf8charfromcodepoint (codepoint)
local val = codepoint
local cp = val
local hex = (cp:match("[Uu]%+(%x+)") or cp:match("0[xX](%x+)"))
if hex then
cp = tonumber("0x" .. hex)
elseif tonumber(cp) then
cp = tonumber(cp)
end
if type(cp) == "number" then
val = luautf8.char(cp)
end
return val
end
function utilities.utf16codes (ustr, endian)
local pos = 1
return function ()
if pos > #ustr then
return nil
else
local c1, c2, c3, c4, wchar, lowchar
c1 = string.byte(ustr, pos, pos + 1)
pos = pos + 1
c2 = string.byte(ustr, pos, pos + 1)
pos = pos + 1
if endian == "be" then
wchar = c1 * 256 + c2
else
wchar = c2 * 256 + c1
end
if not (wchar >= 0xD800 and wchar <= 0xDBFF) then
return wchar
end
c3 = string.byte(ustr, pos, pos + 1)
pos = pos + 1
c4 = string.byte(ustr, pos, pos + 1)
pos = pos + 1
if endian == "be" then
lowchar = c3 * 256 + c4
else
lowchar = c4 * 256 + c3
end
return 0x10000 + bitshim.lshift(bitshim.band(wchar, 0x03FF), 10) + bitshim.band(lowchar, 0x03FF)
end
end
end
function utilities.splitUtf8 (str)
local rv = {}
for _, cp in luautf8.next, str do
table.insert(rv, luautf8.char(cp))
end
return rv
end
function utilities.lastChar (str)
local chars = utilities.splitUtf8(str)
return chars[#chars]
end
function utilities.firstChar (str)
local chars = utilities.splitUtf8(str)
return chars[1]
end
local byte, floor, reverse = string.byte, math.floor, string.reverse
function utilities.utf8charat (str, index)
return str:sub(index):match("([%z\1-\127\194-\244][\128-\191]*)")
end
local utf16bom = function (endianness)
return endianness == "be" and "\254\255" or endianness == "le" and "\255\254" or SU.error("Unrecognized endianness")
end
function utilities.hexencoded (str)
local ustr = ""
for i = 1, #str do
ustr = ustr .. string.format("%02x", byte(str, i, i + 1))
end
return ustr
end
function utilities.hexdecoded (str)
if #str % 2 == 1 then
SU.error("Cannot decode hex string with odd len")
end
local ustr = ""
for i = 1, #str, 2 do
ustr = ustr .. string.char(tonumber(string.sub(str, i, i + 1), 16))
end
return ustr
end
local uchr_to_surrogate_pair = function (uchr, endianness)
local hi, lo = floor((uchr - 0x10000) / 0x400) + 0xd800, (uchr - 0x10000) % 0x400 + 0xdc00
local s_hi, s_lo =
string.char(floor(hi / 256)) .. string.char(hi % 256), string.char(floor(lo / 256)) .. string.char(lo % 256)
return endianness == "le" and (reverse(s_hi) .. reverse(s_lo)) or s_hi .. s_lo
end
local uchr_to_utf16_double_byte = function (uchr, endianness)
local ustr = string.char(floor(uchr / 256)) .. string.char(uchr % 256)
return endianness == "le" and reverse(ustr) or ustr
end
local utf8_to_utf16 = function (str, endianness)
local ustr = utf16bom(endianness)
for _, uchr in luautf8.codes(str) do
ustr = ustr
.. (uchr < 0x10000 and uchr_to_utf16_double_byte(uchr, endianness) or uchr_to_surrogate_pair(uchr, endianness))
end
return ustr
end
function utilities.utf8_to_utf16be (str)
return utf8_to_utf16(str, "be")
end
function utilities.utf8_to_utf16le (str)
return utf8_to_utf16(str, "le")
end
function utilities.utf8_to_utf16be_hexencoded (str)
return utilities.hexencoded(utilities.utf8_to_utf16be(str))
end
function utilities.utf8_to_utf16le_hexencoded (str)
return utilities.hexencoded(utilities.utf8_to_utf16le(str))
end
local utf16_to_utf8 = function (str, endianness)
local bom = utf16bom(endianness)
if str:find(bom) == 1 then
str = string.sub(str, 3, #str)
end
local ustr = ""
for uchr in utilities.utf16codes(str, endianness) do
ustr = ustr .. luautf8.char(uchr)
end
return ustr
end
function utilities.utf16be_to_utf8 (str)
return utf16_to_utf8(str, "be")
end
function utilities.utf16le_to_utf8 (str)
return utf16_to_utf8(str, "le")
end
function utilities.breadcrumbs ()
local breadcrumbs = {}
setmetatable(breadcrumbs, {
__index = function (_, key)
local frame = SILE.traceStack[key]
return frame and frame.command or nil
end,
__len = function (_)
return #SILE.traceStack
end,
__tostring = function (self)
return "B»" .. table.concat(self, "»")
end,
})
function breadcrumbs:dump ()
SU.dump(self)
end
function breadcrumbs:parent (count)
return self[#SILE.traceStack - (count or 1)]
end
function breadcrumbs:contains (needle, startdepth)
startdepth = startdepth or 0
for i = startdepth, #SILE.traceStack - 1 do
local frame = SILE.traceStack[#SILE.traceStack - i]
if frame.command == needle then
return true, #self - i
end
end
return false, -1
end
return breadcrumbs
end
utilities.formatNumber = require("core.utilities.numbers")
utilities.collatedSort = require("core.utilities.sorting")
utilities.ast = require("core.utilities.ast")
utilities.debugAST = utilities.ast.debug
function utilities.subContent (content)
SU.deprecated(
"SU.subContent",
"SU.ast.subContent",
"0.15.0",
"0.17.0",
[[
Note that the new implementation no longer introduces an id="stuff" key.]]
)
return utilities.ast.subContent(content)
end
function utilities.hasContent (content)
SU.deprecated("SU.hasContent", "SU.ast.hasContent", "0.15.0", "0.17.0")
return SU.ast.hasContent(content)
end
function utilities.contentToString (content)
SU.deprecated("SU.contentToString", "SU.ast.contentToString", "0.15.0", "0.17.0")
return SU.ast.contentToString(content)
end
function utilities.walkContent (content, action)
SU.deprecated("SU.walkContent", "SU.ast.walkContent", "0.15.0", "0.17.0")
SU.ast.walkContent(content, action)
end
function utilities.stripContentPos (content)
SU.deprecated("SU.stripContentPos", "SU.ast.stripContentPos", "0.15.0", "0.17.0")
return SU.ast.stripContentPos(content)
end
return utilities