|
66 | 66 | //! </html>"#;
|
67 | 67 | //!
|
68 | 68 | //!fn main() -> Result<(), css_inline::InlineError> {
|
69 |
| -//! let inlined = css_inline::inline(HTML)?; |
| 69 | +//! let inlined = css_inline::inline(HTML)?; // shortcut with default options |
70 | 70 | //! // Do something with inlined HTML, e.g. send an email
|
71 | 71 | //! Ok(())
|
72 | 72 | //! }
|
73 |
| -//! |
74 | 73 | //! ```
|
| 74 | +//! |
| 75 | +//! Or if you need more control over inlining you could use `CSSInliner`: |
| 76 | +//! |
| 77 | +//! ```rust |
| 78 | +//! const HTML: &str = r#"<html> |
| 79 | +//! <head> |
| 80 | +//! <title>Test</title> |
| 81 | +//! <style> |
| 82 | +//! h1, h2 { color:blue; } |
| 83 | +//! strong { text-decoration:none } |
| 84 | +//! p { font-size:2px } |
| 85 | +//! p.footer { font-size: 1px} |
| 86 | +//! </style> |
| 87 | +//! </head> |
| 88 | +//! <body> |
| 89 | +//! <h1>Big Text</h1> |
| 90 | +//! <p> |
| 91 | +//! <strong>Solid</strong> |
| 92 | +//! </p> |
| 93 | +//! <p class="footer">Foot notes</p> |
| 94 | +//! </body> |
| 95 | +//! </html>"#; |
| 96 | +//! |
| 97 | +//!fn main() -> Result<(), css_inline::InlineError> { |
| 98 | +//! let options = css_inline::InlineOptions {}; |
| 99 | +//! let inliner = css_inline::CSSInliner::new(options); |
| 100 | +//! let inlined = inliner.inline(HTML)?; |
| 101 | +//! // Do something with inlined HTML, e.g. send an email |
| 102 | +//! Ok(()) |
| 103 | +//! } |
| 104 | +//!``` |
75 | 105 | #![warn(
|
76 | 106 | clippy::doc_markdown,
|
77 | 107 | clippy::redundant_closure,
|
@@ -122,49 +152,88 @@ impl Rule {
|
122 | 152 | }
|
123 | 153 | }
|
124 | 154 |
|
125 |
| -/// Inline CSS styles from <style> tags to matching elements in the HTML tree. |
126 |
| -pub fn inline(html: &str) -> Result<String, InlineError> { |
127 |
| - let document = parse_html().one(html); |
128 |
| - for style_tag in document |
129 |
| - .select("style") |
130 |
| - .map_err(|_| error::InlineError::ParseError("Unknown error".to_string()))? |
131 |
| - { |
132 |
| - if let Some(first_child) = style_tag.as_node().first_child() { |
133 |
| - if let Some(css_cell) = first_child.as_text() { |
134 |
| - let css = css_cell.borrow(); |
135 |
| - let mut parse_input = cssparser::ParserInput::new(css.as_str()); |
136 |
| - let mut parser = parse::CSSParser::new(&mut parse_input); |
137 |
| - for parsed in parser.parse() { |
138 |
| - if let Ok((selector, declarations)) = parsed { |
139 |
| - let rule = Rule::new(&selector, declarations).map_err(|_| { |
140 |
| - error::InlineError::ParseError("Unknown error".to_string()) |
141 |
| - })?; |
142 |
| - let matching_elements = document |
143 |
| - .inclusive_descendants() |
144 |
| - .filter_map(|node| node.into_element_ref()) |
145 |
| - .filter(|element| rule.selectors.matches(element)); |
146 |
| - for matching_element in matching_elements { |
147 |
| - let mut attributes = matching_element.attributes.borrow_mut(); |
148 |
| - let style = if let Some(existing_style) = attributes.get("style") { |
149 |
| - merge_styles(existing_style, &rule.declarations)? |
150 |
| - } else { |
151 |
| - rule.declarations |
152 |
| - .iter() |
153 |
| - .map(|&(ref key, ref value)| format!("{}:{};", key, value)) |
154 |
| - .collect() |
155 |
| - }; |
156 |
| - attributes.insert("style", style); |
| 155 | +/// Configuration options for CSS inlining process. |
| 156 | +#[derive(Debug)] |
| 157 | +pub struct InlineOptions {} |
| 158 | + |
| 159 | +impl Default for InlineOptions { |
| 160 | + #[inline] |
| 161 | + fn default() -> Self { |
| 162 | + InlineOptions {} |
| 163 | + } |
| 164 | +} |
| 165 | + |
| 166 | +/// Customizable CSS inliner. |
| 167 | +#[derive(Debug)] |
| 168 | +pub struct CSSInliner { |
| 169 | + options: InlineOptions, |
| 170 | +} |
| 171 | + |
| 172 | +impl CSSInliner { |
| 173 | + /// Create a new `CSSInliner` instance with given options. |
| 174 | + #[inline] |
| 175 | + pub fn new(options: InlineOptions) -> Self { |
| 176 | + CSSInliner { options } |
| 177 | + } |
| 178 | + |
| 179 | + /// Inline CSS styles from <style> tags to matching elements in the HTML tree. |
| 180 | + #[inline] |
| 181 | + pub fn inline(&self, html: &str) -> Result<String, InlineError> { |
| 182 | + let document = parse_html().one(html); |
| 183 | + for style_tag in document |
| 184 | + .select("style") |
| 185 | + .map_err(|_| error::InlineError::ParseError("Unknown error".to_string()))? |
| 186 | + { |
| 187 | + if let Some(first_child) = style_tag.as_node().first_child() { |
| 188 | + if let Some(css_cell) = first_child.as_text() { |
| 189 | + let css = css_cell.borrow(); |
| 190 | + let mut parse_input = cssparser::ParserInput::new(css.as_str()); |
| 191 | + let mut parser = parse::CSSParser::new(&mut parse_input); |
| 192 | + for parsed in parser.parse() { |
| 193 | + if let Ok((selector, declarations)) = parsed { |
| 194 | + let rule = Rule::new(&selector, declarations).map_err(|_| { |
| 195 | + error::InlineError::ParseError("Unknown error".to_string()) |
| 196 | + })?; |
| 197 | + let matching_elements = document |
| 198 | + .inclusive_descendants() |
| 199 | + .filter_map(|node| node.into_element_ref()) |
| 200 | + .filter(|element| rule.selectors.matches(element)); |
| 201 | + for matching_element in matching_elements { |
| 202 | + let mut attributes = matching_element.attributes.borrow_mut(); |
| 203 | + let style = if let Some(existing_style) = attributes.get("style") { |
| 204 | + merge_styles(existing_style, &rule.declarations)? |
| 205 | + } else { |
| 206 | + rule.declarations |
| 207 | + .iter() |
| 208 | + .map(|&(ref key, ref value)| format!("{}:{};", key, value)) |
| 209 | + .collect() |
| 210 | + }; |
| 211 | + attributes.insert("style", style); |
| 212 | + } |
157 | 213 | }
|
| 214 | + // Ignore not parsable entries. E.g. there is no parser for @media queries |
| 215 | + // Which means that they will fall into this category and will be ignored |
158 | 216 | }
|
159 |
| - // Ignore not parsable entries. E.g. there is no parser for @media queries |
160 |
| - // Which means that they will fall into this category and will be ignored |
161 | 217 | }
|
162 | 218 | }
|
163 | 219 | }
|
| 220 | + let mut out = vec![]; |
| 221 | + document.serialize(&mut out)?; |
| 222 | + Ok(String::from_utf8_lossy(&out).to_string()) |
164 | 223 | }
|
165 |
| - let mut out = vec![]; |
166 |
| - document.serialize(&mut out)?; |
167 |
| - Ok(String::from_utf8_lossy(&out).to_string()) |
| 224 | +} |
| 225 | + |
| 226 | +impl Default for CSSInliner { |
| 227 | + #[inline] |
| 228 | + fn default() -> Self { |
| 229 | + CSSInliner::new(Default::default()) |
| 230 | + } |
| 231 | +} |
| 232 | + |
| 233 | +/// Shortcut for inlining CSS with default parameters. |
| 234 | +#[inline] |
| 235 | +pub fn inline(html: &str) -> Result<String, InlineError> { |
| 236 | + CSSInliner::default().inline(html) |
168 | 237 | }
|
169 | 238 |
|
170 | 239 | fn merge_styles(existing_style: &str, new_styles: &[Declaration]) -> Result<String, InlineError> {
|
|
0 commit comments