Skip to content

Update Pandoc to 3.13 - #15320

Draft
cderv wants to merge 32 commits into
mainfrom
chore/deps-v1.12-pandoc
Draft

cderv wants to merge 32 commits into
mainfrom
chore/deps-v1.12-pandoc

Conversation

@cderv

@cderv cderv commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

Update the bundled Pandoc from 3.10 to 3.13.

The Pandoc 3.11 to 3.13 changelogs were reviewed for changes reaching Quarto, and each one was checked by rendering with 3.10 and 3.13. Quarto's customized Pandoc templates (HTML styles, LaTeX/Beamer, Typst, revealjs) are synced with upstream.

Template ports

  • Typst: horizontal rules use Typst 0.15's native divider (typst: read and write divider element for horizontal rules jgm/pandoc#11540), so they are now full-width. A Typst binary set through QUARTO_TYPST must be 0.15 or later. Also ported: the csl-settings block for citeproc bibliographies, and the definition list hanging indent (tight items keep the description on the term line again).
  • LaTeX/Beamer: Pandoc's new caption setup for tables, the footnotehyper longtable hook removed, and the duplicate TOC fix in the Beamer toc partial.
  • HTML theme: pandoc: the print rework, currentColor borders and links inheriting the text color from Pandoc's styles.html. Pandoc 3.13 places the @media print block before the base rules, so links, blockquotes and code keep their screen styles when printing (HTML default styles: @media print resets for links, blockquotes and code are overridden by later rules jgm/pandoc#12064). The block is moved to the end of styles.html, where Pandoc put it when fixing this after 3.13, with a Playwright test.

Pandoc 3.13 also adds automatic dark mode to its styles.html (jgm/pandoc#11831). This is not enabled for theme: pandoc: the page stays light when the OS prefers dark, and the new fontcolordark, backgroundcolordark, linkcolordark, quotecolor, quotebordercolor variables (and their *dark variants) have no effect. Upstream turns dark mode on by default with no variable set, while Quarto's code highlighting and component CSS on theme: pandoc pages are light only, so a straight port would give unreadable code blocks. Supporting it is left for a follow-up.

Other user-visible changes from Pandoc

No Quarto change for these, they will be listed in the release notes:

  • HTML: MathML is now Pandoc's default math method (Set MathML as the default writerHTMLMathMethod jgm/pandoc#11751). With minimal: true, theme: none or theme: pandoc, math now renders as MathML instead of italic text. Bootstrap HTML is unchanged (MathJax).
  • Citeproc: lowercase particles stay in the family name for string names in bibliographies (Particles in family names are not kept in place jgm/pandoc#11911), so de Gaulle sorts under "d".
  • YAML: later metadata blocks are parsed as YAML 1.2, so yes/no become strings, as they already were for Quarto in the first block.
  • Docx: a percent image width is now a percentage of the text width, so small images grow where 3.10 kept their natural size.
  • Pptx: bullets, a table and bullets on one slide now stay on one slide.
  • Typst: typst-label is honored, and spans with lang get #text(lang:).

Stacked on this one: #15321 adds support for Pandoc's --math-method in quarto render. A second stacked PR will follow for heading ids that change when a Div with an id wraps a heading with --section-divs (TOC links and anchors), related to jgm/pandoc#12070. The full test suite run on CI is still to triage, and the release notes are still to write.

Checklist

I have (if applicable):

  • referenced the GitHub issue this PR closes
  • updated the appropriate changelog in the PR
  • ensured the present test suite passes
  • added new tests
  • created a separate documentation PR in Quarto's website repo and linked it to this PR
AI-assisted PR
  • AI tool used: Claude Code
  • Codebase grounding: local clone, Pandoc changelog and source
  • Human review: I have reviewed, tested, and verified the AI-generated content before submitting.

Note: autonomous AI agents submitting PRs without human oversight are not permitted — see the Code of Conduct.

@posit-snyk-bot

posit-snyk-bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

cscheid and others added 25 commits November 10, 2026 17:35
Before the root lockfile was pinned, the bundling step ran deno with cwd
src/ and no --lock, so builds refreshed an untracked src/deno.lock in
developer trees; that stale artifact was swept into the Pandoc 3.13
update commit. All scripted deno invocations now resolve through the
pinned root lockfile, so ignore the path like tests/deno.lock to keep
stale copies out of future commits.
Pandoc 3.11 removed a stray block after the TOC frame in default.beamer
(jgm/pandoc#11819): a duplicate \setcounter{tocdepth} + \tableofcontents
outside the frame followed by an unmatched closing brace. Quarto's wired
beamer toc partial carried the same lines. LaTeX tolerates the glitch, so
rendered output is unchanged; the port keeps the wired template aligned
with the 3.13 reference. Adds a smoke test asserting the generated .tex
has no \tableofcontents after the TOC frame's \end{frame}.
check.ts hardcodes the expected dependency versions (the configuration
file format isn't loadable there; see the FIXME). bf3137d set
PANDOC=3.13 but left this constraint at 3.10.0, so strict checking
(the default) flagged the bundled Pandoc as a mismatch.
Pandoc 3.13 replaced its custom terms.item show rule with
#set terms(hanging-indent: 1.5em), because the typst writer no longer
wraps tight definition-list descriptions in #block (jgm/pandoc#11748).
Keeping the old rule on 3.13 forced every description onto the line
below its term; the set rule restores Typst's native layout where
tight items keep the description on the term's line.

Also port the /nowrap modifier on template keywords for parity with
upstream default.typst. It is a no-op in Quarto's pipeline, which
renders templates with wrap: none.

The definition-item-no-break smoke test asserted the old two-line
layout; it is replaced by definition-lists.qmd asserting the new
geometry for tight and loose items.
…iographies

Pandoc 3.12.1 feeds CSL hanging-indent and entry-spacing to the typst
template as csl-hanging-indent/csl-entry-spacing and applies them via a
show rule on the <refs> label the writer puts on citeproc bibliographies
(jgm/pandoc#11926). Quarto's modular typst template lacked the rule, so
CSL-mandated hanging indents were silently dropped. Port the block into
quarto/definitions.typ (must precede $body$ for the show rule to apply).
Pandoc 3.13's typst writer emits #divider() for HorizontalRule. With
Typst 0.15 that resolves to std.divider, a full-width 0.55pt line,
silently replacing Quarto's centered 25-75% rule. Define divider() as
an alias of the existing horizontalrule so documents keep their look;
the name horizontalrule stays because notes.typ and external typst
templates call it.
Reverts the divider() alias from f67d9f4b7. Quarto's template no longer
shadows divider, so Pandoc 3.13's `#divider()` output resolves to the
native std.divider element, matching pandoc's default template with
typst >= 0.15 (Quarto bundles Typst 0.15.1, so pandoc's polyfill branch
is never taken).

Rationale: the centered 25-75% line was inherited from pandoc's old
definitions.typst (97a1d3d), not a Quarto design decision. Pinning it
would diverge from upstream permanently and shadow the native element,
which users can restyle with `#show divider:` rules. Horizontal rules
in typst output now render full-width instead of centered half-width;
the visual change is tracked for the 1.12 changelog.
User-visible consequence of the Pandoc 3.13 bump: `---` rules in typst
output switch from the inherited centered half-width line to Typst
0.15's native full-width divider element. The restyle hint
(`#show divider: set line(stroke: 2pt)` in a raw typst block) was
verified against the bundled Typst 0.15.1 via SVG output.
Matches the #13928 precedent of filing pandoc
behavior-change notes as sub-bullets of the dependency update, and
creates the 'Update pandoc to 3.13' Dependencies entry the remaining
3.13 release-note lines (yamlet, mathml, weasyprint) will hang off.
…table hook

Pandoc's common.latex loads the caption package and sets
\captionsetup[table]{skip=6pt} under $if(tables)$ (since 3.10.1), and
removes the footnotehyper/footnote \makesavenoteenv{longtable} block
(3.12): the LaTeX writer now generates footnotes in longtable manually
like floating tables (jgm/pandoc#11857), which also resolves the
endfloat incompatibility that motivated Quarto's #14741 test.

Port both changes into the wired pdf and beamer tables.tex partials.
The beamer branch already loaded caption, and crossref's injected
\@ifpackageloaded guard makes the now-unconditional top-level load a
no-op when crossref runs first, so there is no option-clash risk.

Verified: pdf (scrartcl) and beamer docs with crossref tables compile;
smoke-all 14741/14741-knitr, caption-footnotes, issue-4748, 9593,
crossref tables/latex and render-latex-output tests pass.
Bring the wired styles.html (used only with theme: pandoc) in line with
the parity parts of Pandoc 3.13's stock styles: the print block drops
backgrounds and the hardcoded 12pt font size, links inherit the text
color unless linkcolor is set, and hr/tbody/th borders use currentColor.
Rendered output is unchanged on screen when no color variables are set.

Pandoc's light-dark()/color-scheme support is not ported. Its
color-scheme expression emits "light dark" when no variables are set,
which would make OS-preference dark mode the default for every
theme: pandoc document, while the Quarto CSS loaded on that path
(syntax highlighting, quarto-html) is light-only. The quotecolor and
quotebordercolor variables come with that feature and stay out with it.
Pandoc 3.13's styles.html places its @media print block before the a,
blockquote, code and pre rules. Media queries add no specificity, so the
later screen rules win when printing: a linkcolor survives print,
blockquotes stay grey, and monobackgroundcolor keeps its background and
padding. Moving the block to the end of the document-css section lets
the print resets apply as intended. Pandoc's own template has the same
ordering; this is a local divergence until it is fixed upstream.

Adds a Playwright test that emulates print media on a theme: pandoc
document with linkcolor and monobackgroundcolor set.
Pandoc 3.13 writes horizontal rules as #divider(), which Quarto's Typst
template now resolves to Typst 0.15's native divider. With an older
compiler set through QUARTO_TYPST, such documents failed with a raw
"unknown variable: divider" error instead of Quarto's upgrade message.

The check only runs when QUARTO_TYPST is set; the bundled Typst is not
validated. The >=0.8 floor dates from 2023 and was not raised by any
Typst bump since, although bundled packages already need newer
compilers (marginalia and showybox declare 0.12, theorion 0.13).
quarto check already compares against the bundled 0.15.1.
The previous commit moved the @media print block to the end of the
document-css section, as the upstream issue suggested. Pandoc fixed the
issue (jgm/pandoc#12064, 1d639e394) by moving it to the very end of
styles.html, after the highlighting and citation CSS and outside
document-css. Match that placement so the block is identical to
upstream and the next Pandoc sync has no divergence to carry.

Differences from the previous placement: the print resets now also
apply when document-css is off, and they follow the task-list,
highlighting and citation rules too.
The comment above the bibliography path workaround cited jgm/pandoc#11511,
a closed issue about punctuation directly after a link. The leading-dot
escaping that the workaround avoids is tracked in jgm/pandoc#11761, which
is still open. Point the comment at #11761 so the next Pandoc bump checks
the right issue.
patch.sh and reveal.patch were added in 2022 to rebuild Quarto's
template.revealjs from Pandoc's revealjs.template. Later that year the
template was renamed to template.html and reworked for partials, but the
patch was never updated: the script still writes template.revealjs, the
patch only applies with fuzz, and it lacks changes template.html has
gained since. Nothing calls it, and template.html has been synced with
revealjs.template by hand ever since, as the update-pandoc checklist
describes for every format.
Since Pandoc 3.12 (jgm/pandoc#11845) every docx bookmark name starts
with `_`, which hides it from screen readers, and an id Word would
reject as a bookmark name is replaced by a SHA-1 hash of the id. Quarto
crossref ids all contain `-`, so `fig-elephant` no longer appears in
word/document.xml. Compute each expected name the way Pandoc does and
assert that the bookmark exists and the cross-reference link points at
it, so the test still checks each id rather than only that some target
exists.
Pandoc 3.10.1 fixed slide splitting in the pptx writer (jgm/pandoc#11433):
a slide holding a bullet list, a table, then another bullet list used to be
split across several slides and now stays on one "Title and Content" slide.
Quarto had no pptx test containing a table, so the change was unpinned.

This is a characterization test of the 3.13 output: it passes with Pandoc 3.13
and fails with Pandoc 3.10 (three slides instead of two).
pptx is tested through smoke-all documents with four verifiers whose slide
numbering and argument shapes are easy to get wrong: slide 1 is the title
slide, text before the first heading adds a slide, and the regex verifier
takes its slide number after the regex list. Layout and slide-count
assertions also depend on the bundled Pandoc, so they need a re-check on
each Pandoc bump. A path-scoped rule points to the guide from the pptx
smoke-all directory.
The runtime-boundary check counts direct standard-library imports in
tests against a baseline. The docx crossref test imported assert from
testing/asserts, pushing the count over it. tests/asserts.ts re-exports
the same assert, so the test now uses it and the baseline stays as is.
Pandoc no longer wraps a definition made of a single plain block in
#block[...] (changelog, tight term list improvements), so the inner
term renders as "/ Inner term: Inner definition." The old regex pinned
the #block[ that Pandoc happened to emit. The new one checks what the
test is about: the inner list sits inside the outer term's block.
Same dev build with Pandoc 3.10 gives the old output, so this is writer
drift, and the compiled PDF still shows the nesting.
Pandoc 3.13 writes a code block whose class is not a known language
without a space after the fence ("```text" instead of "``` text"),
from the fix for jgm/pandoc#11987. That is the only difference; the
same dev build with Pandoc 3.10 matches the old snapshot exactly.
Pandoc 3.13 parses YAML with yamlet, and a duplicate key in a mapping
is now an error instead of a warning, so this fixture failed the whole
render. The duplicate only showed Quarto's reader keeping the last value
as Pandoc did; the fixture's purpose, a ---delimited YAML bibliography
still producing Scholar citation tags (#15128), does not need it.
Pandoc has no option to restore the old behavior, so the changelog
tells users how to fix the error.
The delimited fixture used to carry a duplicate title to show that
Quarto's own read of the file (json: true in bibliography.ts) keeps
the last value, as Pandoc did. Pandoc 3.13 rejects duplicate keys when
the render reads the bibliography, so that path is unreachable and the
fixture dropped the duplicate. This document keeps the case covered
from the user's side: the render fails with an error naming the file,
line and column. With Pandoc 3.10 it renders and the test fails.

The bibliography.ts comment claimed Pandoc also keeps the last value;
it now says Pandoc rejects the duplicate.
Pandoc stopped wrapping a single-paragraph tight definition in #block
and replaced the default template's terms show rule with a hanging
indent (jgm/pandoc@1d6f33b897), so the definition now renders on the
term's line. The Quarto template already follows this, but the
visible layout change had no changelog entry.
Pandoc 3.12 prefixes every docx bookmark name with _ and hashes ids holding characters other than letters, digits and _, so every Quarto crossref id becomes a hashed, hidden bookmark. Internal crossref links keep working; references by id from outside the document do not. Decided 2026-11-10: accept the upstream change rather than rename bookmarks in a docx post-processing step.
Earlier changelogs never bold package names.
@cderv
cderv force-pushed the chore/deps-v1.12-pandoc branch from e3b1aa1 to 0a3b147 Compare November 10, 2026 16:40
cderv added a commit that referenced this pull request Nov 10, 2026
* Add triage-ci-failures skill for red PR checks

A dependency bump like Pandoc 3.13 turns a PR red across dozens of
bucket x OS jobs, each log several hundred KB. Check-run annotations
only name the bucket, and `gh run view --log` silently truncates near
750 KB, so finding which tests failed meant pulling raw logs into
context.

The script takes a PR number (every failing run on the head commit)
or a run id, caches failed-job logs per job id via the jobs/ID/logs
endpoint, and prints one row per unique failing test with the OSes it
failed on and the verify message. When verify hits a missing output
file, the row shows the render error from the OUTPUT tail instead.
`##[error]` lines are reported only for jobs with no failing test,
since smoke jobs echo each failed bucket there. Jobs whose log shape
is not parsed (Playwright, ff-matrix are unverified) are listed with
their log path.

The skill then has the agent group rows by cause and classify each as
dependency output drift or a regression before touching assertions
or snapshots.

On PR #15320 it reports the 18 failing tests plus the runtime-boundary
error in about 6 KB.

* Keep diagnostics for more test failure shapes in ci-failures.sh

- Capture the assertion message up to the stack trace (max 6 lines).
  noErrors and noErrorsOrWarnings put the actual errors after a
  generic first line, which the digest dropped.
- Treat "No output created message" and "Path ... doesn't exist" as a
  failed render, like an OS NotFound, and show the OUTPUT tail.
- A FAILED test with no TEST FAILURE block (prereq or teardown-only
  failure, which test.ts rethrows undecorated) now names the log to
  grep instead of printing an empty row.
- Download logs to a .part file and rename on success, so a failed
  fetch is retried next run instead of being cached truncated.

The PR #15320 digest is unchanged. The new shapes were checked on a
log written in the test.ts failure format.

* Keep assertion messages across blank lines in ci-failures.sh

A Sass failure reaches noErrorsOrWarnings as "Theme file compilation
failed:\n\n<diagnostics>" (src/core/dart-sass.ts), and the diagnostics
start with "Error:". Stopping the message at a blank line, or at any
"Error:" line, dropped the compiler error.

The message now ends at the stack trace, recognised as "at" frames or
as "<ErrorName>: <first message line>", the line JS stacks begin with.
Blank lines are skipped. XPath and layout verifiers append the whole
document after "... follows:", so capture ends at that line; the blank
line had been doing that job before.

The PR #15320 digest only gains the "Path:" line of the NotFound
message.

* Read the OS label from suffixed smoke job names in ci-failures.sh

test-smokes.yml names jobs "Run smoke (<os>)" plus an optional
" with timed file" or " - with some extra R packages". Requiring the
parenthesis at the end of the name made those rows show the full job
name instead of the OS.

* Keep CI digest excerpts traceable across OSes

A merged test row can contain failures with different causes. Identify the representative excerpt's OS and job and list all contributing logs so those differences can be checked before grouping failures by cause.

Clarify that missing output and failures limited to one OS require investigation before assigning a cause.

Verified on PR #15320: 18 unique failing tests and the runtime-boundary error, with the render diagnostics preserved in an 8.5 KB digest. Shell syntax and diff checks pass.
Pandoc 3.12+ writes crossref bookmarks under hashed hidden names
(jgm/pandoc#11845), in both w:bookmarkStart/@w:name and
w:hyperlink/@w:anchor, so smoke-all docx checks naming the raw crossref
id returned nothing and failed.

ensureDocxXpath and ensureDocxRegexMatches now replace a
{{bookmark:<id>}} placeholder with the name Pandoc computes for that id,
using the toBookmarkName port moved from smoke/crossref/docx.test.ts
into verify.ts. The docs keep readable ids rather than hardcoded hashes
that would break on the next naming change. The placeholder cannot
collide with shortcodes ({{< >}}), and the expanded name is hex plus an
underscore, so it needs no regex escaping.
… ids

The ref-hyperlink: false check targeted tbl-foo with link text
"Table 1", but this document defines tbl-letters and fig-elephant and
sets tbl-prefix to "T.", so it could never match, even after the
bookmark placeholder expansion. Assert that no hyperlink anchors either
id's bookmark, without a text predicate.

Checked by rendering a copy with ref-hyperlink: true: the check now
fails on the tbl-letters hyperlink.
The only document.xml differences are writer drift: Pandoc 3.12+
writes the heading bookmark under a hashed hidden name inside the
heading paragraph (jgm/pandoc#11845), and drawing/relationship ids
are renumbered. The callout content is unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants