html_index/
lib.rs

1#![cfg_attr(test, deny(warnings))]
2#![forbid(unsafe_code, missing_debug_implementations)]
3
4//! Over the years the HTML spec has added lots of new capabilities in a backwards
5//! compatible fashion. This means that even if something from the 90's might still
6//! work in today's browsers, it might not always be the most efficient.
7//!
8//! This crate makes it easy to build performant HTML without needing to remember
9//! all boilerplate involved.
10//!
11//! ## Features
12//!
13//! The `http-types` feature enables a `Builder::into()` conversion for [http_types::Response][].
14//! This feature is enabled by default.
15//!
16//! If you are not using other http-types ecosystem crates, you can disable the feature:
17//! ```toml
18//! html-index = { version = "*", default-features = false }
19//! ```
20//!
21//! [http_types::Response]: https://docs.rs/http-types/2.3.0/http_types/struct.Response.html
22//!
23//! ## Examples
24//!
25//! ```rust
26//! let res = html_index::Builder::new()
27//!     .raw_body("<body>hello world</body>")
28//!     .script("/bundle.js")
29//!     .style("/bundle.css")
30//!     .build();
31//! println!("{}", res);
32//! ```
33//!
34//! Which generates:
35//!
36//! ```html
37//! <!DOCTYPE html>
38//! <html>
39//!   <head>
40//!     <meta charset="utf-8">
41//!     <meta name="viewport" content="width=device-width, initial-scale=1.0">
42//!     <link rel="preload" as="style" href="/bundle.css" onload="this.rel='stylesheet'">
43//!     <script src="/bundle.js" defer></script>
44//!   </head>
45//!   <body>hello world</body>
46//! </html>
47//! ```
48
49const DOCTYPE: &str = "<!DOCTYPE html>";
50const CHARSET: &str = r#"<meta charset="utf-8">"#;
51const VIEWPORT: &str = r#"<meta name="viewport" content="width=device-width, initial-scale=1.0">"#;
52const HTML_CLOSE: &str = "</html>";
53const HEAD_OPEN: &str = "<head>";
54const HEAD_CLOSE: &str = "</head>";
55
56use std::default::Default;
57
58/// Create a new `Builder` instance.
59pub fn new<'a>() -> Builder<'a> {
60    Builder::new()
61}
62
63/// Create a new HTML builder.
64#[derive(Debug, Clone, Default)]
65pub struct Builder<'b> {
66    color: Option<String>,
67    desc: Option<String>,
68    lang: &'b str,
69    favicon: Option<String>,
70    fonts: Vec<String>,
71    manifest: Option<String>,
72    scripts: Vec<String>,
73    styles: Vec<String>,
74    title: Option<String>,
75    body: Option<&'b str>,
76    has_async_style: bool,
77}
78
79impl<'b> Builder<'b> {
80    /// Create a new instance from an HTML body, including `<body></body>` tags.
81    pub fn new() -> Self {
82        Self {
83            lang: "en-US",
84            ..Default::default()
85        }
86    }
87
88    /// Add a body to the document. The body must include `<body></body>` tags.
89    pub fn raw_body(mut self, body: &'b str) -> Self {
90        self.body = Some(body);
91        self
92    }
93
94    /// Set the document language.
95    pub fn lang(mut self, lang: &'b str) -> Self {
96        self.lang = lang;
97        self
98    }
99
100    /// Add a `<meta name="description">` tag.
101    pub fn description(mut self, desc: &str) -> Self {
102        let val = format!(r#"<meta name="description" content="{}">"#, desc);
103        self.desc = Some(val);
104        self
105    }
106
107    /// Add a `<meta name="theme-color">` tag.
108    pub fn theme_color(mut self, color: &str) -> Self {
109        let val = format!(r#"<meta name="theme-color" content="{}">"#, color);
110        self.color = Some(val);
111        self
112    }
113
114    /// Add a `<title>` tag.
115    pub fn title(mut self, title: &str) -> Self {
116        let val = format!(r#"<title>{}</title>"#, title);
117        self.title = Some(val);
118        self
119    }
120
121    /// Add a `<script defer>` tag. This is ideal for loading scripts that are
122    /// important for the main application, but shouldn't interfere with the
123    /// initial rendering.
124    // TODO: also allow passing a sha512
125    pub fn script(mut self, src: &str) -> Self {
126        let val = format!(r#"<script src="{}" defer></script>"#, src);
127        self.scripts.push(val);
128        self
129    }
130
131    /// Add a `<script type="module">` tag that fetches a remote resource.
132    // TODO: also allow passing a sha512
133    pub fn module(mut self, src: &str) -> Self {
134        let val = format!(r#"<script src="{}" type="module"></script>"#, src);
135        self.scripts.push(val);
136        self
137    }
138
139    /// Add a `<script type="module">` tag that contains an inline resource.
140    // TODO: also allow passing a sha512
141    pub fn inline_module(mut self, src: &str) -> Self {
142        let val = format!(r#"<script type="module">{}</script>"#, src);
143        self.scripts.push(val);
144        self
145    }
146
147    /// Add a `<link rel="prefetch">` tag. This is ideal for loading scripts in
148    /// the background after the main application has loaded.
149    // TODO: also allow passing a sha512
150    pub fn lazy_script(mut self, src: &str) -> Self {
151        let val = format!(r#"<link rel="prefetch" href="{}">"#, src);
152        self.scripts.push(val);
153        self
154    }
155
156    /// Add a `<script>` tag. This is ideal for loading scripts that should be
157    /// loaded before any rendering can start.
158    // TODO: also allow passing a sha512
159    pub fn blocking_script(mut self, src: &str) -> Self {
160        let val = format!(r#"<script src="{}"></script>"#, src);
161        self.scripts.push(val);
162        self
163    }
164
165    /// Add a `<script></script>` tag. This is ideal for loading custom scripts
166    /// that are essential for loading.
167    // TODO: also allow passing a sha512
168    pub fn inline_script(mut self, src: &str) -> Self {
169        let val = format!(r#"<script>{}</script>"#, src);
170        self.scripts.push(val);
171        self
172    }
173
174    /// Add a non-blocking `<link as="style">` tag. This is ideal for including
175    /// styles that aren't essential for an initial render pass.
176    ///
177    /// Generally this should be combined with `.inline_style()` to optimize a
178    /// render pipeline.
179    ///
180    /// `onerror` exists because of a bug in firefox. See https://github.com/filamentgroup/loadCSS/issues/246 for more details
181    // TODO: also allow passing a sha512
182    pub fn style(mut self, src: &str) -> Self {
183        let val = format!(
184            r#"<link rel="preload" as="style" href="{}" onload="this.rel='stylesheet'" onerror="this.rel='stylesheet'">"#,
185            src
186        );
187        self.styles.push(val);
188
189        if !self.has_async_style {
190            self = self.inline_script(css_rel_preload::CSS_REL_PRELOAD);
191            self.has_async_style = true;
192        }
193
194        self
195    }
196
197    /// Add an inline `<style>` tag. This is ideal for including styles that
198    /// should be available for an initial render pass.
199    ///
200    /// Generally this should be combined with `.style()` to optimize a render
201    /// pipeline.
202    // TODO: also allow passing a sha512
203    pub fn inline_style(mut self, src: &str) -> Self {
204        let val = format!(r#"<style>{}</style>"#, src);
205        self.styles.push(val);
206        self
207    }
208
209    /// Add a blocking `<link rel="stylesheet">` tag. This is ideal for externally
210    /// loading scripts that should be loaded before any rendering can be
211    /// initialized.
212    // TODO: also allow passing a sha512
213    pub fn blocking_style(mut self, src: &str) -> Self {
214        let val = format!(r#"<link rel="stylesheet" href="{}">"#, src);
215        self.styles.push(val);
216        self
217    }
218
219    /// Add a favicon.
220    pub fn favicon(mut self, src: &str) -> Self {
221        let val = format!(r#"<link rel="icon" type="image/x-icon" href="{}">"#, src);
222        self.favicon = Some(val);
223        self
224    }
225
226    /// Add a `manifest.json` link.
227    pub fn manifest(mut self, src: &str) -> Self {
228        let val = format!(r#"<link rel="manifest" href="{}">"#, src);
229        self.manifest = Some(val);
230        self
231    }
232
233    /// Add a `<link as="font">` tag.
234    pub fn font(mut self, src: &str) -> Self {
235        let val = format!(
236            r#"<link rel="preload" as="font" crossorigin href="{}">"#,
237            src
238        );
239        self.fonts.push(val);
240        self
241    }
242
243    /// Create an HTML document.
244    pub fn build(self) -> String {
245        let mut html: String = DOCTYPE.into();
246        html.push_str(&format!(r#"<html lang="{}">"#, self.lang));
247        html.push_str(HEAD_OPEN);
248        html.push_str(CHARSET);
249        html.push_str(VIEWPORT);
250        if let Some(title) = self.title {
251            html.push_str(&title);
252        }
253        if let Some(desc) = self.desc {
254            html.push_str(&desc);
255        }
256
257        for script in self.scripts {
258            html.push_str(&script);
259        }
260        for style in self.styles {
261            html.push_str(&style);
262        }
263        for font in self.fonts {
264            html.push_str(&font);
265        }
266        if let Some(manifest) = self.manifest {
267            html.push_str(&manifest);
268        }
269
270        if let Some(color) = self.color {
271            html.push_str(&color);
272        }
273        if let Some(favicon) = self.favicon {
274            html.push_str(&favicon);
275        }
276        html.push_str(HEAD_CLOSE);
277        if let Some(body) = self.body {
278            html.push_str(&body);
279        }
280        html.push_str(HTML_CLOSE);
281        html
282    }
283}
284
285#[cfg(feature = "http-types")]
286impl Into<http_types::Response> for Builder<'_> {
287    fn into(self) -> http_types::Response {
288        let mut res = http_types::Response::new(200);
289        res.set_content_type(http_types::mime::HTML);
290        res.set_body(self.build());
291        res
292    }
293}