-
Notifications
You must be signed in to change notification settings - Fork 6
/
lcmark.lua
452 lines (419 loc) · 13.7 KB
/
lcmark.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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
local cmark = require("cmark")
local lpeg = require("lpeg")
local S, C, P, R, V, Ct =
lpeg.S, lpeg.C, lpeg.P, lpeg.R, lpeg.V, lpeg.Ct
local nl = P"\r\n" + P"\r" + P"\n"
local sp = S" \t"^0
local lcmark = {}
lcmark.version = "0.30.2"
lcmark.writers = {
html = function(d, opts, _) return cmark.render_html(d, opts) end,
man = cmark.render_man,
xml = function(d, opts, _) return cmark.render_xml(d, opts) end,
latex = cmark.render_latex,
commonmark = cmark.render_commonmark
}
local default_yaml_parser = nil
local function try_load(module_name, func_name)
if default_yaml_parser then -- already loaded; skip
return
end
local success, loaded = pcall(require, module_name)
if not success then
return
end
if type(loaded) ~= "table" or type(loaded[func_name]) ~= "function" then
return
end
default_yaml_parser = loaded[func_name]
lcmark.yaml_parser_name = module_name .. "." .. func_name
end
try_load("lyaml", "load")
try_load("yaml", "load") -- must come before yaml.eval
try_load("yaml", "eval")
-- the reason yaml.load must come before yaml.eval is that the 'yaml' library
-- prints error messages if you try to index non-existent fields such as 'eval'
local toOptions = function(opts)
if type(opts) == 'table' then
return (cmark.OPT_VALIDATE_UTF8 + cmark.OPT_NORMALIZE +
(opts.smart and cmark.OPT_SMART or 0) +
(opts.safe and 0 or cmark.OPT_UNSAFE) +
(opts.hardbreaks and cmark.OPT_HARDBREAKS or 0) +
(opts.sourcepos and cmark.OPT_SOURCEPOS or 0)
)
else
return opts
end
end
-- walk nodes of table, applying a callback to each
local function walk_table(table, callback, inplace)
assert(type(table) == 'table')
local new = {}
local res
for k, v in pairs(table) do
if type(v) == 'table' then
res = walk_table(v, callback, inplace)
else
res = callback(v)
end
if not inplace then
new[k] = res
end
end
if not inplace then
return new
end
end
-- We inject cmark into environment where filters are
-- run, so users don't need to qualify each function with 'cmark.'.
local defaultEnv = setmetatable({}, { __index = _G })
for k,v in pairs(cmark) do
defaultEnv[k] = v
end
local loadfile_with_env
if setfenv then
-- Lua 5.1/LuaJIT
loadfile_with_env = function(filename)
local result, msg = loadfile(filename)
if result then
return setfenv(result, defaultEnv)
else
return result, msg
end
end
else
-- Lua 5.2+
loadfile_with_env = function(filename)
return loadfile(filename, 't', defaultEnv)
end
end
-- Loads a filter from a Lua file and populates the loaded function's
-- environment with all the fields from `cmark-lua`.
-- Returns the filter function on success, or `nil, msg` on failure.
function lcmark.load_filter(filename)
local result, msg = loadfile_with_env(filename)
if result then
local evaluated = result()
if type(evaluated) == 'function' then
return evaluated
else
return nil, ("Filter " .. filename .. " returns a " ..
type(evaluated) .. ", not a function")
end
else
return nil, msg
end
end
-- Render a metadata node in the target format.
local render_metadata = function(node, writer, options, columns)
local firstblock = cmark.node_first_child(node)
if cmark.node_get_type(firstblock) == cmark.NODE_PARAGRAPH and
not cmark.node_next(firstblock) then
-- render as inlines
local ils = cmark.node_new(cmark.NODE_CUSTOM_INLINE)
local b = cmark.node_first_child(firstblock)
while b do
local nextb = cmark.node_next(b)
cmark.node_append_child(ils, b)
b = nextb
end
local result = string.gsub(writer(ils, options, columns), "%s*$", "")
cmark.node_free(ils)
return result
else -- render as blocks
return writer(node, options, columns)
end
end
-- Iterate over the metadata, converting to cmark nodes.
-- Returns a new table.
local convert_metadata = function(table, options)
return walk_table(table,
function(s)
if type(s) == "string" then
return cmark.parse_string(s, options)
elseif type(s) == "userdata" then
return tostring(s)
else
return s
end
end, false)
end
local yaml_begin_line = P"---" * sp * nl
local yaml_end_line = (P"---" + P"...") * sp * nl
local yaml_content_line = -yaml_end_line * P(1 - S"\r\n")^0 * nl
local yaml_block = yaml_begin_line * (yaml_content_line^1 + sp) * yaml_end_line
-- Parses document with optional front YAML metadata; returns document,
-- metadata.
local parse_document_with_metadata = function(inp, parser, options)
local metadata = {}
local meta_end = lpeg.match(yaml_block, inp)
if meta_end then
if meta_end then
local ok, yaml_meta, err = pcall(parser, string.sub(inp, 1, meta_end))
if not ok then
return nil, yaml_meta -- the error message
elseif not yaml_meta then -- parser may return nil, err instead of error
return nil, tostring(err)
end
if type(yaml_meta) == 'table' then
metadata = convert_metadata(yaml_meta, options)
if type(metadata) ~= 'table' then
metadata = {}
end
-- We insert blank lines where the header was, so sourcepos is accurate:
inp = string.gsub(string.sub(inp, 1, meta_end), '[^\n\r]+', '') ..
string.sub(inp, meta_end)
end
end
end
local doc = cmark.parse_string(inp, options)
return doc, metadata
end
-- Apply a compiled template to a context (a dictionary-like
-- table).
function lcmark.apply_template(m, ctx)
if type(m) == 'function' then
return m(ctx)
elseif type(m) == 'table' then
local buffer = {}
for i,v in ipairs(m) do
buffer[i] = lcmark.apply_template(v, ctx)
end
return table.concat(buffer)
else
return tostring(m)
end
end
local get_value = function(var, ctx)
local result = ctx
assert(type(var) == 'table')
for _,varpart in ipairs(var) do
if type(result) ~= 'table' then
return nil
end
result = result[varpart]
if result == nil then
return nil
end
end
return result
end
local set_value = function(var, newval, ctx)
local result = ctx
assert(type(var) == 'table')
for i,varpart in ipairs(var) do
if i == #var then
-- last one
result[varpart] = newval
else
result = result[varpart]
if result == nil then
return nil
end
end
end
return true
end
local is_truthy = function(val)
local is_empty_tbl = type(val) == "table" and #val == 0
return val and not is_empty_tbl
end
-- if s starts with newline, remove initial and final newline
local trim = function(s)
if s:match("^[\r\n]") then
return s:gsub("^[\r]?[\n]?", ""):gsub("[\r]?[\n]?$", "")
else
return s
end
end
local conditional = function(var, ifpart, elsepart)
return function(ctx)
local result
if is_truthy(get_value(var, ctx)) then
result = lcmark.apply_template(ifpart, ctx)
elseif elsepart then
result = lcmark.apply_template(elsepart, ctx)
else
result = ""
end
return trim(result)
end
end
local forloop = function(var, inner, sep)
return function(ctx)
local val = get_value(var, ctx)
local vs
if not is_truthy(val) then
return ""
end
if type(val) == 'table' then
vs = val
else
-- if not a table, just iterate once
vs = {val}
end
local buffer = {}
for i,v in ipairs(vs) do
set_value(var, v, ctx) -- set temporary context
buffer[#buffer + 1] = lcmark.apply_template(inner, ctx)
if sep and i < #vs then
buffer[#buffer + 1] = lcmark.apply_template(sep, ctx)
end
set_value(var, val, ctx) -- restore original context
end
local result = lcmark.apply_template(buffer, ctx)
return trim(result)
end
end
-- Template syntax.
local TemplateGrammar = Ct{"Main",
Main = V"Template" * (-1 + lpeg.Cp()),
Template = Ct((V"Text" +
V"EscapedDollar" +
V"ConditionalNl" +
V"Conditional" +
V"ForLoopNl" +
V"ForLoop" +
V"Var")^0),
EscapedDollar = P"$$" / "$",
-- the Nl forms eat an extra newline after the end, if the
-- opening if() or for() ends with a newline. This is to avoid
-- excess blank space when a document contains many ifs or fors
-- that evaluate to false.
ConditionalNl = P"$if(" * Ct(V"Variable") * P")$" * nl * Ct(V"Template") *
(P"$else$" * Ct(V"Template"))^-1 * P"$endif$" * nl / conditional,
ForLoopNl = P"$for(" * Ct(V"Variable") * P")$" * nl * Ct(V"Template") *
(P"$sep$" * Ct(V"Template"))^-1 * P"$endfor$" * nl / forloop,
Conditional = P"$if(" * Ct(V"Variable") * P")$" * Ct(V"Template") *
(P"$else$" * Ct(V"Template"))^-1 * P"$endif$" / conditional,
ForLoop = P"$for(" * Ct(V"Variable") * P")$" * Ct(V"Template") *
(P"$sep$" * Ct(V"Template"))^-1 * P"$endfor$" / forloop,
Text = C((1 - P"$")^1),
Reserved = P"if$" + P"endif$" + P"else$" + P"for$" + P"endfor$" + P"sep$",
VarPart = (R"az" + R"AZ" + R"09" + S"_-")^1,
Variable = C(V"VarPart") * (P"." * C(V"VarPart"))^0,
Var = P"$" * - V"Reserved" * Ct(V"Variable") * P"$" /
function(var)
return function(ctx)
local val = get_value(var, ctx)
if is_truthy(val) then
return tostring(val)
else
return ""
end
end
end,
}
-- Compiles a template string into an arbitrary template object
-- which can then be passed to `lcmark.apply_template()`.
-- Returns the template object on success, or `nil, msg` on failure.
lcmark.compile_template = function(tpl)
local matches = lpeg.match(TemplateGrammar, tpl, nil)
if matches[2] == nil then
if matches[1] == nil then
return nil, "parse failed at the end of the template"
else
return matches[1]
end
else
local line_num = 1
local parse_failure_pos = matches[2]
tpl:sub(1,parse_failure_pos):gsub('[^\n]*[\n]',
function() line_num = line_num + 1 end)
return nil, ("parse failure at line " .. line_num ..
": '" .. string.sub(tpl, parse_failure_pos,
parse_failure_pos + 40) .. "'")
end
end
-- Compiles and applies a template string to a context table.
-- Returns the resulting document string on success, or
-- `nil, msg` on failure.
function lcmark.render_template(tpl, ctx)
local compiled_template, msg = lcmark.compile_template(tpl)
if not compiled_template then
return nil, msg
end
return lcmark.apply_template(compiled_template, ctx)
end
-- Converts `inp` (a CommonMark formatted string) to the output
-- format specified by `to` (a string; one of `html`, `commonmark`,
-- `latex`, `man`, or `xml`). `options` is a table with the
-- following fields (all optional):
-- * `smart` - enable "smart punctuation"
-- * `hardbreaks` - treat newlines as hard breaks
-- * `safe` - filter out potentially unsafe HTML and links
-- * `sourcepos` - include source position in HTML and XML output
-- * `filters` - an array of filters to run (see `load_filter` above)
-- * `columns` - column width, or 0 to preserve wrapping in input
-- * `yaml_metadata` - whether to parse initial YAML metadata block
-- * `yaml_parser` - a function to parse YAML with (see
-- [YAML Metadata](#yaml-metadata))
-- Returns `body`, `meta` on success, where `body` is the rendered
-- document body and `meta` is the YAML metadata as a table. If the
-- `yaml_metadata` option is false or if the document contains no
-- YAML metadata, `meta` will be an empty table. In case of an
-- error, the function returns `nil, nil, msg`.
function lcmark.convert(inp, to, options)
local writer = lcmark.writers[to]
if not writer then
return nil, nil, ("Unknown output format " .. tostring(to))
end
local opts, columns, filters, yaml_metadata, yaml_parser
if options then
opts = toOptions(options)
columns = options.columns or 0
filters = options.filters or {}
yaml_metadata = options.yaml_metadata
yaml_parser = options.yaml_parser or default_yaml_parser
else
opts = cmark.OPT_DEFAULT
columns = 0
filters = {}
yaml_metadata = false
yaml_parser = default_yaml_parser
end
if not yaml_parser then
error("no YAML libraries were found and no yaml_parser was specified")
end
local doc, meta
if yaml_metadata then
doc, meta = parse_document_with_metadata(inp, yaml_parser, opts)
if not doc then
return nil, nil, ("YAML parsing error: " .. meta)
end
else
doc = cmark.parse_string(inp, opts)
meta = {}
end
if not doc then
return nil, nil, "Unable to parse document"
end
for _, f in ipairs(filters) do
-- do we want filters to apply automatically to metadata?
-- better to let users do this manually when they want to.
-- walk_table(meta, function(node) f(node, meta, to) end, true)
local ok, msg = pcall(function() f(doc, meta, to) end)
if not ok then
return nil, nil, ("Error running filter:\n" .. msg)
end
end
local body = writer(doc, opts, columns)
local data = walk_table(meta,
function(node)
if type(node) == "userdata" then
return render_metadata(node, writer, opts, columns)
else
return node
end
end, false)
-- free memory allocated by libcmark
cmark.node_free(doc)
walk_table(meta,
function(node)
if type(node) == "userdata" then
cmark.node_free(node)
end
end, true)
return body, data
end
return lcmark