271 lines
9.1 KiB
Lua
271 lines
9.1 KiB
Lua
|
--[[
|
||
|
logging.lua: pandoc-aware logging functions (can also be used standalone)
|
||
|
Copyright: (c) 2022 William Lupton
|
||
|
License: MIT - see LICENSE file for details
|
||
|
Usage: See README.md for details
|
||
|
]]
|
||
|
|
||
|
-- if running standalone, create a 'pandoc' global
|
||
|
if not pandoc then
|
||
|
_G.pandoc = {utils = {}}
|
||
|
end
|
||
|
|
||
|
-- if there's no pandoc.utils, create a local one
|
||
|
if not pcall(require, 'pandoc.utils') then
|
||
|
pandoc.utils = {}
|
||
|
end
|
||
|
|
||
|
-- if there's no pandoc.utils.type, create a local one
|
||
|
if not pandoc.utils.type then
|
||
|
pandoc.utils.type = function(value)
|
||
|
local typ = type(value)
|
||
|
if not ({table=1, userdata=1})[typ] then
|
||
|
-- unchanged
|
||
|
elseif value.__name then
|
||
|
typ = value.__name
|
||
|
elseif value.tag and value.t then
|
||
|
typ = value.tag
|
||
|
if typ:match('^Meta.') then
|
||
|
typ = typ:sub(5)
|
||
|
end
|
||
|
if typ == 'Map' then
|
||
|
typ = 'table'
|
||
|
end
|
||
|
end
|
||
|
return typ
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- namespace
|
||
|
local logging = {}
|
||
|
|
||
|
-- helper function to return a sensible typename
|
||
|
logging.type = function(value)
|
||
|
-- this can return 'Inlines', 'Blocks', 'Inline', 'Block' etc., or
|
||
|
-- anything that built-in type() can return, namely 'nil', 'number',
|
||
|
-- 'string', 'boolean', 'table', 'function', 'thread', or 'userdata'
|
||
|
local typ = pandoc.utils.type(value)
|
||
|
|
||
|
-- it seems that it can also return strings like 'pandoc Row'; replace
|
||
|
-- spaces with periods
|
||
|
-- XXX I'm not sure that this is done consistently, e.g. I don't think
|
||
|
-- it's done for pandoc.Attr or pandoc.List?
|
||
|
typ = typ:gsub(' ', '.')
|
||
|
|
||
|
-- map Inline and Block to the tag name
|
||
|
-- XXX I guess it's intentional that it doesn't already do this?
|
||
|
return ({Inline=1, Block=1})[typ] and value.tag or typ
|
||
|
end
|
||
|
|
||
|
-- derived from https://www.lua.org/pil/19.3.html pairsByKeys()
|
||
|
logging.spairs = function(list, comp)
|
||
|
local keys = {}
|
||
|
for key, _ in pairs(list) do
|
||
|
table.insert(keys, tostring(key))
|
||
|
end
|
||
|
table.sort(keys, comp)
|
||
|
local i = 0
|
||
|
local iter = function()
|
||
|
i = i + 1
|
||
|
return keys[i] and keys[i], list[keys[i]] or nil
|
||
|
end
|
||
|
return iter
|
||
|
end
|
||
|
|
||
|
-- helper function to dump a value with a prefix (recursive)
|
||
|
-- XXX should detect repetition/recursion
|
||
|
-- XXX would like maxlen logic to apply at all levels? but not trivial
|
||
|
local function dump_(prefix, value, maxlen, level, add)
|
||
|
local buffer = {}
|
||
|
if prefix == nil then prefix = '' end
|
||
|
if level == nil then level = 0 end
|
||
|
if add == nil then add = function(item) table.insert(buffer, item) end end
|
||
|
local indent = maxlen and '' or (' '):rep(level)
|
||
|
|
||
|
-- get typename, mapping to pandoc tag names where possible
|
||
|
local typename = logging.type(value)
|
||
|
|
||
|
-- don't explicitly indicate 'obvious' typenames
|
||
|
local typ = (({boolean=1, number=1, string=1, table=1, userdata=1})
|
||
|
[typename] and '' or typename)
|
||
|
|
||
|
-- light userdata is just a pointer (can't iterate over it)
|
||
|
-- XXX is there a better way of checking for light userdata?
|
||
|
if type(value) == 'userdata' and not pcall(pairs(value)) then
|
||
|
value = tostring(value):gsub('userdata:%s*', '')
|
||
|
|
||
|
-- modify the value heuristically
|
||
|
elseif ({table=1, userdata=1})[type(value)] then
|
||
|
local valueCopy, numKeys, lastKey = {}, 0, nil
|
||
|
for key, val in pairs(value) do
|
||
|
-- pandoc >= 2.15 includes 'tag', nil values and functions
|
||
|
if key ~= 'tag' and val and type(val) ~= 'function' then
|
||
|
valueCopy[key] = val
|
||
|
numKeys = numKeys + 1
|
||
|
lastKey = key
|
||
|
end
|
||
|
end
|
||
|
if numKeys == 0 then
|
||
|
-- this allows empty tables to be formatted on a single line
|
||
|
-- XXX experimental: render Doc objects
|
||
|
value = typename == 'Doc' and '|' .. value:render() .. '|' or
|
||
|
typename == 'Space' and '' or '{}'
|
||
|
elseif numKeys == 1 and lastKey == 'text' then
|
||
|
-- this allows text-only types to be formatted on a single line
|
||
|
typ = typename
|
||
|
value = value[lastKey]
|
||
|
typename = 'string'
|
||
|
else
|
||
|
value = valueCopy
|
||
|
-- XXX experimental: indicate array sizes
|
||
|
if #value > 0 then
|
||
|
typ = typ .. '[' .. #value .. ']'
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- output the possibly-modified value
|
||
|
local presep = #prefix > 0 and ' ' or ''
|
||
|
local typsep = #typ > 0 and ' ' or ''
|
||
|
local valtyp = type(value)
|
||
|
if valtyp == 'nil' then
|
||
|
add('nil')
|
||
|
elseif ({boolean=1, number=1, string=1})[valtyp] then
|
||
|
typsep = #typ > 0 and valtyp == 'string' and #value > 0 and ' ' or ''
|
||
|
-- don't use the %q format specifier; doesn't work with multi-bytes
|
||
|
local quo = typename == 'string' and '"' or ''
|
||
|
add(string.format('%s%s%s%s%s%s%s%s', indent, prefix, presep, typ,
|
||
|
typsep, quo, value, quo))
|
||
|
-- light userdata is just a pointer (can't iterate over it)
|
||
|
-- XXX is there a better way of checking for light userdata?
|
||
|
elseif valtyp == 'userdata' and not pcall(pairs(value)) then
|
||
|
add(string.format('%s%s%s%s %s', indent, prefix, presep, typ,
|
||
|
tostring(value):gsub('userdata:%s*', '')))
|
||
|
elseif ({table=1, userdata=1})[valtyp] then
|
||
|
add(string.format('%s%s%s%s%s{', indent, prefix, presep, typ, typsep))
|
||
|
-- Attr and Attr.attributes have both numeric and string keys, so
|
||
|
-- ignore the numeric ones
|
||
|
-- XXX this is no longer the case for pandoc >= 2.15, so could remove
|
||
|
-- the special case?
|
||
|
local first = true
|
||
|
if prefix ~= 'attributes:' and typ ~= 'Attr' then
|
||
|
for i, val in ipairs(value) do
|
||
|
local pre = maxlen and not first and ', ' or ''
|
||
|
dump_(string.format('%s[%s]', pre, i), val, maxlen,
|
||
|
level + 1, add)
|
||
|
first = false
|
||
|
end
|
||
|
end
|
||
|
-- report keys in alphabetical order to ensure repeatability
|
||
|
for key, val in logging.spairs(value) do
|
||
|
local pre = maxlen and not first and ', ' or ''
|
||
|
-- this check can avoid an infinite loop, e.g. with metatables
|
||
|
-- XXX should have more general and robust infinite loop avoidance
|
||
|
if key:match('^__') and type(val) ~= 'string' then
|
||
|
add(string.format('%s%s: %s', pre, key, tostring(val)))
|
||
|
|
||
|
-- pandoc >= 2.15 includes 'tag'
|
||
|
elseif not tonumber(key) and key ~= 'tag' then
|
||
|
dump_(string.format('%s%s:', pre, key), val, maxlen,
|
||
|
level + 1, add)
|
||
|
end
|
||
|
first = false
|
||
|
end
|
||
|
add(string.format('%s}', indent))
|
||
|
end
|
||
|
return table.concat(buffer, maxlen and '' or '\n')
|
||
|
end
|
||
|
|
||
|
logging.dump = function(value, maxlen)
|
||
|
if maxlen == nil then maxlen = 70 end
|
||
|
local text = dump_(nil, value, maxlen)
|
||
|
if #text > maxlen then
|
||
|
text = dump_(nil, value, nil)
|
||
|
end
|
||
|
return text
|
||
|
end
|
||
|
|
||
|
logging.output = function(...)
|
||
|
local need_newline = false
|
||
|
for i, item in ipairs({...}) do
|
||
|
-- XXX space logic could be cleverer, e.g. no space after newline
|
||
|
local maybe_space = i > 1 and ' ' or ''
|
||
|
local text = ({table=1, userdata=1})[type(item)] and
|
||
|
logging.dump(item) or tostring(item)
|
||
|
io.stderr:write(maybe_space, text)
|
||
|
need_newline = text:sub(-1) ~= '\n'
|
||
|
end
|
||
|
if need_newline then
|
||
|
io.stderr:write('\n')
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- basic logging support (-1=errors, 0=warnings, 1=info, 2=debug, 3=debug2)
|
||
|
-- XXX should support string levels?
|
||
|
logging.loglevel = 0
|
||
|
|
||
|
-- set log level and return the previous level
|
||
|
logging.setloglevel = function(loglevel)
|
||
|
local oldlevel = logging.loglevel
|
||
|
logging.loglevel = loglevel
|
||
|
return oldlevel
|
||
|
end
|
||
|
|
||
|
-- verbosity default is WARNING; --quiet -> ERROR and --verbose -> INFO
|
||
|
-- --trace sets TRACE or DEBUG (depending on --verbose)
|
||
|
if type(PANDOC_STATE) == 'nil' then
|
||
|
-- use the default level
|
||
|
elseif PANDOC_STATE.trace then
|
||
|
logging.loglevel = PANDOC_STATE.verbosity == 'INFO' and 3 or 2
|
||
|
elseif PANDOC_STATE.verbosity == 'INFO' then
|
||
|
logging.loglevel = 1
|
||
|
elseif PANDOC_STATE.verbosity == 'WARNING' then
|
||
|
logging.loglevel = 0
|
||
|
elseif PANDOC_STATE.verbosity == 'ERROR' then
|
||
|
logging.loglevel = -1
|
||
|
end
|
||
|
|
||
|
logging.error = function(...)
|
||
|
if logging.loglevel >= -1 then
|
||
|
logging.output('(E)', ...)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
logging.warning = function(...)
|
||
|
if logging.loglevel >= 0 then
|
||
|
logging.output('(W)', ...)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
logging.info = function(...)
|
||
|
if logging.loglevel >= 1 then
|
||
|
logging.output('(I)', ...)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
logging.debug = function(...)
|
||
|
if logging.loglevel >= 2 then
|
||
|
logging.output('(D)', ...)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
logging.debug2 = function(...)
|
||
|
if logging.loglevel >= 3 then
|
||
|
logging.warning('debug2() is deprecated; use trace()')
|
||
|
logging.output('(D2)', ...)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
logging.trace = function(...)
|
||
|
if logging.loglevel >= 3 then
|
||
|
logging.output('(T)', ...)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- for temporary unconditional debug output
|
||
|
logging.temp = function(...)
|
||
|
logging.output('(#)', ...)
|
||
|
end
|
||
|
|
||
|
return logging
|