lol_inline_assets/
lib.rs

1use css::Css;
2use lol_html::{element, html_content::ContentType, HtmlRewriter, Settings};
3use std::{
4    fs,
5    io::{Error, ErrorKind},
6    path::{Path, PathBuf},
7    sync::{Arc, Mutex},
8};
9
10mod css;
11pub struct InlineResult {
12    pub html: String,
13    pub files: Vec<PathBuf>,
14}
15
16pub fn inline<P>(file: P) -> anyhow::Result<InlineResult>
17where
18    P: AsRef<Path>,
19{
20    let html = fs::read_to_string(&file)?;
21    let root = file.as_ref().parent().unwrap_or(Path::new(""));
22
23    let mut output = vec![];
24    let deps = Arc::new(Mutex::new(vec![]));
25
26    let css = Css::new(file.as_ref(), root, &deps);
27
28    let mut rewriter = HtmlRewriter::new(
29        Settings {
30            element_content_handlers: vec![
31                element!("img", |el| {
32                    let src = el.get_attribute("src");
33                    if src.is_none() {
34                        return Ok(());
35                    }
36                    let src = src.unwrap();
37                    if src.starts_with("http") || src.starts_with("data:") {
38                        return Ok(());
39                    }
40
41                    let path = root.join(&src);
42                    if !path.exists() {
43                        return Err(Box::new(Error::new(
44                            ErrorKind::NotFound,
45                            format!(
46                                "Can't inline image to {}: file \"{}\" does not exist",
47                                file.as_ref().file_name().unwrap().to_str().unwrap(),
48                                src,
49                            ),
50                        )));
51                    }
52                    let img_contents = fs::read(&path)?;
53
54                    let mime_type:String = if path.extension().map_or(false, |element| element == "svg") {
55                        "image/svg+xml".into()
56                    } else if let Some(kind) = infer::get(&img_contents) {
57                        kind.mime_type().into()
58                    } else {
59                        let extension = path.extension()
60                            .and_then(|element| element.to_str())
61                            .unwrap_or("");
62                        mime_guess::from_ext(extension)
63                            .first_or_octet_stream()
64                            .to_string()
65                    };
66                    
67                    if !mime_type.starts_with("image/") {
68                        return Err(Box::new(Error::new(
69                            ErrorKind::InvalidData,
70                            format!("File {} is not a recognized image type", src),
71                        )))
72                    }
73
74                    let mut deps = deps.lock().unwrap();
75                    deps.push(path);
76                    let new_src = base64::encode(img_contents);
77                    let new_src = format!("data:{};base64,{}", mime_type, new_src);
78
79                    el.set_attribute("src", &new_src)?;
80                    Ok(())
81                }),
82                element!("link", |el| match css.handle(el) {
83                    Ok(_) => Ok(()),
84                    Err(e) => {
85                        let err: Box<Error> = Box::new(e.downcast().unwrap());
86                        Err(err)
87                    }
88                }),
89                element!("include", |el| {
90                    let src = el.get_attribute("src");
91                    if src.is_none() {
92                        return Ok(());
93                    }
94                    let src = src.unwrap();
95
96                    if src.starts_with("http") || src.starts_with("data:") {
97                        return Ok(());
98                    }
99
100                    let path = root.join(&src);
101                    if !path.exists() {
102                        return Err(Box::new(Error::new(
103                            ErrorKind::NotFound,
104                            format!(
105                                "Can't include to {}: file \"{}\" does not exist",
106                                file.as_ref().file_name().unwrap().to_str().unwrap(),
107                                src,
108                            ),
109                        )));
110                    }
111                    let contents = fs::read_to_string(&path)?;
112                    let mut deps = deps.lock().unwrap();
113                    deps.push(path);
114
115                    el.replace(&contents, ContentType::Html);
116
117                    Ok(())
118                }),
119                element!("script", |el| {
120                    let typ = el.get_attribute("type");
121                    if let Some(typ) = typ {
122                        if typ != "text/javascript" {
123                            return Ok(());
124                        }
125                    }
126
127                    let src = el.get_attribute("src");
128                    if src.is_none() {
129                        return Ok(());
130                    }
131                    let src = src.unwrap();
132
133                    if src.starts_with("http") || src.starts_with("data:") {
134                        return Ok(());
135                    }
136
137                    let base64 = el.get_attribute("base64");
138                    if base64.is_some() {
139                        let path = root.join(&src);
140                        if !path.exists() {
141                            return Err(Box::new(Error::new(
142                                ErrorKind::NotFound,
143                                format!(
144                                    "Can't inline script to {}: file \"{}\" does not exist",
145                                    file.as_ref().file_name().unwrap().to_str().unwrap(),
146                                    src,
147                                ),
148                            )));
149                        }
150                        let js = fs::read(&path)?;
151                        let mut deps = deps.lock().unwrap();
152                        deps.push(path);
153                        let new_src = base64::encode(js);
154                        let new_src = format!("data:application/javascript;base64,{}", new_src);
155
156                        el.set_attribute("src", &new_src)?;
157                        return Ok(());
158                    }
159
160                    let path = root.join(&src);
161                    if !path.exists() {
162                        return Err(Box::new(Error::new(
163                            ErrorKind::NotFound,
164                            format!(
165                                "Can't inline script to {}: file \"{}\" does not exist",
166                                file.as_ref().file_name().unwrap().to_str().unwrap(),
167                                src,
168                            ),
169                        )));
170                    }
171                    let js = fs::read_to_string(&path)?;
172                    let mut deps = deps.lock().unwrap();
173                    deps.push(path);
174
175                    el.replace(&format!("<script>{}</script>", js), ContentType::Html);
176
177                    Ok(())
178                }),
179            ],
180            ..Settings::default()
181        },
182        |c: &[u8]| output.extend_from_slice(c),
183    );
184
185    rewriter.write(html.as_bytes())?;
186    rewriter.end()?;
187
188    let html = String::from_utf8(output)?;
189    let files = Arc::try_unwrap(deps).unwrap();
190    let files = files.into_inner().unwrap();
191    Ok(InlineResult { html, files })
192}