diff --git a/src/resources/filters/ast/customnodes.lua b/src/resources/filters/ast/customnodes.lua index ecda0ccb24a..081840a7274 100644 --- a/src/resources/filters/ast/customnodes.lua +++ b/src/resources/filters/ast/customnodes.lua @@ -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 @@ -73,7 +73,21 @@ function run_emulated_filter(doc, filter) -- luacov: enable end end - return node:walk(filter_param) + + local old_traverse = _quarto.traverser + if traverser == nil or traverser == 'pandoc' or traverser == 'walk' then + _quarto.traverser = _quarto.utils.walk + elseif traverser == 'jog' then + _quarto.traverser = _quarto.modules.jog + elseif type(traverser) == 'function' then + _quarto.traverser = traverser + else + warn('Unknown traverse method: ' .. tostring(traverse)) + end + local result = _quarto.traverser(node, filter_param) + _quarto.traverse = old_traverse + + return result end -- performance: if filter is empty, do nothing diff --git a/src/resources/filters/ast/emulatedfilter.lua b/src/resources/filters/ast/emulatedfilter.lua index 34d05278f82..ebfe023328f 100644 --- a/src/resources/filters/ast/emulatedfilter.lua +++ b/src/resources/filters/ast/emulatedfilter.lua @@ -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. + traverser = 'walk', } if is_many_filters then filter.filters = wrapped @@ -76,4 +79,4 @@ inject_user_filters_at_entry_points = function(filter_list) end table.insert(filter_list, index, filter) end -end \ No newline at end of file +end diff --git a/src/resources/filters/ast/runemulation.lua b/src/resources/filters/ast/runemulation.lua index c107051bf4c..f11622c95b4 100644 --- a/src/resources/filters/ast/runemulation.lua +++ b/src/resources/filters/ast/runemulation.lua @@ -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.traverser) ensure_vault(doc) add_trace(doc, v.name) @@ -204,4 +204,4 @@ function run_as_extended_ast(specTable) end return pandocFilterList -end \ No newline at end of file +end diff --git a/src/resources/filters/common/layout.lua b/src/resources/filters/common/layout.lua index 0f0d7672c55..c50e5561f04 100644 --- a/src/resources/filters/common/layout.lua +++ b/src/resources/filters/common/layout.lua @@ -56,7 +56,7 @@ end -- we often wrap a table in a div, unwrap it function tableFromLayoutCell(cell) local tbl - cell:walk({ + _quarto.traverser(cell, { Table = function(t) tbl = t end @@ -106,4 +106,4 @@ function asLatexSize(size, macro) else return size end -end \ No newline at end of file +end diff --git a/src/resources/filters/common/pandoc.lua b/src/resources/filters/common/pandoc.lua index e52a0de9cd1..de7dd68d1d4 100644 --- a/src/resources/filters/common/pandoc.lua +++ b/src/resources/filters/common/pandoc.lua @@ -216,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.traverser(doc, filter.filter) end -- compute flags so we don't skip filters that depend on them - doc:walk(compute_flags()) + _quarto.traverser(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 \ No newline at end of file +end diff --git a/src/resources/filters/common/wrapped-filter.lua b/src/resources/filters/common/wrapped-filter.lua index 7bb7e1f189d..4d4adee424f 100644 --- a/src/resources/filters/common/wrapped-filter.lua +++ b/src/resources/filters/common/wrapped-filter.lua @@ -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.traverser(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) @@ -130,7 +130,7 @@ function makeWrappedJsonFilter(scriptFile, filterHandler) return nil end if has_custom_nodes then - doc:walk({ + _quarto.traverser(doc, { Meta = function(meta) _quarto.ast.reset_custom_tbl(meta["quarto-custom-nodes"]) end @@ -250,4 +250,4 @@ function filterSeq(filters) return result end } -end \ No newline at end of file +end diff --git a/src/resources/filters/main.lua b/src/resources/filters/main.lua index 86acd898c46..6e0f6ece463 100644 --- a/src/resources/filters/main.lua +++ b/src/resources/filters/main.lua @@ -233,73 +233,113 @@ local quarto_init_filters = { local quarto_normalize_filters = { { name = "normalize-draft", - filter = normalize_draft() }, + filter = normalize_draft(), + traverser = 'jog', + }, - { name = "normalize", filter = filterIf(function() - if quarto_global_state.active_filters == nil then - return false - end - return quarto_global_state.active_filters.normalization - end, normalize_filter()) }, + { name = "normalize", + filter = filterIf( + function() + if quarto_global_state.active_filters == nil then + return false + end + return quarto_global_state.active_filters.normalization + end, + normalize_filter()), + traverser = 'jog', + }, - { name = "normalize-capture-reader-state", filter = normalize_capture_reader_state() } + { name = "normalize-capture-reader-state", + filter = normalize_capture_reader_state(), + traverser = 'jog', + } } tappend(quarto_normalize_filters, quarto_ast_pipeline()) local quarto_pre_filters = { -- quarto-pre - { name = "flags", filter = compute_flags() }, + { name = "flags", + filters = compute_flags(), + traverser = 'jog', + }, - { name = "pre-server-shiny", filter = server_shiny() }, + { name = "pre-server-shiny", + filter = server_shiny(), + traverser = 'jog', + }, -- https://github.com/quarto-dev/quarto-cli/issues/5031 -- recompute options object in case user filters have changed meta -- this will need to change in the future; users will have to indicate -- when they mutate options - { name = "pre-read-options-again", filter = init_options() }, + { name = "pre-read-options-again", + filter = init_options(), + traverser = 'jog', + }, + + { name = "pre-bibliography-formats", + filter = bibliography_formats(), + traverser = 'jog', + }, - { name = "pre-bibliography-formats", filter = bibliography_formats() }, - { name = "pre-shortcodes-filter", filter = shortcodes_filter(), - flags = { "has_shortcodes" } }, + flags = { "has_shortcodes" }, + traverser = 'jog', + }, { name = "pre-contents-shortcode-filter", filter = contents_shortcode_filter(), - flags = { "has_contents_shortcode" } }, + flags = { "has_contents_shortcode" }, + traverser = 'jog', + }, { name = "pre-combined-hidden", filter = combineFilters({ hidden(), content_hidden() }), - flags = { "has_hidden", "has_conditional_content" } }, + flags = { "has_hidden", "has_conditional_content" }, + traverser = 'jog', + }, - { name = "pre-table-captions", + { name = "pre-table-captions", filter = table_captions(), - flags = { "has_table_captions" } }, - - { name = "pre-code-annotations", + flags = { "has_table_captions" }, + traverser = 'jog', + }, + + { name = "pre-code-annotations", filter = code_annotations(), - flags = { "has_code_annotations" } }, - - { name = "pre-code-annotations-meta", filter = code_meta() }, + flags = { "has_code_annotations" }, + traverser = 'jog', + }, - { name = "pre-unroll-cell-outputs", + { name = "pre-code-annotations-meta", + filter = code_meta(), + traverser = 'jog', + }, + + { name = "pre-unroll-cell-outputs", filter = unroll_cell_outputs(), - flags = { "needs_output_unrolling" } }, + flags = { "needs_output_unrolling" }, + traverser = 'jog', + }, - { name = "pre-output-location", - filter = output_location() + { name = "pre-output-location", + filter = output_location(), + traverser = 'jog', }, { name = "pre-scope-resolution", filter = resolve_scoped_elements(), + traverser = 'jog', flags = { "has_tables" } }, - { name = "pre-combined-figures-theorems-etc", filter = combineFilters({ + { name = "pre-combined-figures-theorems-etc", + filter = combineFilters({ file_metadata(), index_book_file_targets(), book_numbering(), @@ -314,143 +354,296 @@ local quarto_pre_filters = { bootstrap_panel_layout(), bootstrap_panel_sidebar(), table_respecify_gt_css(), - -- table_colwidth(), + -- table_colwidth(), table_classes(), input_traits(), resolve_book_file_targets(), project_paths() - }) }, + }), + traverser = 'jog', + }, - { name = "pre-quarto-pre-meta-inject", filter = quarto_pre_meta_inject() }, - { name = "pre-write-results", filter = write_results() }, + { name = "pre-quarto-pre-meta-inject", + filter = quarto_pre_meta_inject(), + traverser = 'jog', + }, + { name = "pre-write-results", + filter = write_results(), + traverser = 'jog', + }, } local quarto_post_filters = { - { name = "post-cell-cleanup", + { name = "post-cell-cleanup", filter = cell_cleanup(), - flags = { "has_output_cells" } + flags = { "has_output_cells" }, + traverser = 'jog', }, - { name = "post-combined-cites-bibliography", - filter = combineFilters({ + { name = "post-combined-cites-bibliography", + filter = combineFilters{ indexCites(), bibliography() - }) + }, + traverser = 'jog', }, - { name = "post-landscape-div", + { name = "post-landscape-div", filter = landscape_div(), - flags = { "has_landscape" } - }, - { name = "post-ipynb", filters = ipynb()}, - { name = "post-figureCleanupCombined", filter = combineFilters({ - latexDiv(), - responsive(), - quartoBook(), - reveal(), - tikz(), - pdfImages(), - delink(), - figCleanup(), - responsive_table(), - }) }, - - { name = "post-postMetaInject", filter = quartoPostMetaInject() }, - - { name = "post-render-jats", filter = filterIf(function() - return quarto_global_state.active_filters.jats_subarticle == nil or not quarto_global_state.active_filters.jats_subarticle - end, jats()) }, - { name = "post-render-jats-subarticle", filter = filterIf(function() - return quarto_global_state.active_filters.jats_subarticle ~= nil and quarto_global_state.active_filters.jats_subarticle - end, jatsSubarticle()) }, - - { name = "post-code-options", filter = filterIf(function() - return param("clear-cell-options", false) == true - end, removeCodeOptions()) }, + flags = { "has_landscape" }, + traverser = 'jog', + }, + { name = "post-ipynb", + filters = ipynb(), + traverser = 'jog', + }, + { name = "post-figureCleanupCombined", + filter = combineFilters{ + latexDiv(), + responsive(), + quartoBook(), + reveal(), + tikz(), + pdfImages(), + delink(), + figCleanup(), + responsive_table(), + }, + traverser = 'jog', + }, + { name = "post-postMetaInject", + filter = quartoPostMetaInject(), + traverser = 'jog', + }, + { name = "post-render-jats", + filter = filterIf( + function() + return quarto_global_state.active_filters.jats_subarticle == nil or + not quarto_global_state.active_filters.jats_subarticle + end, + jats() + ), + traverser = 'jog', + }, + { name = "post-render-jats-subarticle", + filter = filterIf( + function() + return quarto_global_state.active_filters.jats_subarticle ~= nil and + quarto_global_state.active_filters.jats_subarticle + end, + jatsSubarticle() + ), + traverser = 'jog', + }, + { name = "post-code-options", + filter = filterIf( + function() return param("clear-cell-options", false) == true end, + removeCodeOptions() + ), + traverser = 'jog', + }, -- format-specific rendering - { name = "post-render-asciidoc", filter = render_asciidoc() }, - { name = "post-render-latex", filter = render_latex() }, - { name = "post-render-typst", filters = render_typst() }, - { name = "post-render-dashboard", filters = render_dashboard() }, + { name = "post-render-asciidoc", filter = render_asciidoc(), + traverser = 'jog', + }, + { name = "post-render-latex", filter = render_latex(), + traverser = 'jog', + }, + { name = "post-render-typst", filters = render_typst(), + traverser = 'jog', + }, + { name = "post-render-dashboard", filters = render_dashboard(), + traverser = 'jog', + }, - { name = "post-ojs", filter = ojs() }, + { name = "post-ojs", filter = ojs(), + traverser = 'jog', + }, - { name = "post-render-pandoc3-figure", filter = render_pandoc3_figure(), - flags = { "has_pandoc3_figure" } }, + { name = "post-render-pandoc3-figure", + filter = render_pandoc3_figure(), + flags = { "has_pandoc3_figure" }, + traverser = 'jog', + }, -- extensible rendering - { name = "post-render_extended_nodes", filter = render_extended_nodes() }, + { name = "post-render_extended_nodes", + filter = render_extended_nodes(), + traverser = 'jog', + }, -- inject required packages post-rendering - { name = "layout-meta-inject-latex-packages", filter = layout_meta_inject_latex_packages() }, + { name = "layout-meta-inject-latex-packages", + filter = layout_meta_inject_latex_packages(), + traverser = 'jog', + }, -- format fixups post rendering - { name = "post-render-latex-fixups", filter = render_latex_fixups() }, - { name = "post-render-html-fixups", filter = render_html_fixups() }, - { name = "post-render-ipynb-fixups", filter = render_ipynb_fixups() }, - { name = "post-render-typst-fixups", filter = render_typst_fixups() }, - { name = "post-render-typst-css-to-props", filter = render_typst_css_property_processing() }, - { name = "post-render-typst-brand-yaml", filter = render_typst_brand_yaml() }, - { name = "post-render-gfm-fixups", filter = render_gfm_fixups() }, - { name = "post-render-hugo-fixups", filter = render_hugo_fixups() }, - { name = "post-render-email", filters = render_email() }, - { name = "post-render-pptx-fixups", filter = render_pptx_fixups() }, - { name = "post-render-revealjs-fixups", filter = render_reveal_fixups() } + { name = "post-render-latex-fixups", + filters = render_latex_fixups(), + traverser = 'jog', + }, + { name = "post-render-html-fixups", + filter = render_html_fixups(), + traverser = 'jog', + }, + { name = "post-render-ipynb-fixups", + filter = render_ipynb_fixups(), + traverser = 'jog', + }, + { name = "post-render-typst-fixups", + filter = render_typst_fixups(), + traverser = 'jog', + }, + { name = "post-render-typst-css-to-props", + filter = render_typst_css_property_processing(), + traverser = 'jog', + }, + { name = "post-render-typst-brand-yaml", + filter = render_typst_brand_yaml(), + traverser = 'jog', + }, + { name = "post-render-gfm-fixups", + filter = render_gfm_fixups(), + traverser = 'jog', + }, + { name = "post-render-hugo-fixups", + filter = render_hugo_fixups(), + traverser = 'jog', + }, + { name = "post-render-email", + filters = render_email(), + traverser = 'jog', + }, + { name = "post-render-pptx-fixups", + filter = render_pptx_fixups(), + traverser = 'jog', + }, + { name = "post-render-revealjs-fixups", + filter = render_reveal_fixups(), + traverser = 'jog', + } } local quarto_finalize_filters = { -- quarto-finalize - { name = "finalize-combined", filter = - combineFilters({ + { name = "finalize-combined", + filter = combineFilters{ file_metadata(), mediabag_filter(), inject_vault_content_into_rawlatex(), - })}, - { name = "finalize-bookCleanup", filter = bookCleanup() }, - { name = "finalize-cites", filter = writeCites() }, - { name = "finalize-metaCleanup", filter = metaCleanup() }, - { name = "finalize-dependencies", filter = dependencies() }, - { name = "finalize-coalesce-raw", filters = coalesce_raw() }, - { name = "finalize-descaffold", filter = descaffold() }, - { name = "finalize-wrapped-writer", filter = wrapped_writer() }, - { name = "finalize-typst-state", filter = setup_typst_state() }, + }, + traverser = 'jog', + }, + { name = "finalize-bookCleanup", + filter = bookCleanup(), + traverser = 'jog', + }, + { name = "finalize-cites", + filter = writeCites(), + traverser = 'jog', + }, + { name = "finalize-metaCleanup", + filter = metaCleanup(), + traverser = 'jog', + }, + { name = "finalize-dependencies", + filter = dependencies(), + traverser = 'jog', + }, + { name = "finalize-coalesce-raw", + filters = coalesce_raw(), + traverser = 'jog', + }, + { name = "finalize-descaffold", + filter = descaffold(), + traverser = 'jog', + }, + { name = "finalize-wrapped-writer", + filter = wrapped_writer(), + traverser = 'jog', + }, + { name = "finalize-typst-state", + filter = setup_typst_state(), + traverser = 'jog', + }, } local quarto_layout_filters = { - { name = "manuscript filtering", filter = manuscript() }, - { name = "manuscript filtering", filter = manuscriptUnroll() }, - { name = "layout-lightbox", filters = lightbox(), flags = { "has_lightbox" }}, - { name = "layout-columns-preprocess", filter = columns_preprocess() }, - { name = "layout-columns", filter = columns() }, - { name = "layout-cites-preprocess", filter = cites_preprocess() }, - { name = "layout-cites", filter = cites() }, - { name = "layout-panels", filter = layout_panels() }, - { name = "post-fold-code-and-lift-codeblocks-from-floats", filter = fold_code_and_lift_codeblocks() }, + { name = "manuscript filtering", + filter = manuscript(), + traverser = 'jog', + }, + { name = "manuscript filtering", + filter = manuscriptUnroll(), + traverser = 'jog', + }, + { name = "layout-lightbox", + filters = lightbox(), + flags = { "has_lightbox" }, + traverser = 'jog', + }, + { name = "layout-columns-preprocess", + filter = columns_preprocess(), + traverser = 'jog', + }, + { name = "layout-columns", + filter = columns(), + traverser = 'jog', + }, + { name = "layout-cites-preprocess", + filter = cites_preprocess(), + traverser = 'jog', + }, + { name = "layout-cites", + filter = cites(), + traverser = 'jog', + }, + { name = "layout-panels", + filter = layout_panels(), + traverser = 'jog', + }, + { name = "post-fold-code-and-lift-codeblocks-from-floats", + filter = fold_code_and_lift_codeblocks(), + traverser = 'jog', + }, } local quarto_crossref_filters = { - { name = "crossref-preprocess-floats", filter = crossref_mark_subfloats(), + { name = "crossref-preprocess-floats", + filter = crossref_mark_subfloats(), + traverser = 'jog', }, - { name = "crossref-preprocessTheorems", filter = crossref_preprocess_theorems(), - flags = { "has_theorem_refs" } }, - - { name = "crossref-combineFilters", filter = combineFilters({ - file_metadata(), - qmd(), - sections(), - crossref_figures(), - equations(), - crossref_theorems(), - crossref_callouts(), - })}, - - { name = "crossref-resolveRefs", filter = resolveRefs(), - flags = { "has_cites" } }, - - { name = "crossref-crossrefMetaInject", filter = crossrefMetaInject() }, - { name = "crossref-writeIndex", filter = writeIndex() }, + flags = { "has_theorem_refs" }, + traverser = 'jog', + }, + { name = "crossref-combineFilters", + filter = combineFilters{ + file_metadata(), + qmd(), + sections(), + crossref_figures(), + equations(), + crossref_theorems(), + crossref_callouts(), + }, + traverser = 'jog', + }, + { name = "crossref-resolveRefs", + filter = resolveRefs(), + flags = { "has_cites" }, + traverser = 'jog', + }, + { name = "crossref-crossrefMetaInject", + filter = crossrefMetaInject(), + traverser = 'jog', + }, + { name = "crossref-writeIndex", + filter = writeIndex(), + traverser = 'jog', + }, } local quarto_filter_list = {} diff --git a/src/resources/filters/modules/import_all.lua b/src/resources/filters/modules/import_all.lua index 1369a183c9a..2b97fdef94e 100644 --- a/src/resources/filters/modules/import_all.lua +++ b/src/resources/filters/modules/import_all.lua @@ -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"), @@ -20,4 +21,4 @@ _quarto.modules = { string = require("modules/string"), tablecolwidths = require("modules/tablecolwidths"), typst = require("modules/typst") -} \ No newline at end of file +} diff --git a/src/resources/filters/modules/jog.lua b/src/resources/filters/modules/jog.lua new file mode 100644 index 00000000000..f3ab5c07302 --- /dev/null +++ b/src/resources/filters/modules/jog.lua @@ -0,0 +1,320 @@ +--- jog.lua – walk the pandoc AST with context, and with inplace modification. +--- +--- Copyright: © 2024 Albert Krewinkel, Carlos Scheidegger +--- License: MIT – see LICENSE for details + +local pandoc = require 'pandoc' +local List = require 'pandoc.List' + +local debug_getmetatable = debug.getmetatable + +--- Get the element type; like pandoc.utils.type, but faster. +local function ptype (x) + local mt = debug_getmetatable(x) + if mt then + local name = mt.__name + return name or type(x) + else + return type(x) + end +end + +--- Checks whether the object is a list type. +local listy_type = { + Blocks = true, + Inlines = true, + List = true, +} + +local function run_filter_function (fn, element, context) + if fn == nil then + return element + end + + local result, continue = fn(element, context) + if result == nil then + return element, continue + else + return result, continue + end +end + +--- Set of Block and Inline tags that are leaf nodes. +local leaf_node_tags = { + Code = true, + CodeBlock = true, + HorizontalRule = true, + LineBreak = true, + Math = true, + RawBlock = true, + RawInline = true, + Space = true, + SoftBreak = true, + Str = true, +} + +--- Set of Block and Inline tags that have nested items in `.contents` only. +local content_only_node_tags = { + -- Blocks with Blocks content + BlockQuote = true, + Div = true, + Header = true, + -- Blocks with Inlines content + Para = true, + Plain = true, + -- Blocks with List content + LineBlock = true, + BulletList = true, + OrderedList = true, + DefinitionList = true, + -- Inlines with Inlines content + Cite = true, + Emph = true, + Link = true, + Quoted = true, + SmallCaps = true, + Span = true, + Strikeout = true, + Strong = true, + Subscript = true, + Superscript = true, + Underline = true, + -- Inline with Blocks content + Note = true, +} + +--- Apply the filter on the nodes below the given element. +local function recurse (element, tp, jogger) + tp = tp or ptype(element) + local tag = element.tag + if leaf_node_tags[tag] then + -- do nothing, cannot traverse any deeper + elseif tp == 'table' then + for key, value in pairs(element) do + element[key] = jogger(value) + end + elseif content_only_node_tags[tag] or + tp == 'Cell' or tp == 'pandoc Cell' then + element.content = jogger(element.content) + elseif tag == 'Image' then + element.caption = jogger(element.caption) + elseif tag == 'Table' then + element.caption = jogger(element.caption) + element.head = jogger(element.head) + element.bodies = jogger(element.bodies) + element.foot = jogger(element.foot) + elseif tag == 'Caption' then + element.long = jogger(element.long) + element.short = element.short and jogger(element.short) + elseif tag == 'Figure' then + element.caption = jogger(element.caption) + element.content = jogger(element.content) + elseif tp == 'Meta' then + for key, value in pairs(element) do + element[key] = jogger(value) + end + elseif tp == 'Row' or tp == 'pandoc Row' then + element.cells = jogger(element.cells) + elseif tp == 'pandoc TableHead' or tp == 'pandoc TableFoot' or + tp == 'TableHead' or tp == 'TableFoot' then + element.rows = jogger(element.rows) + elseif tp == 'Blocks' or tp == 'Inlines' then + local expected_itemtype = tp == 'Inlines' and 'Inline' or 'Block' + local pos = 0 + local filtered_index = 1 + local filtered_items = element:map(function (x) + return jogger(x) + end) + local item = filtered_items[filtered_index] + local itemtype + while item ~= nil do + itemtype = ptype(item) + if itemtype ~= tp and itemtype ~= expected_itemtype then + -- neither the list type nor the list's item type. Try to convert. + item = pandoc[tp](item) + itemtype = tp + end + if itemtype == tp then + local sublist_index = 1 + local sublistitem = item[sublist_index] + while sublistitem ~= nil do + pos = pos + 1 + element[pos] = sublistitem + sublist_index = sublist_index + 1 + sublistitem = item[sublist_index] + end + else + -- not actually a sublist, just an element + pos = pos + 1 + element[pos] = item + end + filtered_index = filtered_index + 1 + item = filtered_items[filtered_index] + end + -- unset remaining indices if the new list is shorter than the old + pos = pos + 1 + while element[pos] do + element[pos] = nil + pos = pos + 1 + end + elseif tp == 'List' then + local i, item = 1, element[1] + while item do + element[i] = jogger(item) + i, item = i+1, element[i+1] + end + elseif tp == 'Caption' then + element.long = jogger(element.long) + element.short = element.short and jogger(element.short) + elseif tp == 'Pandoc' then + element.meta = jogger(element.meta) + element.blocks = jogger(element.blocks) + else + error("Don't know how to traverse " .. (element.t or tp)) + end + return element +end + +local non_joggable_types = { + ['Attr'] = true, + ['boolean'] = true, + ['nil'] = true, + ['number'] = true, + ['string'] = true, +} + +local function get_filter_function(element, filter, tp) + if non_joggable_types[tp] or tp == 'table' then + return nil + elseif tp == 'Block' then + return filter[element.tag] or filter.Block + elseif tp == 'Inline' then + return filter[element.tag] or filter.Inline + else + return filter[tp] + end +end + +local function make_jogger (filter, context) + local is_topdown = filter.traverse == 'topdown' + local jogger + + jogger = function (element) + if context then + context:insert(element) + end + local tp = ptype(element) + local result, continue = nil, true + if non_joggable_types[tp] then + result = element + elseif tp == 'table' then + result = recurse(element, tp, jogger) + else + local fn = get_filter_function(element, filter, tp) + if is_topdown then + result, continue = run_filter_function(fn, element, context) + if continue ~= false then + -- recurse on the original element, but discard the result. + recurse(element, tp, jogger) + end + return result + else + element = recurse(element, tp, jogger) + result = run_filter_function(fn, element, context) + end + end + + if context then + context:remove() -- remove this element from the context + end + return result + end + return jogger +end + +local element_name_map = { + Cell = 'pandoc Cell', + Row = 'pandoc Row', + TableHead = 'pandoc TableHead', + TableFoot = 'pandoc TableFoot', +} + +--- Function to traverse the pandoc AST with context. +local function jog(element, filter) + local context = filter.context and List{} or nil + + -- Table elements have a `pandoc ` prefix in the name + for from, to in pairs(element_name_map) do + filter[to] = filter[from] + end + + -- Check if we can just call Pandoc and Meta and be done + if ptype(element) == 'Pandoc' then + local must_recurse = false + for name in pairs(filter) do + if string.match(name, '^[A-Z]') and + name ~= 'Pandoc' and name ~= 'Meta' then + must_recurse = true + break + end + end + if not must_recurse then + element.meta = run_filter_function(filter.Meta, element.meta, context) + element = run_filter_function(filter.Pandoc, element, context) + return element + end + end + + -- Create and call traversal function + local jog_internal = make_jogger(filter, context) + return jog_internal(element) +end + +--- Add `jog` as a method to all pandoc AST elements +-- This uses undocumented features and might break! +local function add_method(funname) + funname = funname or 'jog' + pandoc.Space() -- init metatable 'Inline' + pandoc.HorizontalRule() -- init metatable 'Block' + pandoc.Meta{} -- init metatable 'Meta' + pandoc.Pandoc{} -- init metatable 'Pandoc' + pandoc.Blocks{} -- init metatable 'Blocks' + pandoc.Inlines{} -- init metatable 'Inlines' + pandoc.Caption{} -- init metatable 'Caption' + pandoc.Cell{} -- init metatable 'Cell' + pandoc.Row{} -- init metatable 'Row' + pandoc.TableHead{} -- init metatable 'TableHead' + pandoc.TableFoot{} -- init metatable 'TableFoot' + local reg = debug.getregistry() + List{ + 'Block', 'Inline', 'Pandoc', + 'pandoc Cell', 'pandoc Row', 'pandoc TableHead', 'pandoc TableFoot', + 'Caption', 'Cell', 'Row', 'TableHead', 'TableFoot', + }:map( + function (name) + if reg[name] then + reg[name].methods[funname] = jog + end + end + ) + for name in pairs(listy_type) do + if reg[name] then + reg[name][funname] = jog + end + end + if reg['Meta'] then + reg['Meta'][funname] = jog + end +end + +local mt = { + __call = function (_, ...) + return jog(...) + end +} + +local M = setmetatable({}, mt) +M.jog = jog +M.add_method = add_method + +return M diff --git a/src/resources/filters/normalize/astpipeline.lua b/src/resources/filters/normalize/astpipeline.lua index 8777c5e3079..8469c58307f 100644 --- a/src/resources/filters/normalize/astpipeline.lua +++ b/src/resources/filters/normalize/astpipeline.lua @@ -22,32 +22,41 @@ function quarto_ast_pipeline() } end return { - { name = "normalize-table-merge-raw-html", filter = table_merge_raw_html() }, + { name = "normalize-table-merge-raw-html", + filter = table_merge_raw_html(), + traverser = 'jog', + }, -- this filter can't be combined with others because it's top-down processing. -- unfortunate. - { name = "normalize-html-table-processing", filter = parse_html_tables() }, + { name = "normalize-html-table-processing", + filter = parse_html_tables(), + traverser = 'jog', + }, - { name = "normalize-combined-1", filter = combineFilters({ - extract_latex_quartomarkdown_commands(), - forward_cell_subcaps(), - parse_extended_nodes(), - code_filename(), - normalize_fixup_data_uri_image_extension(), - warn_on_stray_triple_colons(), - }) + { name = "normalize-combined-1", + filter = combineFilters({ + extract_latex_quartomarkdown_commands(), + forward_cell_subcaps(), + parse_extended_nodes(), + code_filename(), + normalize_fixup_data_uri_image_extension(), + warn_on_stray_triple_colons(), + }), + traverser = 'jog', }, { name = "normalize-combine-2", filter = combineFilters({ - parse_md_in_html_rawblocks(), - parse_floatreftargets(), - parse_blockreftargets() + parse_md_in_html_rawblocks(), + parse_floatreftargets(), + parse_blockreftargets() }), }, { name = "normalize-3", filter = handle_subfloatreftargets(), + traverser = 'jog', } } end diff --git a/src/resources/filters/normalize/flags.lua b/src/resources/filters/normalize/flags.lua index d695d39b452..8d72f10009e 100644 --- a/src/resources/filters/normalize/flags.lua +++ b/src/resources/filters/normalize/flags.lua @@ -24,15 +24,7 @@ function compute_flags() return false end - return { - Meta = function(el) - local lightbox_auto = lightbox_module.automatic(el) - if lightbox_auto then - flags.has_lightbox = true - elseif lightbox_auto == false then - flags.has_lightbox = false - end - end, + return {{ Header = function(el) if find_shortcode_in_attributes(el) then flags.has_shortcodes = true @@ -76,7 +68,6 @@ function compute_flags() if el.text:find("%{%{%<") then flags.has_shortcodes = true end - end, Div = function(node) if find_shortcode_in_attributes(node) then @@ -187,5 +178,14 @@ function compute_flags() Figure = function(node) flags.has_pandoc3_figure = true end - } -end \ No newline at end of file + }, { + Meta = function(el) + local lightbox_auto = lightbox_module.automatic(el) + if lightbox_auto then + flags.has_lightbox = true + elseif lightbox_auto == false then + flags.has_lightbox = false + end + end, + }} +end diff --git a/src/resources/filters/quarto-post/book.lua b/src/resources/filters/quarto-post/book.lua index ff354ec200c..4f108263d85 100644 --- a/src/resources/filters/quarto-post/book.lua +++ b/src/resources/filters/quarto-post/book.lua @@ -8,10 +8,11 @@ local license = require 'modules/license' local function clean (inlines) -- this is in post, so it's after render, so we don't need to worry about -- custom ast nodes - return inlines:walk { - Note = function (_) return {} end, + return _quarto.traverser(inlines, { + traverse = 'topdown', + Note = function (_) return {}, false end, Link = function (link) return link.content end, - } + }) end --- Creates an Inlines singleton containing the raw LaTeX. diff --git a/src/resources/filters/quarto-post/delink.lua b/src/resources/filters/quarto-post/delink.lua index f391781c29d..e7a08125fc9 100644 --- a/src/resources/filters/quarto-post/delink.lua +++ b/src/resources/filters/quarto-post/delink.lua @@ -19,7 +19,7 @@ function delink() -- find links and transform them to spans -- this is in post, so it's after render, so we don't need to worry about -- custom ast nodes - return pandoc.walk_block(div, { + return _quarto.traverser(div, { Link = function(link) return pandoc.Span(link.content) end diff --git a/src/resources/filters/quarto-post/latex.lua b/src/resources/filters/quarto-post/latex.lua index 132252a6528..f29a1100760 100644 --- a/src/resources/filters/quarto-post/latex.lua +++ b/src/resources/filters/quarto-post/latex.lua @@ -407,7 +407,7 @@ function render_latex() end, Note = function(el) tappend(noteContents, {el.content}) - el.content:walk({ + _quarto.traverser(el.content, { CodeBlock = function(el) hasVerbatimInNotes = true end @@ -570,19 +570,7 @@ function render_latex_fixups() return emit_color("{rgb}{0,0,0}") end end - return { - Meta = function(meta) - if not need_inject then - return - end - metaInjectLatex(meta, function(inject) - for v, i in pairs(emitted_colors) do - local def = "\\definecolor{QuartoInternalColor" .. i .. "}" .. v - inject(def) - end - end) - return meta - end, + return {{ RawBlock = function(raw) if _quarto.format.isRawLatex(raw) then local long_table_match = _quarto.modules.patterns.match_all_in_table(_quarto.patterns.latexLongtablePattern) @@ -615,5 +603,18 @@ function render_latex_fixups() return pandoc.RawBlock('latex', table.concat(new_lines, "\n")) end end - } + }, { + Meta = function(meta) + if not need_inject then + return + end + metaInjectLatex(meta, function(inject) + for v, i in pairs(emitted_colors) do + local def = "\\definecolor{QuartoInternalColor" .. i .. "}" .. v + inject(def) + end + end) + return meta + end, + }} end diff --git a/src/resources/filters/quarto-post/render-asciidoc.lua b/src/resources/filters/quarto-post/render-asciidoc.lua index 10ad3603eed..44a4806dabc 100644 --- a/src/resources/filters/quarto-post/render-asciidoc.lua +++ b/src/resources/filters/quarto-post/render-asciidoc.lua @@ -89,7 +89,7 @@ function render_asciidoc() local noteEl = el[i+1] -- if the note contains a code inline, we need to add a space local hasCode = false - pandoc.walk_inline(noteEl, { + _quarto.traverser(noteEl, { Code = function(_el) hasCode = true end diff --git a/src/resources/filters/quarto-pre/shiny.lua b/src/resources/filters/quarto-pre/shiny.lua index 75dbdd9197b..ccf2cb5e64f 100644 --- a/src/resources/filters/quarto-pre/shiny.lua +++ b/src/resources/filters/quarto-pre/shiny.lua @@ -67,7 +67,7 @@ function server_shiny() -- blocks.) local context = nil - local res = pandoc.walk_block(divEl, { + local res = _quarto.traverser(divEl, { CodeBlock = function(el) if el.attr.classes:includes("python") and el.attr.classes:includes("cell-code") then diff --git a/src/resources/pandoc/datadir/_utils.lua b/src/resources/pandoc/datadir/_utils.lua index 2c9bd6d17fd..a23abf9d9fd 100644 --- a/src/resources/pandoc/datadir/_utils.lua +++ b/src/resources/pandoc/datadir/_utils.lua @@ -599,6 +599,14 @@ local function is_empty_node (node) end end +--- Call the node's walk method with the given filters. +-- @param node a pandoc AST node +-- @param filter table with filter functions +local function walk(node, filter) + quarto_assert(node and node.walk) + return node:walk(filter) +end + return { dump = dump, type = get_type, @@ -611,6 +619,7 @@ return { as_blocks = as_blocks, is_empty_node = is_empty_node, match = match, + walk = walk, add_to_blocks = function(blocks, block) if pandoc.utils.type(blocks) ~= "Blocks" then fatal("add_to_blocks: invalid type " .. pandoc.utils.type(blocks)) diff --git a/src/resources/pandoc/datadir/init.lua b/src/resources/pandoc/datadir/init.lua index deb7ea47b6b..e0ce7488363 100644 --- a/src/resources/pandoc/datadir/init.lua +++ b/src/resources/pandoc/datadir/init.lua @@ -1882,6 +1882,7 @@ _quarto = { latexTablePatterns = latexTablePatterns, latexCaptionPattern = latexCaptionPattern_table }, + traverser = utils.walk, utils = utils, withScriptFile = function(file, callback) table.insert(scriptFile, file)