Skip to main content

autumn_plugin_markstream/
script.rs

1//! Head tags: the module script, module preloads and stylesheets.
2//!
3//! Put [`markstream_head()`] in the page `<head>`. It writes:
4//!
5//! 1. The stylesheets ([`markstream_stylesheet()`]).
6//! 2. One `<link rel="modulepreload">` for `core.js` and for each module
7//!    that loads before the init module runs ([`markstream_preloads()`]). The browser fetches
8//!    them in parallel and checks each `integrity` hash.
9//! 3. The init module script ([`markstream_script()`]).
10//!
11//! Each tag has an SRI hash that Autumn computes from the embedded bytes.
12//! There is no inline script and no import map.
13
14use autumn_web::{Markup, html};
15
16use crate::assets::{CORE_JS, INIT_JS, MARKSTREAM_ASSETS, MARKSTREAM_CSS, PLUGIN_CSS};
17use crate::vendored::EAGER_MODULES;
18
19/// Writes all markstream head tags: styles, preloads, then the script.
20///
21/// ```rust
22/// use autumn_plugin_markstream::markstream_head;
23/// use autumn_web::html;
24///
25/// let head = html! { head { (markstream_head()) } }.into_string();
26/// assert!(head.contains(r#"type="module""#));
27/// ```
28#[must_use]
29pub fn markstream_head() -> Markup {
30    html! {
31        (markstream_stylesheet())
32        (markstream_preloads())
33        (markstream_script())
34    }
35}
36
37/// Writes the `<script type="module">` tag for the init module.
38///
39/// Module scripts run after the document is parsed, in order. This tag
40/// checks only the init module. Use [`markstream_head()`] or add
41/// [`markstream_preloads()`]: the preloads check the eager modules.
42#[must_use]
43pub fn markstream_script() -> Markup {
44    html! {
45        @if let Some(init) = MARKSTREAM_ASSETS.get(INIT_JS) {
46            script type="module" src=(init.url()) integrity=(init.integrity())
47                crossorigin="anonymous" {}
48        }
49    }
50}
51
52/// Writes one `<link rel="modulepreload">` for `core.js` and for each
53/// eager module.
54///
55/// The `href` is the plain URL, because relative imports resolve to it.
56/// The browser then uses the preloaded and checked module.
57#[must_use]
58pub fn markstream_preloads() -> Markup {
59    let paths = std::iter::once(CORE_JS).chain(EAGER_MODULES.iter().copied());
60    html! {
61        @for asset in paths.filter_map(|p| MARKSTREAM_ASSETS.get(p)) {
62            link rel="modulepreload" href=(asset.plain_url())
63                integrity=(asset.integrity()) crossorigin="anonymous";
64        }
65    }
66}
67
68/// Writes the `<link rel="stylesheet">` tags: markstream, then the plugin.
69#[must_use]
70pub fn markstream_stylesheet() -> Markup {
71    html! {
72        (MARKSTREAM_ASSETS.stylesheet_tag(MARKSTREAM_CSS))
73        (MARKSTREAM_ASSETS.stylesheet_tag(PLUGIN_CSS))
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use crate::assets::{CORE_JS, INIT_JS, MARKSTREAM_ASSETS, MARKSTREAM_CSS, PLUGIN_CSS};
81    use crate::vendored::EAGER_MODULES;
82
83    fn asset(path: &str) -> &'static autumn_web::assets::PluginAsset {
84        MARKSTREAM_ASSETS.get(path).expect("bundled")
85    }
86
87    #[test]
88    fn script_is_one_module_tag_with_sri_and_hashed_url() {
89        let html = markstream_script().into_string();
90        let init = asset(INIT_JS);
91        assert_eq!(
92            html,
93            format!(
94                r#"<script type="module" src="{}" integrity="{}" crossorigin="anonymous"></script>"#,
95                init.url(),
96                init.integrity()
97            )
98        );
99    }
100
101    #[test]
102    fn preloads_cover_core_and_each_eager_module_at_plain_urls() {
103        let html = markstream_preloads().into_string();
104        let mut paths = vec![CORE_JS];
105        paths.extend(EAGER_MODULES);
106        assert_eq!(
107            html.matches(r#"rel="modulepreload""#).count(),
108            paths.len(),
109            "{html}"
110        );
111        for path in paths {
112            let a = asset(path);
113            let tag = format!(
114                r#"<link rel="modulepreload" href="{}" integrity="{}" crossorigin="anonymous">"#,
115                a.plain_url(),
116                a.integrity()
117            );
118            assert!(html.contains(&tag), "missing {tag}");
119        }
120    }
121
122    #[test]
123    fn stylesheet_links_both_files_with_sri() {
124        let html = markstream_stylesheet().into_string();
125        for path in [MARKSTREAM_CSS, PLUGIN_CSS] {
126            let a = asset(path);
127            assert!(html.contains(&format!(r#"href="{}""#, a.url())), "{html}");
128            assert!(
129                html.contains(&format!(r#"integrity="{}""#, a.integrity())),
130                "{html}"
131            );
132        }
133        let vendored_at = html
134            .find(asset(MARKSTREAM_CSS).url())
135            .expect("vendored css");
136        let plugin_at = html.find(asset(PLUGIN_CSS).url()).expect("plugin css");
137        assert!(vendored_at < plugin_at, "plugin styles must win: {html}");
138    }
139
140    #[test]
141    fn head_has_styles_then_preloads_then_the_script() {
142        let html = markstream_head().into_string();
143        let css = html.find("stylesheet").expect("css");
144        let preload = html.find("modulepreload").expect("preload");
145        let script = html.find("<script").expect("script");
146        assert!(css < preload && preload < script, "{html}");
147        assert_eq!(html.matches("<script").count(), 1, "{html}");
148        assert!(html.contains("></script>"), "no inline script body: {html}");
149        assert!(!html.contains("importmap"), "{html}");
150    }
151}