Skip to content

Shortcode extensions: Q1 compatibility port (bd-540a976a) - #450

Merged
cscheid merged 9 commits into
mainfrom
feature/shortcode-extensions
Aug 1, 2026
Merged

Shortcode extensions: Q1 compatibility port (bd-540a976a)#450
cscheid merged 9 commits into
mainfrom
feature/shortcode-extensions

Conversation

@cscheid

@cscheid cscheid commented Jul 31, 2026

Copy link
Copy Markdown
Member

Closes the gap between Quarto 1's shortcode-extension support and Q2, per the plan at claude-notes/plans/2026-07-31-shortcode-extensions-port.md (braid epic bd-540a976a).

Headline fix

Extension shortcode scripts are now eagerly activated: previously, on-demand loading was keyed by extension id == shortcode name, so any real-world extension whose shortcode names differ from its directory name (e.g. quarto-tiers contributing tier, fontawesome contributing fa) silently failed with "Unknown shortcode". Verified end-to-end against the Connect docs project.

What's in here (one commit per theme)

  • Eager activation + shortcodes: key fix — load all discovered extensions' scripts on first Lua dispatch; document-level shortcodes: entries stored as PandocInlines no longer dropped (as_plain_text).
  • Handler contract parityraw_args includes keyword values; meta is the full metadata tree (dotted meta["a.b.c"] and chained meta.a.b.c, native scalars, 1-based array segments); nested shortcode args resolve bottom-up; pandoc.utils.stringify handles booleans/numbers (pandoc parity).
  • Q1-compat intake + loud failures_extension.yml no longer hard-requires title/author (bd-8b0af414); new Q-16 error subsystem (Q-16-1…6); manifest/script failures surface at discovery/load with the file named as cause instead of a misattributed unknown-shortcode at the use site (bd-nzdm1wry); handler shadowing reported as info-level Q-16-4.
  • Lua engine — script-dir-aware require (sibling + extension-root-relative, cached, SystemRuntime-backed so WASM shares the path); quarto.shortcode.error_output accepts table args (Q1 contract); shortcode_to_span's process::exit(1) replaced with Q1-style recursive span encoding.
  • Built-insvar (project-scoped _variables.yml, markdown values render), env (fallback arg), pagebreak (embedded Lua extension, near-verbatim Q1 port).
  • Unknown-shortcode policy — explicit shortcode-passthrough: [names] metadata key replaces Q1's silent pass-anything-through (composes with q2 render --strict); the HTML writer renders a visible ?name marker instead of silently dropping unresolved shortcodes; Q-16-6 warns when block-level shortcode output is dropped in inline position (bd-u145dg3y).

Tests

14 new smoke fixtures under crates/quarto/tests/smoke-all/extensions/ (contract-*, q1-compat-minimal-manifest, block-shortcode-inline-warns) — a compatibility corpus derived from Q1's documented contract and Q1's own test fixtures — plus pampa/quarto-core unit tests. One fixture is parked via tests.run.skip (Hugo {{</* */>}} comment-escape, deferred to bd-kmo1pzc2).

Workspace suite green (10,821 tests); cargo xtask verify (incl. WASM + hub-client legs) and cargo xtask lint pass locally.

Also adds claude-notes/instructions/q1-feature-porting.md, the process guide for future Q1→Q2 ports distilled from this work.

🤖 Generated with Claude Code

cscheid and others added 8 commits July 31, 2026 16:32
…ontract corpus (bd-540a976a)

Phase 1 core fix: extension shortcode scripts are now eagerly loaded on the
first dispatch that reaches the Lua stage, instead of looked up on demand by
extension id == shortcode name. Handler names come from Lua table keys and are
decoupled from the extension id (extension quarto-tiers contributes shortcode
tier), so the by-name model failed for most real-world extensions. Load order
fixes precedence per plan D4: document shortcodes: scripts < extensions in
discovery order (built-ins first, more-local last); Rust built-ins always win.
Per-script load failures now warn with the extension id and script path named
as the cause instead of hijacking the triggering shortcode's result.

Phase 2 early fix: extract_shortcode_paths dropped document-level shortcodes:
entries stored as ConfigValueKind::PandocInlines (the metadata-as-str failure
class); now uses as_plain_text().

New Phase 0 compatibility corpus (smoke-all/extensions/contract-*): six
passing fixtures covering table-return + global-fn registration, dash-named
shortcodes, block/inline context, kwargs missing-key semantics, return-value
coercions, brace escaping, and document-level shortcodes: with
quarto.shortcode.error_output. contract-escape-comment is parked via
tests.run.skip pending a decision on Q1's Hugo-style {{</* */>}} escape form.

Verified end-to-end: connect-docs {{< tier Enhanced >}} now renders
badge-enhanced spans with zero unknown-shortcode warnings.

Plan: claude-notes/plans/2026-07-31-shortcode-extensions-port.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…args, stringify scalars (bd-540a976a)

Four Q1-parity fixes in the Lua shortcode calling convention, each with a
previously-failing contract fixture or regression test:

- raw_args now carries keyword-argument values after positionals (names
  stripped), matching TS Quarto; Q1's video.lua relies on raw_args[1] as the
  src fallback. Exact source interleaving of positional-after-keyword args is
  not recoverable from the q2 AST and is documented as an approximation.

- The handler meta parameter is now the full metadata tree as nested Lua
  tables with native scalar types, plus a dotted-path __index fallback on the
  top level. Supports Q1's both documented access styles: meta['a.b.c'] and
  meta.a.b.c, including 1-based array segments (meta['authors.2']). Replaces
  the flat top-level stringified pairs, which dropped nested and compound
  values entirely. pampa's ShortcodeArgs.metadata is now a ConfigValue.

- Nested shortcode arguments ({{< outer {{< inner >}} >}}) resolve bottom-up
  before the outer handler runs and arrive as stringified plain args (TS
  Quarto semantics). They were previously silently dropped. Keyword-value
  nested shortcodes are handled the same way.

- pandoc.utils.stringify now converts booleans/integers/numbers to their
  string value (pandoc-documented behavior). This also fixes the regression
  the meta change would have caused in video.lua's auto-stretch gate, which
  stringifies meta['auto-stretch'].

Contract fixtures contract-args-kwargs, contract-meta-dotted (extended with
chaining/boolean/array probes), contract-nested-arg now pass; full workspace
suite green (10,810 tests).

Plan: claude-notes/plans/2026-07-31-shortcode-extensions-port.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fo (bd-540a976a)

Closes the Phase 1 loud-failure work:

- _extension.yml no longer hard-requires title/author (Extension.title/author
  are now Option<String>, read via as_plain_text). Only contributes remains
  structurally required. Real Q1 extensions with minimal manifests now load
  (bd-8b0af414). New smoke fixture q1-compat-minimal-manifest verifies the
  end-to-end path.

- New Q-16 extension subsystem in the error catalog: Q-16-1 Extension Not
  Loaded, Q-16-2 Shortcode Script Load Failure, Q-16-3 Unknown Shortcode,
  Q-16-4 Shortcode Handler Shadowed (info).

- discover_extensions now returns (extensions, diagnostics); manifest
  read/parse failures surface as Q-16-1 warnings naming the manifest file
  and cause, seeded into StageContext diagnostics — no longer a tracing
  log line followed by a misattributed unknown-shortcode at the use site
  (bd-nzdm1wry). Verified end-to-end: broken manifest renders with
  'Warning [Q-16-1]: Extension not loaded' + Q-16-3 at the invocation.

- Backfilled .with_code on shortcode resolution diagnostics (Q-16-2 script
  load failures, Q-16-3 unknown shortcode / handler not found).

- D4 shadowing diagnostic: the Lua engine records cross-script handler-name
  collisions (same-script reload is not a collision); the resolve transform
  drains them as Q-16-4 info diagnostics. Info level deliberately does not
  trip noErrorsOrWarnings or --strict — overriding a built-in is a
  supported pattern (lipsum-override fixture unchanged).

Full workspace suite green (10,813 tests).

Plan: claude-notes/plans/2026-07-31-shortcode-extensions-port.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cess::exit (bd-540a976a)

Completes Phase 2 of the shortcode-extensions port:

- Script-dir-aware require (register_scoped_require): resolves module names
  against the script-dir stack top-down — sibling-relative from the
  requiring module, then extension-root-relative (Q1's
  require('modules/brand/brand') convention). Sandboxed module env, cached
  by resolved path (nil result caches as true, like Lua), nested requires
  supported, falls back to the native stdlib require when present (WASM has
  none). All file access via SystemRuntime so native and WASM share the
  code path. New contract-require fixture + pampa unit test.

- quarto.shortcode.error_output now accepts a table as message_or_args
  (values concatenated with spaces), matching Q1's contract; Q1's own
  fixture calls error_output('shorty', args, 'inline'). The error_args
  case is now ported into contract-doc-shortcodes.

- shortcode_to_span no longer kills the process on nested shortcodes in
  keyword args: they encode Q1-style (param span with data-key, no
  data-value, nested shortcode span as content); the impossible
  KeyValue-in-kwargs case degrades visibly instead of exiting.

Full workspace suite green (10,817 tests).

Plan: claude-notes/plans/2026-07-31-shortcode-extensions-port.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 4 of the shortcode-extensions port — the three easy-tier built-ins Q1
has and Q2 lacked:

- var (Rust VarShortcodeHandler): reads _variables.yml from the project root
  (project-scoped, Q1 parity), parsed with InterpretationContext::
  DocumentMetadata so markdown values render as markdown (documented Q1
  behavior). Dotted-path lookup via get_nested. Unknown variable → Q-16-5
  warning + visible ?var marker. StageContext loads the file once per
  document and threads it through build_transform_pipeline; an existing but
  unparseable _variables.yml warns naming the file.

- env (Rust EnvShortcodeHandler): positional name + optional fallback arg
  (Q1 1.5 #8316). Unset without fallback → Q-16-5 warning (stricter than
  silently emitting nothing; on wasm32 env reads always fail, so env is
  uniformly unset there).

- pagebreak: new embedded built-in Lua extension
  (resources/extensions/quarto/pagebreak/), a near-verbatim port of Q1's
  handlePagebreak: per-format raw payloads (html/epub/latex/openxml/odt/
  context/typst), pptx empty, form-feed fallback.

- New Q-16-5 'Shortcode Value Not Found' catalog entry; also applied to the
  meta handler's unknown-key warning.

Fixtures: contract-var (simple/nested/markdown-link values), contract-var-
unknown (printsMessage WARN + ?var marker), contract-env (fallback),
contract-pagebreak; Rust unit tests for env (set/fallback/unset) and var
(nested lookup/unknown). Full workspace suite green (10,819).

Plan: claude-notes/plans/2026-07-31-shortcode-extensions-port.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ps (bd-540a976a)

Phase 5 of the shortcode-extensions port:

- New shortcode-passthrough metadata key: a list of shortcode names to pass
  through verbatim (renders the literal {{< ... >}} text, no warning). This
  replaces Q1's implicit pass-anything-unknown-through (a Hugo-interop
  behavior) with an explicit declaration; anything not declared keeps the
  source-mapped Q-16-3 warning. Composes with q2 render --strict for
  unknown-shortcode-as-error. Fixture: contract-passthrough.

- HTML writer no longer silently drops a surviving Inline::Shortcode — it
  renders a visible <span class="quarto-unresolved-shortcode">?name</span>
  marker (a surviving node is a resolution leak; silent disappearance made
  it undiagnosable). Unit test added.

Note: catalog codes Q-3-30/Q-3-42 remain unreferenced — writers have no
diagnostics channel today; the visible marker covers discoverability. Left
as-is rather than gold-plating a writer diagnostics channel.

Full workspace suite green (10,821).

Plan: claude-notes/plans/2026-07-31-shortcode-extensions-port.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n (bd-u145dg3y)

A shortcode used mid-sentence whose handler returns block-level output
(e.g. video's raw HTML block) used to vanish into an empty flatten with no
trace. Flattening that loses ALL output now emits a source-mapped Q-16-6
warning naming the shortcode, with the fix hint (own paragraph, blank
lines around it). Partial flattens stay silent — that's Q1's documented
blocks_to_inlines behavior.

Also adds claude-notes/instructions/q1-feature-porting.md — the process
guide for future Q1→Q2 feature ports, distilled from this port (three-way
study, two divergence levers, corpus-first TDD, diagnostics idiom, repo
gotchas), plus plan checklist updates for phases 4/5.

Full workspace green; cargo xtask verify (all 14 steps incl. WASM +
hub-client) passed; cargo xtask lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per review: how Q2 does Hugo interop is not yet designed, so the
{{</* */>}} escape form (and the related paired-shortcode question) moves
to its own question strand rather than complicating this port. Parked
fixture's skip reason now points at bd-kmo1pzc2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@posit-snyk-bot

posit-snyk-bot commented Jul 31, 2026

Copy link
Copy Markdown

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.

…16-1..6)

Both sides appended entries at the file tail; no key or subsystem
collisions. Resolved per snapshot-of-truth policy for generated-ish
files: took main's version and re-added the Q-16 extension-subsystem
entries. Workspace suite green post-merge (10,853 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cscheid
cscheid merged commit 6ff4221 into main Aug 1, 2026
8 checks passed
@cscheid
cscheid deleted the feature/shortcode-extensions branch August 1, 2026 01:37
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