File: logging.lua

package info (click to toggle)
bird2 2.17.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 5,584 kB
  • sloc: ansic: 74,910; sh: 3,712; perl: 3,444; lex: 883; python: 495; makefile: 464; xml: 260; sed: 13
file content (271 lines) | stat: -rw-r--r-- 9,343 bytes parent folder | download | duplicates (4)
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
271
--[[
    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
    Source:	 https://github.com/pandoc-ext/logging/blob/main/logging.lua
]]

-- 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