#![warn(
clippy::doc_markdown,
clippy::redundant_closure,
clippy::explicit_iter_loop,
clippy::match_same_arms,
clippy::needless_borrow,
clippy::print_stdout,
clippy::integer_arithmetic,
clippy::cast_possible_truncation,
clippy::result_unwrap_used,
clippy::result_map_unwrap_or_else,
clippy::option_unwrap_used,
clippy::option_map_unwrap_or_else,
clippy::option_map_unwrap_or,
clippy::trivially_copy_pass_by_ref,
clippy::needless_pass_by_value,
missing_docs,
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unused_extern_crates,
unused_import_braces,
unused_qualifications,
variant_size_differences
)]
use kuchiki::traits::TendrilSink;
use kuchiki::{parse_html, NodeRef, Selectors};
pub mod error;
mod parser;
pub use error::InlineError;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};
pub use url::{ParseError, Url};
#[derive(Debug)]
struct Rule<'i> {
selectors: Selectors,
declarations: Vec<parser::Declaration<'i>>,
}
impl<'i> Rule<'i> {
pub fn new(
selectors: &str,
declarations: Vec<parser::Declaration<'i>>,
) -> Result<Rule<'i>, ()> {
Ok(Rule {
selectors: Selectors::compile(selectors)?,
declarations,
})
}
}
#[derive(Debug)]
pub struct InlineOptions {
pub remove_style_tags: bool,
pub base_url: Option<Url>,
pub load_remote_stylesheets: bool,
}
impl InlineOptions {
#[inline]
pub fn compact() -> Self {
InlineOptions {
remove_style_tags: true,
base_url: None,
load_remote_stylesheets: true,
}
}
}
impl Default for InlineOptions {
#[inline]
fn default() -> Self {
InlineOptions {
remove_style_tags: false,
base_url: None,
load_remote_stylesheets: true,
}
}
}
#[derive(Debug)]
pub struct CSSInliner {
options: InlineOptions,
}
impl CSSInliner {
#[inline]
pub fn new(options: InlineOptions) -> Self {
CSSInliner { options }
}
#[inline]
pub fn compact() -> Self {
CSSInliner {
options: InlineOptions::compact(),
}
}
#[inline]
pub fn inline(&self, html: &str) -> Result<String, InlineError> {
let mut out = vec![];
self.inline_to(html, &mut out)?;
Ok(String::from_utf8_lossy(&out).to_string())
}
#[inline]
pub fn inline_to<W: Write>(&self, html: &str, target: &mut W) -> Result<(), InlineError> {
let document = parse_html().one(html);
for style_tag in document
.select("style")
.map_err(|_| error::InlineError::ParseError("Unknown error".to_string()))?
{
if let Some(first_child) = style_tag.as_node().first_child() {
if let Some(css_cell) = first_child.as_text() {
process_css(&document, css_cell.borrow().as_str())?;
}
}
if self.options.remove_style_tags {
style_tag.as_node().detach()
}
}
if self.options.load_remote_stylesheets {
for link_tag in document
.select("link[rel~=stylesheet]")
.map_err(|_| error::InlineError::ParseError("Unknown error".to_string()))?
{
if let Some(href) = &link_tag.attributes.borrow().get("href") {
let url = self.get_full_url(href);
let css = self.load_external(url.as_ref())?;
process_css(&document, css.as_str())?;
}
}
}
document.serialize(target)?;
Ok(())
}
fn get_full_url<'u>(&self, href: &'u str) -> Cow<'u, str> {
if Url::parse(href).is_ok() {
return Cow::Borrowed(href);
};
if let Some(base_url) = &self.options.base_url {
if href.starts_with("//") {
return Cow::Owned(format!("{}:{}", base_url.scheme(), href));
} else {
if let Ok(new_url) = base_url.join(href) {
return Cow::Owned(new_url.to_string());
}
}
};
Cow::Borrowed(href)
}
fn load_external(&self, url: &str) -> Result<String, InlineError> {
if url.starts_with("http") | url.starts_with("https") {
let response = attohttpc::get(url).send()?;
Ok(response.text()?)
} else {
let mut file = File::open(url)?;
let mut css = String::new();
file.read_to_string(&mut css)?;
Ok(css)
}
}
}
fn process_css(document: &NodeRef, css: &str) -> Result<(), InlineError> {
let mut parse_input = cssparser::ParserInput::new(css);
let mut parser = parser::CSSParser::new(&mut parse_input);
for parsed in parser.parse() {
if let Ok((selector, declarations)) = parsed {
if let Ok(rule) = Rule::new(selector, declarations) {
let matching_elements = document
.inclusive_descendants()
.filter_map(|node| node.into_element_ref())
.filter(|element| rule.selectors.matches(element));
for matching_element in matching_elements {
if let Ok(mut attributes) = matching_element.attributes.try_borrow_mut() {
let style = if let Some(existing_style) = attributes.get("style") {
merge_styles(existing_style, &rule.declarations)?
} else {
rule.declarations
.iter()
.map(|&(ref key, value)| format!("{}:{};", key, value))
.collect()
};
attributes.insert("style", style);
}
}
}
}
}
Ok(())
}
impl Default for CSSInliner {
#[inline]
fn default() -> Self {
CSSInliner::new(Default::default())
}
}
#[inline]
pub fn inline(html: &str) -> Result<String, InlineError> {
CSSInliner::default().inline(html)
}
#[inline]
pub fn inline_to<W: Write>(html: &str, target: &mut W) -> Result<(), InlineError> {
CSSInliner::default().inline_to(html, target)
}
fn merge_styles(
existing_style: &str,
new_styles: &[parser::Declaration],
) -> Result<String, InlineError> {
let mut input = cssparser::ParserInput::new(existing_style);
let mut parser = cssparser::Parser::new(&mut input);
let declarations =
cssparser::DeclarationListParser::new(&mut parser, parser::CSSDeclarationListParser);
let mut styles: HashMap<String, &str> =
HashMap::with_capacity(new_styles.len().saturating_add(1));
for declaration in declarations {
let (property, value) = declaration?;
styles.insert(property.to_string(), value);
}
for (property, value) in new_styles {
styles.insert(property.to_string(), value);
}
Ok(styles
.into_iter()
.map(|(key, value)| format!("{}:{};", key, value))
.collect::<String>())
}