Skip to content

Evaluate the {r} inline spelling, and give r expr its documented escaping - #658

Merged
gordonwoodhull merged 1 commit into
bugfix/bd-0gwekaem-nested-cell-mask-inline-expressionsfrom
feature/bd-inline-r-brace-spelling-not-evaluated-lk9s3iwe
Sep 4, 2026
Merged

Evaluate the {r} inline spelling, and give r expr its documented escaping#658
gordonwoodhull merged 1 commit into
bugfix/bd-0gwekaem-nested-cell-mask-inline-expressionsfrom
feature/bd-inline-r-brace-spelling-not-evaluated-lk9s3iwe

Conversation

@gordonwoodhull

Copy link
Copy Markdown
Member

Quarto has two spellings for an inline executable expression, and the difference between them is deliberate:

  • `{r} expr` — the cross-engine brace spelling quarto.org has recommended since 1.4. Markdown specials in the value are escaped, so a value of **bold** renders as the literal text **bold**.
  • `r expr` — knitr's native rmarkdown spelling, which predates Quarto. The value is inserted as live markdown, so **bold** renders bold. The docs state the relationship as an exact equivalence: `r x` is `{r} I(x)`.

We implemented neither.

The brace spelling was never evaluated at all. It fell through to the markdown parser and rendered as an ordinary code span — <code>{r} release_version</code> in prose, and the literal backticked text inside an attribute value — with no diagnostic and exit 0. The classic spelling was evaluated, but wrapped in .QuartoInlineRender(), which escapes, so it carried the brace form's default rather than its own. Read together we had one inline spelling with one semantics, and it was the brace semantics wearing the classic spelling.

Both halves come from the same regex. It had no alternation for {r}, and it applied the wrapper to the one spelling Quarto 1 deliberately leaves alone. Quarto 1 is not a superset here, it is the mirror image: execute-inline.ts matches the brace form and nothing else, and the native form reaches knitr unwrapped, which is precisely why it passes markdown through.

Now:

  • `{r} expr` is rewritten to `r .QuartoInlineRender(expr)`, so it evaluates and escapes. It works in prose, in a fenced-div attribute value and in a link title.
  • `r expr` goes to knitr unwrapped, where knitr's own inline hook inserts the value as markdown. The only edit it gets is separator normalization — knitr accepts [ #] between the r and the expression, so a tab-separated expression would otherwise stop evaluating.
  • An empty brace expression is wrapped rather than passed through, so it fails loudly as it does under Q1 instead of rendering as a silent code span. That is reachable in an attribute value, the position whose text survives to this pass verbatim.

Wrapping the classic form as .QuartoInlineRender(I(expr)) would express the same intent, being the documented equivalence taken literally, but I(NULL) is an error in R, so it would turn `r NULL` from a rendered value into a failed render. Handing the expression through untouched reaches the same markdown-passthrough semantics via knitr with no such edge.

Three behaviour changes for existing documents, all matching Quarto 1:

  1. A `r expr` whose value contains markdown specials now renders as markdown rather than as literal text. This is the documented default for that spelling; authors who want the escaping have it on {r}.
  2. `r NULL` now renders as the empty string rather than the literal text NULL, because knitr's inline hook replaces the wrapper on this path.
  3. A knitr document that displays the {r} syntax now fails the render if the expression doesn't resolve, unless the surrounding block is one the nested-cell mask protects. Q1 fails identically. Don't execute an inline expression that is being displayed #657, which this stacks on, is what makes the markdown-block escape work here.

Where it bit

The Positron website's download page carries the version on the element its own JavaScript reads to build the platform download URLs:

::: {#download-hero .download-hero data-version="`{r} release_version`"}

Under Quarto 1 that renders as data-version="2026.08.1-2". Under q2 it rendered as the literal backticked source, and the page's primary function broke. Nothing caught it: the render log is silent, text diffing compares visible text and this is an attribute, and link checkers don't follow data-version.

On the fence guards

The preprocessor's prefix guard exists because this pass runs over the whole serialized document, where a fence's last opening backtick can otherwise anchor a match that swallows the block body up to the closing fence (bd-knitr-inline-r-eats-fence-2ofk91x1). The brace spelling brings its own fence shape into range, and a sharper one: ```{r} appears in every document the knitr engine runs, and needs only a trailing space to satisfy the separator. The guard covers it at unit level. At render level the nested-cell mask intercepts that shape first, so the render-level test pins the chain rather than the guard alone — established by mutation and documented on the test so it isn't later "strengthened" into something vacuous.

Docs

guides/authoring/computations.qmd gains an "Inline expressions" section: the two spellings and why an author would pick one, a table that shows the escaping contrast rather than describing it, the I() equivalence, where the expressions work including the easy-to-miss attribute positions, and how to write about the syntax without running it. That last part is why this stacks on #657 — it teaches the same rule the page already teaches for cells, which only becomes true once the mask covers inline expressions.

The section says markdown block rather than "fenced code block" deliberately. #657 kept the display-class predicate narrow (info string empty or markdown), so a ```r block displays its text but does not stop an expression inside it from running. Measured on the stacked tree: both spellings are displayed verbatim inside a markdown block and inside a bare fence, and both execute inside a ```r block. That matches Quarto 1; widening it is bd-tiidc899.

Tests

27 → 46 unit tests in preprocess.rs, covering both spellings, every fence shape, idempotency and the empty-expression split. A new render-level suite tests/integration/knitr_inline_expressions.rs (8 tests) drives render_document_to_file — the entry q2 render uses — for both spellings in prose, attribute values and link titles, the escaping split, the documented I() equivalence, per-spelling NULL rendering, and fence survival.

…-r-brace-spelling-not-evaluated-lk9s3iwe)

Quarto has two spellings for an inline executable expression with
deliberately different semantics: `{r} expr` escapes markdown specials in
the value, and knitr's native `r expr` inserts the value as live markdown.
quarto.org states the relationship as an equivalence -- `r x` is `{r} I(x)`.

We implemented neither. The brace spelling was never evaluated: it fell
through to the markdown parser as a code span, with no diagnostic and exit 0.
The classic spelling was evaluated but wrapped in `.QuartoInlineRender()`, so
it carried the brace form's escaping default rather than its own -- one
spelling with one semantics, and it was the brace semantics wearing the
classic spelling.

Both halves came from the same regex, which had no alternation for `{r}` and
applied the wrapper to the spelling Quarto 1 deliberately leaves alone. Q1's
`execute-inline.ts` matches the brace form and nothing else; the native form
reaches knitr unwrapped, which is why it passes markdown through.

The pattern now carries a spelling marker and the replacement branches on it:
`{r} expr` becomes `r .QuartoInlineRender(expr)`; `r expr` goes to knitr
unwrapped, with only its separator normalized to a single space (knitr's own
class is `[ #]`, so a tab-separated expression would otherwise stop
evaluating). Wrapping the classic form as `.QuartoInlineRender(I(expr))` --
the documented equivalence taken literally -- would break `r NULL`, because
`I(NULL)` is an error in R.

An empty brace expression is now wrapped rather than passed through, so it
fails loudly as it does under Q1 instead of rendering as a silent code span.
That is reachable in an attribute value, the position whose text survives to
this pass verbatim; in prose the reader normalizes `{r}   ` to `{r}` first, so
the pattern never sees it.

Three behaviour changes for existing documents, all matching Quarto 1:

1. A `r expr` whose value contains markdown specials now renders as markdown
   rather than as literal text. This is the documented default for that
   spelling; authors wanting the escaping have it on `{r}`.
2. `r NULL` now renders as the empty string rather than the literal text
   `NULL`, because knitr's inline hook replaces the wrapper on this path.
3. A knitr document that *displays* the `{r}` syntax now fails the render if
   the expression doesn't resolve, unless the surrounding block is one the
   nested-cell mask protects. Q1 fails identically.

The brace spelling brings its own fence hazard into the pattern's range, and a
sharper one than the classic form's: ```{r} is in every document the knitr
engine runs and needs only a trailing space to satisfy the separator. The
existing prefix guard covers it, with regression tests for that shape alongside
the display-fence ones already present
(bd-knitr-inline-r-eats-fence-2ofk91x1). At render level the nested-cell mask
intercepts that shape first, so the render-level test pins the chain rather
than the guard alone -- established by mutation and documented on the test, so
it isn't later "strengthened" into something vacuous.

Tests: 27 -> 46 unit tests in preprocess.rs, and a new render-level suite
`tests/integration/knitr_inline_expressions.rs` (8 tests) driving
`render_document_to_file`, covering both spellings in prose, fenced-div
attribute values and link titles, the escaping split, the documented I()
equivalence, per-spelling NULL rendering, and fence survival.

DOCS

`guides/authoring/computations.qmd` covered executable cells and how to display
one without running it, but said nothing about inline expressions. A reader had
to go to quarto.org, which describes Quarto 1, or read the source. The new
section covers the two spellings and why an author would pick one, with a table
that shows the escaping contrast rather than describing it -- the same value
renders as literal `**important**` in one row and as bold in the other. Since
markdown admits inline HTML, that default is also the reason to prefer the
brace spelling for a value you did not author, which the section says plainly.
It documents the attribute positions too, which are easy to miss precisely
because nothing in the rendered text changes when they fail.

This branch is stacked on bd-0gwekaem (PR #657), which extends the nested-cell
mask to inline expressions. Without it, an inline expression inside a display
block still executes and the section's advice would be wrong. With it, the rule
for inline expressions collapses into the rule the page already teaches for
cells.

The section says `markdown` block rather than "fenced code block" on purpose.
bd-0gwekaem kept the display-class predicate narrow -- info string empty or
`markdown` -- so a ```r block displays its text but does not stop an expression
inside it from running. Measured on this tree: brace and classic spellings are
both displayed verbatim inside a `markdown` block and inside a bare fence, and
both execute inside a ```r block. That is Quarto 1's behaviour too; widening it
is bd-tiidc899.

No snapshot files added, modified or removed.

Closes bd-inline-r-brace-spelling-not-evaluated-lk9s3iwe
@posit-snyk-bot

posit-snyk-bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

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.

@gordonwoodhull
gordonwoodhull merged commit 3669ab0 into main Sep 4, 2026
9 checks passed
@gordonwoodhull
gordonwoodhull deleted the feature/bd-inline-r-brace-spelling-not-evaluated-lk9s3iwe branch September 4, 2026 19:17
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.

2 participants