Skip to content

perf: Use non-lazy AST traversal for filters when possible #11052

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fa6750b
Use the built-in `error` function to trigger a fatal error
tarleb Nov 25, 2024
52b83d9
Provide global function `quarto_assert`
tarleb Nov 25, 2024
17b1dd1
Add Lua utils function `quarto.utils.is_empty_node`
tarleb Nov 25, 2024
c7e4597
Use `Inlines` metatable for "list of Inline" return values
tarleb Nov 25, 2024
4274ec0
Rewrite `as_inlines`, `as_blocks` with focus on performance
tarleb Nov 22, 2024
5ade0e2
Rewrite `as_inlines`, `as_blocks` with focus on performance
tarleb Nov 22, 2024
2967ace
Prevent unintended side-effects of filter functions
tarleb Nov 18, 2024
3ac2284
table-rawhtml: destructively modify the input list
tarleb Nov 19, 2024
00f20e0
lightbox: Restart imgCount each time the filter is invoked.
tarleb Nov 19, 2024
4f93ae2
Return modified meta object from init filter
tarleb Nov 19, 2024
e5111ca
Fix check for empty captions in parsefiguredivs filters
tarleb Nov 19, 2024
ed2747b
Use proper metadata types when resolving dependencies
tarleb Nov 19, 2024
5101add
Ensure that the internal Meta copy is a true, deep copy
tarleb Nov 20, 2024
b9b051c
Don't add number values to the metadata object
tarleb Nov 20, 2024
13636f9
Custom nodes: cleanup code for slots/forwarder
tarleb Nov 21, 2024
594b00a
Fix type of return value in typst filter function
tarleb Nov 21, 2024
adc49e9
Fix type in Caption
tarleb Nov 22, 2024
f624315
Ensure proper Callout rendering if title is set but empty
tarleb Nov 25, 2024
fa9af92
Rely less on pandoc's type implicit conversions
tarleb Oct 24, 2024
7d56b40
Ensure proper types when setting custom node slots
tarleb Nov 25, 2024
ae4056e
Use global function to check if caption if empty
tarleb Nov 26, 2024
6fe2889
Avoid type confusion with captions during crossref handling
tarleb Nov 26, 2024
d8cbd96
Ensure Blocks when normalizing draft posts
tarleb Nov 26, 2024
395b84a
Add `jog` Lua module
tarleb Sep 13, 2024
c62362b
Allow to use jog instead of walk
tarleb Sep 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 28 additions & 17 deletions src/resources/filters/ast/customnodes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function is_regular_node(node, name)
return node
end

function run_emulated_filter(doc, filter)
function run_emulated_filter(doc, filter, traverse)
if doc == nil then
return nil
end
Expand Down Expand Up @@ -73,7 +73,17 @@ function run_emulated_filter(doc, filter)
-- luacov: enable
end
end
return node:walk(filter_param)
local old_use_walk = _QUARTO_USE_WALK
if traverse == nil or traverse == 'walk' then
_QUARTO_USE_WALK = true
elseif traverse == 'jog' then
_QUARTO_USE_WALK = false
else
warn('Unknown traverse method: ' .. tostring(traverse))
end
local result = _quarto.modules.jog(node, filter_param)
_QUARTO_USE_WALK = old_use_walk
return result
end

-- performance: if filter is empty, do nothing
Expand Down Expand Up @@ -336,22 +346,21 @@ _quarto.ast = {
return
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
local valtype = pandoc.utils.type(value)
quarto_assert(valtype ~= 'Div' and valtype ~= 'Span', "")
if index > #node.content then
_quarto.ast.grow_scaffold(node, index)
end
local pt = pandoc.utils.type(value)
if pt == "Block" or pt == "Inline" then
node.content[index].content = {value}
local inner_node = node.content[index]
local innertype = pandoc.utils.type(inner_node)
if innertype == 'Block' then
inner_node.content = quarto.utils.as_blocks(value)
elseif innertype == 'Inline' then
inner_node.content = quarto.utils.as_inlines(value)
else
node.content[index].content = value
warn(debug.traceback(
'Cannot find the right content type for value ' .. valtype))
inner_node.content = value
end
end
}
Expand Down Expand Up @@ -422,13 +431,15 @@ _quarto.ast = {
-- luacov: enable
end

local forwarder = { }
local forwarder
if tisarray(handler.slots) then
forwarder = pandoc.List{}
for i, slot in ipairs(handler.slots) do
forwarder[slot] = i
end
else
forwarder = handler.slots
elseif handler.slots ~= nil then
warn('Expected `slots` to be either an array or nil, got ' ..
tostring(handler.slots))
end

quarto[handler.ast_name] = function(params)
Expand Down
3 changes: 3 additions & 0 deletions src/resources/filters/ast/emulatedfilter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ inject_user_filters_at_entry_points = function(filter_list)
end
local filter = {
name = entry_point .. "-user-" .. tostring(entry_point_counts[entry_point]),
-- The filter might not work as expected when doing a non-lazy jog, so
-- make sure it is processed with the default 'walk' function.
traverse = 'walk',
}
if is_many_filters then
filter.filters = wrapped
Expand Down
2 changes: 1 addition & 1 deletion src/resources/filters/ast/runemulation.lua
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ local function run_emulated_filter_chain(doc, filters, afterFilterPass, profilin
print(pandoc.write(doc, "native"))
else
_quarto.ast._current_doc = doc
doc = run_emulated_filter(doc, v.filter)
doc = run_emulated_filter(doc, v.filter, v.traverse)
ensure_vault(doc)

add_trace(doc, v.name)
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
2 changes: 1 addition & 1 deletion src/resources/filters/common/layout.lua
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ end
-- we often wrap a table in a div, unwrap it
function tableFromLayoutCell(cell)
local tbl
cell:walk({
_quarto.modules.jog(cell, {
Table = function(t)
tbl = t
end
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
29 changes: 14 additions & 15 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 Expand Up @@ -217,14 +216,14 @@ function string_to_quarto_ast_blocks(text, opts)

-- run the whole normalization pipeline here to get extended AST nodes, etc.
for _, filter in ipairs(quarto_ast_pipeline()) do
doc = doc:walk(filter.filter)
doc = _quarto.modules.jog(doc, filter.filter)
end

-- compute flags so we don't skip filters that depend on them
doc:walk(compute_flags())
_quarto.modules.jog(doc, compute_flags())
return doc.blocks
end

function string_to_quarto_ast_inlines(text, sep)
return pandoc.utils.blocks_to_inlines(string_to_quarto_ast_blocks(text), sep)
end
end
4 changes: 2 additions & 2 deletions src/resources/filters/common/wrapped-filter.lua
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function makeWrappedJsonFilter(scriptFile, filterHandler)
path = quarto.utils.resolve_path_relative_to_document(scriptFile)
local custom_node_map = {}
local has_custom_nodes = false
doc = doc:walk({
doc = _quarto.modules.jog(doc, {
-- FIXME: This is broken with new AST. Needs to go through Custom node instead.
RawInline = function(raw)
local custom_node, t, kind = _quarto.ast.resolve_custom_data(raw)
Expand Down Expand Up @@ -130,7 +130,7 @@ function makeWrappedJsonFilter(scriptFile, filterHandler)
return nil
end
if has_custom_nodes then
doc:walk({
_quarto.modules.jog(doc, {
Meta = function(meta)
_quarto.ast.reset_custom_tbl(meta["quarto-custom-nodes"])
end
Expand Down
2 changes: 1 addition & 1 deletion src/resources/filters/crossref/equations.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function process_equations(blockEl)
end

local mathInlines = nil
local targetInlines = pandoc.List()
local targetInlines = pandoc.Inlines{}

for i, el in ipairs(inlines) do

Expand Down
4 changes: 2 additions & 2 deletions src/resources/filters/crossref/index.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ end
-- add an entry to the index
function indexAddEntry(label, parent, order, caption, appendix)
if caption ~= nil then
caption = pandoc.List(caption)
caption = _quarto.utils.as_blocks(caption)
else
caption = pandoc.List({})
caption = pandoc.Blocks({})
end
crossref.index.entries[label] = {
parent = parent,
Expand Down
2 changes: 1 addition & 1 deletion src/resources/filters/crossref/preprocess.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ function crossref_mark_subfloats()
return {
traverse = "topdown",
FloatRefTarget = function(float)
float.content = _quarto.ast.walk(float.content, {
float.content = _quarto.ast.walk(float.content or pandoc.Blocks{}, {
FloatRefTarget = function(subfloat)
float.has_subfloats = true
crossref.subfloats[subfloat.identifier] = {
Expand Down
5 changes: 3 additions & 2 deletions src/resources/filters/customnodes/callout.lua
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,8 @@ function _callout_main()
return _quarto.format.typst.function_call("callout", {
{ "body", _quarto.format.typst.as_typst_content(callout.content) },
{ "title", _quarto.format.typst.as_typst_content(
callout.title or pandoc.Plain(_quarto.modules.callouts.displayName(callout.type))
(not _quarto.utils.is_empty_node(callout.title) and callout.title) or
pandoc.Plain(_quarto.modules.callouts.displayName(callout.type))
)},
{ "background_color", pandoc.RawInline("typst", background_color) },
{ "icon_color", pandoc.RawInline("typst", icon_color) },
Expand Down Expand Up @@ -406,4 +407,4 @@ function crossref_callouts()
return callout
end
}
end
end
3 changes: 2 additions & 1 deletion src/resources/filters/customnodes/content-hidden.lua
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ local _content_hidden_meta = nil
function content_hidden_meta(meta)
-- return {
-- Meta = function(meta)
_content_hidden_meta = meta
-- The call to `pandoc.Meta` ensures that we hold a copy.
_content_hidden_meta = pandoc.Meta(meta)
-- end
-- }
end
Expand Down
4 changes: 2 additions & 2 deletions src/resources/filters/customnodes/floatreftarget.lua
Original file line number Diff line number Diff line change
Expand Up @@ -767,7 +767,7 @@ function float_reftarget_render_html_figure(float)
local float_content = pandoc.Div(_quarto.ast.walk(float.content, {
-- strip image captions
Image = function(image)
image.caption = {}
image.caption = pandoc.Inlines{}
return image
end
}) or pandoc.Div({})) -- this should never happen but the lua analyzer doesn't know it
Expand Down Expand Up @@ -1098,4 +1098,4 @@ end, function(float)
return pandoc.Para({im})
end)

global_table_guid_id = 0
global_table_guid_id = 0
8 changes: 4 additions & 4 deletions src/resources/filters/customnodes/shortcodes.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ _quarto.ast.add_handler({
kind = "Inline",

parse = function(span)
local inner_content = pandoc.List({})
local inner_content = pandoc.Inlines({})

span.content = span.content:filter(function(el)
return el.t == "Span"
Expand Down Expand Up @@ -78,9 +78,9 @@ _quarto.ast.add_handler({
end

local node = _quarto.ast.create_custom_node_scaffold("Shortcode", "Inline")
node.content = inner_content:map(function(el)
return pandoc.Span({el})
end)
node.content = pandoc.Inlines(inner_content:map(function(el)
return pandoc.Span({el})
end))
local tbl = {
__quarto_custom_node = node,
name = name,
Expand Down
2 changes: 1 addition & 1 deletion src/resources/filters/layout/html.lua
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ function renderHtmlFigure(el, render)
end)

-- remove identifier (it is now on the div)
el.identifier = ""
el.attr.identifier = ""

if not figureDiv.classes:find_if(function(str) return str:match("quarto%-figure%-.+") end) then
-- apply standalone figure css if not already set
Expand Down
5 changes: 3 additions & 2 deletions src/resources/filters/layout/lightbox.lua
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,11 @@ function lightbox()
return {{
traverse = "topdown",

Meta = function(meta)
Meta = function(meta)
-- Set auto lightbox mode, if need be
auto = lightbox_module.automatic(meta) == true
end,
imgCount = 0
end,
-- Find images that are already within links
-- we'll use this to filter out these images if
-- the most is auto
Expand Down
2 changes: 1 addition & 1 deletion src/resources/filters/layout/typst.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function make_typst_figure(tbl)
local identifier = tbl.identifier
local separator = tbl.separator

if (not caption or #caption.content == 0) and tbl.separator == nil then
if _quarto.utils.is_empty_node(caption) and tbl.separator == nil then
separator = ""
end

Expand Down
3 changes: 2 additions & 1 deletion src/resources/filters/modules/import_all.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ _quarto.modules = {
dashboard = require("modules/dashboard"),
filenames = require("modules/filenames"),
filters = require("modules/filters"),
jog = require("modules/jog"),
license = require("modules/license"),
lightbox = require("modules/lightbox"),
mediabag = require("modules/mediabag"),
Expand All @@ -20,4 +21,4 @@ _quarto.modules = {
string = require("modules/string"),
tablecolwidths = require("modules/tablecolwidths"),
typst = require("modules/typst")
}
}
Loading
Loading