diff --git a/src/resources/filters/crossref/theorems.lua b/src/resources/filters/crossref/theorems.lua index 68a7704caf9..de1fbb989b5 100644 --- a/src/resources/filters/crossref/theorems.lua +++ b/src/resources/filters/crossref/theorems.lua @@ -37,98 +37,6 @@ function crossref_theorems() proof.order = add_crossref(label, type, title) return proof end, - Div = function(el) - local type = refType(el.attr.identifier) - local theoremType = theorem_types[type] - if theoremType then - internal_error() - else - -- see if this is a proof, remark, or solution - local proof = proof_type(el) - if proof ~= nil then - - -- ensure requisite latex is injected - crossref.using_theorems = true - - if proof.env ~= "proof" then - el.attr.classes:insert("proof") - end - - -- capture then remove name - -- - -- we have string_to_quarto_ast_inlines but we don't need it here - -- because this filter happened after shortcode processing, and this - -- is a regular div we're processing - local name = markdownToInlines(el.attr.attributes["name"]) - if not name or #name == 0 then - name = resolveHeadingCaption(el) - end - el.attr.attributes["name"] = nil - - -- output - if _quarto.format.isLatexOutput() then - local preamble = pandoc.List() - preamble:insert(pandoc.RawInline("latex", "\\begin{" .. proof.env .. "}")) - if name ~= nil then - preamble:insert(pandoc.RawInline("latex", "[")) - tappend(preamble, name) - preamble:insert(pandoc.RawInline("latex", "]")) - end - preamble:insert(pandoc.RawInline("latex", "\n")) - -- https://github.com/quarto-dev/quarto-cli/issues/6077 - if el.content[1].t == "Para" then - preamble:extend(el.content[1].content) - el.content[1].content = preamble - else - if (el.content[1].t ~= "Para") then - -- required trick to get correct alignement when non Para content first - preamble:insert(pandoc.RawInline('latex', "\\leavevmode")) - end - el.content:insert(1, pandoc.Plain(preamble)) - end - local end_env = "\\end{" .. proof.env .. "}" - -- https://github.com/quarto-dev/quarto-cli/issues/6077 - if el.content[#el.content].t == "Para" then - el.content[#el.content].content:insert(pandoc.RawInline("latex", "\n" .. end_env)) - elseif el.content[#el.content].t == "RawBlock" and el.content[#el.content].format == "latex" then - -- this is required for no empty line between end_env and previous latex block - el.content[#el.content].text = el.content[#el.content].text .. "\n" .. end_env - else - el.content:insert(pandoc.RawBlock("latex", end_env)) - end - elseif _quarto.format.isJatsOutput() then - el = jatsTheorem(el, nil, name ) - else - local span = pandoc.Span( - { pandoc.Emph(pandoc.Str(envTitle(proof.env, proof.title)))}, - pandoc.Attr("", { "proof-title" }) - ) - if name ~= nil then - span.content:insert(pandoc.Str(" (")) - tappend(span.content, name) - span.content:insert(pandoc.Str(")")) - end - tappend(span.content, { pandoc.Str(". ")}) - - -- if the first block is a paragraph, then prepend the title span - if #el.content > 0 and - el.content[1].t == "Para" and - el.content[1].content ~= nil and - #el.content[1].content > 0 then - el.content[1].content:insert(1, span) - else - -- else insert a new paragraph - el.content:insert(1, pandoc.Para{span}) - end - end - - end - - end - - return el - - end } end diff --git a/src/resources/filters/layout/pandoc3_figure.lua b/src/resources/filters/layout/pandoc3_figure.lua index 42b66e54f28..2ebb335fb96 100644 --- a/src/resources/filters/layout/pandoc3_figure.lua +++ b/src/resources/filters/layout/pandoc3_figure.lua @@ -11,14 +11,8 @@ function render_pandoc3_figure() local function html_handle_linked_image(figure) local div = pandoc.Div({}) div.identifier = "fig-yesiamafigure" -- this is a bad hack to make discoverLinkedFigureDiv work - local link = nil - if figure.content[1].t == "Plain" then - local plain = figure.content[1] - if plain.content[1].t == "Link" then - link = plain.content[1] - end - end - if link == nil then + local link = quarto.utils.match("[1]/Plain/[1]/Link")(figure) + if not link then return nil end div.content:insert(pandoc.Para({link})) @@ -197,6 +191,9 @@ function render_pandoc3_figure() end end end + if #figure.content == 0 then + return nil + end return make_typst_figure({ content = figure.content[1], caption = figure.caption.long[1], diff --git a/src/resources/filters/quarto-post/latexdiv.lua b/src/resources/filters/quarto-post/latexdiv.lua index 4d0a0d0c384..ca74cb94f0e 100644 --- a/src/resources/filters/quarto-post/latexdiv.lua +++ b/src/resources/filters/quarto-post/latexdiv.lua @@ -34,7 +34,7 @@ function latexDiv() -- if the first and last div blocks are paragraphs then we can -- bring the environment begin/end closer to the content - if divEl.content[1].t == "Para" and divEl.content[#divEl.content].t == "Para" then + if quarto.utils.match("[1]/Para")(divEl) and divEl.content[#divEl.content].t == "Para" then table.insert(divEl.content[1].content, 1, pandoc.RawInline('tex', beginEnv .. "\n")) table.insert(divEl.content[#divEl.content].content, pandoc.RawInline('tex', "\n" .. endEnv)) else diff --git a/src/resources/filters/quarto-pre/parsefiguredivs.lua b/src/resources/filters/quarto-pre/parsefiguredivs.lua index 6215af8f2e0..466f0475f5c 100644 --- a/src/resources/filters/quarto-pre/parsefiguredivs.lua +++ b/src/resources/filters/quarto-pre/parsefiguredivs.lua @@ -305,11 +305,9 @@ function parse_floatreftargets() local identifier = div.identifier local attr = pandoc.Attr(identifier, div.classes, div.attributes) assert(content) - if (#content == 1 and content[1].t == "Para" and - content[1].content[1].t == "Image") then - -- if the div contains a single image, then we simply use the image as - -- the content - content = content[1].content[1] + local single_image = quarto.utils.match("[1]/Para/[1]/Image")(content) + if #content == 1 and single_image then + content = single_image -- don't merge classes because they often have CSS consequences -- but merge attributes because they're needed to correctly resolve @@ -486,7 +484,7 @@ function parse_floatreftargets() if category == nil then return nil end - if #fig.content ~= 1 and fig.content[1].t ~= "Plain" then + if #fig.content ~= 1 or fig.content[1].t ~= "Plain" then -- we don't know how to parse this pandoc 3 figure -- just return as is return nil diff --git a/tests/docs/smoke-all/crossrefs/float/empty-figure-content-filter.lua b/tests/docs/smoke-all/crossrefs/float/empty-figure-content-filter.lua new file mode 100644 index 00000000000..ab15f03d174 --- /dev/null +++ b/tests/docs/smoke-all/crossrefs/float/empty-figure-content-filter.lua @@ -0,0 +1,7 @@ +-- Test helper: empties Figure content to trigger unguarded .content[1].t access +function Figure(fig) + if fig.identifier == "fig-emptied" then + fig.content = pandoc.Blocks({}) + return fig + end +end diff --git a/tests/docs/smoke-all/crossrefs/float/empty-figure-content.qmd b/tests/docs/smoke-all/crossrefs/float/empty-figure-content.qmd new file mode 100644 index 00000000000..0fe211a47bc --- /dev/null +++ b/tests/docs/smoke-all/crossrefs/float/empty-figure-content.qmd @@ -0,0 +1,25 @@ +--- +title: Empty Figure content guard +format: pdf +keep-tex: true +filters: + - at: pre-ast + path: empty-figure-content-filter.lua +_quarto: + tests: + pdf: + noErrors: default + ensureLatexFileRegexMatches: + - + - '\\begin\{figure\}' +--- + +## Normal figure + +![A caption](img/surus.jpg){#fig-normal} + +See @fig-normal. + +## Figure with content stripped by filter + +![Emptied caption](img/surus.jpg){#fig-emptied} diff --git a/tests/docs/smoke-all/crossrefs/float/empty-pandoc3-figure-filter.lua b/tests/docs/smoke-all/crossrefs/float/empty-pandoc3-figure-filter.lua new file mode 100644 index 00000000000..18155d7b19b --- /dev/null +++ b/tests/docs/smoke-all/crossrefs/float/empty-pandoc3-figure-filter.lua @@ -0,0 +1,8 @@ +-- Test helper: empties non-crossref Figure content to trigger +-- unguarded .content[1].t access in pandoc3_figure.lua +function Figure(fig) + if fig.identifier == "" then + fig.content = pandoc.Blocks({}) + return fig + end +end diff --git a/tests/docs/smoke-all/crossrefs/float/empty-pandoc3-figure.qmd b/tests/docs/smoke-all/crossrefs/float/empty-pandoc3-figure.qmd new file mode 100644 index 00000000000..5742c805c32 --- /dev/null +++ b/tests/docs/smoke-all/crossrefs/float/empty-pandoc3-figure.qmd @@ -0,0 +1,15 @@ +--- +title: Empty Pandoc 3 Figure content guard +format: html +filters: + - at: pre-ast + path: empty-pandoc3-figure-filter.lua +_quarto: + tests: + html: + noErrors: default +--- + +## Figure without cross-ref identifier + +![A plain figure caption](img/surus.jpg) diff --git a/tests/docs/smoke-all/crossrefs/float/empty-para-inlines-filter.lua b/tests/docs/smoke-all/crossrefs/float/empty-para-inlines-filter.lua new file mode 100644 index 00000000000..49886f1a437 --- /dev/null +++ b/tests/docs/smoke-all/crossrefs/float/empty-para-inlines-filter.lua @@ -0,0 +1,10 @@ +-- Test helper: creates a fig-div with a single empty Para (no inlines) +-- and a fig-cap attribute for caption, triggering unguarded +-- content[1].content[1].t access in parsefiguredivs.lua:309 +function Div(div) + if div.identifier == "fig-empty-para" then + div.content = pandoc.Blocks({pandoc.Para({})}) + div.attributes["fig-cap"] = "External caption" + return div + end +end diff --git a/tests/docs/smoke-all/crossrefs/float/empty-para-inlines.qmd b/tests/docs/smoke-all/crossrefs/float/empty-para-inlines.qmd new file mode 100644 index 00000000000..0dea796941f --- /dev/null +++ b/tests/docs/smoke-all/crossrefs/float/empty-para-inlines.qmd @@ -0,0 +1,21 @@ +--- +title: Empty Para inlines guard +format: html +filters: + - at: pre-ast + path: empty-para-inlines-filter.lua +_quarto: + tests: + html: + noErrors: default +--- + +## Figure div with content replaced by empty Para + +::: {#fig-empty-para} +![Normal image](img/surus.jpg) + +Normal caption +::: + +See @fig-empty-para. diff --git a/tests/docs/smoke-all/crossrefs/theorem/proof-rendering-html.qmd b/tests/docs/smoke-all/crossrefs/theorem/proof-rendering-html.qmd new file mode 100644 index 00000000000..89d9286abf0 --- /dev/null +++ b/tests/docs/smoke-all/crossrefs/theorem/proof-rendering-html.qmd @@ -0,0 +1,35 @@ +--- +title: Proof rendering (HTML) +format: html +_quarto: + tests: + html: + noErrors: default + ensureHtmlElements: + - + - 'div.proof' + - '.proof-title' +--- + +## Proof with content + +::: {.proof} +This is a proof with content. +::: + +## Named proof + +::: {.proof name="Of the main theorem"} +Named proof body. +::: + +## Empty proof + +::: {.proof} +::: + +## Remark + +::: {.remark} +This is a remark. +::: diff --git a/tests/docs/smoke-all/crossrefs/theorem/proof-rendering.qmd b/tests/docs/smoke-all/crossrefs/theorem/proof-rendering.qmd new file mode 100644 index 00000000000..ec401f26fb4 --- /dev/null +++ b/tests/docs/smoke-all/crossrefs/theorem/proof-rendering.qmd @@ -0,0 +1,39 @@ +--- +title: Proof rendering +format: pdf +keep-tex: true +_quarto: + tests: + pdf: + noErrors: default + ensureLatexFileRegexMatches: + - + - '\\begin\{proof\}' + - '\\end\{proof\}' + - '\\begin\{proof\}\[Of the main theorem\]' + - '\\begin\{remark\}' + - '\\end\{remark\}' +--- + +## Proof with content + +::: {.proof} +This is a proof with content. +::: + +## Named proof + +::: {.proof name="Of the main theorem"} +Named proof body. +::: + +## Empty proof + +::: {.proof} +::: + +## Remark + +::: {.remark} +This is a remark. +::: diff --git a/tests/docs/smoke-all/latex/empty-latex-div.qmd b/tests/docs/smoke-all/latex/empty-latex-div.qmd new file mode 100644 index 00000000000..fc97fed664a --- /dev/null +++ b/tests/docs/smoke-all/latex/empty-latex-div.qmd @@ -0,0 +1,26 @@ +--- +title: Empty LaTeX div +format: pdf +keep-tex: true +_quarto: + tests: + pdf: + noErrors: default + ensureLatexFileRegexMatches: + - + - '\\begin\{center\}' + - '\\end\{center\}' + - '\\begin\{flushright\}' + - '\\end\{flushright\}' +--- + +## Non-empty LaTeX div + +::: {.center latex="true"} +Some centered content. +::: + +## Empty LaTeX div + +::: {.flushright latex="true"} +:::