diff --git a/dev-docs/feature-format-matrix/qmd-files/crossref/float/table/document.qmd b/dev-docs/feature-format-matrix/qmd-files/crossref/float/table/document.qmd index 6c7fa1b7781..f3ba1d839bf 100644 --- a/dev-docs/feature-format-matrix/qmd-files/crossref/float/table/document.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/crossref/float/table/document.qmd @@ -32,6 +32,7 @@ _quarto: - "a[href=\"#tbl-2\"].quarto-xref" dashboard: *dom-tests revealjs: # reveal resolves anchors differently, using the section headings instead of the float ids + ensureHtmlElements: - - "div#tbl-1.quarto-float figure.quarto-float.quarto-float-tbl table" - "div#tbl-2.quarto-float figure.quarto-float.quarto-float-tbl img" diff --git a/dev-docs/feature-format-matrix/qmd-files/raw-blocks/interpreted/pandoc-json/document.qmd b/dev-docs/feature-format-matrix/qmd-files/raw-blocks/interpreted/pandoc-json/document.qmd index d45463c7149..604bbff0474 100644 --- a/dev-docs/feature-format-matrix/qmd-files/raw-blocks/interpreted/pandoc-json/document.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/raw-blocks/interpreted/pandoc-json/document.qmd @@ -11,7 +11,7 @@ format: typst: quality: 2 comment: "Typst itself interprets the -- as –" - output-ext: typ + keep-typ: true revealjs: quality: 1 docx: diff --git a/dev-docs/feature-format-matrix/qmd-files/raw-blocks/interpreted/pandoc-native/document.qmd b/dev-docs/feature-format-matrix/qmd-files/raw-blocks/interpreted/pandoc-native/document.qmd index 520fe613f3e..116689039d3 100644 --- a/dev-docs/feature-format-matrix/qmd-files/raw-blocks/interpreted/pandoc-native/document.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/raw-blocks/interpreted/pandoc-native/document.qmd @@ -10,7 +10,7 @@ format: dashboard: quality: 1 typst: - output-ext: typ + keep-typ: true quality: 2 revealjs: quality: 1 diff --git a/src/resources/filters/quarto-post/latex.lua b/src/resources/filters/quarto-post/latex.lua index d3c159a6fd0..15f2f6dadf6 100644 --- a/src/resources/filters/quarto-post/latex.lua +++ b/src/resources/filters/quarto-post/latex.lua @@ -438,7 +438,7 @@ function render_latex_fixups() if _quarto.format.isRawLatex(raw) then local long_table_match = _quarto.modules.patterns.match_all_in_table(_quarto.patterns.latexLongtablePattern) local caption_match = _quarto.modules.patterns.match_all_in_table(_quarto.patterns.latexCaptionPattern) - if long_table_match(raw.text) and caption_match(raw.text) then + if long_table_match(raw.text) and not caption_match(raw.text) then raw.text = raw.text:gsub( _quarto.modules.patterns.combine_patterns(_quarto.patterns.latexLongtablePattern), "\\begin{longtable*}%2\\end{longtable*}", 1) return raw diff --git a/tests/docs/smoke-all/2024/01/19/docusaurus/docusaurus-tabsets.qmd b/tests/docs/smoke-all/2024/01/19/docusaurus/docusaurus-tabsets.qmd index a7a5981549d..f92c816494f 100644 --- a/tests/docs/smoke-all/2024/01/19/docusaurus/docusaurus-tabsets.qmd +++ b/tests/docs/smoke-all/2024/01/19/docusaurus/docusaurus-tabsets.qmd @@ -3,7 +3,7 @@ format: docusaurus-md _quarto: tests: docusaurus-md: - ensure-snapshot-matches: true + ensureSnapshotMatches: true --- ::: {.panel-tabset} diff --git a/tests/docs/smoke-all/2024/02/13/empty-floats.qmd b/tests/docs/smoke-all/2024/02/13/empty-floats.qmd index 81225f0a8fa..a9d55b23c84 100644 --- a/tests/docs/smoke-all/2024/02/13/empty-floats.qmd +++ b/tests/docs/smoke-all/2024/02/13/empty-floats.qmd @@ -4,7 +4,7 @@ format: latex _quarto: tests: latex: - verifyNoErrors: true + noErrors: true --- :::{#fig-1} diff --git a/tests/docs/smoke-all/2024/03/22/8998.qmd b/tests/docs/smoke-all/2024/03/22/8998.qmd index bd9f5d5990a..2b7a2e9b139 100644 --- a/tests/docs/smoke-all/2024/03/22/8998.qmd +++ b/tests/docs/smoke-all/2024/03/22/8998.qmd @@ -6,8 +6,9 @@ engine: jupyter _quarto: tests: revealjs: - - [] - - ["python"] + ensureFileRegexMatches: + - [] + - ["python"] --- - Turn off alarm diff --git a/tests/docs/smoke-all/crossrefs/float/jats/jats-float-options-1.qmd b/tests/docs/smoke-all/crossrefs/float/jats/jats-float-options-1.qmd index 40fbb2c65ac..c8dac2022a7 100644 --- a/tests/docs/smoke-all/crossrefs/float/jats/jats-float-options-1.qmd +++ b/tests/docs/smoke-all/crossrefs/float/jats/jats-float-options-1.qmd @@ -10,7 +10,7 @@ crossref: _quarto: tests: jats: - ensureJatsXPath: + ensureJatsXpath: - [] - - "//xref[@rid=\"tbl-letters\"]" diff --git a/tests/docs/smoke-all/crossrefs/float/latex/latex-longtable-fixup-caption-no-crossref.qmd b/tests/docs/smoke-all/crossrefs/float/latex/latex-longtable-fixup-caption-no-crossref.qmd new file mode 100644 index 00000000000..d886d255e16 --- /dev/null +++ b/tests/docs/smoke-all/crossrefs/float/latex/latex-longtable-fixup-caption-no-crossref.qmd @@ -0,0 +1,33 @@ +--- +format: pdf +keep-tex: true +title: "Long Table kable fixups etc" +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + - ['\\begin\{longtable\}'] + - [] +--- + +## Raw longtable table with no crossref and caption from kable function + +In this case we'll issue a warning about of the type +```` +Raw LaTeX table found with non-tbl label: tab:not-tbl +Won't be able to cross-reference this table using Quarto's native crossref system. +```` +```{r} +#| label: not-tbl +#| echo: false +df <- tibble::tibble( + x = 1:20, + y = rnorm(20), + z = rnorm(20) +) +knitr::kable(df, + format = "latex", + longtable = TRUE, + booktabs = TRUE, + caption = "A long table with a caption") +``` diff --git a/tests/docs/smoke-all/crossrefs/float/latex/latex-longtable-fixup-no-cap-no-crossref.qmd b/tests/docs/smoke-all/crossrefs/float/latex/latex-longtable-fixup-no-cap-no-crossref.qmd new file mode 100644 index 00000000000..6511d5582e5 --- /dev/null +++ b/tests/docs/smoke-all/crossrefs/float/latex/latex-longtable-fixup-no-cap-no-crossref.qmd @@ -0,0 +1,29 @@ +--- +format: pdf +keep-tex: true +title: "Long Table kable fixups etc" +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + - ['\\begin\{longtable\*\}'] + - ['\\begin\{longtable\}'] +--- + +## Raw longtable table with no crossref and no caption + +In this case, Quarto will transform to longtable* + +```{r} +#| echo: false +df <- tibble::tibble( + x = 1:20, + y = rnorm(20), + z = rnorm(20) +) +knitr::kable(df, + format = "latex", + longtable = TRUE, + booktabs = TRUE) +``` + diff --git a/tests/docs/smoke-all/crossrefs/theorem/lemma-1.qmd b/tests/docs/smoke-all/crossrefs/theorem/lemma-1.qmd index 4d7057f55be..a3f05e0108e 100644 --- a/tests/docs/smoke-all/crossrefs/theorem/lemma-1.qmd +++ b/tests/docs/smoke-all/crossrefs/theorem/lemma-1.qmd @@ -1,7 +1,7 @@ --- format: typst: - output-ext: typ + keep-typ: true _quarto: tests: html: diff --git a/tests/docs/smoke-all/crossrefs/theorem/theorem-1.qmd b/tests/docs/smoke-all/crossrefs/theorem/theorem-1.qmd index 69b49df5a40..e59826a27c8 100644 --- a/tests/docs/smoke-all/crossrefs/theorem/theorem-1.qmd +++ b/tests/docs/smoke-all/crossrefs/theorem/theorem-1.qmd @@ -1,7 +1,7 @@ --- format: typst: - output-ext: typ + keep-typ: true _quarto: tests: html: diff --git a/tests/docs/smoke-all/crossrefs/theorem/theorem-2.qmd b/tests/docs/smoke-all/crossrefs/theorem/theorem-2.qmd index 48b80eb9bbf..978dac3df0e 100644 --- a/tests/docs/smoke-all/crossrefs/theorem/theorem-2.qmd +++ b/tests/docs/smoke-all/crossrefs/theorem/theorem-2.qmd @@ -1,7 +1,7 @@ --- format: typst: - output-ext: typ + keep-typ: true _quarto: tests: html: diff --git a/tests/smoke/smoke-all.test.ts b/tests/smoke/smoke-all.test.ts index 0e9a9588658..e609a54dba8 100644 --- a/tests/smoke/smoke-all.test.ts +++ b/tests/smoke/smoke-all.test.ts @@ -32,6 +32,7 @@ import { ensurePptxXpath, ensurePptxLayout, ensurePptxMaxSlides, + ensureLatexFileRegexMatches, } from "../verify.ts"; import { readYaml, readYamlFromMarkdown } from "../../src/core/yaml.ts"; import { outputForInput } from "../utils.ts"; @@ -96,6 +97,7 @@ function resolveTestSpecs( const verifyMap: Record = { ensureHtmlElements, ensureFileRegexMatches, + ensureLatexFileRegexMatches, ensureTypstFileRegexMatches, ensureDocxRegexMatches, ensureDocxXpath, @@ -112,7 +114,7 @@ function resolveTestSpecs( for (const [format, testObj] of Object.entries(specs)) { let checkWarnings = true; const verifyFns: Verify[] = []; - if (testObj) { + if (testObj && typeof testObj === "object") { for ( // deno-lint-ignore no-explicit-any const [key, value] of Object.entries(testObj as Record) @@ -150,11 +152,23 @@ function resolveTestSpecs( verifyFns.push(verifyMap[key](outputFile.outputPath, ...value)); } } else if (verifyMap[key]) { + // FIXME: We should find another way that having this requirement of keep-* in the metadata + if (key === "ensureTypstFileRegexMatches") { + if (!metadata.format?.typst?.['keep-typ'] && !metadata['keep-typ']) { + throw new Error(`Using ensureTypstFileRegexMatches requires setting 'keep-typ: true' in file ${input}`); + } + } else if (key === "ensureLatexFileRegexMatches") { + if (!metadata.format?.pdf?.['keep-tex'] && !metadata['keep-tex']) { + throw new Error(`Using ensureLatexFileRegexMatches requires setting 'keep-tex: true' in file ${input}`); + } + } if (typeof value === "object") { verifyFns.push(verifyMap[key](outputFile.outputPath, ...value)); } else { verifyFns.push(verifyMap[key](outputFile.outputPath, value)); } + } else { + throw new Error(`Unknown verify function used: ${key} in file ${input} for format ${format}`) ; } } } diff --git a/tests/verify.ts b/tests/verify.ts index 73c5934d211..4e375dd881e 100644 --- a/tests/verify.ts +++ b/tests/verify.ts @@ -276,80 +276,6 @@ export const directoryEmptyButFor = ( }; }; -// FIXME: do this properly without resorting on file having keep-typ -export const ensureTypstFileRegexMatches = ( - file: string, - matchesUntyped: (string | RegExp)[], - noMatchesUntyped?: (string | RegExp)[], -): Verify => { - const matches = matchesUntyped.map(asRegexp); - const noMatches = noMatchesUntyped?.map(asRegexp); - return { - name: `Inspecting ${file} for Regex matches`, - verify: async (_output: ExecuteOutput[]) => { - const keptTyp = file.replace(".pdf", ".typ"); - const typ = await Deno.readTextFile(keptTyp); - - try { - matches.forEach((regex) => { - assert( - regex.test(typ), - `Required match ${String(regex)} is missing from file ${keptTyp}.`, - ); - }); - - if (noMatches) { - noMatches.forEach((regex) => { - assert( - !regex.test(typ), - `Illegal match ${String(regex)} was found in file ${keptTyp}.`, - ); - }); - } - } finally { - await Deno.remove(keptTyp); - } - }, - }; -}; - -export const ensurePdfRegexMatches = ( - file: string, - matchesUntyped: (string | RegExp)[], - noMatchesUntyped?: (string | RegExp)[], -): Verify => { - const matches = matchesUntyped.map(asRegexp); - const noMatches = noMatchesUntyped?.map(asRegexp); - return { - name: `Inspecting ${file} for Regex matches`, - verify: async (_output: ExecuteOutput[]) => { - const cmd = new Deno.Command("pdftotext", { - args: [file, "-"], - stdout: "piped", - }) - const output = await cmd.output(); - assert(output.success, `Failed to extract text from ${file}.`) - const text = new TextDecoder().decode(output.stdout); - - matches.forEach((regex) => { - assert( - regex.test(text), - `Required match ${String(regex)} is missing from file ${file}.`, - ); - }); - - if (noMatches) { - noMatches.forEach((regex) => { - assert( - !regex.test(text), - `Illegal match ${String(regex)} was found in file ${file}.`, - ); - }); - } - }, - }; -} - export const ensureHtmlElements = ( file: string, selectors: string[], @@ -436,27 +362,119 @@ export const ensureSnapshotMatches = ( }; } +const regexChecker = async function(file: string, matches: RegExp[], noMatches: RegExp[] | undefined) { + const content = await Deno.readTextFile(file); + matches.forEach((regex) => { + assert( + regex.test(content), + `Required match ${String(regex)} is missing from file ${file}.`, + ); + }); + + if (noMatches) { + noMatches.forEach((regex) => { + assert( + !regex.test(content), + `Illegal match ${String(regex)} was found in file ${file}.`, + ); + }); + } +} + +export const verifyFileRegexMatches = ( + callback: (file: string, matches: RegExp[], noMatches: RegExp[] | undefined) => Promise, + name?: string, +): (file: string, matchesUntyped: (string | RegExp)[], noMatchesUntyped?: (string | RegExp)[]) => Verify => { + return (file: string, matchesUntyped: (string | RegExp)[], noMatchesUntyped?: (string | RegExp)[]) => { + // Use mutliline flag for regexes so that ^ and $ can be used + const asRegexp = (m: string | RegExp) => { + if (typeof m === "string") { + return new RegExp(m, "m"); + } else { + return m; + } + }; + const matches = matchesUntyped.map(asRegexp); + const noMatches = noMatchesUntyped?.map(asRegexp); + return { + name: name ?? `Inspecting ${file} for Regex matches`, + verify: async (_output: ExecuteOutput[]) => { + const tex = await Deno.readTextFile(file); + await callback(file, matches, noMatches); + } + }; + } +} + +// Use this function to Regex match text in the output file export const ensureFileRegexMatches = ( file: string, matchesUntyped: (string | RegExp)[], noMatchesUntyped?: (string | RegExp)[], ): Verify => { - const asRegexp = (m: string | RegExp) => { - if (typeof m === "string") { - return new RegExp(m, "m"); - } else { - return m; + return(verifyFileRegexMatches(regexChecker)(file, matchesUntyped, noMatchesUntyped)); +}; + +// Use this function to Regex match text in the intermediate kept file +// FIXME: do this properly without resorting on file having keep-* +export const verifyKeepFileRegexMatches = ( + toExt: string, + keepExt: string, +): (file: string, matchesUntyped: (string | RegExp)[], noMatchesUntyped?: (string | RegExp)[]) => Verify => { + return (file: string, matchesUntyped: (string | RegExp)[], noMatchesUntyped?: (string | RegExp)[]) => { + const keptFile = file.replace(`.${toExt}`, `.${keepExt}`); + const keptFileChecker = async (file: string, matches: RegExp[], noMatches: RegExp[] | undefined) => { + try { + await regexChecker(file, matches, noMatches); + } finally { + await Deno.remove(file); + } } - }; + return verifyFileRegexMatches(keptFileChecker, `Inspecting intermediate ${keptFile} for Regex matches`)(keptFile, matchesUntyped, noMatchesUntyped); + } +}; + +// FIXME: do this properly without resorting on file having keep-typ +export const ensureTypstFileRegexMatches = ( + file: string, + matchesUntyped: (string | RegExp)[], + noMatchesUntyped?: (string | RegExp)[], +): Verify => { + return(verifyKeepFileRegexMatches("pdf", "typ")(file, matchesUntyped, noMatchesUntyped)); +}; + +// FIXME: do this properly without resorting on file having keep-typ +export const ensureLatexFileRegexMatches = ( + file: string, + matchesUntyped: (string | RegExp)[], + noMatchesUntyped?: (string | RegExp)[], +): Verify => { + return(verifyKeepFileRegexMatches("pdf", "tex")(file, matchesUntyped, noMatchesUntyped)); +}; + +// Use this function to Regex match text in a rendered PDF file +// This requires pdftotext to be available on PATH +export const ensurePdfRegexMatches = ( + file: string, + matchesUntyped: (string | RegExp)[], + noMatchesUntyped?: (string | RegExp)[], +): Verify => { const matches = matchesUntyped.map(asRegexp); const noMatches = noMatchesUntyped?.map(asRegexp); return { name: `Inspecting ${file} for Regex matches`, verify: async (_output: ExecuteOutput[]) => { - const tex = await Deno.readTextFile(file); + const cmd = new Deno.Command("pdftotext", { + args: [file, "-"], + stdout: "piped", + }) + const output = await cmd.output(); + assert(output.success, `Failed to extract text from ${file}.`) + const text = new TextDecoder().decode(output.stdout); + matches.forEach((regex) => { assert( - regex.test(tex), + regex.test(text), `Required match ${String(regex)} is missing from file ${file}.`, ); }); @@ -464,14 +482,14 @@ export const ensureFileRegexMatches = ( if (noMatches) { noMatches.forEach((regex) => { assert( - !regex.test(tex), + !regex.test(text), `Illegal match ${String(regex)} was found in file ${file}.`, ); }); } }, }; -}; +} export const verifyJatsDocument = ( callback: (doc: string) => Promise,