-
Notifications
You must be signed in to change notification settings - Fork 1
/
logging.lua
270 lines (242 loc) · 9.05 KB
/
logging.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
--[[
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