Evaluate the {r} inline spelling, and give r expr its documented escaping - #658
Merged
gordonwoodhull merged 1 commit intoSep 4, 2026
Conversation
…-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
Contributor
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
gordonwoodhull
deleted the
feature/bd-inline-r-brace-spelling-not-evaluated-lk9s3iwe
branch
September 4, 2026 19:17
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.tsmatches 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 therand the expression, so a tab-separated expression would otherwise stop evaluating.Wrapping the classic form as
.QuartoInlineRender(I(expr))would express the same intent, being the documented equivalence taken literally, butI(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:
`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}.`r NULL`now renders as the empty string rather than the literal textNULL, because knitr's inline hook replaces the wrapper on this path.{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 themarkdown-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:
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 followdata-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.qmdgains 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, theI()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
markdownblock rather than "fenced code block" deliberately. #657 kept the display-class predicate narrow (info string empty ormarkdown), so a```rblock displays its text but does not stop an expression inside it from running. Measured on the stacked tree: both spellings are displayed verbatim inside amarkdownblock and inside a bare fence, and both execute inside a```rblock. 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 suitetests/integration/knitr_inline_expressions.rs(8 tests) drivesrender_document_to_file— the entryq2 renderuses — for both spellings in prose, attribute values and link titles, the escaping split, the documentedI()equivalence, per-spellingNULLrendering, and fence survival.