diff --git a/configuration b/configuration index 63c8647e46..b69039912c 100644 --- a/configuration +++ b/configuration @@ -7,17 +7,16 @@ # but it also used when packaging (e.g. run configure.sh, then prepare-dist.sh, then package.sh) # deno_dom should match release at https://github.com/b-fuze/deno-dom/releases -# NB: When these are updated, you must also update the versions +# IMPORTANT: When these are updated, you must also update the versions # in src/command/check/check.ts - # Binary dependencies export DENO=v2.4.5 # TODO figure out where 0.1.41 apple silicon libs are available export DENO_DOM=v0.1.41-alpha-artifacts -export PANDOC=3.6.3 +export PANDOC=3.8.3 export DARTSASS=1.87.0 export ESBUILD=0.25.10 -export TYPST=0.13.0 +export TYPST=0.14.2 # NB: we can't put comments in the same line as export statements because it diff --git a/dev-docs/feature-format-matrix/qmd-files/math/document.qmd b/dev-docs/feature-format-matrix/qmd-files/math/document.qmd index 7356e59275..082d393e0a 100644 --- a/dev-docs/feature-format-matrix/qmd-files/math/document.qmd +++ b/dev-docs/feature-format-matrix/qmd-files/math/document.qmd @@ -5,6 +5,8 @@ format: html: default _quarto: tests: + run: + skip: "Pandoc 3.7+ removes newlines in display math blocks (fixed in pandoc@8123be6, awaiting release)" html: ensureHtmlElements: - [] diff --git a/dev-docs/update-pandoc-checklist.md b/dev-docs/update-pandoc-checklist.md index ac3115da0a..e59a80321d 100644 --- a/dev-docs/update-pandoc-checklist.md +++ b/dev-docs/update-pandoc-checklist.md @@ -2,10 +2,30 @@ Carlos needs to run this: -- [ ] Ensure archives are upgraded -- [ ] Run `AWS_PROFILE=... ./package/src/quarto-bld update-pandoc PANDOC_VERSION` +- [x] Ensure archives are upgraded +- [x] Run `AWS_PROFILE=... ./package/src/quarto-bld update-pandoc PANDOC_VERSION` - [ ] look at `git diff`, specifically for changes in Pandoc templates, and adjust as needed. +As a reminder, our templates are kept in the same directories as Pandoc's templates, but with different names. `git diff` will show the diff in Pandoc's template; we have to manually patch +ours. (We can't just use `patch` because the templates have diverged too much) + +### Pandoc templates + +The general rule for the naming is that "format.template" indicates Pandoc naming, and "template.format" indicates ours. Examples below: + +#### beamer + +- Pandoc's: src/resources/formats/beamer/pandoc/beamer.template +- Ours: src/resources/formats/beamer/pandoc/template.tex + +Partials: + +- Pandoc's: + - src/resources/formats/beamer/pandoc/latex.common +- Ours: + - src/resources/formats/beamer/pandoc/common.latex + + ## Manual steps - [ ] Update schemas by inspecting [their changelog](https://github.com/jgm/pandoc/blob/main/changelog.md) for new commands, deprecation removals, etc diff --git a/llm-docs/pandoc-quarto-typst-templates.md b/llm-docs/pandoc-quarto-typst-templates.md new file mode 100644 index 0000000000..7739ea44f3 --- /dev/null +++ b/llm-docs/pandoc-quarto-typst-templates.md @@ -0,0 +1,210 @@ +# Pandoc and Quarto Typst Templates + +This document describes how Quarto integrates Pandoc's typst templates and transforms them into a more modular structure suitable for Quarto's extended functionality. + +## 1. How Pandoc Templates Are Copied into Quarto + +The `writePandocTemplates` function in `package/src/common/update-pandoc.ts` handles copying Pandoc's templates into Quarto's resources during the Pandoc update process. + +### Source and Destination + +- **Source**: Pandoc templates are downloaded from the Pandoc GitHub repository's `data/templates/` directory +- **Destination**: `src/resources/formats/typst/pandoc/` + +### Files Copied + +Two files are copied from Pandoc for typst support: + +| Pandoc Source | Quarto Destination | +|---------------|-------------------| +| `default.typst` | `typst.template` | +| `template.typst` | `template.typst` (unchanged name) | + +### Purpose of Each Pandoc File + +**default.typst** (becomes `typst.template`): The main Pandoc template that orchestrates the document structure. It: +- Defines a horizontal rule helper +- Sets up term list rendering with indented descriptions +- Configures table styling with no strokes +- Sets figure caption positions for tables (top) and images (bottom) +- Optionally includes syntax highlighting definitions +- Either imports a user-provided template or inlines the `template.typst` content via `$template.typst()$` +- Disables smart quotes if not enabled +- Processes header-includes +- Invokes the `conf()` function with all document metadata +- Renders include-before content, TOC, body, bibliography, and include-after content + +**template.typst**: Defines the `conf()` document function and helpers. It: +- Provides a `content-to-string` helper to extract text from content nodes +- Defines `conf()` which accepts all document metadata parameters +- Sets document metadata (title, keywords, author) for PDF accessibility +- Configures page, paragraph, text, and heading settings +- Applies link, reference, and file colors +- Renders the title block with optional thanks footnote +- Returns the document content + +## 2. Quarto's Modular Template Structure + +Quarto breaks the Pandoc templates into a modular structure in `src/resources/formats/typst/pandoc/quarto/`. This allows for: +- Better separation of concerns +- Easier customization and extension +- Support for Quarto-specific features (brand typography, callouts, subfloats) + +### Template Files + +The `typstFormat` function in `src/format/typst/format-typst.ts` configures the template context, specifying `template.typ` as the main template with these partials: `definitions.typ`, `typst-template.typ`, `page.typ`, `typst-show.typ`, `notes.typ`, and `biblio.typ`. + +### template.typ - The Orchestrator + +Assembles the document by including all partials in order: +1. `definitions.typ()` - utility definitions +2. `typst-template.typ()` - the article function +3. Header-includes loop +4. `page.typ()` - page configuration +5. `typst-show.typ()` - applies the article function +6. Include-before loop +7. Body +8. `notes.typ()` - endnotes handling +9. `biblio.typ()` - bibliography +10. Include-after loop + +### definitions.typ - Utility Definitions + +Combines Pandoc definitions with Quarto-specific functionality: + +**From Pandoc**: +- `content-to-string` helper to extract text from content nodes (used for PDF metadata and link colors) +- `horizontalrule` definition for horizontal rules +- Term list show rule with indented descriptions +- Code highlighting definitions (conditionally included) + +**Quarto additions**: +- `endnote` helper for endnote rendering +- Code block styling (gray background, padding, rounded corners) +- `block_with_new_content` helper for reconstructing blocks with modified content +- `empty` function to check if content is empty (handles strings and content nodes) +- Subfloat support via `quartosubfloatcounter` and `quarto_super` function for nested figures with sub-numbering +- Callout figure show rule that transforms callout figures with proper titles and cross-reference numbering +- `callout` function for rendering callout boxes with customizable colors, icons, and styling + +### typst-template.typ - The Article Function + +Corresponds to `conf()` in Pandoc's `template.typst`. Defines the `article()` function with these parameters: + +**Document Metadata** (from Pandoc): +- `title`, `subtitle`, `authors`, `keywords`, `date` +- `abstract-title`, `abstract`, `thanks` + +**Layout** (from Pandoc): +- `cols`, `lang`, `region` + +**Typography** (from Pandoc): +- `font`, `fontsize`, `mathfont`, `codefont`, `linestretch`, `sectionnumbering` +- `linkcolor`, `citecolor`, `filecolor` + +**Quarto Extensions**: +- `title-size`, `subtitle-size` for customizable title sizing +- `heading-family`, `heading-weight`, `heading-style`, `heading-color`, `heading-line-height` for brand typography +- `toc`, `toc_title`, `toc_depth`, `toc_indent` for integrated table of contents + +**Functionality**: +- Sets `document()` metadata (title, keywords, author string) for PDF accessibility +- Configures paragraph justification and leading from `linestretch` +- Applies conditional font settings using `set ... if` pattern (Typst 0.14+) +- Applies link colors using `content-to-string` helper +- Renders the title block with thanks footnote, author grid, date, and abstract +- Optionally renders table of contents +- Handles single or multi-column layout + +### page.typ - Page Configuration + +Extracted from Pandoc's `conf()` function to allow independent page setup: +- Sets paper size (defaults to "us-letter") +- Sets margins (defaults to x: 1.25in, y: 1.25in) +- Sets page numbering +- Sets column count +- Optionally sets a background logo image with configurable location, inset, width, and alt text (Quarto extension) + +### typst-show.typ - Parameter Mapping + +Applies the `article()` function via a show rule, mapping Pandoc metadata and brand.yaml values to parameters. Follows precedence rules where Pandoc metadata takes priority and brand.yaml provides fallbacks. + +**Pandoc Metadata Mappings**: +- `title`, `subtitle`, `date`, `abstract` → direct pass-through +- `by-author` → `authors` (uses Quarto's author normalization with `it.name.literal` and `it.affiliations`) +- `labels.abstract` → `abstract-title` (localized) +- `mainfont` → `font` +- `fontsize` → `fontsize` +- `mathfont` → `mathfont` +- `codefont` → `codefont` +- `linestretch` → `linestretch` +- `section-numbering` → `sectionnumbering` +- `thanks` → `thanks` +- `linkcolor`, `citecolor`, `filecolor` → link color parameters +- `keywords` → `keywords` +- `toc`, `toc-title`, `toc-depth`, `toc-indent` → TOC parameters +- `columns` → `cols` + +**Brand.yaml Fallbacks** (used when Pandoc metadata not set): +- `brand.typography.base.family` → `font` +- `brand.typography.base.size` → `fontsize` +- `brand.typography.monospace.family` → `codefont` +- `brand.typography.headings.family` → `heading-family` +- `brand.typography.headings.weight` → `heading-weight` +- `brand.typography.headings.style` → `heading-style` +- `brand.typography.headings.color` → `heading-color` +- `brand.typography.headings.line-height` → `heading-line-height` + +### notes.typ - Endnotes Section + +Renders endnotes when present: +- Adds vertical space and a horizontal rule +- Sets smaller text size (0.88em) +- Renders the notes content + +### biblio.typ - Bibliography + +Handles bibliography rendering: +- Sets bibliography style from CSL file or bibliography style option +- Renders bibliography from specified files + +### typst.template - Reference Copy + +This is the copy of Pandoc's `default.typst`, kept for reference. It is used when rendering without Quarto's template context (e.g., when a user provides a custom template). + +## 3. Parameter Summary Table + +| Pandoc Parameter | Quarto Parameter | Brand.yaml Fallback | Notes | +|-----------------|------------------|---------------------|-------| +| `title` | `title` | - | | +| `subtitle` | `subtitle` | - | | +| `author` | `authors` | - | Normalized via `by-author` | +| `keywords` | `keywords` | - | Array of quoted strings | +| `date` | `date` | - | | +| `abstract` | `abstract` | - | | +| `abstract-title` | `abstract-title` | `labels.abstract` | Localized | +| `thanks` | `thanks` | - | Title footnote | +| `mainfont` | `font` | `brand.typography.base.family` | | +| `fontsize` | `fontsize` | `brand.typography.base.size` | | +| `mathfont` | `mathfont` | - | | +| `codefont` | `codefont` | `brand.typography.monospace.family` | | +| `linestretch` | `linestretch` | - | Multiplied by 0.65em for leading | +| `section-numbering` | `sectionnumbering` | - | | +| `linkcolor` | `linkcolor` | - | Hex color string | +| `citecolor` | `citecolor` | - | Hex color string | +| `filecolor` | `filecolor` | - | Hex color string | +| `columns` | `cols` | - | | +| `papersize` | (in page.typ) | - | | +| `margin` | (in page.typ) | - | | +| `page-numbering` | (in page.typ) | - | | +| - | `title-size` | - | Quarto extension | +| - | `subtitle-size` | - | Quarto extension | +| - | `heading-family` | `brand.typography.headings.family` | Quarto extension | +| - | `heading-weight` | `brand.typography.headings.weight` | Quarto extension | +| - | `heading-style` | `brand.typography.headings.style` | Quarto extension | +| - | `heading-color` | `brand.typography.headings.color` | Quarto extension | +| - | `heading-line-height` | `brand.typography.headings.line-height` | Quarto extension | +| `toc` | `toc` | - | | +| `toc-title` | `toc_title` | - | | +| `toc-depth` | `toc_depth` | - | | +| `toc-indent` | `toc_indent` | - | Quarto extension | diff --git a/src/command/check/check.ts b/src/command/check/check.ts index 97c349dc82..dfba6f0346 100644 --- a/src/command/check/check.ts +++ b/src/command/check/check.ts @@ -243,19 +243,18 @@ async function checkVersions(conf: CheckConfiguration) { // loading the configuration file directly, but that // file is in an awkward format and it is not packaged // with our installers - const checkData: [string | undefined, string, string][] = strict - ? [ - [pandocVersion, "3.6.3", "Pandoc"], - [sassVersion, "1.87.0", "Dart Sass"], - [denoVersion, "2.4.5", "Deno"], - [typstVersion, "0.13.0", "Typst"], - ] - : [ - [pandocVersion, ">=3.6.3", "Pandoc"], - [sassVersion, ">=1.87.0", "Dart Sass"], - [denoVersion, ">=2.3.1", "Deno"], - [typstVersion, ">=0.13.0", "Typst"], - ]; + const versionConstraints: [string | undefined, string, string][] = [ + [pandocVersion, "3.8.3", "Pandoc"], + [sassVersion, "1.87.0", "Dart Sass"], + [denoVersion, "2.4.5", "Deno"], + [typstVersion, "0.14.2", "Typst"], + ]; + const checkData: [string | undefined, string, string][] = versionConstraints + .map(([version, ver, name]) => [ + version, + strict ? ver : `>=${ver}`, + name, + ]); const fun = strict ? strictCheckVersion : checkVersion; for (const [version, constraint, name] of checkData) { if (version === undefined) { diff --git a/src/core/pandoc/format-extension.ts b/src/core/pandoc/format-extension.ts index f305ce48ba..a03caf7430 100644 --- a/src/core/pandoc/format-extension.ts +++ b/src/core/pandoc/format-extension.ts @@ -38,8 +38,6 @@ export const kPandocExtensions = [ "-bracketed_spans", "+citations", "-citations", - "+compact_definition_lists", - "-compact_definition_lists", "+definition_lists", "-definition_lists", "+east_asian_line_breaks", @@ -146,12 +144,16 @@ export const kPandocExtensions = [ "-simple_tables", "+smart", "-smart", + "+smart_quotes", + "-smart_quotes", "+sourcepos", "-sourcepos", "+space_in_atx_header", "-space_in_atx_header", "+spaced_reference_links", "-spaced_reference_links", + "+special_strings", + "-special_strings", "+startnum", "-startnum", "+strikeout", @@ -162,6 +164,8 @@ export const kPandocExtensions = [ "-subscript", "+superscript", "-superscript", + "+table_attributes", + "-table_attributes", "+table_captions", "-table_captions", "+tagging", diff --git a/src/execute/engine-shared.ts b/src/execute/engine-shared.ts deleted file mode 100644 index e0b9e14a52..0000000000 --- a/src/execute/engine-shared.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * engine-shared.ts - * - * Copyright (C) 2021-2022 Posit Software, PBC - */ - -import { dirname, isAbsolute, join } from "../deno_ral/path.ts"; - -import { restorePreservedHtml } from "../core/jupyter/preserve.ts"; -import { PostProcessOptions } from "./types.ts"; - -export function postProcessRestorePreservedHtml(options: PostProcessOptions) { - // read the output file - - const outputPath = isAbsolute(options.output) - ? options.output - : join(dirname(options.target.input), options.output); - let output = Deno.readTextFileSync(outputPath); - - // substitute - output = restorePreservedHtml( - output, - options.preserve, - ); - - // re-write the output - Deno.writeTextFileSync(outputPath, output); -} - -export function languagesInMarkdownFile(file: string) { - return languagesInMarkdown(Deno.readTextFileSync(file)); -} - -export function languagesInMarkdown(markdown: string) { - // see if there are any code chunks in the file - const languages = new Set(); - const kChunkRegex = /^[\t >]*```+\s*\{([a-zA-Z0-9_]+)( *[ ,].*)?\}\s*$/gm; - kChunkRegex.lastIndex = 0; - let match = kChunkRegex.exec(markdown); - while (match) { - const language = match[1].toLowerCase(); - if (!languages.has(language)) { - languages.add(language); - } - match = kChunkRegex.exec(markdown); - } - kChunkRegex.lastIndex = 0; - return languages; -} diff --git a/src/execute/engine.ts b/src/execute/engine.ts index 497b8262aa..44b4b1fa7b 100644 --- a/src/execute/engine.ts +++ b/src/execute/engine.ts @@ -27,7 +27,7 @@ import { ExecutionTarget, kQmdExtensions, } from "./types.ts"; -import { languagesInMarkdown } from "./engine-shared.ts"; +import { languagesInMarkdown } from "../core/pandoc/pandoc-partition.ts"; import { languages as handlerLanguages } from "../core/handlers/base.ts"; import { RenderContext, RenderFlags } from "../command/render/types.ts"; import { mergeConfigs } from "../core/config.ts"; diff --git a/src/execute/ojs/compile.ts b/src/execute/ojs/compile.ts index cc33bc02b3..e87cbeb6dd 100644 --- a/src/execute/ojs/compile.ts +++ b/src/execute/ojs/compile.ts @@ -65,7 +65,7 @@ import { logError } from "../../core/log.ts"; import { breakQuartoMd, QuartoMdCell } from "../../core/lib/break-quarto-md.ts"; import { MappedString } from "../../core/mapped-text.ts"; -import { languagesInMarkdown } from "../engine-shared.ts"; +import { languagesInMarkdown } from "../../core/pandoc/pandoc-partition.ts"; import { pandocBlock, diff --git a/src/resources/filters/customnodes/floatreftarget.lua b/src/resources/filters/customnodes/floatreftarget.lua index 6a9a469d2b..0bb2678a59 100644 --- a/src/resources/filters/customnodes/floatreftarget.lua +++ b/src/resources/filters/customnodes/floatreftarget.lua @@ -455,6 +455,9 @@ end, function(float) "triggered this error.") return {} end + -- Strip Pandoc 3.8+ LTcaptype definition since we're adding our own caption + -- Keep the { } wrapper (harmless) to avoid orphan braces + longtable_preamble = longtable_preamble:gsub("\\def\\LTcaptype{none}[^\n]*\n?", "") -- split the content into params and actual content -- params are everything in the first line of longtable_content -- actual content is everything else diff --git a/src/resources/filters/quarto-post/typst-brand-yaml.lua b/src/resources/filters/quarto-post/typst-brand-yaml.lua index 3ca5be17bb..f23190d727 100644 --- a/src/resources/filters/quarto-post/typst-brand-yaml.lua +++ b/src/resources/filters/quarto-post/typst-brand-yaml.lua @@ -170,11 +170,12 @@ function render_typst_brand_yaml() end end + -- monospace font family is handled by codefont in typst-template.typ via typst-show.typ + -- here we only handle properties that Pandoc doesn't support: weight, size, color local monospaceInline = _quarto.modules.brand.get_typography(brandMode, 'monospace-inline') if monospaceInline and next(monospaceInline) then quarto.doc.include_text('in-header', table.concat({ '#show raw.where(block: false): set text(', - conditional_entry('font', monospaceInline.family and _quarto.modules.typst.css.translate_font_family_list(monospaceInline.family), false), conditional_entry('weight', _quarto.modules.typst.css.translate_font_weight(monospaceInline.weight)), conditional_entry('size', monospaceInline.size, false), conditional_entry('fill', monospaceInline.color, false), @@ -189,11 +190,12 @@ function render_typst_brand_yaml() })) end + -- monospace font family is handled by codefont in typst-template.typ via typst-show.typ + -- here we only handle properties that Pandoc doesn't support: weight, size, color local monospaceBlock = _quarto.modules.brand.get_typography(brandMode, 'monospace-block') if monospaceBlock and next(monospaceBlock) then quarto.doc.include_text('in-header', table.concat({ '#show raw.where(block: true): set text(', - conditional_entry('font', monospaceBlock.family and _quarto.modules.typst.css.translate_font_family_list(monospaceBlock.family), false), conditional_entry('weight', _quarto.modules.typst.css.translate_font_weight(monospaceBlock.weight)), conditional_entry('size', monospaceBlock.size, false), conditional_entry('fill', monospaceBlock.color, false), @@ -307,7 +309,7 @@ function render_typst_brand_yaml() inset = '0.75in' end logoOptions.width = _quarto.modules.typst.css.translate_length(logoOptions.width or '1.5in') - logoOptions.inset = inset + logoOptions.inset = pandoc.RawInline('typst', inset) logoOptions.location = logoOptions.location and location_to_typst_align(logoOptions.location) or 'left+top' quarto.log.debug('logo options', logoOptions) @@ -356,6 +358,13 @@ function render_typst_brand_yaml() ['line-height'] = line_height_to_leading(headings['line-height'] or base['line-height']), } end + + local monospace = _quarto.modules.brand.get_typography(brandMode, 'monospace') + if monospace and monospace.family then + meta.brand.typography.monospace = { + family = pandoc.RawInline('typst', _quarto.modules.typst.css.translate_font_family_list(monospace.family)), + } + end return meta end, } diff --git a/src/resources/formats/beamer/pandoc/babel-lang.tex b/src/resources/formats/beamer/pandoc/babel-lang.tex index f29c833cb1..ad73df7b0d 100644 --- a/src/resources/formats/beamer/pandoc/babel-lang.tex +++ b/src/resources/formats/beamer/pandoc/babel-lang.tex @@ -3,9 +3,9 @@ $-- $if(lang)$ \ifLuaTeX -\usepackage[bidi=basic$for(babeloptions)$,$babeloptions$$endfor$]{babel} +\usepackage[bidi=basic$if(shorthands)$$else$,shorthands=off$endif$$for(babeloptions)$,$babeloptions$$endfor$]{babel} \else -\usepackage[bidi=default$for(babeloptions)$,$babeloptions$$endfor$]{babel} +\usepackage[bidi=default$if(shorthands)$$else$,shorthands=off$endif$$for(babeloptions)$,$babeloptions$$endfor$]{babel} \fi $if(babel-lang)$ $if(mainfont)$ @@ -18,12 +18,7 @@ $for(babelfonts/pairs)$ \babelfont[$babelfonts.key$]{rm}{$babelfonts.value$} $endfor$ -% get rid of language-specific shorthands (see #6817): -\let\LanguageShortHands\languageshorthands -\def\languageshorthands#1{} -$if(selnolig-langs)$ \ifLuaTeX - \usepackage[$for(selnolig-langs)$$it$$sep$,$endfor$]{selnolig} % disable illegal ligatures + \usepackage{selnolig} % disable illegal ligatures \fi -$endif$ $endif$ \ No newline at end of file diff --git a/src/resources/formats/beamer/pandoc/beamer.template b/src/resources/formats/beamer/pandoc/beamer.template index b2eafaadf9..6e2c0ed80c 100644 --- a/src/resources/formats/beamer/pandoc/beamer.template +++ b/src/resources/formats/beamer/pandoc/beamer.template @@ -108,12 +108,12 @@ $after-header-includes.latex()$ $hypersetup.latex()$ $if(title)$ -\title$if(shorttitle)$[$shorttitle$]$endif${$title$$if(thanks)$\thanks{$thanks$}$endif$} +\title$if(shorttitle)$[$shorttitle$]$endif${\texorpdfstring{$title$}{$title-meta$}$if(thanks)$\thanks{$thanks$}$endif$} $endif$ $if(subtitle)$ -\subtitle$if(shortsubtitle)$[$shortsubtitle$]$endif${$subtitle$} +\subtitle$if(shortsubtitle)$[$shortsubtitle$]$endif${\texorpdfstring{$subtitle$}{$subtitle-meta$}} $endif$ -\author$if(shortauthor)$[$shortauthor$]$endif${$for(author)$$author$$sep$ \and $endfor$} +\author$if(shortauthor)$[$shortauthor$]$endif${\texorpdfstring{$for(author)$$author$$sep$ \and $endfor$}{$author-meta$}} \date$if(shortdate)$[$shortdate$]$endif${$date$} $if(institute)$ \institute$if(shortinstitute)$[$shortinstitute$]$endif${$for(institute)$$institute$$sep$ \and $endfor$} diff --git a/src/resources/formats/beamer/pandoc/latex.common b/src/resources/formats/beamer/pandoc/latex.common index 3f93b1b883..1f26b339fa 100644 --- a/src/resources/formats/beamer/pandoc/latex.common +++ b/src/resources/formats/beamer/pandoc/latex.common @@ -69,6 +69,7 @@ $-- tables $-- $if(tables)$ \usepackage{longtable,booktabs,array} +\newcounter{none} % for unnumbered tables $if(multirow)$ \usepackage{multirow} $endif$ @@ -183,9 +184,9 @@ $-- Babel language support $-- $if(lang)$ \ifLuaTeX -\usepackage[bidi=basic$for(babeloptions)$,$babeloptions$$endfor$]{babel} +\usepackage[bidi=basic$if(shorthands)$$else$,shorthands=off$endif$$for(babeloptions)$,$babeloptions$$endfor$]{babel} \else -\usepackage[bidi=default$for(babeloptions)$,$babeloptions$$endfor$]{babel} +\usepackage[bidi=default$if(shorthands)$$else$,shorthands=off$endif$$for(babeloptions)$,$babeloptions$$endfor$]{babel} \fi $if(babel-lang)$ $if(mainfont)$ @@ -198,15 +199,10 @@ $endif$ $for(babelfonts/pairs)$ \babelfont[$babelfonts.key$]{rm}{$babelfonts.value$} $endfor$ -% get rid of language-specific shorthands (see #6817): -\let\LanguageShortHands\languageshorthands -\def\languageshorthands#1{} -$if(selnolig-langs)$ \ifLuaTeX - \usepackage[$for(selnolig-langs)$$it$$sep$,$endfor$]{selnolig} % disable illegal ligatures + \usepackage{selnolig} % disable illegal ligatures \fi $endif$ -$endif$ $-- $-- pagestyle $-- @@ -239,6 +235,12 @@ $if(dir)$ \newenvironment{RTL}{\beginR}{\endR} \newenvironment{LTR}{\beginL}{\endL} \fi +\ifluatex + \newcommand{\RL}[1]{\bgroup\textdir TRT#1\egroup} + \newcommand{\LR}[1]{\bgroup\textdir TLT#1\egroup} + \newenvironment{RTL}{\textdir TRT\pardir TRT\bodydir TRT}{} + \newenvironment{LTR}{\textdir TLT\pardir TLT\bodydir TLT}{} +\fi $endif$ $-- $-- bibliography support support for natbib and biblatex diff --git a/src/resources/formats/beamer/pandoc/pandoc.tex b/src/resources/formats/beamer/pandoc/pandoc.tex index c69bf78e5e..993d9da388 100644 --- a/src/resources/formats/beamer/pandoc/pandoc.tex +++ b/src/resources/formats/beamer/pandoc/pandoc.tex @@ -75,6 +75,12 @@ \newenvironment{RTL}{\beginR}{\endR} \newenvironment{LTR}{\beginL}{\endL} \fi +\ifluatex + \newcommand{\RL}[1]{\bgroup\textdir TRT#1\egroup} + \newcommand{\LR}[1]{\bgroup\textdir TLT#1\egroup} + \newenvironment{RTL}{\textdir TRT\pardir TRT\bodydir TRT}{} + \newenvironment{LTR}{\textdir TLT\pardir TLT\bodydir TLT}{} +\fi $endif$ $biblio-config.tex()$ diff --git a/src/resources/formats/beamer/pandoc/tables.tex b/src/resources/formats/beamer/pandoc/tables.tex index c539bd640f..443a14ab2f 100644 --- a/src/resources/formats/beamer/pandoc/tables.tex +++ b/src/resources/formats/beamer/pandoc/tables.tex @@ -3,6 +3,7 @@ $-- $if(tables)$ \usepackage{longtable,booktabs,array} +\newcounter{none} % for unnumbered tables $if(multirow)$ \usepackage{multirow} $endif$ diff --git a/src/resources/formats/beamer/pandoc/title.tex b/src/resources/formats/beamer/pandoc/title.tex index d55b4cd4e9..a3163a911c 100644 --- a/src/resources/formats/beamer/pandoc/title.tex +++ b/src/resources/formats/beamer/pandoc/title.tex @@ -1,11 +1,11 @@ $-- Specific title partial for Beamer presentations $if(title)$ -\title$if(shorttitle)$[$shorttitle$]$endif${$title$$if(thanks)$\thanks{$thanks$}$endif$} +\title$if(shorttitle)$[$shorttitle$]$endif${\texorpdfstring{$title$}{$title-meta$}$if(thanks)$\thanks{$thanks$}$endif$} $endif$ $if(subtitle)$ -\subtitle$if(shortsubtitle)$[$shortsubtitle$]$endif${$subtitle$} +\subtitle$if(shortsubtitle)$[$shortsubtitle$]$endif${\texorpdfstring{$subtitle$}{$subtitle-meta$}} $endif$ -$-- This supports QUarto's authors normalization +$-- This supports QUarto's authors normalization (no support for author-meta yet) \author$if(shortauthor)$[$shortauthor$]$endif${$for(authors)$$it.name.literal$$sep$ \and $endfor$} \date$if(shortdate)$[$shortdate$]$endif${$date$} $if(institute)$ diff --git a/src/resources/formats/html/pandoc/html.styles b/src/resources/formats/html/pandoc/html.styles index 9e253e3e14..800f63c7a6 100644 --- a/src/resources/formats/html/pandoc/html.styles +++ b/src/resources/formats/html/pandoc/html.styles @@ -1,3 +1,6 @@ +/* Default styles provided by pandoc. +** See https://pandoc.org/MANUAL.html#variables-for-html for config info. +*/ $if(document-css)$ html { $if(mainfont)$ diff --git a/src/resources/formats/html/pandoc/styles.html b/src/resources/formats/html/pandoc/styles.html index a10531a967..ebcc014be1 100644 --- a/src/resources/formats/html/pandoc/styles.html +++ b/src/resources/formats/html/pandoc/styles.html @@ -1,3 +1,6 @@ +/* Default styles provided by pandoc. +** See https://pandoc.org/MANUAL.html#variables-for-html for config info. +*/ $if(document-css)$ html { $if(mainfont)$ diff --git a/src/resources/formats/jats/pandoc/default-templates/article.jats_publishing b/src/resources/formats/jats/pandoc/default-templates/article.jats_publishing index d402b8bbd3..b33263cfd3 100644 --- a/src/resources/formats/jats/pandoc/default-templates/article.jats_publishing +++ b/src/resources/formats/jats/pandoc/default-templates/article.jats_publishing @@ -98,7 +98,7 @@ $if(author.surname)$ $if(author.non-dropping-particle)$${author.non-dropping-particle} $endif$$author.surname$ $author.given-names$$if(author.dropping-particle)$ ${author.dropping-particle}$endif$ $if(author.prefix)$ -${author.suffix} +${author.prefix} $endif$ $if(author.suffix)$ ${author.suffix} diff --git a/src/resources/formats/jats/pandoc/default-templates/default.jats_articleauthoring b/src/resources/formats/jats/pandoc/default-templates/default.jats_articleauthoring index 627911ae51..5a4fa21287 100644 --- a/src/resources/formats/jats/pandoc/default-templates/default.jats_articleauthoring +++ b/src/resources/formats/jats/pandoc/default-templates/default.jats_articleauthoring @@ -31,7 +31,7 @@ $if(author.surname)$ $if(author.non-dropping-particle)$${author.non-dropping-particle} $endif$${author.surname} ${author.given-names}$if(author.dropping-particle)$ ${author.dropping-particle}$endif$ $if(author.prefix)$ -${author.suffix} +${author.prefix} $endif$ $if(author.suffix)$ ${author.suffix} diff --git a/src/resources/formats/pdf/pandoc/babel-lang.tex b/src/resources/formats/pdf/pandoc/babel-lang.tex index f29c833cb1..ad73df7b0d 100644 --- a/src/resources/formats/pdf/pandoc/babel-lang.tex +++ b/src/resources/formats/pdf/pandoc/babel-lang.tex @@ -3,9 +3,9 @@ $-- $if(lang)$ \ifLuaTeX -\usepackage[bidi=basic$for(babeloptions)$,$babeloptions$$endfor$]{babel} +\usepackage[bidi=basic$if(shorthands)$$else$,shorthands=off$endif$$for(babeloptions)$,$babeloptions$$endfor$]{babel} \else -\usepackage[bidi=default$for(babeloptions)$,$babeloptions$$endfor$]{babel} +\usepackage[bidi=default$if(shorthands)$$else$,shorthands=off$endif$$for(babeloptions)$,$babeloptions$$endfor$]{babel} \fi $if(babel-lang)$ $if(mainfont)$ @@ -18,12 +18,7 @@ $for(babelfonts/pairs)$ \babelfont[$babelfonts.key$]{rm}{$babelfonts.value$} $endfor$ -% get rid of language-specific shorthands (see #6817): -\let\LanguageShortHands\languageshorthands -\def\languageshorthands#1{} -$if(selnolig-langs)$ \ifLuaTeX - \usepackage[$for(selnolig-langs)$$it$$sep$,$endfor$]{selnolig} % disable illegal ligatures + \usepackage{selnolig} % disable illegal ligatures \fi -$endif$ $endif$ \ No newline at end of file diff --git a/src/resources/formats/pdf/pandoc/latex.common b/src/resources/formats/pdf/pandoc/latex.common index 3f93b1b883..1f26b339fa 100644 --- a/src/resources/formats/pdf/pandoc/latex.common +++ b/src/resources/formats/pdf/pandoc/latex.common @@ -69,6 +69,7 @@ $-- tables $-- $if(tables)$ \usepackage{longtable,booktabs,array} +\newcounter{none} % for unnumbered tables $if(multirow)$ \usepackage{multirow} $endif$ @@ -183,9 +184,9 @@ $-- Babel language support $-- $if(lang)$ \ifLuaTeX -\usepackage[bidi=basic$for(babeloptions)$,$babeloptions$$endfor$]{babel} +\usepackage[bidi=basic$if(shorthands)$$else$,shorthands=off$endif$$for(babeloptions)$,$babeloptions$$endfor$]{babel} \else -\usepackage[bidi=default$for(babeloptions)$,$babeloptions$$endfor$]{babel} +\usepackage[bidi=default$if(shorthands)$$else$,shorthands=off$endif$$for(babeloptions)$,$babeloptions$$endfor$]{babel} \fi $if(babel-lang)$ $if(mainfont)$ @@ -198,15 +199,10 @@ $endif$ $for(babelfonts/pairs)$ \babelfont[$babelfonts.key$]{rm}{$babelfonts.value$} $endfor$ -% get rid of language-specific shorthands (see #6817): -\let\LanguageShortHands\languageshorthands -\def\languageshorthands#1{} -$if(selnolig-langs)$ \ifLuaTeX - \usepackage[$for(selnolig-langs)$$it$$sep$,$endfor$]{selnolig} % disable illegal ligatures + \usepackage{selnolig} % disable illegal ligatures \fi $endif$ -$endif$ $-- $-- pagestyle $-- @@ -239,6 +235,12 @@ $if(dir)$ \newenvironment{RTL}{\beginR}{\endR} \newenvironment{LTR}{\beginL}{\endL} \fi +\ifluatex + \newcommand{\RL}[1]{\bgroup\textdir TRT#1\egroup} + \newcommand{\LR}[1]{\bgroup\textdir TLT#1\egroup} + \newenvironment{RTL}{\textdir TRT\pardir TRT\bodydir TRT}{} + \newenvironment{LTR}{\textdir TLT\pardir TLT\bodydir TLT}{} +\fi $endif$ $-- $-- bibliography support support for natbib and biblatex diff --git a/src/resources/formats/pdf/pandoc/latex.template b/src/resources/formats/pdf/pandoc/latex.template index 781133776c..6c70459554 100644 --- a/src/resources/formats/pdf/pandoc/latex.template +++ b/src/resources/formats/pdf/pandoc/latex.template @@ -24,6 +24,9 @@ $if(geometry)$ \usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry} $endif$ \usepackage{amsmath,amssymb} +$if(cancel)$ +\usepackage{cancel} +$endif$ $-- $-- section numbering $-- @@ -40,6 +43,19 @@ $header-includes$ $endfor$ $after-header-includes.latex()$ $hypersetup.latex()$ +$if(pdf-trailer-id)$ + +\ifXeTeX +\special{pdf:trailerid [ $pdf-trailer-id$ ]} +\fi +\ifPDFTeX +\pdftrailerid{} +\pdftrailer{/ID [ $pdf-trailer-id$ ]} +\fi +\ifLuaTeX +\pdfvariable trailerid {[ $pdf-trailer-id$ ]} +\fi +$endif$ $if(title)$ \title{$title$$if(thanks)$\thanks{$thanks$}$endif$} @@ -79,7 +95,9 @@ $if(toc-title)$ $endif$ { $if(colorlinks)$ -\hypersetup{linkcolor=$if(toccolor)$$toccolor$$else$$endif$} +$if(toccolor)$ +\hypersetup{linkcolor=$toccolor$} +$endif$ $endif$ \setcounter{tocdepth}{$toc-depth$} \tableofcontents diff --git a/src/resources/formats/pdf/pandoc/pandoc.tex b/src/resources/formats/pdf/pandoc/pandoc.tex index c69bf78e5e..993d9da388 100644 --- a/src/resources/formats/pdf/pandoc/pandoc.tex +++ b/src/resources/formats/pdf/pandoc/pandoc.tex @@ -75,6 +75,12 @@ \newenvironment{RTL}{\beginR}{\endR} \newenvironment{LTR}{\beginL}{\endL} \fi +\ifluatex + \newcommand{\RL}[1]{\bgroup\textdir TRT#1\egroup} + \newcommand{\LR}[1]{\bgroup\textdir TLT#1\egroup} + \newenvironment{RTL}{\textdir TRT\pardir TRT\bodydir TRT}{} + \newenvironment{LTR}{\textdir TLT\pardir TLT\bodydir TLT}{} +\fi $endif$ $biblio-config.tex()$ diff --git a/src/resources/formats/pdf/pandoc/tables.tex b/src/resources/formats/pdf/pandoc/tables.tex index c539bd640f..443a14ab2f 100644 --- a/src/resources/formats/pdf/pandoc/tables.tex +++ b/src/resources/formats/pdf/pandoc/tables.tex @@ -3,6 +3,7 @@ $-- $if(tables)$ \usepackage{longtable,booktabs,array} +\newcounter{none} % for unnumbered tables $if(multirow)$ \usepackage{multirow} $endif$ diff --git a/src/resources/formats/pdf/pandoc/template.tex b/src/resources/formats/pdf/pandoc/template.tex index 431d79b4cb..a2d0943360 100644 --- a/src/resources/formats/pdf/pandoc/template.tex +++ b/src/resources/formats/pdf/pandoc/template.tex @@ -11,6 +11,9 @@ \usepackage[$for(geometry)$$geometry$$sep$,$endfor$]{geometry} $endif$ \usepackage{amsmath,amssymb} +$if(cancel)$ +\usepackage{cancel} +$endif$ $-- $-- section numbering $-- @@ -24,6 +27,19 @@ $common.latex()$ $after-header-includes.latex()$ $hypersetup.latex()$ +$if(pdf-trailer-id)$ + +\ifXeTeX +\special{pdf:trailerid [ $pdf-trailer-id$ ]} +\fi +\ifPDFTeX +\pdftrailerid{} +\pdftrailer{/ID [ $pdf-trailer-id$ ]} +\fi +\ifLuaTeX +\pdfvariable trailerid {[ $pdf-trailer-id$ ]} +\fi +$endif$ $before-title.tex()$ diff --git a/src/resources/formats/pdf/pandoc/toc.tex b/src/resources/formats/pdf/pandoc/toc.tex index 7e6b2173d1..0da4745483 100644 --- a/src/resources/formats/pdf/pandoc/toc.tex +++ b/src/resources/formats/pdf/pandoc/toc.tex @@ -4,7 +4,9 @@ $endif$ { $if(colorlinks)$ -\hypersetup{linkcolor=$if(toccolor)$$toccolor$$else$$endif$} +$if(toccolor)$ +\hypersetup{linkcolor=$toccolor$} +$endif$ $endif$ \setcounter{tocdepth}{$toc-depth$} \tableofcontents diff --git a/src/resources/formats/revealjs/pandoc/revealjs.template b/src/resources/formats/revealjs/pandoc/revealjs.template index 78edbae77c..5278dd1457 100644 --- a/src/resources/formats/revealjs/pandoc/revealjs.template +++ b/src/resources/formats/revealjs/pandoc/revealjs.template @@ -232,6 +232,19 @@ $endif$ // devices. It is advisable to set this to a lower number than // viewDistance in order to save resources. mobileViewDistance: $mobileViewDistance$, +$if(view)$ + + // Enable scroll view + view: '$view/nowrap$', + // see https://revealjs.com/scroll-view/#scrollbar + scrollProgress: $scrollProgress$, + // see https://revealjs.com/scroll-view/#url-activation + scrollActivationWidth: '$scrollActivationWidth$', + // see https://revealjs.com/scroll-view/#scroll-snapping + scrollSnap: '$scrollSnap$', + // Experimental. see https://revealjs.com/scroll-view/#scroll-snapping + scrollLayout: '$scrollLayout$', +$endif$ $if(parallaxBackgroundImage)$ // Parallax background image diff --git a/src/resources/formats/revealjs/pandoc/template.html b/src/resources/formats/revealjs/pandoc/template.html index 24f2922ec5..5c1c1649e0 100644 --- a/src/resources/formats/revealjs/pandoc/template.html +++ b/src/resources/formats/revealjs/pandoc/template.html @@ -217,6 +217,7 @@ // devices. It is advisable to set this to a lower number than // viewDistance in order to save resources. mobileViewDistance: $mobileViewDistance$, +$-- // TODO: Add scroll view option in template: https://github.com/quarto-dev/quarto-cli/issues/13852 $if(parallaxBackgroundImage)$ // Parallax background image diff --git a/src/resources/formats/typst/pandoc/quarto/definitions.typ b/src/resources/formats/typst/pandoc/quarto/definitions.typ index fff6dcdc4c..c5b2f55e64 100644 --- a/src/resources/formats/typst/pandoc/quarto/definitions.typ +++ b/src/resources/formats/typst/pandoc/quarto/definitions.typ @@ -1,4 +1,16 @@ // Some definitions presupposed by pandoc's typst output. +#let content-to-string(content) = { + if content.has("text") { + content.text + } else if content.has("children") { + content.children.map(content-to-string).join("") + } else if content.has("body") { + content-to-string(content.body) + } else if content == [ ] { + " " + } +} + #let horizontalrule = line(start: (25%,0%), end: (75%,0%)) #let endnote(num, contents) = [ @@ -171,3 +183,8 @@ ) } +$if(highlighting-definitions)$ +// syntax highlighting functions from skylighting: +$highlighting-definitions$ + +$endif$ diff --git a/src/resources/formats/typst/pandoc/quarto/page.typ b/src/resources/formats/typst/pandoc/quarto/page.typ index f5af676afd..a028942860 100644 --- a/src/resources/formats/typst/pandoc/quarto/page.typ +++ b/src/resources/formats/typst/pandoc/quarto/page.typ @@ -2,6 +2,7 @@ paper: $if(papersize)$"$papersize$"$else$"us-letter"$endif$, margin: $if(margin)$($for(margin/pairs)$$margin.key$: $margin.value$,$endfor$)$else$(x: 1.25in, y: 1.25in)$endif$, numbering: $if(page-numbering)$"$page-numbering$"$else$none$endif$, + columns: $if(columns)$$columns$$else$1$endif$, ) $if(logo)$ #set page(background: align($logo.location$, box(inset: $logo.inset$, image("$logo.path$", width: $logo.width$$if(logo.alt)$, alt: "$logo.alt$"$endif$)))) diff --git a/src/resources/formats/typst/pandoc/quarto/typst-show.typ b/src/resources/formats/typst/pandoc/quarto/typst-show.typ index 472e71c0c4..e929d2777e 100644 --- a/src/resources/formats/typst/pandoc/quarto/typst-show.typ +++ b/src/resources/formats/typst/pandoc/quarto/typst-show.typ @@ -61,6 +61,32 @@ $endif$ $if(section-numbering)$ sectionnumbering: "$section-numbering$", $endif$ +$if(mathfont)$ + mathfont: ($for(mathfont)$"$mathfont$",$endfor$), +$endif$ +$if(codefont)$ + codefont: ($for(codefont)$"$codefont$",$endfor$), +$elseif(brand.typography.monospace.family)$ + codefont: $brand.typography.monospace.family$, +$endif$ +$if(linestretch)$ + linestretch: $linestretch$, +$endif$ +$if(thanks)$ + thanks: [$thanks$], +$endif$ +$if(linkcolor)$ + linkcolor: [$linkcolor$], +$endif$ +$if(citecolor)$ + citecolor: [$citecolor$], +$endif$ +$if(filecolor)$ + filecolor: [$filecolor$], +$endif$ +$if(keywords)$ + keywords: ($for(keywords)$"$keywords$",$endfor$), +$endif$ $if(toc)$ toc: $toc$, $endif$ @@ -71,6 +97,5 @@ $if(toc-indent)$ toc_indent: $toc-indent$, $endif$ toc_depth: $toc-depth$, - cols: $if(columns)$$columns$$else$1$endif$, doc, ) diff --git a/src/resources/formats/typst/pandoc/quarto/typst-template.typ b/src/resources/formats/typst/pandoc/quarto/typst-template.typ index 02c0dc041e..a1c80798c6 100644 --- a/src/resources/formats/typst/pandoc/quarto/typst-template.typ +++ b/src/resources/formats/typst/pandoc/quarto/typst-template.typ @@ -1,85 +1,114 @@ - #let article( title: none, subtitle: none, authors: none, + keywords: (), date: none, - abstract: none, abstract-title: none, + abstract: none, + thanks: none, cols: 1, lang: "en", region: "US", - font: "libertinus serif", + font: none, fontsize: 11pt, title-size: 1.5em, subtitle-size: 1.25em, - heading-family: "libertinus serif", + heading-family: none, heading-weight: "bold", heading-style: "normal", heading-color: black, heading-line-height: 0.65em, + mathfont: none, + codefont: none, + linestretch: 1, sectionnumbering: none, + linkcolor: none, + citecolor: none, + filecolor: none, toc: false, toc_title: none, toc_depth: none, toc_indent: 1.5em, doc, ) = { - set par(justify: true) + // Set document metadata for PDF accessibility + set document(title: title, keywords: keywords) + set document( + author: authors.map(author => content-to-string(author.name)).join(", ", last: " & "), + ) if authors != none and authors != () + set par( + justify: true, + leading: linestretch * 0.65em + ) set text(lang: lang, region: region, - font: font, size: fontsize) + set text(font: font) if font != none + show math.equation: set text(font: mathfont) if mathfont != none + show raw: set text(font: codefont) if codefont != none + set heading(numbering: sectionnumbering) - if title != none { - align(center)[#block(inset: 2em)[ - #set par(leading: heading-line-height) - #if (heading-family != none or heading-weight != "bold" or heading-style != "normal" - or heading-color != black) { - set text(font: heading-family, weight: heading-weight, style: heading-style, fill: heading-color) - text(size: title-size)[#title] - if subtitle != none { + + show link: set text(fill: rgb(content-to-string(linkcolor))) if linkcolor != none + show ref: set text(fill: rgb(content-to-string(citecolor))) if citecolor != none + show link: this => { + if filecolor != none and type(this.dest) == label { + text(this, fill: rgb(content-to-string(filecolor))) + } else { + text(this) + } + } + + place(top, float: true, scope: "parent", clearance: 4mm)[ + #if title != none { + align(center, block(inset: 2em)[ + #set par(leading: heading-line-height) if heading-line-height != none + #set text(font: heading-family) if heading-family != none + #set text(weight: heading-weight) + #set text(style: heading-style) if heading-style != "normal" + #set text(fill: heading-color) if heading-color != black + + #text(size: title-size)[#title #if thanks != none { + footnote(thanks, numbering: "*") + counter(footnote).update(n => n - 1) + }] + #(if subtitle != none { parbreak() text(size: subtitle-size)[#subtitle] - } - } else { - text(weight: "bold", size: title-size)[#title] - if subtitle != none { - parbreak() - text(weight: "bold", size: subtitle-size)[#subtitle] - } - } - ]] - } + }) + ]) + } - if authors != none { - let count = authors.len() - let ncols = calc.min(count, 3) - grid( - columns: (1fr,) * ncols, - row-gutter: 1.5em, - ..authors.map(author => - align(center)[ - #author.name \ - #author.affiliation \ - #author.email - ] + #if authors != none and authors != () { + let count = authors.len() + let ncols = calc.min(count, 3) + grid( + columns: (1fr,) * ncols, + row-gutter: 1.5em, + ..authors.map(author => + align(center)[ + #author.name \ + #author.affiliation \ + #author.email + ] + ) ) - ) - } + } - if date != none { - align(center)[#block(inset: 1em)[ - #date - ]] - } + #if date != none { + align(center)[#block(inset: 1em)[ + #date + ]] + } - if abstract != none { - block(inset: 2em)[ - #text(weight: "semibold")[#abstract-title] #h(1em) #abstract - ] - } + #if abstract != none { + block(inset: 2em)[ + #text(weight: "semibold")[#abstract-title] #h(1em) #abstract + ] + } + ] if toc { let title = if toc_title == none { @@ -96,11 +125,7 @@ ] } - if cols == 1 { - doc - } else { - columns(cols, doc) - } + doc } #set table( diff --git a/src/resources/formats/typst/pandoc/template.typst b/src/resources/formats/typst/pandoc/template.typst index 1889bef6fa..648476620f 100644 --- a/src/resources/formats/typst/pandoc/template.typst +++ b/src/resources/formats/typst/pandoc/template.typst @@ -15,74 +15,104 @@ authors: (), keywords: (), date: none, + abstract-title: none, abstract: none, + thanks: none, cols: 1, margin: (x: 1.25in, y: 1.25in), paper: "us-letter", lang: "en", region: "US", - font: (), + font: none, fontsize: 11pt, + mathfont: none, + codefont: none, + linestretch: 1, sectionnumbering: none, + linkcolor: none, + citecolor: none, + filecolor: none, pagenumbering: "1", doc, ) = { set document( title: title, - author: authors.map(author => content-to-string(author.name)), keywords: keywords, ) + set document( + author: authors.map(author => content-to-string(author.name)).join(", ", last: " & "), + ) if authors != none and authors != () set page( paper: paper, margin: margin, numbering: pagenumbering, - columns: cols, - ) - set par(justify: true) + columns: cols + ) + + set par( + justify: true, + leading: linestretch * 0.65em + ) set text(lang: lang, region: region, - font: font, size: fontsize) + + set text(font: font) if font != none + show math.equation: set text(font: mathfont) if mathfont != none + show raw: set text(font: codefont) if codefont != none + set heading(numbering: sectionnumbering) - place(top, float: true, scope: "parent", clearance: 4mm)[ - #if title != none { - align(center)[#block(inset: 2em)[ - #text(weight: "bold", size: 1.5em)[#title] - #(if subtitle != none { - parbreak() - text(weight: "bold", size: 1.25em)[#subtitle] - }) - ]] + show link: set text(fill: rgb(content-to-string(linkcolor))) if linkcolor != none + show ref: set text(fill: rgb(content-to-string(citecolor))) if citecolor != none + show link: this => { + if filecolor != none and type(this.dest) == label { + text(this, fill: rgb(content-to-string(filecolor))) + } else { + text(this) + } } - #if authors != none and authors != [] { - let count = authors.len() - let ncols = calc.min(count, 3) - grid( - columns: (1fr,) * ncols, - row-gutter: 1.5em, - ..authors.map(author => - align(center)[ - #author.name \ - #author.affiliation \ - #author.email - ] + block(below: 1em, width: 100%)[ + #if title != none { + align(center, block[ + #text(weight: "bold", size: 1.5em)[#title #if thanks != none { + footnote(thanks, numbering: "*") + counter(footnote).update(n => n - 1) + }] + #( + if subtitle != none { + parbreak() + text(weight: "bold", size: 1.25em)[#subtitle] + } + )]) + } + + #if authors != none and authors != [] { + let count = authors.len() + let ncols = calc.min(count, 3) + grid( + columns: (1fr,) * ncols, + row-gutter: 1.5em, + ..authors.map(author => align(center)[ + #author.name \ + #author.affiliation \ + #author.email + ]) ) - ) - } + } - #if date != none { - align(center)[#block(inset: 1em)[ - #date - ]] - } + #if date != none { + align(center)[#block(inset: 1em)[ + #date + ]] + } - #if abstract != none { - block(inset: 2em)[ - #text(weight: "semibold")[Abstract] #h(1em) #abstract - ] - } + #if abstract != none { + block(inset: 2em)[ + #text(weight: "semibold")[#abstract-title] #h(1em) #abstract + ] + } ] doc diff --git a/src/resources/formats/typst/pandoc/typst.template b/src/resources/formats/typst/pandoc/typst.template index 79b2c33f58..a499cd8db9 100644 --- a/src/resources/formats/typst/pandoc/typst.template +++ b/src/resources/formats/typst/pandoc/typst.template @@ -22,6 +22,11 @@ kind: image ): set figure.caption(position: $if(figure-caption-position)$$figure-caption-position$$else$bottom$endif$) +$if(highlighting-definitions)$ +// syntax highlighting functions from skylighting: +$highlighting-definitions$ + +$endif$ $if(template)$ #import "$template$": conf $else$ @@ -60,7 +65,7 @@ $endfor$ ), $endif$ $if(keywords)$ - keywords: ($for(keywords)$$keyword$$sep$,$endfor$), + keywords: ($for(keywords)$$keywords$$sep$,$endfor$), $endif$ $if(date)$ date: [$date$], @@ -71,9 +76,15 @@ $endif$ $if(region)$ region: "$region$", $endif$ +$if(abstract-title)$ + abstract-title: [$abstract-title$], +$endif$ $if(abstract)$ abstract: [$abstract$], $endif$ +$if(thanks)$ + thanks: [$thanks$], +$endif$ $if(margin)$ margin: ($for(margin/pairs)$$margin.key$: $margin.value$,$endfor$), $endif$ @@ -86,10 +97,28 @@ $endif$ $if(fontsize)$ fontsize: $fontsize$, $endif$ +$if(mathfont)$ + mathfont: ($for(mathfont)$"$mathfont$",$endfor$), +$endif$ +$if(codefont)$ + codefont: ($for(codefont)$"$codefont$",$endfor$), +$endif$ +$if(linestretch)$ + linestretch: $linestretch$, +$endif$ $if(section-numbering)$ sectionnumbering: "$section-numbering$", $endif$ pagenumbering: $if(page-numbering)$"$page-numbering$"$else$none$endif$, +$if(linkcolor)$ + linkcolor: [$linkcolor$], +$endif$ +$if(citecolor)$ + citecolor: [$citecolor$], +$endif$ +$if(filecolor)$ + filecolor: [$filecolor$], +$endif$ cols: $if(columns)$$columns$$else$1$endif$, doc, ) @@ -108,6 +137,9 @@ $endif$ $body$ $if(citations)$ +$for(nocite-ids)$ +#cite(label("${it}"), form: none) +$endfor$ $if(csl)$ #set bibliography(style: "$csl$") @@ -117,7 +149,7 @@ $elseif(bibliographystyle)$ $endif$ $if(bibliography)$ -#bibliography($for(bibliography)$"$bibliography$"$sep$,$endfor$) +#bibliography(($for(bibliography)$"$bibliography$"$sep$,$endfor$)$if(full-bibliography)$, full: true$endif$) $endif$ $endif$ $for(include-after)$ diff --git a/src/resources/pandoc/datadir/readqmd.lua b/src/resources/pandoc/datadir/readqmd.lua index bd0a9a59ee..570584cd76 100644 --- a/src/resources/pandoc/datadir/readqmd.lua +++ b/src/resources/pandoc/datadir/readqmd.lua @@ -138,6 +138,15 @@ local function readqmd(txt, opts) end end + -- ### Opt-out some extensions that we know we won't support for now ### + -- https://pandoc.org/MANUAL.html#extension-table_attributes + -- https://github.com/quarto-dev/quarto-cli/pull/13249#issuecomment-3715267414 + -- Only disable if the extension is actually supported by the format + local all_exts = pandoc.format.all_extensions(flavor.format) + if all_exts:includes('table_attributes') then + flavor.extensions["table_attributes"] = false + end + -- Format flavor, i.e., which extensions should be enabled/disabled. local function restore_invalid_tags(tag) return tags[tag] or tag diff --git a/src/resources/schema/definitions.yml b/src/resources/schema/definitions.yml index 3aaa43e278..d222c043e7 100644 --- a/src/resources/schema/definitions.yml +++ b/src/resources/schema/definitions.yml @@ -2443,6 +2443,11 @@ boolean: description: "Run tests on CI (true = run, false = skip)" default: true + skip: + description: "Skip test unconditionally (true = skip with default message, string = skip with custom message)" + anyOf: + - boolean + - string os: description: "Run tests ONLY on these platforms (whitelist)" anyOf: diff --git a/tests/docs/manuscript/base/.gitignore b/tests/docs/manuscript/base/.gitignore index 075b2542af..0e3521a7d0 100644 --- a/tests/docs/manuscript/base/.gitignore +++ b/tests/docs/manuscript/base/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/manuscript/ipynb-full/.gitignore b/tests/docs/manuscript/ipynb-full/.gitignore index 075b2542af..0e3521a7d0 100644 --- a/tests/docs/manuscript/ipynb-full/.gitignore +++ b/tests/docs/manuscript/ipynb-full/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/manuscript/ipynb-single/.gitignore b/tests/docs/manuscript/ipynb-single/.gitignore index 075b2542af..0e3521a7d0 100644 --- a/tests/docs/manuscript/ipynb-single/.gitignore +++ b/tests/docs/manuscript/ipynb-single/.gitignore @@ -1 +1,3 @@ /.quarto/ + +**/*.quarto_ipynb diff --git a/tests/docs/manuscript/ipynb-single/_quarto.yml b/tests/docs/manuscript/ipynb-single/_quarto.yml index 95cec092a7..eaae2f8164 100644 --- a/tests/docs/manuscript/ipynb-single/_quarto.yml +++ b/tests/docs/manuscript/ipynb-single/_quarto.yml @@ -8,12 +8,8 @@ manuscript: format: html: - comments: + comments: hypothesis: true agu-pdf: default docx: default jats: default - - - - diff --git a/tests/docs/manuscript/qmd-full/.gitignore b/tests/docs/manuscript/qmd-full/.gitignore index ded205c812..977b63fbca 100644 --- a/tests/docs/manuscript/qmd-full/.gitignore +++ b/tests/docs/manuscript/qmd-full/.gitignore @@ -1,3 +1,5 @@ /.quarto/ /.luarc.json + +**/*.quarto_ipynb diff --git a/tests/docs/manuscript/qmd-single/.gitignore b/tests/docs/manuscript/qmd-single/.gitignore index ded205c812..977b63fbca 100644 --- a/tests/docs/manuscript/qmd-single/.gitignore +++ b/tests/docs/manuscript/qmd-single/.gitignore @@ -1,3 +1,5 @@ /.quarto/ /.luarc.json + +**/*.quarto_ipynb diff --git a/tests/docs/smoke-all/2023/10/18/7267.qmd b/tests/docs/smoke-all/2023/10/18/7267.qmd index 9252d9e233..e6b5126eec 100644 --- a/tests/docs/smoke-all/2023/10/18/7267.qmd +++ b/tests/docs/smoke-all/2023/10/18/7267.qmd @@ -1,6 +1,12 @@ --- title: "Test table numbering" format: pdf +_quarto: + tests: + run: + skip: | + This test is wrong because of https://github.com/quarto-dev/quarto-cli/issues/13862. + TODO: to fix. # TODO: we should have _quarto: tests: pdf: # but that doesn't exist yet. --- @@ -35,3 +41,27 @@ knitr::kable(df) See @tbl-table1 and @tbl-table2. +# Table 3 + +::: {#tbl-table3} + +::: {} + + +| Column 1 | Column 2 | Column 3 | +|----------|----------|----------| +| Value 1 | Value 2 | Value 3 | +| Value 4 | Value 5 | Value 6 | +| Value 7 | Value 8 | Value 9 | + +| Column 1 | Column 2 | Column 3 | +|----------|----------|----------| +| Value 1 | Value 2 | Value 3 | +| Value 4 | Value 5 | Value 6 | +| Value 7 | Value 8 | Value 9 | + +::: + +A caption + +::: \ No newline at end of file diff --git a/tests/docs/smoke-all/2023/10/24/7334.qmd b/tests/docs/smoke-all/2023/10/24/7334.qmd index e7db595163..30cd490b4f 100644 --- a/tests/docs/smoke-all/2023/10/24/7334.qmd +++ b/tests/docs/smoke-all/2023/10/24/7334.qmd @@ -2,7 +2,33 @@ title: "table in a div" format: html: default - pdf: default + pdf: + keep-tex: true +_quarto: + tests: + html: + ensureHtmlElements: + - + # Verify callout-tip div exists + - 'div.callout.callout-tip' + # Verify table with id inside callout + - 'div.callout-tip #tbl-table1' + # Verify figcaption exists + - '#tbl-table1 figcaption' + # Verify cross-reference link + - 'a.quarto-xref[href="#tbl-table1"]' + pdf: + ensureLatexFileRegexMatches: + - + # Verify tcolorbox for callout-tip exists + - '\\begin\{tcolorbox\}\[.*colframe=quarto-callout-tip-color-frame' + # Verify longtable with caption and label inside tcolorbox + - '\\begin\{tcolorbox\}[\s\S]*\\begin\{longtable\}[\s\S]*\\caption\{\\label\{tbl-table1\}' + # Verify tcolorbox ends after longtable + - '\\end\{longtable\}[\s\S]*\\end\{tcolorbox\}' + # Verify cross-reference to table works + - 'Table~\\ref\{tbl-table1\}' + - [] --- Table in a div @@ -16,4 +42,6 @@ table <- tribble(~a, ~b, ~c, "others", 2, "Long text") knitr::kable(table) ``` -::: \ No newline at end of file +::: + +See @tbl-table1 in the callout above \ No newline at end of file diff --git a/tests/docs/smoke-all/2023/11/02/7262.qmd b/tests/docs/smoke-all/2023/11/02/7262.qmd index 082d2f8317..841c575a53 100644 --- a/tests/docs/smoke-all/2023/11/02/7262.qmd +++ b/tests/docs/smoke-all/2023/11/02/7262.qmd @@ -1,6 +1,30 @@ --- format: pdf: default +keep-tex: true +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + - + # Verify outer figure environment wraps the layout + - '\\begin\{figure\}[\s\S]*\\begin\{minipage\}' + # Verify two minipages with equal width + - '\\begin\{minipage\}\{0\.50\\linewidth\}' + # Verify figure[H] inside first minipage with caption and label + - '\\begin\{minipage\}[\s\S]*\\begin\{figure\}\[H\][\s\S]*\\caption\{\\label\{fig-example\}Figure caption\}' + # Verify longtable (NOT wrapped in table environment) inside second minipage + - '\\begin\{minipage\}\{0\.50\\linewidth\}[\s\S]*\\begin\{longtable\}' + # Verify table caption with label + - '\\caption\{\\label\{tbl-example\}Table caption\}' + # Verify minipage ends before outer figure ends + - '\\end\{minipage\}%[\s\S]*\\end\{figure\}%' + # Verify cross-references work + - 'Figure~\\ref\{fig-example\}' + - 'Table~\\ref\{tbl-example\}' + - + # Verify NO table environment wrapper around longtable + - '\\begin\{table\}[\s\S]*\\begin\{longtable\}' execute: echo: false --- @@ -19,4 +43,6 @@ plot(cars) knitr::kable(head(cars)) ``` -::: \ No newline at end of file +::: + +See @fig-example and @tbl-example \ No newline at end of file diff --git a/tests/docs/smoke-all/2023/11/21/7655.qmd b/tests/docs/smoke-all/2023/11/21/7655.qmd index 11a510d4e8..2241a74cfb 100644 --- a/tests/docs/smoke-all/2023/11/21/7655.qmd +++ b/tests/docs/smoke-all/2023/11/21/7655.qmd @@ -1,6 +1,27 @@ --- title: "Untitled" format: pdf +keep-tex: true +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + - + # Verify longtable with multiline column specifications + - '\\begin\{longtable\}\[\]\{@\{\}[\s\S]*>\{[^}]+\}p\{[^}]+\}[\s\S]*@\{\}\}' + # Verify caption with label + - '\\caption\{\\label\{tbl-t-quantile\}The Student.*Distribution\}' + # Verify table structure elements + - '\\toprule\\noalign\{\}' + - '\\midrule\\noalign\{\}' + - '\\endhead' + - '\\bottomrule\\noalign\{\}' + - '\\endlastfoot' + # Verify minipage column headers + - '\\begin\{minipage\}\[b\]\{\\linewidth\}\\raggedleft[\s\S]*0\.75[\s\S]*\\end\{minipage\}' + # Verify document ends properly (no compilation error) + - '\\end\{document\}' + - [] --- ```{r} diff --git a/tests/docs/smoke-all/2024/01/18/docusaurus/equations.qmd b/tests/docs/smoke-all/2024/01/18/docusaurus/equations.qmd index 249d3202b5..fe51423782 100644 --- a/tests/docs/smoke-all/2024/01/18/docusaurus/equations.qmd +++ b/tests/docs/smoke-all/2024/01/18/docusaurus/equations.qmd @@ -3,6 +3,8 @@ title: Equations format: docusaurus-md _quarto: tests: + run: + skip: "Pandoc 3.7+ removes newlines in display math blocks (fixed in pandoc@8123be6, awaiting release)" docusaurus-md: ensureFileRegexMatches: - ["\\$\\$", "price = \\\\hat", "\\$e = mc\\^2\\$"] diff --git a/tests/docs/smoke-all/2024/01/19/8354.tex.snapshot b/tests/docs/smoke-all/2024/01/19/8354.tex.snapshot index 305c0eb274..5ba8345fa2 100644 --- a/tests/docs/smoke-all/2024/01/19/8354.tex.snapshot +++ b/tests/docs/smoke-all/2024/01/19/8354.tex.snapshot @@ -107,6 +107,7 @@ \newcommand{\WarningTok}[1]{\textcolor[rgb]{0.37,0.37,0.37}{\textit{#1}}} \usepackage{longtable,booktabs,array} +\newcounter{none} % for unnumbered tables \usepackage{calc} % for calculating minipage widths % Correct order of tables after \paragraph or \subparagraph \usepackage{etoolbox} diff --git a/tests/docs/smoke-all/2024/01/31/8507.docx.snapshot b/tests/docs/smoke-all/2024/01/31/8507.docx.snapshot index 049710e473..16f0a65adf 100644 Binary files a/tests/docs/smoke-all/2024/01/31/8507.docx.snapshot and b/tests/docs/smoke-all/2024/01/31/8507.docx.snapshot differ diff --git a/tests/docs/smoke-all/2026/01/06/13249.qmd b/tests/docs/smoke-all/2026/01/06/13249.qmd new file mode 100644 index 0000000000..a18d9c9a01 --- /dev/null +++ b/tests/docs/smoke-all/2026/01/06/13249.qmd @@ -0,0 +1,22 @@ +--- +title: Math with cancellation terms +format: pdf +keep-tex: true +_quarto: + tests: + pdf: + noErrors: default + ensureLatexFileRegexMatches: + - ['\\usepackage\{cancel\}'] + - [] +--- + +--- + +format: pdf +--- + +$$ + f = \left(\cancel{\frac{\partial v}{\partial x}}\right) \\ + g = \left(\cancel{\frac{\partial v}{\partial z}}\right) +$$ \ No newline at end of file diff --git a/tests/docs/smoke-all/article-layout/tables/compute-table-margin.qmd b/tests/docs/smoke-all/article-layout/tables/compute-table-margin.qmd index 85be06408c..a8597c88db 100644 --- a/tests/docs/smoke-all/article-layout/tables/compute-table-margin.qmd +++ b/tests/docs/smoke-all/article-layout/tables/compute-table-margin.qmd @@ -1,8 +1,23 @@ --- -format: +format: pdf: geometry: - showframe +keep-tex: true +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + - + # Verify margintable environment wraps the table + - '\\begin\{margintable\}' + # Verify longtable with caption and label inside margintable + - '\\begin\{margintable\}[\s\S]*\\begin\{longtable\}[\s\S]*\\caption\{\\label\{tbl-cars\}Cars\}' + # Verify margintable ends after longtable + - '\\end\{longtable\}[\s\S]*\\end\{margintable\}%' + # Verify cross-reference works + - 'Table~\\ref\{tbl-cars\}' + - [] --- {{< lipsum 1 >}} @@ -16,4 +31,6 @@ format: knitr::kable(head(cars)) ``` -{{< lipsum 1 >}} \ No newline at end of file +{{< lipsum 1 >}} + +See @tbl-cars \ No newline at end of file diff --git a/tests/docs/smoke-all/crossrefs/float/typst/typst-listings-1.qmd b/tests/docs/smoke-all/crossrefs/float/typst/typst-listings-1.qmd index 1660a2ee45..3ac30a4d8c 100644 --- a/tests/docs/smoke-all/crossrefs/float/typst/typst-listings-1.qmd +++ b/tests/docs/smoke-all/crossrefs/float/typst/typst-listings-1.qmd @@ -7,8 +7,8 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '#figure\(\[\s+#set align\(left\)' - - '```sql\s+SELECT \* FROM Customers\s+```' + - '#figure\(\[\s+#set align\(left\)\s+#Skylighting' + - 'kind: "quarto-float-lst"' - '#ref\(, supplement: \[Listing\]\)' - 'Customers Query' - [] diff --git a/tests/docs/smoke-all/crossrefs/float/typst/typst-listings-2.qmd b/tests/docs/smoke-all/crossrefs/float/typst/typst-listings-2.qmd index cd37ee1cd2..77ad5f23e5 100644 --- a/tests/docs/smoke-all/crossrefs/float/typst/typst-listings-2.qmd +++ b/tests/docs/smoke-all/crossrefs/float/typst/typst-listings-2.qmd @@ -7,8 +7,8 @@ _quarto: typst: ensureTypstFileRegexMatches: - - - '#figure\(\[\s+#set align\(left\)' - - '```sql\s+SELECT \* FROM Customers\s+```' + - '#figure\(\[\s+#set align\(left\)\s+#Skylighting' + - 'kind: "quarto-float-lst"' - '#ref\(, supplement: \[Listing\]\)' - 'Customers Query' - [] diff --git a/tests/docs/smoke-all/typst/brand-yaml/typography/font-list.qmd b/tests/docs/smoke-all/typst/brand-yaml/typography/font-list.qmd index 4b76245b8a..968ea05845 100644 --- a/tests/docs/smoke-all/typst/brand-yaml/typography/font-list.qmd +++ b/tests/docs/smoke-all/typst/brand-yaml/typography/font-list.qmd @@ -20,8 +20,8 @@ _quarto: ensureTypstFileRegexMatches: - - 'font: \("InvalidFont", "Roboto"\),' - - '#show raw.where\(block: false\): set text\(font: \("InvalidFont", "Inconsolata"\), \)' - - '#show raw.where\(block: true\): set text\(font: \("InvalidFont", "Inconsolata"\), \)' + # monospace font now goes through codefont parameter instead of header injection + - 'codefont: \("InvalidFont", "Inconsolata"\),' - [] ensurePdfRegexMatches: - diff --git a/tests/docs/smoke-all/typst/brand-yaml/typography/kitchen-sink-1/brand-typography.qmd b/tests/docs/smoke-all/typst/brand-yaml/typography/kitchen-sink-1/brand-typography.qmd index 23ce2483fe..6912828f2d 100644 --- a/tests/docs/smoke-all/typst/brand-yaml/typography/kitchen-sink-1/brand-typography.qmd +++ b/tests/docs/smoke-all/typst/brand-yaml/typography/kitchen-sink-1/brand-typography.qmd @@ -35,10 +35,12 @@ _quarto: - '^#show heading: set text\(font: \("Montserrat",\), weight: 800, style: "normal", fill: rgb\("#0b8005"\), \)$' - '^#show heading: set par\(leading: 0.25em\)$' - - '^#show raw.where\(block: false\): set text\(font: \("Inconsolata",\), weight: 600, size: 0.57\*14pt, fill: rgb\(8, 111, 15\), \)$' + - 'codefont: \("Inconsolata",\),$' + - 'show raw: set text\(font: codefont\) if codefont != none' + - '^#show raw.where\(block: false\): set text\(weight: 600, size: 0.57\*14pt, fill: rgb\(8, 111, 15\), \)$' - '^#show raw.where\(block: false\): content => highlight\(fill: rgb\(255, 250, 224\), content\)$' - - '^#show raw.where\(block: true\): set text\(font: \("Inconsolata",\), size: 18pt, fill: rgb\(245, 255, 250\), \)$' + - '^#show raw.where\(block: true\): set text\(size: 18pt, fill: rgb\(245, 255, 250\), \)$' - '^#show raw.where\(block: true\): set block\(fill: rgb\("#77aae1"\)\)$' - '^#show raw.where\(block: true\): set par\(leading: 1\.25em\)$' diff --git a/tests/docs/smoke-all/typst/brand-yaml/typography/kitchen-sink-2/brand-typography.qmd b/tests/docs/smoke-all/typst/brand-yaml/typography/kitchen-sink-2/brand-typography.qmd index bbac8f51ce..3f59bd8747 100644 --- a/tests/docs/smoke-all/typst/brand-yaml/typography/kitchen-sink-2/brand-typography.qmd +++ b/tests/docs/smoke-all/typst/brand-yaml/typography/kitchen-sink-2/brand-typography.qmd @@ -33,10 +33,12 @@ _quarto: - '^#show heading: set text\(font: \("Raleway",\), weight: 500, style: "normal", fill: rgb\("#042f02"\), \)$' - '^#show heading: set par\(leading: 0.25em\)$' - - '^#show raw.where\(block: false\): set text\(font: \("Space Mono",\), weight: 400, size: 0.75\*12pt, fill: rgb\(8, 111, 15\), \)$' + - 'codefont: \("Space Mono",\),$' + - 'show raw: set text\(font: codefont\) if codefont != none' + - '^#show raw.where\(block: false\): set text\(weight: 400, size: 0.75\*12pt, fill: rgb\(8, 111, 15\), \)$' - '^#show raw.where\(block: false\): content => highlight\(fill: rgb\(255, 250, 224\), content\)$' - - '^#show raw.where\(block: true\): set text\(font: \("Space Mono",\), weight: 400, size: 0.75\*12pt, fill: rgb\("#eee"\), \)$' + - '^#show raw.where\(block: true\): set text\(weight: 400, size: 0.75\*12pt, fill: rgb\("#eee"\), \)$' - '^#show raw.where\(block: true\): set block\(fill: rgb\("#0a3c07"\)\)$' - '^#show link: set text\(weight: 200, fill: maroon, \)$' diff --git a/tests/docs/smoke-all/typst/columns/basic-two-column.qmd b/tests/docs/smoke-all/typst/columns/basic-two-column.qmd new file mode 100644 index 0000000000..de3e923e0b --- /dev/null +++ b/tests/docs/smoke-all/typst/columns/basic-two-column.qmd @@ -0,0 +1,35 @@ +--- +title: "Basic Two Column Layout" +format: + typst: + columns: 2 + keep-typ: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - # Patterns that MUST be found + # Page columns set via set page() + - 'columns: 2,' + # Title block wrapped with place(scope: "parent") for column spanning + - 'place\(top, float: true, scope: "parent", clearance: 4mm\)' + - # Patterns that must NOT be found + # Should NOT have columns() function call wrapping doc at end of article() + - 'columns\(cols, doc\)\n\}' +--- + +## Introduction + +This document tests the basic two-column layout in Typst. The title should span both columns, while the body content flows in two columns. + +## First Section + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. + +## Second Section + +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident. + +## Third Section + +Sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium. diff --git a/tests/docs/smoke-all/typst/columns/two-column-landscape.qmd b/tests/docs/smoke-all/typst/columns/two-column-landscape.qmd new file mode 100644 index 0000000000..fb33b2582f --- /dev/null +++ b/tests/docs/smoke-all/typst/columns/two-column-landscape.qmd @@ -0,0 +1,99 @@ +--- +title: "Two Column with Landscape Section" +subtitle: "Testing Column Layout with Page Orientation Changes" +author: + - name: Alice Smith + email: alice@example.com + affiliation: University A + - name: Bob Jones + email: bob@example.com + affiliation: University B +date: "2025-01-08" +abstract: "This document tests combining two-column layout with landscape page sections. The title block should span both columns, and content should flow correctly when switching between portrait and landscape orientations." +format: + typst: + columns: 2 + keep-typ: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - # Patterns that MUST be found + # Page columns set via set page() + - 'columns: 2,' + # Title block wrapped with place(scope: "parent") for column spanning + - 'place\(top, float: true, scope: "parent", clearance: 4mm\)' + # Landscape page start + - '#set page\(flipped: true\)' + # Landscape page end + - '#set page\(flipped: false\)' + - # Patterns that must NOT be found + # Should NOT have columns() function call wrapping doc at end of article() + - 'columns\(cols, doc\)\n\}' +--- + +## Introduction + +This document tests the interaction between two-column layout and landscape page sections. The title block above should span the full page width, while this body content flows in two columns. + +## Portrait Section + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. + +Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. + +::: {.landscape} + +## Landscape Section + +This section appears in landscape orientation. The two-column layout should persist but now on a rotated page, giving more horizontal space for wide content like tables. + +### The Bug This Test Catches + +When Quarto's `article()` function used `columns(cols, doc)` to wrap the document content (the old approach), landscape sections would crash with the error: + +``` +error: page configuration is not allowed inside of containers + ┌─ two-column-landscape.typ:351:1 + │ +351 │ #set page(flipped: true) + │ ^^^^^^^^^^^^^^^^^^^^^^^ +``` + +This happened because `columns()` creates a container, and Typst does not allow `set page()` directives inside containers. The landscape filter emits `#set page(flipped: true)` to rotate the page, which fails when nested inside a `columns()` call. + +### The Fix + +Pandoc fixed this in October 2024 (commit e01023c1f) for Typst 0.12+ compatibility. They changed from: + +```typst +if cols == 1 { doc } else { columns(cols, doc) } +``` + +to using `set page(columns: cols)` at page level, which allows page configuration changes anywhere in the document. This approach also requires wrapping the title block with `place(top, float: true, scope: "parent")` so it spans both columns. + +Quarto updated its copy of `template.typst` but missed updating the `article()` function in `typst-template.typ`, leaving the old `columns()` approach in place until this fix. + +| Column A | Column B | Column C | Column D | Column E | Column F | +|----------|----------|----------|----------|----------|----------| +| Data 1 | Data 2 | Data 3 | Data 4 | Data 5 | Data 6 | +| Data 7 | Data 8 | Data 9 | Data 10 | Data 11 | Data 12 | +| Data 13 | Data 14 | Data 15 | Data 16 | Data 17 | Data 18 | + +::: + +## Back to Portrait + +This section returns to portrait orientation with two columns. The content should continue flowing normally after the landscape section. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +## Conclusion + +The document demonstrates that two-column layouts work correctly with landscape page sections when using `set page(columns:)` instead of the `columns()` function wrapper. diff --git a/tests/docs/smoke-all/typst/columns/two-column-title-block.qmd b/tests/docs/smoke-all/typst/columns/two-column-title-block.qmd new file mode 100644 index 0000000000..f7c2addf06 --- /dev/null +++ b/tests/docs/smoke-all/typst/columns/two-column-title-block.qmd @@ -0,0 +1,67 @@ +--- +title: "Two Column with Full Title Block" +subtitle: "Testing Column-Spanning Title Elements" +author: + - name: Alice Smith + email: alice@example.com + affiliation: University A + - name: Bob Jones + email: bob@example.com + affiliation: University B + - name: Carol White + email: carol@example.com + affiliation: University C +date: "2025-01-08" +keywords: + - columns + - typst + - title-block +thanks: "We thank the Quarto team for their work." +abstract: "This document tests that the full title block (title, subtitle, authors, date, abstract, and thanks) correctly spans both columns in a two-column layout." +format: + typst: + columns: 2 + keep-typ: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - # Patterns that MUST be found + # Page columns set via set page() + - 'columns: 2,' + # Title block wrapped with place(scope: "parent") for column spanning + - 'place\(top, float: true, scope: "parent", clearance: 4mm\)' + # Title passed to article() + - 'title: \[Two Column with Full Title Block\]' + # Subtitle passed to article() + - 'subtitle: \[Testing Column-Spanning Title Elements\]' + # Thanks parameter passed to article() + - 'thanks: \[We thank the Quarto team' + # Abstract passed to article() + - 'abstract: \[This document tests that the full title block' + # Authors grid (3 authors = 3 columns) + - 'columns: \(1fr,\) \* ncols' + - # Patterns that must NOT be found + # Should NOT have columns() function call wrapping doc at end of article() + - 'columns\(cols, doc\)\n\}' +--- + +## Introduction + +This document verifies that all title block elements span both columns correctly in a two-column layout. + +## First Section + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +## Second Section + +Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +## Third Section + +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + +## Conclusion + +The title block should appear above both columns, spanning the full page width. diff --git a/tests/docs/smoke-all/typst/columns/two-column-toc.qmd b/tests/docs/smoke-all/typst/columns/two-column-toc.qmd new file mode 100644 index 0000000000..e3e2f6c29a --- /dev/null +++ b/tests/docs/smoke-all/typst/columns/two-column-toc.qmd @@ -0,0 +1,67 @@ +--- +title: "Two Column with Table of Contents" +abstract: "This document tests that the table of contents works correctly with a two-column layout." +toc: true +toc-title: "Contents" +toc-depth: 2 +format: + typst: + columns: 2 + keep-typ: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - # Patterns that MUST be found + # Page columns set via set page() + - 'columns: 2,' + # Title block wrapped with place(scope: "parent") for column spanning + - 'place\(top, float: true, scope: "parent", clearance: 4mm\)' + # TOC enabled in article() call + - 'toc: true' + # TOC title passed + - 'toc_title: \[Contents\]' + # TOC depth passed + - 'toc_depth: 2' + # Outline function call in article() + - '#outline\(' + - # Patterns that must NOT be found + # Should NOT have columns() function call wrapping doc at end of article() + - 'columns\(cols, doc\)\n\}' +--- + +## Introduction + +This document tests that the table of contents is rendered correctly in a two-column layout. + +## First Chapter + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. + +### First Subsection + +Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + +### Second Subsection + +Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris. + +## Second Chapter + +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore. + +### Third Subsection + +Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia. + +### Fourth Subsection + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium. + +## Third Chapter + +At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis. + +## Conclusion + +The TOC should appear after the title block and before the main content. diff --git a/tests/docs/smoke-all/typst/pandoc-template-features.qmd b/tests/docs/smoke-all/typst/pandoc-template-features.qmd new file mode 100644 index 0000000000..479bf4a3e4 --- /dev/null +++ b/tests/docs/smoke-all/typst/pandoc-template-features.qmd @@ -0,0 +1,96 @@ +--- +title: "Test Document" +subtitle: "Pandoc Template Features" +author: + - name: Alice Smith + email: alice@example.com + affiliation: University A + - name: Bob Jones + email: bob@example.com + affiliation: University B +date: "2024-01-15" +keywords: + - quarto + - typst + - testing +thanks: "We thank our reviewers for their feedback." +abstract: "This document tests the Pandoc typst template features merged into Quarto." +mainfont: "Libertinus Serif" +mathfont: "STIX Two Math" +linestretch: 1.2 +linkcolor: "#0000ff" +citecolor: "#00ff00" +filecolor: "#ff0000" +brand: + typography: + fonts: + - family: Fira Code + source: google + monospace: + family: Fira Code +format: + typst: + keep-typ: true +_quarto: + tests: + typst: + ensureTypstFileRegexMatches: + - # Patterns that MUST be found + # content-to-string function defined (from Pandoc) + - '#let content-to-string\(content\)' + # Document metadata in article() function + - 'set document\(title: title, keywords: keywords\)' + - 'set document\(\s*author: authors\.map' + # Keywords passed to article() call (quoted strings) + - 'keywords: \("quarto","typst","testing",\)' + # Thanks parameter passed to article() + - 'thanks: \[We thank our reviewers' + # Thanks footnote in title block + - 'footnote\(thanks, numbering: "\*"\)' + # Font settings passed to article() + # codefont comes from brand.typography.monospace.family fallback + - 'codefont: \("Fira Code",\)' + - 'mathfont: \("STIX Two Math",\)' + # mainfont (Libertinus Serif is bundled with Typst) + - 'font: \("Libertinus Serif",\)' + # Linestretch + - 'linestretch: 1\.2' + # Link colors passed to article() (# escaped as \#) + - 'linkcolor: \[\\#0000ff\]' + - 'citecolor: \[\\#00ff00\]' + - 'filecolor: \[\\#ff0000\]' + # Link color show rules in article() function + - 'show link: set text\(fill: rgb\(content-to-string\(linkcolor\)\)\)' + - 'show ref: set text\(fill: rgb\(content-to-string\(citecolor\)\)\)' + - # Patterns that must NOT be found + [] +--- + +## Introduction + +This document tests all the Pandoc typst template features that have been merged into Quarto's modular template structure. + +## Code Example + +Here is some code with custom font: + +```python +def hello(): + print("Hello, world!") +``` + +## Math Example + +Here is some math with custom font: + +$$ +E = mc^2 +$$ + +## Links + +Here is a [link to Quarto](https://quarto.org) which should be colored. + +## Conclusion + +All features work correctly. diff --git a/tests/docs/websites/website-sidebar-auto/.gitignore b/tests/docs/websites/website-sidebar-auto/.gitignore new file mode 100644 index 0000000000..b5d6aa19d5 --- /dev/null +++ b/tests/docs/websites/website-sidebar-auto/.gitignore @@ -0,0 +1,4 @@ +/.quarto/ +**/*.quarto_ipynb +*_files/ +_site/ diff --git a/tests/docs/websites/website-sidebar-auto/_quarto.yml b/tests/docs/websites/website-sidebar-auto/_quarto.yml new file mode 100644 index 0000000000..a743825dfd --- /dev/null +++ b/tests/docs/websites/website-sidebar-auto/_quarto.yml @@ -0,0 +1,11 @@ +project: + type: website + +website: + title: "Sidebar Auto Test" + sidebar: + contents: auto + +format: + html: + theme: default diff --git a/tests/docs/websites/website-sidebar-auto/index.qmd b/tests/docs/websites/website-sidebar-auto/index.qmd new file mode 100644 index 0000000000..50187e5080 --- /dev/null +++ b/tests/docs/websites/website-sidebar-auto/index.qmd @@ -0,0 +1,5 @@ +--- +title: "Home" +--- + +This is the home page. diff --git a/tests/docs/websites/website-sidebar-auto/subdir/index.ipynb b/tests/docs/websites/website-sidebar-auto/subdir/index.ipynb new file mode 100644 index 0000000000..b03257b03b --- /dev/null +++ b/tests/docs/websites/website-sidebar-auto/subdir/index.ipynb @@ -0,0 +1,33 @@ +{ + "cells": [ + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "---\n", + "title: \"Subdir Index\"\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is the index page for the subdir." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.9.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/tests/smoke/check/check-versions-no-mismatch.test.ts b/tests/smoke/check/check-versions-no-mismatch.test.ts index 24cb9d77af..b080ab19c0 100644 --- a/tests/smoke/check/check-versions-no-mismatch.test.ts +++ b/tests/smoke/check/check-versions-no-mismatch.test.ts @@ -8,6 +8,8 @@ import { testQuartoCmd } from "../../test.ts"; import { printsMessage } from "../../verify.ts"; +// If this test fails, it indicates that there is a version mismatch +// Check 'src\command\check\check.ts' for the recorded versions, and compare to 'configuration' file testQuartoCmd( "check", ["versions"], diff --git a/tests/smoke/smoke-all.test.ts b/tests/smoke/smoke-all.test.ts index f8643423bd..b5aaafa689 100644 --- a/tests/smoke/smoke-all.test.ts +++ b/tests/smoke/smoke-all.test.ts @@ -103,6 +103,11 @@ function skipTest(metadata: Record): string | undefined { return undefined; } + // Check explicit skip with message + if (runConfig.skip) { + return typeof runConfig.skip === "string" ? runConfig.skip : "tests.run.skip is true"; + } + // Check CI if (runningInCI() && runConfig.ci === false) { return "tests.run.ci is false"; diff --git a/tests/smoke/website/website-sidebar-auto.test.ts b/tests/smoke/website/website-sidebar-auto.test.ts new file mode 100644 index 0000000000..236d0e7077 --- /dev/null +++ b/tests/smoke/website/website-sidebar-auto.test.ts @@ -0,0 +1,37 @@ +/* + * website-sidebar-auto.test.ts + * + * Verifies that sidebar auto-generation properly detects index files with + * non-qmd extensions (like .ipynb) in subdirectories. This tests the + * engineValidExtensions() call in indexFileHrefForDir() which must work + * before resolveEngines() has been called. + * + * Copyright (C) 2020-2025 Posit Software, PBC + */ + +import { docs } from "../../utils.ts"; +import { join } from "../../../src/deno_ral/path.ts"; +import { existsSync } from "../../../src/deno_ral/fs.ts"; +import { testQuartoCmd } from "../../test.ts"; +import { fileExists, noErrorsOrWarnings } from "../../verify.ts"; + +const renderDir = docs("websites/website-sidebar-auto"); +const outDir = join(Deno.cwd(), renderDir, "_site"); + +// Test that sidebar auto-generation detects index.ipynb in subdirectory +testQuartoCmd( + "render", + [renderDir], + [ + noErrorsOrWarnings, + fileExists(join(outDir, "index.html")), // Main index page + fileExists(join(outDir, "subdir", "index.html")), // Subdir index from .ipynb should be rendered + ], + { + teardown: async () => { + if (existsSync(outDir)) { + await Deno.remove(outDir, { recursive: true }); + } + }, + }, +); diff --git a/tests/verify-snapshot.ts b/tests/verify-snapshot.ts index d8d380e901..e2c64da380 100644 --- a/tests/verify-snapshot.ts +++ b/tests/verify-snapshot.ts @@ -7,6 +7,7 @@ import { extname } from "../src/deno_ral/path.ts"; import { normalizeNewlines } from "../src/core/text.ts"; import { withDocxContent } from "./verify.ts"; +import { createPatch, diffWordsWithSpace, diffChars } from "../src/core/lib/external/diff.js"; import * as slimdom from "slimdom"; import xpath from "fontoxpath"; @@ -57,3 +58,31 @@ export const checkSnapshot = async (file: string) => { const snapshotCanonical = await canonicalizeSnapshot(file + ".snapshot"); return outputCanonical === snapshotCanonical; } + +export const generateSnapshotDiff = async (file: string): Promise => { + const outputCanonical = await canonicalizeSnapshot(file); + const snapshotCanonical = await canonicalizeSnapshot(file + ".snapshot"); + return createPatch( + file + ".snapshot", + snapshotCanonical, + outputCanonical, + "expected", + "actual" + ); +} + +export type WordDiffPart = { value: string; added?: boolean; removed?: boolean }; + +export const generateInlineDiff = async (file: string): Promise => { + const outputCanonical = await canonicalizeSnapshot(file); + const snapshotCanonical = await canonicalizeSnapshot(file + ".snapshot"); + + const stripWhitespace = (s: string) => s.replace(/\s+/g, ""); + const isWhitespaceOnlyChange = stripWhitespace(outputCanonical) === stripWhitespace(snapshotCanonical); + + if (isWhitespaceOnlyChange) { + return diffChars(snapshotCanonical, outputCanonical); + } + + return diffWordsWithSpace(snapshotCanonical, outputCanonical); +} diff --git a/tests/verify.ts b/tests/verify.ts index 2776237152..a38dbbd01d 100644 --- a/tests/verify.ts +++ b/tests/verify.ts @@ -20,7 +20,8 @@ import { unzip } from "../src/core/zip.ts"; import { dirAndStem, safeRemoveSync, which } from "../src/core/path.ts"; import { isWindows } from "../src/deno_ral/platform.ts"; import { execProcess, ExecProcessOptions } from "../src/core/process.ts"; -import { canonicalizeSnapshot, checkSnapshot } from "./verify-snapshot.ts"; +import { checkSnapshot, generateSnapshotDiff, generateInlineDiff, WordDiffPart } from "./verify-snapshot.ts"; +import * as colors from "fmt/colors"; export const withDocxContent = async ( file: string, @@ -440,6 +441,63 @@ export const ensureHtmlElementCount = ( }; }; +const printColoredDiff = (diff: string) => { + for (const line of diff.split("\n")) { + if (line.startsWith("+") && !line.startsWith("+++")) { + console.log(colors.green(line)); + } else if (line.startsWith("-") && !line.startsWith("---")) { + console.log(colors.red(line)); + } else if (line.startsWith("@@")) { + console.log(colors.dim(line)); + } else { + console.log(line); + } + } +}; + +const escapeWhitespace = (s: string): string => { + return s.replace(/\n/g, "⏎\\n").replace(/\t/g, "→\\t").replace(/ /g, "·"); +}; + +const printCompactInlineDiff = (parts: WordDiffPart[]) => { + const chunks: string[] = []; + let currentChunk = ""; + let hasChanges = false; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + if (part.added || part.removed) { + hasChanges = true; + const displayValue = /^\s+$/.test(part.value) ? escapeWhitespace(part.value) : part.value; + if (part.added) { + currentChunk += colors.bgGreen(colors.black(displayValue)); + } else { + currentChunk += colors.bgRed(colors.white(displayValue)); + } + } else { + if (hasChanges) { + const contextBefore = part.value.slice(0, 40); + chunks.push(currentChunk + colors.dim(contextBefore + (part.value.length > 40 ? "..." : ""))); + currentChunk = ""; + hasChanges = false; + } + const nextHasChange = parts.slice(i + 1).some(p => p.added || p.removed); + if (nextHasChange) { + const contextAfter = part.value.slice(-40); + currentChunk = colors.dim((part.value.length > 40 ? "..." : "") + contextAfter); + } + } + } + if (currentChunk) { + chunks.push(currentChunk); + } + + for (const chunk of chunks) { + console.log(chunk); + console.log(""); + } +}; + export const ensureSnapshotMatches = ( file: string, ): Verify => { @@ -447,11 +505,23 @@ export const ensureSnapshotMatches = ( name: "Inspecting Snapshot", verify: async (_output: ExecuteOutput[]) => { const good = await checkSnapshot(file); + const diffFile = file + ".diff"; if (!good) { - console.log("output:"); - console.log(await canonicalizeSnapshot(file)); - console.log("snapshot:"); - console.log(await canonicalizeSnapshot(file + ".snapshot")); + const diff = await generateSnapshotDiff(file); + const inlineParts = await generateInlineDiff(file); + + await Deno.writeTextFile(diffFile, diff); + console.log(`\nDiff saved to: ${diffFile}`); + + console.log("\n--- Unified Diff ---"); + printColoredDiff(diff); + console.log("--- End Unified Diff ---\n"); + + console.log("--- Word-level Changes (with context) ---"); + printCompactInlineDiff(inlineParts); + console.log("--- End Word-level Changes ---\n"); + } else { + safeRemoveSync(diffFile); } assert( good,