@@ -138,6 +138,37 @@ pub fn is_revealjs_target(target_format: &str) -> bool {
138138 matches ! ( target_format, "revealjs" | "q2-slides" )
139139}
140140
141+ /// The canonical Pandoc output format a Lua filter or shortcode should see as
142+ /// its `FORMAT` global, given the pipeline's `target_format`.
143+ ///
144+ /// The preview pipeline runs with a *pseudo-format* `target_format`
145+ /// (`q2-preview`, `q2-slides`, …) so q2-core can branch on it for AST-vs-HTML
146+ /// output and reveal-tree construction (see [`builtin_pseudo_format`] /
147+ /// [`is_revealjs_target`]). But Lua extensions don't know about those
148+ /// pseudo-formats: their `quarto.doc.is_format("html:js")` /
149+ /// `is_format("revealjs")` checks (e.g. the built-in `video` shortcode) only
150+ /// recognize real Pandoc formats. If we hand the pseudo-format straight to Lua,
151+ /// those checks fail and format-gated shortcodes/filters silently degrade
152+ /// (`{{< video >}}` collapsed to a plain link in preview — bd-5b21rbaq).
153+ ///
154+ /// This maps each preview pseudo-format back to the real format it emulates so
155+ /// preview Lua behaves identically to render:
156+ /// - `q2-preview` / `q2-debug` / `q2-raw` → `html`
157+ /// - `q2-slides` → `revealjs` (so `is_format("revealjs")` is true, matching
158+ /// what [`is_revealjs_target`] already does for pipeline decisions; note this
159+ /// intentionally differs from [`builtin_pseudo_format`], which reports the
160+ /// *output writer* base `html`).
161+ ///
162+ /// Any non-pseudo `target_format` (`html`, `revealjs`, `latex`, …) passes
163+ /// through unchanged.
164+ pub fn lua_format_for ( target_format : & str ) -> & str {
165+ match target_format {
166+ "q2-preview" | "q2-debug" | "q2-raw" => "html" ,
167+ "q2-slides" => "revealjs" ,
168+ other => other,
169+ }
170+ }
171+
141172/// Extract the chosen output format from a document's leading YAML
142173/// front-matter: the `format:` scalar, or the first key when `format:` is a
143174/// map (e.g. `format: {revealjs: {...}}`). Returns `None` when there is no
@@ -329,6 +360,35 @@ impl Format {
329360 }
330361 }
331362
363+ /// The canonical Pandoc output format a Lua filter or shortcode should see
364+ /// as its `FORMAT` global.
365+ ///
366+ /// Lua extensions branch on real Pandoc formats
367+ /// (`quarto.doc.is_format("html:js")`, `is_format("revealjs")`, …), not on
368+ /// q2's preview pseudo-formats or extension-style format strings. This
369+ /// resolves a `Format` to the base format it emulates:
370+ /// - reveal targets (`revealjs` render **and** `q2-slides` preview) →
371+ /// `"revealjs"`, so `is_format("revealjs")` fires in preview too. The
372+ /// bare [`identifier`](Self::identifier) would otherwise collapse
373+ /// `q2-slides` to `Html` (its *output writer* is HTML), losing
374+ /// reveal-ness — the reason a user filter in a reveal preview saw
375+ /// `"html"` (bd-5b21rbaq).
376+ /// - everything else → the identifier base (`html`, `pdf`, …), which
377+ /// already canonicalizes extension formats (`acm-pdf` → `pdf`) and the
378+ /// HTML preview pseudo-formats (`q2-preview`/`q2-debug`/`q2-raw` → `html`).
379+ ///
380+ /// This is the [`Format`]-aware companion to the string-only
381+ /// [`lua_format_for`] (used where only a `target_format` string is in hand,
382+ /// e.g. the transform-pipeline builder); the two agree on the pseudo-format
383+ /// and base cases.
384+ pub fn lua_format ( & self ) -> & str {
385+ if is_revealjs_target ( & self . target_format ) {
386+ "revealjs"
387+ } else {
388+ self . identifier . as_str ( )
389+ }
390+ }
391+
332392 /// Parse a format string into a Format.
333393 ///
334394 /// Accepts:
@@ -821,6 +881,79 @@ mod tests {
821881 assert_eq ! ( f. pipeline_kind, Some ( "preview" ) ) ;
822882 }
823883
884+ #[ test]
885+ fn test_lua_format_for_maps_preview_pseudo_formats ( ) {
886+ // HTML-emulating pseudo-formats resolve to `html` so
887+ // `is_format("html:js")` fires in preview (bd-5b21rbaq).
888+ assert_eq ! ( lua_format_for( "q2-preview" ) , "html" ) ;
889+ assert_eq ! ( lua_format_for( "q2-debug" ) , "html" ) ;
890+ assert_eq ! ( lua_format_for( "q2-raw" ) , "html" ) ;
891+ // The reveal preview pseudo-format resolves to `revealjs` so
892+ // `is_format("revealjs")` fires — distinct from
893+ // `builtin_pseudo_format`, which reports the output-writer base `html`.
894+ assert_eq ! ( lua_format_for( "q2-slides" ) , "revealjs" ) ;
895+ }
896+
897+ #[ test]
898+ fn test_lua_format_for_passes_through_real_formats ( ) {
899+ for f in [ "html" , "revealjs" , "latex" , "pdf" , "gfm" , "typst" , "docx" ] {
900+ assert_eq ! ( lua_format_for( f) , f, "real format {f} must pass through" ) ;
901+ }
902+ }
903+
904+ #[ test]
905+ fn test_format_lua_format_canonicalizes ( ) {
906+ // Real formats: identifier base.
907+ assert_eq ! (
908+ Format :: from_format_string( "html" ) . unwrap( ) . lua_format( ) ,
909+ "html"
910+ ) ;
911+ assert_eq ! (
912+ Format :: from_format_string( "revealjs" ) . unwrap( ) . lua_format( ) ,
913+ "revealjs"
914+ ) ;
915+ // Extension formats canonicalize to their base (the bug `identifier`
916+ // already handled; `lua_format` must preserve it).
917+ assert_eq ! (
918+ Format :: from_format_string( "acm-pdf" ) . unwrap( ) . lua_format( ) ,
919+ "pdf"
920+ ) ;
921+ // HTML preview pseudo-formats → html.
922+ assert_eq ! (
923+ Format :: from_format_string( "q2-preview" )
924+ . unwrap( )
925+ . lua_format( ) ,
926+ "html"
927+ ) ;
928+ assert_eq ! (
929+ Format :: from_format_string( "q2-debug" ) . unwrap( ) . lua_format( ) ,
930+ "html"
931+ ) ;
932+ // Reveal preview pseudo-format → revealjs (NOT its html output base) —
933+ // the parity fix for user filters in reveal preview.
934+ assert_eq ! (
935+ Format :: from_format_string( "q2-slides" )
936+ . unwrap( )
937+ . lua_format( ) ,
938+ "revealjs"
939+ ) ;
940+ }
941+
942+ /// `Format::lua_format` and the string-only `lua_format_for` must agree on
943+ /// the pseudo-format + base cases both handle, so the shortcode pipeline
944+ /// (string-only) and user-filter stage (Format-aware) don't drift.
945+ #[ test]
946+ fn test_lua_format_helpers_agree_on_shared_cases ( ) {
947+ for fmt in [ "html" , "revealjs" , "q2-preview" , "q2-slides" , "q2-debug" ] {
948+ let f = Format :: from_format_string ( fmt) . unwrap ( ) ;
949+ assert_eq ! (
950+ f. lua_format( ) ,
951+ lua_format_for( & f. target_format) ,
952+ "lua_format helpers disagree for {fmt}"
953+ ) ;
954+ }
955+ }
956+
824957 #[ test]
825958 fn test_from_format_string_typst_extension ( ) {
826959 let f = Format :: from_format_string ( "typst" ) . unwrap ( ) ;
0 commit comments