Add quarto.* Pandoc template variable namespace#14530
Open
cderv wants to merge 10 commits into
Open
Conversation
…language Pandoc templates can now resolve `$quarto.language.<key>$` (e.g. `$quarto.language.crossref-ch-prefix$`) without per-key TypeScript copies or Lua surfacing into Pandoc meta. The defaults-file variables: section already accepts nested maps natively, so the whole format.language table is wired with one nested-write in generateDefaults. No format.metadata writes are involved, so writers with +yaml_metadata_block (markdown, native, json) do not serialize these keys to output. Tests: - tests/docs/smoke-all/markdown/lang-fr-template-resolve.qmd — positive resolution via a custom Pandoc template referencing $quarto.language.crossref-ch-prefix$, asserts the localized "Chapitre" value when lang: fr is set. format: plain with a one-line template gives clean end-to-end variable substitution. - tests/docs/smoke-all/markdown/lang-fr-no-yaml-leakage.qmd — leakage guard renders to format: markdown with lang: fr and asserts crossref-* / toc-title-document do NOT appear in the rendered YAML header. Regression guard so any future per-key TypeScript copy or Lua surfacing that re-introduces the leakage fails CI.
…faults variables) Documents how lang: <bcp47> resolves into localized strings across HTML, LaTeX, and Typst. Highlights the new defaults-file variables.quarto.language channel as the canonical bulk channel for new keys consumed by Pandoc templates. §3c covers the orange-book typst-show.typ migration to $quarto.language.crossref-*$. §4 adding-a-new-key decision table recommends 2d for template consumers. §5 pitfalls leads with the 2d vs 2b tradeoff (metadata leakage via +yaml_metadata_block).
Replace the inline format.language wiring in generateDefaults with a named builder so the reserved quarto.* Pandoc template-variable namespace has a single, typed contribution point. Future surfaces (quarto.version, quarto.project, etc.) add a field to the QuartoTemplateVariables interface and a contribution branch to buildQuartoTemplateVariables rather than another if-block at the defaults call site. The language field is typed as FormatLanguage (the same canonical shape used across HTML/website/book consumers), not Record<string, unknown>, so future template references like $quarto.language.<key>$ are grounded in the existing schema. No behavior change: smoke-all guards lang-fr-template-resolve and lang-fr-no-yaml-leakage still pass.
Channel 2d's contribution point moved from an inline block in generateDefaults to a dedicated builder file. Update the section 2d prose, the "Adding a new localized string" table (separate row for adding to format.language vs. exposing a brand-new quarto.<area> key), and key_files so future readers find the builder rather than reading defaults.ts looking for the wiring.
isObject (already re-exported) is the broad lodash variant that returns true for arrays and functions. For the "user-supplied YAML value that should be a plain map before we spread it" use-case the narrower lodash.isPlainObject is the right contract: rejects arrays, functions, Date, Map, etc. Add it to the curated re-export surface in src/core/lodash.ts so call sites can use ld.isPlainObject instead of inlining `value !== null && typeof value === "object" && !Array.isArray(value)`. First consumer lands with the variables.quarto.* override-precedence fix in src/command/render/defaults.ts.
variables.quarto.* is an internal namespace populated by Quarto; the
user-facing override path for localized strings is the top-level
language: YAML key, which already flows through formatLanguage into
options.format.language and through the builder.
But if a user explicitly sets variables: { quarto: ... } in YAML as
an escape hatch, the previous spread order in generateDefaults
silently clobbered their value with the builder output. Reverse the
spread so builder values go in first and any pre-existing
variables.quarto.* survives on top. Guard the merge with
ld.isPlainObject so a non-object value (string, number, array)
written into variables.quarto is ignored defensively rather than
producing a runtime spread on a non-object.
The builder file's contribution-point comment and the channel-2d
section of llm-docs/localization-architecture.md now spell out the
internal-namespace contract and the precedence rule.
New smoke-all guard tests/docs/smoke-all/markdown/lang-fr-user-override-quarto-variables.qmd
pins the precedence: with lang: fr (default Chapitre) and
variables.quarto.language.crossref-ch-prefix: "Bouquin", the user
value resolves in the template.
Add unit tests for buildQuartoTemplateVariables that lock its contract directly, without going through the render pipeline: missing format.language returns undefined, populated language is surfaced under the language field, the reference is passed through verbatim, and an empty (defined) language still produces a wrapper. These run in tests/unit/ and give future contributors a fast feedback target when adding a new field to the QuartoTemplateVariables interface. Add a smoke-all guard with lang: ja that asserts the Japanese value "チャプター" for crossref-ch-prefix resolves through the entire stringify -> defaults YAML -> Pandoc template tokenizer pipeline. Guards against Unicode breakage in any of those stages.
Collaborator
✅ 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. |
Member
|
(I think this is a great idea.) |
The previous spread `{ ...quartoVars, ...existingQuarto }` only merged at
depth 1 of `variables.quarto`. A user-supplied
`variables.quarto.language.crossref-ch-prefix: Bouquin` would replace the
entire localized `language` map, silently dropping every other
`$quarto.language.*$` resolution in templates (toc-title-document,
crossref-fig-title, etc.). The smoke test added with the original
precedence fix passed only because its template read the single
overridden key.
Switch to the in-tree canonical deep-merge helper `mergeConfigs` in
`src/core/config.ts`. Same helper is used in `core/language.ts`
`formatLanguage` for the upstream merge of user-supplied `language:`
onto `_language.yml` defaults — so the FormatLanguage tree is now
merged with consistent semantics at both ends of the pipeline.
The new smoke-all test exercises a two-key template
`$quarto.language.crossref-ch-prefix$|$quarto.language.toc-title-document$`
under `lang: fr` with the user overriding only the first key. Pre-fix
the second key would resolve empty; with deep-merge both render
correctly.
…l 2d row Section 3c previously described orange-book typst-show.typ as already migrated to `$quarto.language.*$` with a `supplement-chapter` argument wired. Verified at the source: line 28-31 still uses the legacy channel-2b form `$if(crossref.lof-title)$$crossref.lof-title$$else$$crossref-lof-title$$endif$` and there is no `supplement-chapter` argument. The migration is still pending at #14524. Describe the actual current state and flag the +yaml_metadata_block leakage path that channel-2d migration will eliminate. Section 2d prose updated to describe the new deep-merge semantics from the previous commit, with a worked example showing leaf-key precedence. Section 4 channel-2d table row tightened so it no longer reads as if adding to `_language.yml` is sufficient on its own. The key still has to reach `format.language`, which is filtered by `kLanguageDefaultsKeys` + the `crossref-*-{title,prefix}` patterns at `src/core/language.ts:167-173`. Step 3 of the general "Steps for every new key" already covers that; the row now points back to it explicitly.
PR #14530 surfaced two pieces of architectural knowledge that were not written down anywhere in the repo: 1. The canonical deep-merge helper family. mergeConfigs in core/config.ts wraps lodash.mergeWith with mergeArrayCustomizer (array union-concat with JSON.stringify dedup — surprisingly NOT last-in wins for arrays). mergeFormatMetadata, mergeProjectMetadata, and mergeConfigsCustomized layer additional per-key rules on top. Raw lodash.merge is never used in the render path. Without this written down, contributors reach for a shallow spread or for lodash.merge directly; both are wrong on nested config trees. 2. The multi-key smoke-test heuristic. A precedence smoke-all test where the template reads ONLY the overridden key can pass even under a deep-merge bug that drops every other sibling key. The fix is to probe at least one non-overridden sibling key in the same template. The lang-fr-user-override-deep-merge.qmd test added in the previous commit is the worked example. Files: - .claude/rules/typescript/config-merging.md (paths-scoped to src/**/*.ts): short rule that fires on TS work; points at the architectural doc. - llm-docs/config-merging.md: full architectural reference following the existing llm-docs frontmatter convention (main_commit, analyzed_date, key_files). Covers helper hierarchy, call-site map, common pitfalls, guidance for adding a new wrapper. - llm-docs/testing-patterns.md: appended a section under the existing "Smoke-All Tests (YAML-Based)" heading covering the multi-key probe heuristic.
Member
Author
|
Documentation PR |
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.
Pandoc templates today cannot see Quarto's resolved localized strings
without per-key meta copies. Those copies leak into the rendered YAML
header for writers that emit
yaml_metadata_block(markdown, native,json) and require format-by-format gating to suppress.
format.language(the merged result of Quarto's bundled_language.ymlplus per-locale and per-document overrides) is nowsurfaced to Pandoc templates via the defaults-file
variables:section, under a reserved internal namespace
quarto.*:Templates access keys via dotted form:
The defaults-file
variables:section is not serialized back intorendered output by any writer, so the leakage problem goes away
without per-format gating.
Why a new namespace
quarto.*is reserved as an internal, Quarto-owned namespace insidePandoc template variables. Users overriding a localized string keep
using the existing top-level
language:YAML key (see #14466). Theschema does not surface
variables.quarto.*as a user-facing option,and on collision the user-set value wins, so an explicit YAML escape
hatch keeps working.
Contribution point
A single file —
src/command/render/quarto-template-variables.ts—is the contribution point for new keys under
quarto.*. Adding a newinternal value to expose to templates is a typed two-step:
QuartoTemplateVariables(use the tightestexisting type, e.g.
FormatLanguage).buildQuartoTemplateVariablesthatreads from
PandocOptionsand assigns to that field.The wiring into the defaults file (
generateDefaultsinsrc/command/render/defaults.ts) is single-point and does not needchanges per new key.
Opportunities this opens
template-channel design work —
$quarto.format.X$,$quarto.project.X$,$quarto.version$, etc.lang:is set to a non-English locale #14524, tracked as aseparate PR) becomes a one-line template edit on top of this
channel, replacing a Lua filter workaround.
$toc-title$/$abstract-title$(populated via per-key meta copies in
src/command/render/pandoc.ts)can migrate to
$quarto.language.toc-title-document$/$quarto.language.section-title-abstract$. Once migrated, thoseper-key meta copies and their per-format gating can be removed.
Localization architecture
Channel 2d (defaults-file
variables:) is now documented as thecanonical bulk channel for new language keys, alongside Lua filter
params and Pandoc meta. See
llm-docs/localization-architecture.md.Test plan
tests/docs/smoke-all/markdown/lang-fr-template-resolve.qmd— custom Pandoc template +lang: frresolves$quarto.language.crossref-ch-prefix$to "Chapitre"tests/docs/smoke-all/markdown/lang-fr-no-yaml-leakage.qmd— regression guard:crossref-*andtoc-title-documentstay out of the rendered markdown YAML headertests/docs/smoke-all/markdown/lang-fr-user-override-quarto-variables.qmd— user-setvariables.quarto.language.crossref-ch-prefixwins on collisiontests/docs/smoke-all/markdown/lang-ja-template-resolve.qmd— unicode round-trip:lang: jaresolves to "チャプター"tests/unit/render/quarto-template-variables.test.ts— builder contract: empty in → undefined out; reference-passing; empty-object caseRelated to #14524