Skip to content

Commit b4ce69d

Browse files
authored
Merge pull request #11526 from tarleb/general-lua-improvements
General Lua improvements
2 parents f1de7a2 + a347bc1 commit b4ce69d

File tree

7 files changed

+126
-70
lines changed

7 files changed

+126
-70
lines changed

news/changelog-1.7.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,11 @@ All changes included in 1.7:
99
## `quarto check`
1010

1111
- ([#11608](https://github.com/quarto-dev/quarto-cli/pull/11608)): Do not issue error message when calling `quarto check info`.
12+
13+
## Lua Filters and extensions
14+
15+
- ([#11526](https://github.com/quarto-dev/quarto-cli/pull/11526)):
16+
General improvements to the style and robustness of Quarto's Lua code.
17+
This also provides a new public function `quarto.utils.is_empty_node`
18+
that allows to check whether a node is empty, i.e., whether it's an
19+
empty list, has no child nodes, and contains no text.

src/resources/filters/ast/customnodes.lua

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -337,13 +337,7 @@ _quarto.ast = {
337337
end
338338
local node = node_accessor(table)
339339
local t = pandoc.utils.type(value)
340-
-- FIXME this is broken; that can only be "Block", "Inline", etc
341-
if t == "Div" or t == "Span" then
342-
local custom_data, t, kind = _quarto.ast.resolve_custom_data(value)
343-
if custom_data ~= nil then
344-
value = custom_data
345-
end
346-
end
340+
quarto_assert(t ~= 'Div' and t ~= 'Span', "")
347341
if index > #node.content then
348342
_quarto.ast.grow_scaffold(node, index)
349343
end

src/resources/filters/common/error.lua

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@ function fail(message, level)
1515
end
1616
end
1717

18-
function internal_error()
19-
fail("This is an internal error. Please file a bug report at https://github.com/quarto-dev/quarto-cli/", 5)
18+
function internal_error(msg, level)
19+
fail((msg and (msg .. '\n') or '') ..
20+
"This is an internal error. Please file a bug report at https://github.com/quarto-dev/quarto-cli/", level or 5)
21+
end
22+
23+
function quarto_assert (test, msg, level)
24+
if not test then
25+
internal_error(msg, level or 6)
26+
end
2027
end
2128

2229
function currentFile()

src/resources/filters/common/log.lua

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
-- could write to named filed (e.g. <docname>.filter.log) and client could read warnings and delete (also delete before run)
66
-- always append b/c multiple filters
77

8+
--- The default, built-in error function.
9+
-- The `error` global is redefined below.
10+
local builtin_error_function = error
11+
812
-- luacov: disable
913
local function caller_info(offset)
1014
offset = offset or 3
@@ -27,6 +31,6 @@ end
2731
function fatal(message, offset)
2832
io.stderr:write(lunacolors.red("FATAL (" .. caller_info(offset) .. ") " ..message .. "\n"))
2933
-- TODO write stack trace into log, and then exit.
30-
crash_with_stack_trace()
34+
builtin_error_function('FATAL QUARTO ERROR', offset)
3135
end
32-
-- luacov: enable
36+
-- luacov: enable

src/resources/filters/common/pandoc.lua

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,14 @@ function inlinesToString(inlines)
8484
return pandoc.utils.stringify(pandoc.Span(inlines))
8585
end
8686

87+
local InlinesMT = getmetatable(pandoc.Inlines{})
88+
8789
-- lua string to pandoc inlines
8890
function stringToInlines(str)
8991
if str then
90-
return pandoc.Inlines({pandoc.Str(str)})
92+
return setmetatable({pandoc.Str(str)}, InlinesMT)
9193
else
92-
return pandoc.Inlines({})
94+
return setmetatable({}, InlinesMT)
9395
end
9496
end
9597

@@ -98,27 +100,24 @@ end
98100
function markdownToInlines(str)
99101
if str then
100102
local doc = pandoc.read(str)
101-
if #doc.blocks == 0 then
102-
return pandoc.List({})
103-
else
104-
return doc.blocks[1].content
105-
end
103+
return pandoc.utils.blocks_to_inlines(doc.blocks)
106104
else
107-
return pandoc.List()
105+
return setmetatable({}, InlinesMT)
108106
end
109107
end
110108

109+
111110
function stripTrailingSpace(inlines)
112-
-- we always convert to pandoc.List to ensure a uniform
111+
-- we always convert to pandoc.Inlines to ensure a uniform
113112
-- return type (and its associated methods)
114113
if #inlines > 0 then
115114
if inlines[#inlines].t == "Space" then
116-
return pandoc.List(tslice(inlines, 1, #inlines - 1))
115+
return setmetatable(tslice(inlines, 1, #inlines - 1), InlinesMT)
117116
else
118-
return pandoc.List(inlines)
117+
return setmetatable(inlines, InlinesMT)
119118
end
120119
else
121-
return pandoc.List(inlines)
120+
return setmetatable(inlines, InlinesMT)
122121
end
123122
end
124123

src/resources/pandoc/datadir/_utils.lua

Lines changed: 90 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -265,59 +265,75 @@ local function get_type(v)
265265
return pandoc_type
266266
end
267267

268-
local function as_inlines(v)
269-
if v == nil then
270-
return pandoc.Inlines({})
271-
end
272-
local t = pandoc.utils.type(v)
273-
if t == "Inlines" then
274-
---@cast v pandoc.Inlines
275-
return v
276-
elseif t == "Blocks" then
277-
return pandoc.utils.blocks_to_inlines(v)
278-
elseif t == "Inline" then
279-
return pandoc.Inlines({v})
280-
elseif t == "Block" then
281-
return pandoc.utils.blocks_to_inlines({v})
282-
end
268+
--- Blocks metatable
269+
local BlocksMT = getmetatable(pandoc.Blocks{})
270+
--- Inlines metatable
271+
local InlinesMT = getmetatable(pandoc.Inlines{})
283272

284-
if type(v) == "table" then
285-
local result = pandoc.Inlines({})
286-
for i, v in ipairs(v) do
287-
tappend(result, as_inlines(v))
273+
--- Turns the given object into a `Inlines` list.
274+
--
275+
-- Works mostly like `pandoc.Inlines`, but doesn't a do a full
276+
-- unmarshal/marshal roundtrip. This buys performance, at the cost of
277+
-- less thorough type checks.
278+
--
279+
-- NOTE: The input object might be modified *destructively*!
280+
local function as_inlines(obj)
281+
local pt = pandoc.utils.type(obj)
282+
if pt == 'Inlines' then
283+
return obj
284+
elseif pt == "Inline" then
285+
-- Faster than calling pandoc.Inlines
286+
return setmetatable({obj}, InlinesMT)
287+
elseif pt == 'List' or pt == 'table' then
288+
if obj[1] and pandoc.utils.type(obj[1]) == 'Block' then
289+
return pandoc.utils.blocks_to_inlines(obj)
288290
end
289-
return result
291+
-- Faster than calling pandoc.Inlines
292+
return setmetatable(obj, InlinesMT)
293+
elseif pt == "Block" then
294+
return pandoc.utils.blocks_to_inlines({obj})
295+
elseif pt == "Blocks" then
296+
return pandoc.utils.blocks_to_inlines(obj)
297+
else
298+
return pandoc.Inlines(obj or {})
290299
end
291-
292-
-- luacov: disable
293-
fatal("as_inlines: invalid type " .. t)
294-
return pandoc.Inlines({})
295-
-- luacov: enable
296300
end
297301

298-
local function as_blocks(v)
299-
if v == nil then
300-
return pandoc.Blocks({})
301-
end
302-
local t = pandoc.utils.type(v)
303-
if t == "Blocks" then
304-
return v
305-
elseif t == "Inlines" then
306-
return pandoc.Blocks({pandoc.Plain(v)})
307-
elseif t == "Block" then
308-
return pandoc.Blocks({v})
309-
elseif t == "Inline" then
310-
return pandoc.Blocks({pandoc.Plain(v)})
311-
end
312-
313-
if type(v) == "table" then
314-
return pandoc.Blocks(v)
302+
--- Turns the given object into a `Blocks` list.
303+
--
304+
-- Works mostly like `pandoc.Blocks`, but doesn't a do a full
305+
-- unmarshal/marshal roundtrip. This buys performance, at the cost of
306+
-- less thorough type checks.
307+
--
308+
-- NOTE: The input object might be modified *destructively*!
309+
--
310+
-- This might need some benchmarking.
311+
local function as_blocks(obj)
312+
local pt = pandoc.utils.type(obj)
313+
if pt == 'Blocks' then
314+
return obj
315+
elseif pt == 'Block' then
316+
-- Assigning a metatable directly is faster than calling
317+
-- `pandoc.Blocks`.
318+
return setmetatable({obj}, BlocksMT)
319+
elseif pt == 'Inline' then
320+
return setmetatable({pandoc.Plain{obj}}, BlocksMT)
321+
elseif pt == 'Inlines' then
322+
if next(obj) then
323+
return setmetatable({pandoc.Plain(obj)}, BlocksMT)
324+
end
325+
return setmetatable({}, BlocksMT)
326+
elseif pt == 'List' or (pt == 'table' and obj[1]) then
327+
if pandoc.utils.type(obj[1]) == 'Inline' then
328+
obj = {pandoc.Plain(obj)}
329+
end
330+
return setmetatable(obj, BlocksMT)
331+
elseif (pt == 'table' and obj.long) or pt == 'Caption' then
332+
-- Looks like a Caption
333+
return as_blocks(obj.long)
334+
else
335+
return pandoc.Blocks(obj or {})
315336
end
316-
317-
-- luacov: disable
318-
fatal("as_blocks: invalid type " .. t)
319-
return pandoc.Blocks({})
320-
-- luacov: enable
321337
end
322338

323339
local function match_fun(reset, ...)
@@ -557,6 +573,32 @@ local function match(...)
557573
return match_fun(reset, table.unpack(result))
558574
end
559575

576+
--- Returns `true` iff the given AST node is empty.
577+
-- A node is considered "empty" if it's an empty list, table, or a node
578+
-- without any text or nested AST nodes.
579+
local function is_empty_node (node)
580+
if not node then
581+
return true
582+
elseif type(node) == 'table' then
583+
-- tables are considered empty if they don't have any fields.
584+
return not next(node)
585+
elseif node.content then
586+
return not next(node.content)
587+
elseif node.caption then
588+
-- looks like an image, figure, or table
589+
if node.caption.long then
590+
return not next(node.caption.long)
591+
end
592+
return not next(node.caption)
593+
elseif node.text then
594+
-- looks like a code node or text node
595+
return node.text ~= ''
596+
else
597+
-- Not sure what this is, but it's probably not empty.
598+
return false
599+
end
600+
end
601+
560602
return {
561603
dump = dump,
562604
type = get_type,
@@ -567,6 +609,7 @@ return {
567609
},
568610
as_inlines = as_inlines,
569611
as_blocks = as_blocks,
612+
is_empty_node = is_empty_node,
570613
match = match,
571614
add_to_blocks = function(blocks, block)
572615
if pandoc.utils.type(blocks) ~= "Blocks" then

src/resources/pandoc/datadir/init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2085,6 +2085,7 @@ quarto = {
20852085
resolve_path_relative_to_document = resolvePath,
20862086
as_inlines = utils.as_inlines,
20872087
as_blocks = utils.as_blocks,
2088+
is_empty_node = utils.is_empty_node,
20882089
string_to_blocks = utils.string_to_blocks,
20892090
string_to_inlines = utils.string_to_inlines,
20902091
render = utils.render,

0 commit comments

Comments
 (0)