Skip to main content

autumn_plugin_three/
script.rs

1//! Tag helpers: [`three_script()`] and [`three_stylesheet()`].
2//!
3//! Three.js is a set of ES modules. Module imports resolve to plain URLs
4//! (for example `/static/_plugins/three/three.module.min.js`). The page and
5//! the addons must import the same URL, or two copies of Three.js load.
6//!
7//! [`three_script()`] emits:
8//!
9//! 1. `<link rel="modulepreload">` with SRI for the module graph. The
10//!    browser checks the bytes before the imports use them.
11//! 2. One `<script type="module">` for `init.js` at its hashed URL, with SRI.
12//!
13//! There is no import map and no inline script, so the default Autumn CSP
14//! (`script-src 'self'`) allows all tags.
15
16use autumn_web::{Markup, html};
17
18use crate::assets::{CORE_JS, INIT_JS, MODULE_JS, PARSE_JS, THREE_ASSETS, THREE_CSS};
19
20/// Files that `init.js` imports statically, in dependency order.
21const MODULE_GRAPH: [&str; 3] = [CORE_JS, MODULE_JS, PARSE_JS];
22
23/// Renders the tags that load Three.js and the plugin runtime.
24///
25/// Put it in the page `<head>`, before any page module that imports
26/// [`THREE_MODULE_URL`](crate::THREE_MODULE_URL). Else that import loads
27/// Three.js first, without SRI. Module scripts run after parsing, so the
28/// scenes in `<body>` exist when `init.js` runs.
29///
30/// ```rust
31/// use autumn_plugin_three::three_script;
32/// use autumn_web::html;
33///
34/// let head = html! { head { (three_script()) } }.into_string();
35/// assert!(head.contains(r#"type="module""#));
36/// assert!(head.contains(r#"rel="modulepreload""#));
37/// ```
38#[must_use]
39pub fn three_script() -> Markup {
40    html! {
41        @for path in MODULE_GRAPH {
42            @if let Some(asset) = THREE_ASSETS.get(path) {
43                link rel="modulepreload" href=(asset.plain_url())
44                    integrity=(asset.integrity()) crossorigin="anonymous";
45            }
46        }
47        @if let Some(init) = THREE_ASSETS.get(INIT_JS) {
48            script type="module" src=(init.url()) integrity=(init.integrity())
49                crossorigin="anonymous" {}
50        }
51    }
52}
53
54/// Renders the `<link>` tag for the plugin stylesheet.
55///
56/// The stylesheet gives scenes a default size (`aspect-ratio: 16 / 9`),
57/// hides object declarations, and shows fallback content when needed. Put
58/// it in the page `<head>`.
59///
60/// ```rust
61/// use autumn_plugin_three::three_stylesheet;
62///
63/// let html = three_stylesheet().into_string();
64/// assert!(html.contains(r#"href="/static/_plugins/three/three."#), "{html}");
65/// ```
66#[must_use]
67pub fn three_stylesheet() -> Markup {
68    THREE_ASSETS.stylesheet_tag(THREE_CSS)
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::assets::{CORE_JS, INIT_JS, MODULE_JS, PARSE_JS, THREE_ASSETS, THREE_CSS};
75
76    fn asset(path: &str) -> &'static autumn_web::assets::PluginAsset {
77        THREE_ASSETS.get(path).expect("file is bundled")
78    }
79
80    #[test]
81    fn module_graph_is_preloaded_with_sri_at_plain_urls() {
82        let html = three_script().into_string();
83        for path in [CORE_JS, MODULE_JS, PARSE_JS] {
84            let a = asset(path);
85            let tag = format!(
86                r#"<link rel="modulepreload" href="{}" integrity="{}" crossorigin="anonymous">"#,
87                a.plain_url(),
88                a.integrity()
89            );
90            assert!(html.contains(&tag), "{path}: {html}");
91        }
92    }
93
94    #[test]
95    fn entry_is_a_module_script_with_sri_and_fingerprinted_url() {
96        let html = three_script().into_string();
97        let init = asset(INIT_JS);
98        let tag = format!(
99            r#"<script type="module" src="{}" integrity="{}" crossorigin="anonymous"></script>"#,
100            init.url(),
101            init.integrity()
102        );
103        assert!(html.contains(&tag), "{html}");
104        assert_eq!(html.matches("<script").count(), 1, "one entry: {html}");
105        assert!(!html.contains("importmap"), "no inline import map: {html}");
106    }
107
108    #[test]
109    fn preloads_come_before_the_entry() {
110        let html = three_script().into_string();
111        let entry = html.find("<script").expect("entry");
112        let last_preload = html.rfind("modulepreload").expect("preload");
113        assert!(last_preload < entry, "{html}");
114    }
115
116    #[test]
117    fn stylesheet_link_carries_sri_and_fingerprinted_url() {
118        let html = three_stylesheet().into_string();
119        let css = asset(THREE_CSS);
120        assert!(html.contains(r#"rel="stylesheet""#), "{html}");
121        assert!(html.contains(&format!(r#"href="{}""#, css.url())), "{html}");
122        assert!(
123            html.contains(&format!(r#"integrity="{}""#, css.integrity())),
124            "{html}"
125        );
126    }
127}