Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/src/resources/pandoc/datadir/logging.lua
12926 views
1
--[[
2
logging.lua: pandoc-aware logging functions (can also be used standalone)
3
Copyright: (c) 2022 William Lupton
4
License: MIT - see LICENSE file for details
5
Usage: See README.md for details
6
]]
7
8
-- if running standalone, create a 'pandoc' global
9
if not pandoc then
10
_G.pandoc = {utils = {}}
11
end
12
13
-- if there's no pandoc.utils, create a local one
14
if not pcall(require, 'pandoc.utils') then
15
pandoc.utils = {}
16
end
17
18
-- if there's no pandoc.utils.type, create a local one
19
if not pandoc.utils.type then
20
pandoc.utils.type = function(value)
21
local typ = type(value)
22
if not ({table=1, userdata=1})[typ] then
23
-- unchanged
24
elseif value.__name then
25
typ = value.__name
26
elseif value.tag and value.t then
27
typ = value.tag
28
if typ:match('^Meta.') then
29
typ = typ:sub(5)
30
end
31
if typ == 'Map' then
32
typ = 'table'
33
end
34
end
35
return typ
36
end
37
end
38
39
-- namespace
40
local logging = {}
41
42
-- helper function to return a sensible typename
43
logging.type = function(value)
44
-- this can return 'Inlines', 'Blocks', 'Inline', 'Block' etc., or
45
-- anything that built-in type() can return, namely 'nil', 'number',
46
-- 'string', 'boolean', 'table', 'function', 'thread', or 'userdata'
47
local typ = pandoc.utils.type(value)
48
49
-- it seems that it can also return strings like 'pandoc Row'; replace
50
-- spaces with periods
51
-- XXX I'm not sure that this is done consistently, e.g. I don't think
52
-- it's done for pandoc.Attr or pandoc.List?
53
typ = typ:gsub(' ', '.')
54
55
-- map Inline and Block to the tag name
56
-- XXX I guess it's intentional that it doesn't already do this?
57
return ({Inline=1, Block=1})[typ] and value.tag or typ
58
end
59
60
-- derived from https://www.lua.org/pil/19.3.html pairsByKeys()
61
logging.spairs = function(list, comp)
62
local keys = {}
63
for key, _ in pairs(list) do
64
table.insert(keys, tostring(key))
65
end
66
table.sort(keys, comp)
67
local i = 0
68
local iter = function()
69
i = i + 1
70
return keys[i] and keys[i], list[keys[i]] or nil
71
end
72
return iter
73
end
74
75
-- helper function to dump a value with a prefix (recursive)
76
-- XXX would like maxlen logic to apply at all levels? but not trivial
77
local function dump_(prefix, value, maxlen, level, add, visited)
78
local buffer = {}
79
if prefix == nil then prefix = '' end
80
if level == nil then level = 0 end
81
if visited == nil then visited = {} end
82
if add == nil then add = function(item) table.insert(buffer, item) end end
83
local indent = maxlen and '' or (' '):rep(level)
84
85
-- get typename, mapping to pandoc tag names where possible
86
local typename = logging.type(value)
87
88
-- don't explicitly indicate 'obvious' typenames
89
local typ = (({boolean=1, number=1, string=1, table=1, userdata=1})
90
[typename] and '' or typename)
91
local address = string.format("%p", value)
92
-- modify the value heuristically
93
if ({table=1, userdata=1})[type(value)] then
94
local valueCopy, numKeys, lastKey = {}, 0, nil
95
for key, val in pairs(value) do
96
-- pandoc >= 2.15 includes 'tag', nil values and functions
97
if key ~= 'tag' and val then
98
valueCopy[key] = val
99
numKeys = numKeys + 1
100
lastKey = key
101
end
102
end
103
if numKeys == 0 then
104
-- this allows empty tables to be formatted on a single line
105
value = typename == 'Space' and '' or '{}'
106
elseif numKeys == 1 and lastKey == 'text' then
107
-- this allows text-only types to be formatted on a single line
108
typ = typename
109
value = value[lastKey]
110
typename = 'string'
111
else
112
value = valueCopy
113
end
114
end
115
116
-- output the possibly-modified value
117
local presep = #prefix > 0 and ' ' or ''
118
local typsep = #typ > 0 and ' ' or ''
119
local valtyp = type(value)
120
if visited[address] then
121
add(string.format('%s%scircular-reference(%s)', indent, prefix, address))
122
else
123
if valtyp == 'nil' then
124
add('nil')
125
elseif ({boolean=1, number=1, string=1})[valtyp] then
126
typsep = #typ > 0 and valtyp == 'string' and #value > 0 and ' ' or ''
127
-- don't use the %q format specifier; doesn't work with multi-bytes
128
local quo = typename == 'string' and '"' or ''
129
add(string.format('%s%s%s%s%s%s%s%s', indent, prefix, presep, typ,
130
typsep, quo, value, quo))
131
elseif valtyp == 'function' then
132
typsep = #typ > 0 and valtyp == 'string' and #value > 0 and ' ' or ''
133
-- don't use the %q format specifier; doesn't work with multi-bytes
134
local quo = typename == 'string' and '"' or ''
135
add(string.format('%s%s%s%s%s%s', indent, prefix, presep,
136
quo, value, quo))
137
138
elseif ({table=1, userdata=1})[valtyp] then
139
visited[address] = true
140
add(string.format('%s%s%s%s%s{', indent, prefix, presep, typ, typsep))
141
-- Attr and Attr.attributes have both numeric and string keys, so
142
-- ignore the numeric ones
143
-- XXX this is no longer the case for pandoc >= 2.15, so could remove
144
-- the special case?
145
local first = true
146
if prefix ~= 'attributes:' and typ ~= 'Attr' then
147
for i, val in ipairs(value) do
148
local pre = maxlen and not first and ', ' or ''
149
local text = dump_(string.format('%s[%s]', pre, i), val,
150
maxlen, level + 1, add, visited)
151
first = false
152
end
153
end
154
-- report keys in alphabetical order to ensure repeatability
155
for key, val in logging.spairs(value) do
156
-- pandoc >= 2.15 includes 'tag'
157
if not tonumber(key) and key ~= 'tag' then
158
local pre = maxlen and not first and ', ' or ''
159
local text = dump_(string.format('%s%s:', pre, key), val,
160
maxlen, level + 1, add, visited)
161
end
162
first = false
163
end
164
add(string.format('%s}', indent))
165
end
166
end
167
168
return table.concat(buffer, maxlen and '' or '\n')
169
end
170
171
logging.dump = function(value, maxlen)
172
if maxlen == nil then maxlen = 70 end
173
local text = dump_(nil, value, maxlen)
174
if #text > maxlen then
175
text = dump_(nil, value, nil)
176
end
177
return text
178
end
179
180
logging.output = function(...)
181
local need_newline = false
182
for i, item in ipairs({...}) do
183
-- XXX space logic could be cleverer, e.g. no space after newline
184
local maybe_space = i > 1 and ' ' or ''
185
local text = ({table=1, userdata=1})[type(item)] and
186
logging.dump(item) or tostring(item)
187
io.stderr:write(maybe_space, text)
188
need_newline = text:sub(-1) ~= '\n'
189
end
190
if need_newline then
191
io.stderr:write('\n')
192
end
193
end
194
195
-- basic logging support (-1=errors, 0=warnings, 1=info, 2=debug, 3=debug2)
196
-- XXX should support string levels?
197
logging.loglevel = 0
198
199
-- set log level and return the previous level
200
logging.setloglevel = function(loglevel)
201
local oldlevel = logging.loglevel
202
logging.loglevel = loglevel
203
return oldlevel
204
end
205
206
-- verbosity default is WARNING; --quiet -> ERROR and --verbose -> INFO
207
-- --trace sets TRACE or DEBUG (depending on --verbose)
208
if type(PANDOC_STATE) == 'nil' then
209
-- use the default level
210
elseif PANDOC_STATE.trace then
211
logging.loglevel = PANDOC_STATE.verbosity == 'INFO' and 3 or 2
212
elseif PANDOC_STATE.verbosity == 'INFO' then
213
logging.loglevel = 1
214
elseif PANDOC_STATE.verbosity == 'WARNING' then
215
logging.loglevel = 0
216
elseif PANDOC_STATE.verbosity == 'ERROR' then
217
logging.loglevel = -1
218
end
219
220
logging.error = function(...)
221
if logging.loglevel >= -1 then
222
logging.output('(E)', ...)
223
end
224
end
225
226
logging.warning = function(...)
227
if logging.loglevel >= 0 then
228
logging.output('(W)', ...)
229
end
230
end
231
232
logging.info = function(...)
233
if logging.loglevel >= 1 then
234
logging.output('(I)', ...)
235
end
236
end
237
238
logging.debug = function(...)
239
if logging.loglevel >= 2 then
240
logging.output('(D)', ...)
241
end
242
end
243
244
logging.debug2 = function(...)
245
if logging.loglevel >= 3 then
246
logging.warning('debug2() is deprecated; use trace()')
247
logging.output('(D2)', ...)
248
end
249
end
250
251
logging.trace = function(...)
252
if logging.loglevel >= 3 then
253
logging.output('(T)', ...)
254
end
255
end
256
257
-- for temporary unconditional debug output
258
logging.temp = function(...)
259
logging.output('(#)', ...)
260
end
261
262
return logging
263
264