1use sofie_core::Fnv1a64;
2
3use crate::render::push_display;
4
5#[cfg(feature = "compression")]
6mod precompressed;
7#[cfg(feature = "compression")]
8pub use precompressed::Precompressed;
9#[cfg(feature = "islands")]
10mod island_bundle;
11
12#[derive(Clone, Copy, Debug)]
14pub struct Asset {
15 stem: &'static str,
16 extension: &'static str,
17 pub bytes: &'static [u8],
18 pub content_type: &'static str,
19 hash: u64,
20 #[cfg(feature = "compression")]
21 precompressed: Option<Precompressed>,
22}
23
24#[cfg(feature = "compression")]
26#[doc(hidden)]
27#[macro_export]
28macro_rules! compressed_asset {
29 ($asset:expr, $file:expr) => {
30 $asset.with_precompressed($crate::Precompressed {
31 brotli: include_bytes!(concat!($file, ".br")),
32 gzip: include_bytes!(concat!($file, ".gz")),
33 })
34 };
35}
36
37#[cfg(not(feature = "compression"))]
38#[doc(hidden)]
39#[macro_export]
40macro_rules! compressed_asset {
41 ($asset:expr, $file:expr) => {
42 $asset
43 };
44}
45
46macro_rules! engine_asset {
48 ($stem:literal, $extension:literal, $content_type:expr) => {
49 $crate::compressed_asset!(
50 Asset::new(
51 $stem,
52 $extension,
53 include_bytes!(concat!("../assets/", $stem, ".", $extension)),
54 $content_type,
55 ),
56 concat!(env!("OUT_DIR"), "/", $stem, ".", $extension)
57 )
58 };
59}
60
61pub const CLIENT_SCRIPT: Asset = engine_asset!("htmx", "min.js", JAVASCRIPT);
63
64pub const ENGINE_SCRIPT: Asset = engine_asset!("engine", "js", JAVASCRIPT);
66
67pub const STYLESHEET: Asset = engine_asset!("sofie", "css", CSS);
68
69pub(crate) const JAVASCRIPT: &str = "application/javascript; charset=utf-8";
70pub(crate) const CSS: &str = "text/css; charset=utf-8";
71
72impl Asset {
73 pub(crate) const fn new(
74 stem: &'static str,
75 extension: &'static str,
76 bytes: &'static [u8],
77 content_type: &'static str,
78 ) -> Self {
79 Self {
80 stem,
81 extension,
82 bytes,
83 content_type,
84 hash: Fnv1a64::new().write(bytes).finish(),
85 #[cfg(feature = "compression")]
86 precompressed: None,
87 }
88 }
89
90 #[must_use]
91 pub fn path(&self) -> String {
92 let mut path = String::new();
93 self.push_path(&mut path);
94 path
95 }
96
97 pub(crate) fn push_path(&self, out: &mut String) {
98 let (stem, hash, extension) = (self.stem, self.hash, self.extension);
99 push_display(out, format_args!("/_sofie/{stem}-{hash:016x}.{extension}"));
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::{CLIENT_SCRIPT, STYLESHEET};
106
107 const PROVENANCE: &str = include_str!("../assets/PROVENANCE");
108
109 #[test]
110 fn the_client_script_is_the_file_recorded_in_provenance() {
111 let recorded = PROVENANCE
112 .lines()
113 .find_map(|line| line.strip_prefix("fnv1a64 "))
114 .expect("PROVENANCE records the fnv1a64 of htmx.min.js");
115
116 assert_eq!(
117 CLIENT_SCRIPT.path(),
118 format!("/_sofie/htmx-{recorded}.min.js")
119 );
120 }
121
122 #[test]
123 fn paths_name_the_content() {
124 let path = STYLESHEET.path();
125 assert_eq!(path.len(), "/_sofie/sofie-".len() + 16 + ".css".len());
126 assert!(
127 path.starts_with("/_sofie/sofie-") && path.contains(".css"),
128 "{path}"
129 );
130 assert_eq!(STYLESHEET.content_type, "text/css; charset=utf-8");
131 }
132}