Skip to main content

moss_core/render/
iframe.rs

1//! Iframe embed synthesizer.
2//!
3//! Receives a [`TitleParams`] (Stage 2 dispatcher already parsed it), the
4//! source URL, and an [`AssetSnapshot`]. Emits final `<iframe>` HTML.
5//!
6//! **Sole iframe HTML emitter.** Two call sites converge here:
7//!
8//! 1. The Stage 2 dispatcher in `pipeline.rs`, after pulldown-cmark parses a
9//!    `Tag::Link` carrying `moss:kind=iframe`.
10//! 2. `src-tauri/src/build/folder_embed.rs`'s folder-as-iframe feature, which
11//!    has no pulldown-cmark involvement and calls this synthesizer directly
12//!    with an empty `TitleParams`.
13//!
14//! The pre-Phase-1 moss-core `IframeRenderer::render_to_html` escape valve
15//! retired once folder_embed migrated (P2E prereq #2, 2026-05-25).
16
17use crate::asset_snapshot::AssetSnapshot;
18use crate::resolve::embed_renderer::html_escape_attr;
19use crate::resolve::title_params::TitleParams;
20
21/// Base wrapper class — kept in sync with moss-core's `CLASS_EMBED`
22/// (`crates/moss-core/src/resolve/embed_renderer.rs`).
23const CLASS_EMBED: &str = "moss-embed";
24
25/// Synthesize iframe embed HTML for `Tag::Link` with `moss:kind=iframe` title.
26///
27/// Reads from `params`:
28/// - `query` — appended to `src` after `?`
29/// - `fragment` — appended to `src` after `#`
30/// - `data-width` — canonical width token (`body | wide | page | screen`),
31///   emitted as `data-width="…"` on the iframe
32/// - `width` — pixel/percent dimension (e.g. `400px`, `100%`), emitted as
33///   the HTML `width="…"` attribute
34/// - `height` — pixel/percent dimension, emitted as `height="…"` attribute
35/// - `title` — accessible name, emitted as `title="…"` attribute
36///
37/// Byte shape:
38///
39/// ```text
40/// <iframe class="moss-embed" data-type="iframe"{data-width} src="{src}"{title}{width}{height} loading="lazy"></iframe>
41/// ```
42///
43/// `assets` is currently unused — iframes target HTML files which moss does
44/// not transform, so there is no variant URL to resolve. Kept in the
45/// signature for parity with the other synthesizers (image, video, audio)
46/// where it carries the variant manifest.
47#[allow(unused_variables)]
48pub fn synthesize_iframe_html(
49    params: &TitleParams,
50    src: &str,
51    assets: &AssetSnapshot,
52) -> String {
53    // Reconstruct the iframe `src` from the URL slot + Stage-1-folded
54    // `?query#fragment` params. pulldown-cmark would percent-encode `?`
55    // and `#` if they stayed in the URL slot, so Stage 1 lifts them out;
56    // Stage 2 puts them back here.
57    let mut full_src = String::from(src);
58    if let Some(q) = params.get("query") {
59        full_src.push('?');
60        full_src.push_str(q);
61    }
62    if let Some(f) = params.get("fragment") {
63        full_src.push('#');
64        full_src.push_str(f);
65    }
66
67    let data_width_attr = match params.get("data-width") {
68        Some(w) => format!(r#" data-width="{}""#, html_escape_attr(w)),
69        None => String::new(),
70    };
71
72    let title_attr = match params.get("title") {
73        Some(t) => format!(" title=\"{}\"", html_escape_attr(t)),
74        None => String::new(),
75    };
76
77    let width_attr = match params.get("width") {
78        Some(w) => format!(" width=\"{}\"", html_escape_attr(w)),
79        None => String::new(),
80    };
81
82    let height_attr = match params.get("height") {
83        Some(h) => format!(" height=\"{}\"", html_escape_attr(h)),
84        None => String::new(),
85    };
86
87    format!(
88        "<iframe class=\"{}\" data-type=\"iframe\"{} src=\"{}\"{}{}{} loading=\"lazy\"></iframe>",
89        CLASS_EMBED,
90        data_width_attr,
91        html_escape_attr(&full_src),
92        title_attr,
93        width_attr,
94        height_attr,
95    )
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    fn empty_snapshot() -> AssetSnapshot {
103        AssetSnapshot::new()
104    }
105
106    fn params_with(kvs: &[(&str, &str)]) -> TitleParams {
107        let mut p = TitleParams::default();
108        for (k, v) in kvs {
109            p.insert(*k, *v);
110        }
111        p
112    }
113
114    #[test]
115    fn iframe_basic_shape() {
116        let p = params_with(&[("kind", "iframe")]);
117        let out = synthesize_iframe_html(&p, "widget.html", &empty_snapshot());
118        assert!(out.contains("<iframe"));
119        assert!(out.contains(r#"src="widget.html""#));
120        assert!(out.contains(r#"loading="lazy""#));
121        assert!(out.contains(r#"class="moss-embed""#));
122        assert!(out.contains(r#"data-type="iframe""#));
123    }
124
125    #[test]
126    fn iframe_with_data_width() {
127        let p = params_with(&[("kind", "iframe"), ("data-width", "wide")]);
128        let out = synthesize_iframe_html(&p, "widget.html", &empty_snapshot());
129        assert!(out.contains(r#"data-width="wide""#), "got: {}", out);
130    }
131
132    #[test]
133    fn iframe_with_query_param() {
134        let p = params_with(&[("kind", "iframe"), ("query", "k=v&x=y")]);
135        let out = synthesize_iframe_html(&p, "widget.html", &empty_snapshot());
136        assert!(
137            out.contains(r#"src="widget.html?k=v"#)
138                || out.contains(r#"src="widget.html?k=v&amp;x=y""#),
139            "expected query in src, got: {}",
140            out
141        );
142    }
143
144    #[test]
145    fn iframe_with_title() {
146        let p = params_with(&[("kind", "iframe"), ("title", "My Widget")]);
147        let out = synthesize_iframe_html(&p, "widget.html", &empty_snapshot());
148        assert!(out.contains(r#"title="My Widget""#));
149    }
150
151}