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}