Skip to main content

telar_macros/
lib.rs

1use proc_macro::TokenStream;
2use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
3use quote::{ToTokens, quote};
4use std::path::PathBuf;
5
6mod app_input;
7mod t_macro;
8use app_input::{AppInput, preview_const_ident};
9
10/// Translates a catalog key to a `String`, substituting named arguments: `t!("battery.remaining", time = t)`.
11///
12/// The key and its arguments are validated against the on-disk `locales/` catalog at compile time (an unknown
13/// key or wrong argument is a `compile_error!`). At runtime it reads the active locale reactively, so calling it
14/// inside a widget's content closure makes that widget re-render on a language switch.
15#[proc_macro]
16pub fn t(input: TokenStream) -> TokenStream {
17    match syn::parse::<t_macro::TInput>(input) {
18        Ok(parsed) => t_macro::expand(parsed).into(),
19        Err(e) => e.to_compile_error().into(),
20    }
21}
22
23#[proc_macro]
24pub fn app(input: TokenStream) -> TokenStream {
25    let AppInput {
26        theme_type,
27        setup,
28        config,
29        app_expr,
30    } = match syn::parse::<AppInput>(input) {
31        Ok(v) => v,
32        Err(e) => return e.to_compile_error().into(),
33    };
34
35    // The transpiler has no runtime access to the theme type, so it is passed as a source string; to_string inserts spaces around `::` so we collapse them for a valid turbofish.
36    let theme_type_str = theme_type
37        .to_token_stream()
38        .to_string()
39        .replace(" :: ", "::");
40
41    let TranspileOutput {
42        include_stmts,
43        rerun_stmts,
44        preview_const_idents,
45    } = match transpile_project(Some(theme_type_str.as_str())) {
46        Ok(o) => o,
47        Err(err) => return err.into(),
48    };
49
50    let preview_fn = quote! {
51        pub fn telar_all_preview_entries() -> ::std::vec::Vec<::telar::PreviewEntry> {
52            let mut entries = ::std::vec::Vec::new();
53            #( entries.extend_from_slice(#preview_const_idents); )*
54            entries
55        }
56    };
57
58    // Detected at macro expansion time: cargo-telar sets these env vars.
59    let is_hot_reload = std::env::var("TELAR_HOT_RELOAD_BUILD").is_ok();
60    let is_preview = std::env::var("TELAR_PREVIEW_BUILD").is_ok();
61
62    let run_tail = quote! {
63        #setup
64        if ::std::env::var("TELAR_PREVIEW_LIST").is_ok() {
65            for entry in telar_all_preview_entries() {
66                ::std::println!("{}\t{}", entry.component_name, entry.preview_name);
67            }
68            ::std::process::exit(0);
69        }
70        if ::std::env::var("TELAR_TEST").is_ok() {
71            ::telar::try_run_test(telar_all_preview_entries(), ::telar::AppConfig::from(#config));
72        }
73        if ::std::env::var("TELAR_PREVIEW").is_ok() {
74            if ::telar::try_run_preview(telar_all_preview_entries(), ::telar::AppConfig::from(#config)) {
75                return;
76            }
77        }
78        ::telar::run_app_with_name(
79            ::telar::AppConfig::from(#config),
80            #app_expr,
81            env!("CARGO_PKG_NAME"),
82        )
83    };
84
85    let hot_reload_prefix = if is_hot_reload {
86        quote! {
87            if let (::std::result::Result::Ok(lib_path), ::std::result::Result::Ok(hot_port)) = (
88                ::std::env::var("TELAR_HOT_LIB"),
89                ::std::env::var("TELAR_HOT_PORT"),
90            ) {
91                #setup
92                ::telar::run_hot_reload_host(
93                    &lib_path,
94                    &hot_port,
95                    ::telar::AppConfig::from(#config),
96                    env!("CARGO_PKG_NAME"),
97                );
98                return;
99            }
100        }
101    } else {
102        quote! {}
103    };
104
105    let desktop_run = quote! {
106        #[cfg(not(target_os = "android"))]
107        pub fn run() {
108            #hot_reload_prefix
109            #run_tail
110        }
111    };
112
113    // Only emitted under TELAR_HOT_RELOAD_BUILD so dlopen can find the factory; TELAR_PREVIEW_BUILD lets the macro branch here without leaking a custom cfg into generated output (--cfg=telar_preview in RUSTFLAGS is only for cache-busting recompilation when switching modes).
114    let hot_export = if is_hot_reload {
115        let body: TokenStream2 = if is_preview {
116            quote! {
117                return ::telar::make_hot_preview_app(telar_all_preview_entries());
118            }
119        } else {
120            quote! {
121                return ::std::boxed::Box::new(#app_expr);
122            }
123        };
124        quote! {
125            #[unsafe(no_mangle)]
126            pub unsafe extern "Rust" fn _rsx_hot_create_app() -> ::std::boxed::Box<dyn ::telar::App> {
127                #setup
128                #body
129            }
130        }
131    } else {
132        quote! {}
133    };
134
135    // Cleanup function exported for hot reload: called before dlclose to clean up TLS in the dylib.
136    let hot_cleanup = if is_hot_reload {
137        quote! {
138            #[unsafe(no_mangle)]
139            pub unsafe extern "Rust" fn _rsx_hot_cleanup() {
140                // Drop in-flight animations alongside the signals they target so none outlive this dylib's reset runtime.
141                ::telar::motion::reset();
142                ::telar::reset_runtime();
143            }
144        }
145    } else {
146        quote! {}
147    };
148
149    // State-preservation symbols: the host snapshots the outgoing dylib's hot signals and restores them into the incoming one (see telar::hot_state).
150    let hot_state_symbols = if is_hot_reload {
151        quote! {
152            #[unsafe(no_mangle)]
153            pub unsafe extern "Rust" fn _rsx_hot_snapshot() -> ::std::string::String {
154                ::telar::hot_snapshot_json()
155            }
156            #[unsafe(no_mangle)]
157            pub unsafe extern "Rust" fn _rsx_hot_restore(blob: &str) {
158                ::telar::hot_restore_json(blob);
159            }
160        }
161    } else {
162        quote! {}
163    };
164
165    // Tree-ownership symbols: the dylib mounts and owns the segment tree, so its view effects are created in the
166    // same reactive runtime as the signals they read. Mounting it on the host's side instead leaves every
167    // subscription unestablished, which is what the force-tick workaround exists to paper over — see `telar::tree`.
168    let hot_tree_symbols = if is_hot_reload {
169        quote! {
170            #[unsafe(no_mangle)]
171            pub unsafe extern "Rust" fn _rsx_hot_tree_mount(
172                app: &dyn ::telar::App,
173            ) -> *mut ::telar::HotTree {
174                ::telar::HotTree::mount(app)
175            }
176            #[unsafe(no_mangle)]
177            pub unsafe extern "Rust" fn _rsx_hot_tree_release(tree: *mut ::telar::HotTree) {
178                unsafe { ::telar::HotTree::release(tree) }
179            }
180            #[unsafe(no_mangle)]
181            pub unsafe extern "Rust" fn _rsx_hot_tree_on_event(
182                tree: *mut ::telar::HotTree,
183                event: &::telar::Event,
184            ) -> bool {
185                unsafe { ::telar::HotTree::on_event(tree, event) }
186            }
187            #[unsafe(no_mangle)]
188            pub unsafe extern "Rust" fn _rsx_hot_tree_paint(
189                tree: *mut ::telar::HotTree,
190            ) -> ::std::vec::Vec<::telar::DrawCommand> {
191                unsafe { ::telar::HotTree::paint(tree) }
192            }
193            #[unsafe(no_mangle)]
194            pub unsafe extern "Rust" fn _rsx_hot_tree_dirty(tree: *mut ::telar::HotTree) -> bool {
195                unsafe { ::telar::HotTree::is_dirty(tree) }
196            }
197            #[unsafe(no_mangle)]
198            pub unsafe extern "Rust" fn _rsx_hot_tree_generation(
199                tree: *mut ::telar::HotTree,
200            ) -> u64 {
201                unsafe { ::telar::HotTree::generation(tree) }
202            }
203            #[unsafe(no_mangle)]
204            pub unsafe extern "Rust" fn _rsx_hot_tree_walk(
205                tree: *mut ::telar::HotTree,
206            ) -> ::std::vec::Vec<::telar::SegmentNodeInfo> {
207                unsafe { ::telar::HotTree::walk(tree) }
208            }
209        }
210    } else {
211        quote! {}
212    };
213
214    // Motion-tick symbols: host and dylib link separate copies of motion-core, each with its own registry; the `Animated` values live in the dylib's, so the host must call across this boundary instead of ticking its own (empty) copy.
215    let hot_motion_symbols = if is_hot_reload {
216        quote! {
217            #[unsafe(no_mangle)]
218            pub unsafe extern "Rust" fn _rsx_hot_motion_tick(now: ::std::time::Instant) {
219                ::telar::motion::tick(now);
220            }
221            #[unsafe(no_mangle)]
222            pub unsafe extern "Rust" fn _rsx_hot_motion_active() -> bool {
223                ::telar::motion::has_active()
224            }
225            // Batch the dylib's reactive runtime around event dispatch: host and dylib link separate reactive-core copies, so the host must open/close the batch on the app's own runtime across this boundary — otherwise a handler's signal write flushes mid-dispatch and a segment loses its subscriptions while its widget is borrowed.
226            #[unsafe(no_mangle)]
227            pub unsafe extern "Rust" fn _rsx_hot_begin_batch() {
228                ::telar::begin_batch();
229            }
230            #[unsafe(no_mangle)]
231            pub unsafe extern "Rust" fn _rsx_hot_end_batch() {
232                ::telar::end_batch();
233            }
234            // Relayout the dylib's own layout runtime: the layout tree (taffy nodes) lives in the dylib's
235            // thread-local runtime, so the host must drive relayout across this boundary for a reactive
236            // list change to be laid out — its own copy is empty.
237            #[unsafe(no_mangle)]
238            pub unsafe extern "Rust" fn _rsx_hot_relayout() {
239                ::telar::relayout_if_dirty();
240            }
241            // Consult the dylib's own overlay registry: `overlay` widgets register in this dylib's
242            // thread-local, so the host must route pointer events to overlays (modal priority / background
243            // blocking) across this boundary — its own copy is empty.
244            #[unsafe(no_mangle)]
245            pub unsafe extern "Rust" fn _rsx_hot_dispatch_overlays(event: &::telar::Event) -> bool {
246                ::telar::dispatch_overlays(event)
247            }
248            // Write the OS light/dark preference into the dylib's own theme runtime (where `follow_system`'s
249            // effect lives), across the same boundary the host cannot reach directly.
250            #[unsafe(no_mangle)]
251            pub unsafe extern "Rust" fn _rsx_hot_set_system_dark(dark: bool) {
252                ::telar::set_system_dark(dark);
253            }
254            // Drain the dylib's own window-command queue: a title bar's `on_press` pushes into this dylib's
255            // thread-local, so the host must drain it across this boundary to apply drag/minimize/maximize/
256            // close — its own copy is empty.
257            #[unsafe(no_mangle)]
258            pub unsafe extern "Rust" fn _rsx_hot_drain_window_commands()
259            -> ::std::vec::Vec<::telar::WindowCommand> {
260                ::telar::take_window_commands()
261            }
262        }
263    } else {
264        quote! {}
265    };
266
267    let android_run = quote! {
268        #[cfg(target_os = "android")]
269        #[unsafe(no_mangle)]
270        fn android_main(android_app: ::telar::AndroidApp) {
271            #setup
272            ::telar::run_android_app_with_name(
273                ::telar::AppConfig::from(#config),
274                #app_expr,
275                env!("CARGO_PKG_NAME"),
276                android_app,
277            );
278        }
279    };
280
281    quote! {
282        #rerun_stmts
283        #include_stmts
284        #preview_fn
285        #desktop_run
286        #android_run
287        #hot_export
288        #hot_cleanup
289        #hot_state_symbols
290        #hot_tree_symbols
291        #hot_motion_symbols
292    }
293    .into()
294}
295
296struct TranspileOutput {
297    include_stmts: TokenStream2,
298    rerun_stmts: TokenStream2,
299    preview_const_idents: Vec<Ident>,
300}
301
302// Transpiles every `.rsx` file under `src/` into `.telar/build/`, wiring each as a `#[path] mod` and aliasing
303// nested components to their basenames; also emits `include_str!` rerun triggers and (via `auto_modules`)
304// declares the hand-written `.rs` module tree. Shared by `app!` (which then adds the runner) and
305// `rsx_modules!` (transpilation only). `theme_type_str` types the transpiler's `use_theme` calls; pass `None`
306// when no theme is in scope. `Err` carries a `compile_error!` token stream for the caller to emit.
307fn transpile_project(theme_type_str: Option<&str>) -> Result<TranspileOutput, TokenStream2> {
308    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
309        .map(PathBuf::from)
310        .map_err(|_| quote! { compile_error!("CARGO_MANIFEST_DIR not set") })?;
311
312    let generated_dir = manifest_dir.join(".telar").join("build");
313    if let Err(e) = std::fs::create_dir_all(&generated_dir) {
314        let msg = format!("Failed to create .telar/build/: {e}");
315        return Err(quote! { compile_error!(#msg) });
316    }
317
318    let src_dir = manifest_dir.join("src");
319    let rsx_files = telar_transpiler::find_rsx_files(&src_dir);
320    // Baked `src:"..."` asset paths resolve against one project asset root (default `./assets`), not each
321    // `.rsx`'s own directory — see `[telar] assets` in telar.toml.
322    let assets_root = telar_transpiler::assets_root(&manifest_dir);
323
324    // Pre-pass: collect every component's signature (its Props shape + whether it takes a slot) so each file's
325    // transpile can emit calls to other components correctly — optional props and the slot arg both need the
326    // callee's shape, which lives in another file. Keyed by both the path-flattened stem and the bare basename.
327    let mut registry = telar_transpiler::ComponentRegistry::new();
328    // Seed the built-in component catalogue first so a local `.rsx` of the same name still overrides it.
329    for (name, sig) in telar_transpiler::external_component_sigs() {
330        registry.insert(name.to_string(), sig);
331    }
332    for rsx_file in &rsx_files {
333        let Ok(source) = std::fs::read_to_string(rsx_file) else {
334            continue;
335        };
336        let sig = telar_transpiler::scan_component_sig(&source);
337        let stem = telar_transpiler::relative_stem(rsx_file, &src_dir);
338        registry.insert(telar_transpiler::naming::to_snake_case(&stem), sig.clone());
339        if let Some(base) = rsx_file.file_stem().and_then(|s| s.to_str()) {
340            registry
341                .entry(telar_transpiler::naming::to_snake_case(base))
342                .or_insert(sig);
343        }
344    }
345
346    let mut include_stmts = TokenStream2::new();
347    let mut rerun_stmts = TokenStream2::new();
348    let mut preview_const_idents: Vec<Ident> = Vec::new();
349
350    for rsx_file in &rsx_files {
351        let source = match std::fs::read_to_string(rsx_file) {
352            Ok(s) => s,
353            Err(e) => {
354                let msg = format!("Failed to read {}: {e}", rsx_file.display());
355                return Err(quote! { compile_error!(#msg) });
356            }
357        };
358
359        let stem = telar_transpiler::relative_stem(rsx_file, &src_dir);
360
361        let result = match telar_transpiler::transpile_source_full(
362            &source,
363            &stem,
364            theme_type_str,
365            Some(assets_root.as_path()),
366            Some(&registry),
367        ) {
368            Ok(r) => r,
369            Err(telar_transpiler::TranspileError::Parse(ref pe)) => {
370                let msg = format!("{}:{}: {}", rsx_file.display(), pe.line, pe.message);
371                return Err(quote! { compile_error!(#msg) });
372            }
373            Err(e) => {
374                let msg = format!("Failed to transpile {}: {e}", rsx_file.display());
375                return Err(quote! { compile_error!(#msg) });
376            }
377        };
378
379        // Mirror the source tree under .telar/build/ so files in different directories never collide. find_rsx_files only yields paths under src_dir, so None is unreachable here.
380        let Some(rel_out) = telar_transpiler::relative_output_path(rsx_file, &src_dir) else {
381            continue;
382        };
383        let out_path = generated_dir.join(rel_out);
384        if let Some(parent) = out_path.parent() {
385            if let Err(e) = std::fs::create_dir_all(parent) {
386                let msg = format!("Failed to create {}: {e}", parent.display());
387                return Err(quote! { compile_error!(#msg) });
388            }
389        }
390
391        // Only write when content changed to avoid spurious recompilation.
392        let needs_write = std::fs::read_to_string(&out_path)
393            .map(|existing| existing != result.rust_code)
394            .unwrap_or(true);
395        if needs_write {
396            if let Err(e) = std::fs::write(&out_path, &result.rust_code) {
397                let msg = format!("Failed to write {}: {e}", out_path.display());
398                return Err(quote! { compile_error!(#msg) });
399            }
400        }
401
402        // Persist the per-line source map next to the build file so the editor extension can map
403        // rust-analyzer's diagnostics on the generated Rust back onto the original `.rsx` lines.
404        let map_path = out_path.with_extension("rs.map");
405        let map_json = telar_transpiler::source_map_to_json(&result.source_map);
406        let map_stale = std::fs::read_to_string(&map_path)
407            .map(|existing| existing != map_json)
408            .unwrap_or(true);
409        if map_stale {
410            let _ = std::fs::write(&map_path, &map_json);
411        }
412
413        // Wire each generated file as a real `#[path] mod` (not `include!`) so rust-analyzer treats it as a
414        // first-class module and offers completion inside it; `pub use` keeps the component fns, preview consts
415        // and `Props` types reachable by bare name, exactly as `include!` did.
416        let out_path_str = out_path.to_string_lossy().to_string();
417        let mod_ident = Ident::new(
418            &format!("__rsx_mod_{}", telar_transpiler::naming::to_snake_case(&stem)),
419            Span::call_site(),
420        );
421        include_stmts.extend(quote! {
422            #[path = #out_path_str]
423            mod #mod_ident;
424            #[allow(unused_imports)]
425            pub use #mod_ident::*;
426        });
427
428        // Let a nested component be referenced in markup by its bare file name, not its path-flattened name:
429        // alias the path-derived fn (and Props type) to the basename at crate root. Skipped for files directly
430        // under src/ (basename already equals the full name).
431        let base_name = rsx_file
432            .file_stem()
433            .map(|s| s.to_string_lossy().to_string())
434            .unwrap_or_default();
435        let base_fn = telar_transpiler::naming::to_snake_case(&base_name);
436        let full_fn = telar_transpiler::naming::to_snake_case(&stem);
437        if !base_fn.is_empty() && base_fn != full_fn {
438            let full_fn_ident = Ident::new(&full_fn, Span::call_site());
439            let base_fn_ident = Ident::new(&base_fn, Span::call_site());
440            include_stmts.extend(quote! {
441                #[allow(unused_imports)]
442                pub use #mod_ident::#full_fn_ident as #base_fn_ident;
443            });
444            if result.has_props {
445                let full_props = Ident::new(
446                    &(telar_transpiler::naming::to_pascal_case(&full_fn) + "Props"),
447                    Span::call_site(),
448                );
449                let base_props = Ident::new(
450                    &(telar_transpiler::naming::to_pascal_case(&base_fn) + "Props"),
451                    Span::call_site(),
452                );
453                include_stmts.extend(quote! {
454                    #[allow(unused_imports)]
455                    pub use #mod_ident::#full_props as #base_props;
456                });
457            }
458        }
459
460        let rsx_path_str = rsx_file.to_string_lossy().to_string();
461        rerun_stmts.extend(quote! { const _: &str = include_str!(#rsx_path_str); });
462
463        if !result.preview_names.is_empty() {
464            preview_const_idents.push(preview_const_ident(&stem));
465        }
466    }
467
468    // Opt-in via `[telar] auto_modules = true` in telar.toml: declare the hand-written `.rs` modules by walking the
469    // source tree, so an app needs no `mod.rs`/`mod` statements for them — mirroring how `.rsx` files are wired.
470    let telar_toml = manifest_dir.join("telar.toml");
471    if telar_toml.exists() {
472        // Re-run the macro when telar.toml changes (e.g. toggling auto_modules), like the `.rsx` sources.
473        let telar_toml_str = telar_toml.to_string_lossy().to_string();
474        rerun_stmts.extend(quote! { const _: &str = include_str!(#telar_toml_str); });
475    }
476    if telar_transpiler::auto_modules_enabled(&manifest_dir) {
477        // The discovered tree is split across real generated files (one per directory) so every module is a
478        // file-based `#[path] mod`; see `discover_rust_modules` for why inline `mod` blocks break rust-analyzer.
479        let modtree_dir = generated_dir.join("__modules");
480        if let Err(e) = std::fs::create_dir_all(&modtree_dir) {
481            let msg = format!("Failed to create .telar/build/__modules/: {e}");
482            return Err(quote! { compile_error!(#msg) });
483        }
484        let modules_src = match telar_transpiler::discover_rust_modules(&src_dir, &modtree_dir) {
485            Ok(s) => s,
486            Err(e) => {
487                let msg = format!("Failed to write the auto-discovered module tree: {e}");
488                return Err(quote! { compile_error!(#msg) });
489            }
490        };
491        match modules_src.parse::<TokenStream2>() {
492            Ok(tokens) => include_stmts.extend(tokens),
493            Err(e) => {
494                let msg = format!("Failed to emit auto-discovered modules: {e}");
495                return Err(quote! { compile_error!(#msg) });
496            }
497        }
498    }
499
500    // Bake the i18n catalog when a `locales/` directory exists: parse every `locales/<tag>.toml` into one
501    // generated module wired at the crate root, so `t!`/markup call sites reference `crate::__rsx_i18n::CATALOG`.
502    // Inert (nothing generated) when there is no catalog, mirroring how svg baking only fires for `svg` elements.
503    match telar_transpiler::parse_catalog(&manifest_dir) {
504        Ok(Some(catalog)) => {
505            let src = telar_transpiler::bake_catalog_to_source(&catalog);
506            let out_path = generated_dir.join("__i18n.rs");
507            let needs_write = std::fs::read_to_string(&out_path)
508                .map(|existing| existing != src)
509                .unwrap_or(true);
510            if needs_write && let Err(e) = std::fs::write(&out_path, &src) {
511                let msg = format!("Failed to write {}: {e}", out_path.display());
512                return Err(quote! { compile_error!(#msg) });
513            }
514            let out_path_str = out_path.to_string_lossy().to_string();
515            let mod_ident = Ident::new(telar_transpiler::I18N_MODULE, Span::call_site());
516            include_stmts.extend(quote! {
517                #[path = #out_path_str]
518                #[allow(dead_code)]
519                pub mod #mod_ident;
520            });
521            // Re-bake when any locale file changes, like a `.rsx` edit.
522            for file in telar_transpiler::catalog_files(&manifest_dir) {
523                let path_str = file.to_string_lossy().to_string();
524                rerun_stmts.extend(quote! { const _: &str = include_str!(#path_str); });
525            }
526        }
527        Ok(None) => {}
528        Err(msg) => return Err(quote! { compile_error!(#msg) }),
529    }
530
531    Ok(TranspileOutput {
532        include_stmts,
533        rerun_stmts,
534        preview_const_idents,
535    })
536}
537
538/// Transpile every `.rsx` file under `src/` and declare the module tree — what `app!` does, minus the winit
539/// runner. Use this in a crate that drives rsx through a **custom** `Platform` (e.g. a Wayland layer-shell
540/// backend) instead of the built-in desktop runner: invoke `telar::rsx_modules!()` at the crate root, then build
541/// your own `App` from the transpiled components and run it via `telar::run_with_platform` /
542/// `telar::run_multi_with_platform`. Pass a theme type — `rsx_modules!(MyTheme)` — if your `.rsx` calls
543/// `use_theme`; otherwise `rsx_modules!()`.
544#[proc_macro]
545pub fn rsx_modules(input: TokenStream) -> TokenStream {
546    let theme_type_str = if input.is_empty() {
547        None
548    } else {
549        match syn::parse::<syn::Path>(input) {
550            Ok(path) => Some(path.to_token_stream().to_string().replace(" :: ", "::")),
551            Err(e) => return e.to_compile_error().into(),
552        }
553    };
554    let TranspileOutput {
555        include_stmts,
556        rerun_stmts,
557        preview_const_idents,
558    } = match transpile_project(theme_type_str.as_deref()) {
559        Ok(o) => o,
560        Err(err) => return err.into(),
561    };
562    let preview_fn = quote! {
563        pub fn telar_all_preview_entries() -> ::std::vec::Vec<::telar::PreviewEntry> {
564            let mut entries = ::std::vec::Vec::new();
565            #( entries.extend_from_slice(#preview_const_idents); )*
566            entries
567        }
568    };
569    quote! {
570        #rerun_stmts
571        #include_stmts
572        #preview_fn
573    }
574    .into()
575}