Path: blob/main/src/resources/pandoc/datadir/logging.lua
12922 views
--[[1logging.lua: pandoc-aware logging functions (can also be used standalone)2Copyright: (c) 2022 William Lupton3License: MIT - see LICENSE file for details4Usage: See README.md for details5]]67-- if running standalone, create a 'pandoc' global8if not pandoc then9_G.pandoc = {utils = {}}10end1112-- if there's no pandoc.utils, create a local one13if not pcall(require, 'pandoc.utils') then14pandoc.utils = {}15end1617-- if there's no pandoc.utils.type, create a local one18if not pandoc.utils.type then19pandoc.utils.type = function(value)20local typ = type(value)21if not ({table=1, userdata=1})[typ] then22-- unchanged23elseif value.__name then24typ = value.__name25elseif value.tag and value.t then26typ = value.tag27if typ:match('^Meta.') then28typ = typ:sub(5)29end30if typ == 'Map' then31typ = 'table'32end33end34return typ35end36end3738-- namespace39local logging = {}4041-- helper function to return a sensible typename42logging.type = function(value)43-- this can return 'Inlines', 'Blocks', 'Inline', 'Block' etc., or44-- anything that built-in type() can return, namely 'nil', 'number',45-- 'string', 'boolean', 'table', 'function', 'thread', or 'userdata'46local typ = pandoc.utils.type(value)4748-- it seems that it can also return strings like 'pandoc Row'; replace49-- spaces with periods50-- XXX I'm not sure that this is done consistently, e.g. I don't think51-- it's done for pandoc.Attr or pandoc.List?52typ = typ:gsub(' ', '.')5354-- map Inline and Block to the tag name55-- XXX I guess it's intentional that it doesn't already do this?56return ({Inline=1, Block=1})[typ] and value.tag or typ57end5859-- derived from https://www.lua.org/pil/19.3.html pairsByKeys()60logging.spairs = function(list, comp)61local keys = {}62for key, _ in pairs(list) do63table.insert(keys, tostring(key))64end65table.sort(keys, comp)66local i = 067local iter = function()68i = i + 169return keys[i] and keys[i], list[keys[i]] or nil70end71return iter72end7374-- helper function to dump a value with a prefix (recursive)75-- XXX would like maxlen logic to apply at all levels? but not trivial76local function dump_(prefix, value, maxlen, level, add, visited)77local buffer = {}78if prefix == nil then prefix = '' end79if level == nil then level = 0 end80if visited == nil then visited = {} end81if add == nil then add = function(item) table.insert(buffer, item) end end82local indent = maxlen and '' or (' '):rep(level)8384-- get typename, mapping to pandoc tag names where possible85local typename = logging.type(value)8687-- don't explicitly indicate 'obvious' typenames88local typ = (({boolean=1, number=1, string=1, table=1, userdata=1})89[typename] and '' or typename)90local address = string.format("%p", value)91-- modify the value heuristically92if ({table=1, userdata=1})[type(value)] then93local valueCopy, numKeys, lastKey = {}, 0, nil94for key, val in pairs(value) do95-- pandoc >= 2.15 includes 'tag', nil values and functions96if key ~= 'tag' and val then97valueCopy[key] = val98numKeys = numKeys + 199lastKey = key100end101end102if numKeys == 0 then103-- this allows empty tables to be formatted on a single line104value = typename == 'Space' and '' or '{}'105elseif numKeys == 1 and lastKey == 'text' then106-- this allows text-only types to be formatted on a single line107typ = typename108value = value[lastKey]109typename = 'string'110else111value = valueCopy112end113end114115-- output the possibly-modified value116local presep = #prefix > 0 and ' ' or ''117local typsep = #typ > 0 and ' ' or ''118local valtyp = type(value)119if visited[address] then120add(string.format('%s%scircular-reference(%s)', indent, prefix, address))121else122if valtyp == 'nil' then123add('nil')124elseif ({boolean=1, number=1, string=1})[valtyp] then125typsep = #typ > 0 and valtyp == 'string' and #value > 0 and ' ' or ''126-- don't use the %q format specifier; doesn't work with multi-bytes127local quo = typename == 'string' and '"' or ''128add(string.format('%s%s%s%s%s%s%s%s', indent, prefix, presep, typ,129typsep, quo, value, quo))130elseif valtyp == 'function' then131typsep = #typ > 0 and valtyp == 'string' and #value > 0 and ' ' or ''132-- don't use the %q format specifier; doesn't work with multi-bytes133local quo = typename == 'string' and '"' or ''134add(string.format('%s%s%s%s%s%s', indent, prefix, presep,135quo, value, quo))136137elseif ({table=1, userdata=1})[valtyp] then138visited[address] = true139add(string.format('%s%s%s%s%s{', indent, prefix, presep, typ, typsep))140-- Attr and Attr.attributes have both numeric and string keys, so141-- ignore the numeric ones142-- XXX this is no longer the case for pandoc >= 2.15, so could remove143-- the special case?144local first = true145if prefix ~= 'attributes:' and typ ~= 'Attr' then146for i, val in ipairs(value) do147local pre = maxlen and not first and ', ' or ''148local text = dump_(string.format('%s[%s]', pre, i), val,149maxlen, level + 1, add, visited)150first = false151end152end153-- report keys in alphabetical order to ensure repeatability154for key, val in logging.spairs(value) do155-- pandoc >= 2.15 includes 'tag'156if not tonumber(key) and key ~= 'tag' then157local pre = maxlen and not first and ', ' or ''158local text = dump_(string.format('%s%s:', pre, key), val,159maxlen, level + 1, add, visited)160end161first = false162end163add(string.format('%s}', indent))164end165end166167return table.concat(buffer, maxlen and '' or '\n')168end169170logging.dump = function(value, maxlen)171if maxlen == nil then maxlen = 70 end172local text = dump_(nil, value, maxlen)173if #text > maxlen then174text = dump_(nil, value, nil)175end176return text177end178179logging.output = function(...)180local need_newline = false181for i, item in ipairs({...}) do182-- XXX space logic could be cleverer, e.g. no space after newline183local maybe_space = i > 1 and ' ' or ''184local text = ({table=1, userdata=1})[type(item)] and185logging.dump(item) or tostring(item)186io.stderr:write(maybe_space, text)187need_newline = text:sub(-1) ~= '\n'188end189if need_newline then190io.stderr:write('\n')191end192end193194-- basic logging support (-1=errors, 0=warnings, 1=info, 2=debug, 3=debug2)195-- XXX should support string levels?196logging.loglevel = 0197198-- set log level and return the previous level199logging.setloglevel = function(loglevel)200local oldlevel = logging.loglevel201logging.loglevel = loglevel202return oldlevel203end204205-- verbosity default is WARNING; --quiet -> ERROR and --verbose -> INFO206-- --trace sets TRACE or DEBUG (depending on --verbose)207if type(PANDOC_STATE) == 'nil' then208-- use the default level209elseif PANDOC_STATE.trace then210logging.loglevel = PANDOC_STATE.verbosity == 'INFO' and 3 or 2211elseif PANDOC_STATE.verbosity == 'INFO' then212logging.loglevel = 1213elseif PANDOC_STATE.verbosity == 'WARNING' then214logging.loglevel = 0215elseif PANDOC_STATE.verbosity == 'ERROR' then216logging.loglevel = -1217end218219logging.error = function(...)220if logging.loglevel >= -1 then221logging.output('(E)', ...)222end223end224225logging.warning = function(...)226if logging.loglevel >= 0 then227logging.output('(W)', ...)228end229end230231logging.info = function(...)232if logging.loglevel >= 1 then233logging.output('(I)', ...)234end235end236237logging.debug = function(...)238if logging.loglevel >= 2 then239logging.output('(D)', ...)240end241end242243logging.debug2 = function(...)244if logging.loglevel >= 3 then245logging.warning('debug2() is deprecated; use trace()')246logging.output('(D2)', ...)247end248end249250logging.trace = function(...)251if logging.loglevel >= 3 then252logging.output('(T)', ...)253end254end255256-- for temporary unconditional debug output257logging.temp = function(...)258logging.output('(#)', ...)259end260261return logging262263264