Skip to content

Commit

Permalink
Merge pull request #11526 from tarleb/general-lua-improvements
Browse files Browse the repository at this point in the history
General Lua improvements
  • Loading branch information
tarleb authored Dec 9, 2024
2 parents f1de7a2 + a347bc1 commit b4ce69d
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 70 deletions.
8 changes: 8 additions & 0 deletions news/changelog-1.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
8 changes: 1 addition & 7 deletions src/resources/filters/ast/customnodes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions src/resources/filters/common/error.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
8 changes: 6 additions & 2 deletions src/resources/filters/common/log.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
-- could write to named filed (e.g. <docname>.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
Expand All @@ -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
-- luacov: enable
23 changes: 11 additions & 12 deletions src/resources/filters/common/pandoc.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
137 changes: 90 additions & 47 deletions src/resources/pandoc/datadir/_utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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, ...)
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/resources/pandoc/datadir/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit b4ce69d

Please sign in to comment.