1use serde_json::Value;
5
6use crate::asset::{Asset, CSS, JAVASCRIPT, STYLESHEET};
7use crate::escape::{push_escaped, push_script_json};
8use crate::page::IslandLinks;
9
10#[derive(Clone, Copy, Debug)]
11pub struct ClientBundle {
12 pub area: &'static str,
13 pub screens: &'static [&'static str],
15 pub script: Asset,
16 pub stylesheet: Asset,
17}
18
19impl ClientBundle {
20 #[must_use]
21 pub const fn new(
22 area: &'static str,
23 screens: &'static [&'static str],
24 script: &'static [u8],
25 stylesheet: &'static [u8],
26 ) -> Self {
27 Self {
28 area,
29 screens,
30 script: Asset::new(area, "client.js", script, JAVASCRIPT),
31 stylesheet: Asset::new(area, "client.css", stylesheet, CSS),
32 }
33 }
34
35 #[must_use]
36 pub fn has_screen(&self, key: &str) -> bool {
37 self.screens.contains(&key)
38 }
39
40 #[must_use]
41 pub fn links(&self) -> ClientLinks {
42 ClientLinks {
43 script: self.script.path(),
44 stylesheet: self.stylesheet.path(),
45 }
46 }
47}
48
49#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct ClientLinks {
52 pub script: String,
53 pub stylesheet: String,
54}
55
56#[macro_export]
59macro_rules! client_bundle {
60 ($area:literal, $screens:expr) => {{
61 let bundle = $crate::ClientBundle::new(
62 $area,
63 $screens,
64 include_bytes!(concat!(env!("OUT_DIR"), "/clients/", $area, ".js")),
65 include_bytes!(concat!(env!("OUT_DIR"), "/clients/", $area, ".css")),
66 );
67 $crate::ClientBundle {
68 script: $crate::compressed_asset!(
69 bundle.script,
70 concat!(env!("OUT_DIR"), "/clients/", $area, ".js")
71 ),
72 stylesheet: $crate::compressed_asset!(
73 bundle.stylesheet,
74 concat!(env!("OUT_DIR"), "/clients/", $area, ".css")
75 ),
76 ..bundle
77 }
78 }};
79}
80
81pub const SCREEN_ELEMENT: &str = "sofie-screen";
82
83#[derive(Debug)]
84pub struct ClientShell<'a> {
85 pub title: &'a str,
86 pub lang: &'a str,
87 pub nonce: &'a str,
88 pub envelope: &'a Value,
89 pub links: &'a ClientLinks,
90 pub islands: Option<&'a IslandLinks>,
91}
92
93impl ClientShell<'_> {
94 #[must_use]
95 pub fn render(&self) -> String {
96 let mut html = String::with_capacity(1024);
97 html.push_str("<!doctype html><html lang=\"");
98 push_escaped(&mut html, self.lang);
99 html.push_str(
100 "\"><head><meta charset=\"utf-8\">\
101 <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>",
102 );
103 push_escaped(&mut html, self.title);
104 html.push_str("</title>");
105 html.push_str("<link rel=\"stylesheet\" href=\"");
106 STYLESHEET.push_path(&mut html);
107 html.push_str("\"><link rel=\"stylesheet\" href=\"");
108 push_escaped(&mut html, &self.links.stylesheet);
109 html.push_str("\">");
110 if let Some(islands) = self.islands {
111 islands.push_head(&mut html, self.nonce);
112 }
113 html.push_str("<script type=\"module\" src=\"");
114 push_escaped(&mut html, &self.links.script);
115 html.push_str("\" nonce=\"");
116 push_escaped(&mut html, self.nonce);
117 html.push_str("\"></script></head><body><div id=\"sofie-app\"></div>");
118 html.push_str("<script type=\"application/json\" id=\"");
119 html.push_str(SCREEN_ELEMENT);
120 html.push_str("\">");
121 push_script_json(&mut html, self.envelope);
122 html.push_str("</script></body></html>");
123 html
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use serde_json::json;
130
131 use super::{ClientBundle, ClientShell};
132
133 #[test]
134 fn the_shell_carries_the_screen_as_inert_json_and_loads_the_bundle() {
135 let bundle = ClientBundle::new("member", &["order/member/orders"], b"x", b"y");
136 let envelope = json!({"screen": "order/member/orders", "data": {"note": "</script><b>"}});
137 let html = ClientShell {
138 title: "Orders",
139 lang: "en",
140 nonce: "n1",
141 envelope: &envelope,
142 links: &bundle.links(),
143 islands: None,
144 }
145 .render();
146
147 assert!(html.contains("<title>Orders</title>"), "{html}");
148 assert!(html.contains(&format!(
149 "<script type=\"module\" src=\"{}\" nonce=\"n1\"></script>",
150 bundle.script.path()
151 )));
152 assert!(
153 html.contains("\\u003c/script\\u003e\\u003cb\\u003e"),
154 "{html}"
155 );
156 assert!(bundle.has_screen("order/member/orders"));
157 assert!(!bundle.has_screen("order/member/other"));
158 }
159}