diff --git a/news/changelog-1.7.md b/news/changelog-1.7.md index 64c7451427..2d11fa630c 100644 --- a/news/changelog-1.7.md +++ b/news/changelog-1.7.md @@ -9,3 +9,11 @@ All changes included in 1.7: ## `quarto check` - ([#11608](https://github.com/quarto-dev/quarto-cli/pull/11608)): Do not issue error message when calling `quarto check info`. + +## Lua Filters and extensions + +- ([#11526](https://github.com/quarto-dev/quarto-cli/pull/11526)): + General improvements to the style and robustness of Quarto's Lua code. + This also provides a new public function `quarto.utils.is_empty_node` + that allows to check whether a node is empty, i.e., whether it's an + empty list, has no child nodes, and contains no text. diff --git a/src/resources/filters/ast/customnodes.lua b/src/resources/filters/ast/customnodes.lua index a36bf6a3a0..ca61ebc39b 100644 --- a/src/resources/filters/ast/customnodes.lua +++ b/src/resources/filters/ast/customnodes.lua @@ -337,13 +337,7 @@ _quarto.ast = { end local node = node_accessor(table) local t = pandoc.utils.type(value) - -- FIXME this is broken; that can only be "Block", "Inline", etc - if t == "Div" or t == "Span" then - local custom_data, t, kind = _quarto.ast.resolve_custom_data(value) - if custom_data ~= nil then - value = custom_data - end - end + quarto_assert(t ~= 'Div' and t ~= 'Span', "") if index > #node.content then _quarto.ast.grow_scaffold(node, index) end diff --git a/src/resources/filters/common/error.lua b/src/resources/filters/common/error.lua index 930af18b78..e27f9c0893 100644 --- a/src/resources/filters/common/error.lua +++ b/src/resources/filters/common/error.lua @@ -15,8 +15,15 @@ function fail(message, level) end end -function internal_error() - fail("This is an internal error. Please file a bug report at https://github.com/quarto-dev/quarto-cli/", 5) +function internal_error(msg, level) + fail((msg and (msg .. '\n') or '') .. + "This is an internal error. Please file a bug report at https://github.com/quarto-dev/quarto-cli/", level or 5) +end + +function quarto_assert (test, msg, level) + if not test then + internal_error(msg, level or 6) + end end function currentFile() diff --git a/src/resources/filters/common/log.lua b/src/resources/filters/common/log.lua index 584358be1f..67a736bbe9 100644 --- a/src/resources/filters/common/log.lua +++ b/src/resources/filters/common/log.lua @@ -5,6 +5,10 @@ -- could write to named filed (e.g. .filter.log) and client could read warnings and delete (also delete before run) -- always append b/c multiple filters +--- The default, built-in error function. +-- The `error` global is redefined below. +local builtin_error_function = error + -- luacov: disable local function caller_info(offset) offset = offset or 3 @@ -27,6 +31,6 @@ end function fatal(message, offset) io.stderr:write(lunacolors.red("FATAL (" .. caller_info(offset) .. ") " ..message .. "\n")) -- TODO write stack trace into log, and then exit. - crash_with_stack_trace() + builtin_error_function('FATAL QUARTO ERROR', offset) end --- luacov: enable \ No newline at end of file +-- luacov: enable diff --git a/src/resources/filters/common/pandoc.lua b/src/resources/filters/common/pandoc.lua index d4b69ae859..e52a0de9cd 100644 --- a/src/resources/filters/common/pandoc.lua +++ b/src/resources/filters/common/pandoc.lua @@ -84,12 +84,14 @@ function inlinesToString(inlines) return pandoc.utils.stringify(pandoc.Span(inlines)) end +local InlinesMT = getmetatable(pandoc.Inlines{}) + -- lua string to pandoc inlines function stringToInlines(str) if str then - return pandoc.Inlines({pandoc.Str(str)}) + return setmetatable({pandoc.Str(str)}, InlinesMT) else - return pandoc.Inlines({}) + return setmetatable({}, InlinesMT) end end @@ -98,27 +100,24 @@ end function markdownToInlines(str) if str then local doc = pandoc.read(str) - if #doc.blocks == 0 then - return pandoc.List({}) - else - return doc.blocks[1].content - end + return pandoc.utils.blocks_to_inlines(doc.blocks) else - return pandoc.List() + return setmetatable({}, InlinesMT) end end + function stripTrailingSpace(inlines) - -- we always convert to pandoc.List to ensure a uniform + -- we always convert to pandoc.Inlines to ensure a uniform -- return type (and its associated methods) if #inlines > 0 then if inlines[#inlines].t == "Space" then - return pandoc.List(tslice(inlines, 1, #inlines - 1)) + return setmetatable(tslice(inlines, 1, #inlines - 1), InlinesMT) else - return pandoc.List(inlines) + return setmetatable(inlines, InlinesMT) end else - return pandoc.List(inlines) + return setmetatable(inlines, InlinesMT) end end diff --git a/src/resources/pandoc/datadir/_utils.lua b/src/resources/pandoc/datadir/_utils.lua index e2d986646e..2c9bd6d17f 100644 --- a/src/resources/pandoc/datadir/_utils.lua +++ b/src/resources/pandoc/datadir/_utils.lua @@ -265,59 +265,75 @@ local function get_type(v) return pandoc_type end -local function as_inlines(v) - if v == nil then - return pandoc.Inlines({}) - end - local t = pandoc.utils.type(v) - if t == "Inlines" then - ---@cast v pandoc.Inlines - return v - elseif t == "Blocks" then - return pandoc.utils.blocks_to_inlines(v) - elseif t == "Inline" then - return pandoc.Inlines({v}) - elseif t == "Block" then - return pandoc.utils.blocks_to_inlines({v}) - end +--- Blocks metatable +local BlocksMT = getmetatable(pandoc.Blocks{}) +--- Inlines metatable +local InlinesMT = getmetatable(pandoc.Inlines{}) - if type(v) == "table" then - local result = pandoc.Inlines({}) - for i, v in ipairs(v) do - tappend(result, as_inlines(v)) +--- Turns the given object into a `Inlines` list. +-- +-- Works mostly like `pandoc.Inlines`, but doesn't a do a full +-- unmarshal/marshal roundtrip. This buys performance, at the cost of +-- less thorough type checks. +-- +-- NOTE: The input object might be modified *destructively*! +local function as_inlines(obj) + local pt = pandoc.utils.type(obj) + if pt == 'Inlines' then + return obj + elseif pt == "Inline" then + -- Faster than calling pandoc.Inlines + return setmetatable({obj}, InlinesMT) + elseif pt == 'List' or pt == 'table' then + if obj[1] and pandoc.utils.type(obj[1]) == 'Block' then + return pandoc.utils.blocks_to_inlines(obj) end - return result + -- Faster than calling pandoc.Inlines + return setmetatable(obj, InlinesMT) + elseif pt == "Block" then + return pandoc.utils.blocks_to_inlines({obj}) + elseif pt == "Blocks" then + return pandoc.utils.blocks_to_inlines(obj) + else + return pandoc.Inlines(obj or {}) end - - -- luacov: disable - fatal("as_inlines: invalid type " .. t) - return pandoc.Inlines({}) - -- luacov: enable end -local function as_blocks(v) - if v == nil then - return pandoc.Blocks({}) - end - local t = pandoc.utils.type(v) - if t == "Blocks" then - return v - elseif t == "Inlines" then - return pandoc.Blocks({pandoc.Plain(v)}) - elseif t == "Block" then - return pandoc.Blocks({v}) - elseif t == "Inline" then - return pandoc.Blocks({pandoc.Plain(v)}) - end - - if type(v) == "table" then - return pandoc.Blocks(v) +--- Turns the given object into a `Blocks` list. +-- +-- Works mostly like `pandoc.Blocks`, but doesn't a do a full +-- unmarshal/marshal roundtrip. This buys performance, at the cost of +-- less thorough type checks. +-- +-- NOTE: The input object might be modified *destructively*! +-- +-- This might need some benchmarking. +local function as_blocks(obj) + local pt = pandoc.utils.type(obj) + if pt == 'Blocks' then + return obj + elseif pt == 'Block' then + -- Assigning a metatable directly is faster than calling + -- `pandoc.Blocks`. + return setmetatable({obj}, BlocksMT) + elseif pt == 'Inline' then + return setmetatable({pandoc.Plain{obj}}, BlocksMT) + elseif pt == 'Inlines' then + if next(obj) then + return setmetatable({pandoc.Plain(obj)}, BlocksMT) + end + return setmetatable({}, BlocksMT) + elseif pt == 'List' or (pt == 'table' and obj[1]) then + if pandoc.utils.type(obj[1]) == 'Inline' then + obj = {pandoc.Plain(obj)} + end + return setmetatable(obj, BlocksMT) + elseif (pt == 'table' and obj.long) or pt == 'Caption' then + -- Looks like a Caption + return as_blocks(obj.long) + else + return pandoc.Blocks(obj or {}) end - - -- luacov: disable - fatal("as_blocks: invalid type " .. t) - return pandoc.Blocks({}) - -- luacov: enable end local function match_fun(reset, ...) @@ -557,6 +573,32 @@ local function match(...) return match_fun(reset, table.unpack(result)) end +--- Returns `true` iff the given AST node is empty. +-- A node is considered "empty" if it's an empty list, table, or a node +-- without any text or nested AST nodes. +local function is_empty_node (node) + if not node then + return true + elseif type(node) == 'table' then + -- tables are considered empty if they don't have any fields. + return not next(node) + elseif node.content then + return not next(node.content) + elseif node.caption then + -- looks like an image, figure, or table + if node.caption.long then + return not next(node.caption.long) + end + return not next(node.caption) + elseif node.text then + -- looks like a code node or text node + return node.text ~= '' + else + -- Not sure what this is, but it's probably not empty. + return false + end +end + return { dump = dump, type = get_type, @@ -567,6 +609,7 @@ return { }, as_inlines = as_inlines, as_blocks = as_blocks, + is_empty_node = is_empty_node, match = match, add_to_blocks = function(blocks, block) if pandoc.utils.type(blocks) ~= "Blocks" then diff --git a/src/resources/pandoc/datadir/init.lua b/src/resources/pandoc/datadir/init.lua index 0cde1ccced..1dc45ed14b 100644 --- a/src/resources/pandoc/datadir/init.lua +++ b/src/resources/pandoc/datadir/init.lua @@ -2085,6 +2085,7 @@ quarto = { resolve_path_relative_to_document = resolvePath, as_inlines = utils.as_inlines, as_blocks = utils.as_blocks, + is_empty_node = utils.is_empty_node, string_to_blocks = utils.string_to_blocks, string_to_inlines = utils.string_to_inlines, render = utils.render,