monolith/src/css.rs

431 lines
16 KiB
Rust
Raw Normal View History

2020-03-29 09:54:20 +02:00
use cssparser::{ParseError, Parser, ParserInput, SourcePosition, Token};
use reqwest::blocking::Client;
use std::collections::HashMap;
use crate::url::{data_to_data_url, get_url_fragment, is_http_url, resolve_url, url_with_fragment};
use crate::utils::retrieve_asset;
2020-03-29 09:54:20 +02:00
const CSS_PROPS_WITH_IMAGE_URLS: &[&str] = &[
// Universal
2020-03-29 09:54:20 +02:00
"background",
"background-image",
"border-image",
"border-image-source",
"content",
"cursor",
"list-style",
"list-style-image",
"mask",
"mask-image",
// Specific to @counter-style
"additive-symbols",
"negative",
"pad",
"prefix",
"suffix",
"symbols",
2020-03-29 09:54:20 +02:00
];
const CSS_SPECIAL_CHARS: &str = "~!@$%^&*()+=,./'\";:?><[]{}|`#";
2020-03-29 09:54:20 +02:00
pub fn is_image_url_prop(prop_name: &str) -> bool {
CSS_PROPS_WITH_IMAGE_URLS
.iter()
.find(|p| prop_name.eq_ignore_ascii_case(p))
.is_some()
}
pub fn enquote(input: String, double: bool) -> String {
if double {
format!("\"{}\"", input.replace("\"", "\\\""))
} else {
format!("'{}'", input.replace("'", "\\'"))
}
}
pub fn escape(value: &str) -> String {
let mut res = str!(&value);
res = res.replace("\\", "\\\\");
for c in CSS_SPECIAL_CHARS.chars() {
res = res.replace(c, format!("\\{}", c).as_str());
}
res
}
2020-03-29 09:54:20 +02:00
pub fn process_css<'a>(
cache: &mut HashMap<String, Vec<u8>>,
2020-03-29 09:54:20 +02:00
client: &Client,
parent_url: &str,
parser: &mut Parser,
rule_name: &str,
prop_name: &str,
func_name: &str,
2020-04-22 09:37:02 +02:00
opt_no_fonts: bool,
2020-03-29 09:54:20 +02:00
opt_no_images: bool,
opt_silent: bool,
) -> Result<String, ParseError<'a, String>> {
let mut result: String = str!();
let mut curr_rule: String = str!(rule_name.clone());
let mut curr_prop: String = str!(prop_name.clone());
let mut token: &Token;
let mut token_offset: SourcePosition;
loop {
token_offset = parser.position();
token = match parser.next_including_whitespace_and_comments() {
Ok(token) => token,
Err(_) => {
break;
}
};
match *token {
Token::Comment(_) => {
let token_slice = parser.slice_from(token_offset);
result.push_str(str!(token_slice).as_str());
}
Token::Semicolon => result.push_str(";"),
Token::Colon => result.push_str(":"),
Token::Comma => result.push_str(","),
Token::ParenthesisBlock | Token::SquareBracketBlock | Token::CurlyBracketBlock => {
2020-04-22 09:37:02 +02:00
if opt_no_fonts && curr_rule == "font-face" {
continue;
}
2020-03-29 09:54:20 +02:00
let closure: &str;
if token == &Token::ParenthesisBlock {
result.push_str("(");
closure = ")";
} else if token == &Token::SquareBracketBlock {
result.push_str("[");
closure = "]";
} else {
result.push_str("{");
closure = "}";
}
let block_css: String = parser
.parse_nested_block(|parser| {
process_css(
cache,
client,
parent_url,
parser,
rule_name,
curr_prop.as_str(),
func_name,
2020-04-22 09:37:02 +02:00
opt_no_fonts,
2020-03-29 09:54:20 +02:00
opt_no_images,
opt_silent,
)
})
.unwrap();
result.push_str(block_css.as_str());
result.push_str(closure);
}
Token::CloseParenthesis => result.push_str(")"),
Token::CloseSquareBracket => result.push_str("]"),
Token::CloseCurlyBracket => result.push_str("}"),
Token::IncludeMatch => result.push_str("~="),
Token::DashMatch => result.push_str("|="),
Token::PrefixMatch => result.push_str("^="),
Token::SuffixMatch => result.push_str("$="),
Token::SubstringMatch => result.push_str("*="),
Token::CDO => result.push_str("<!--"),
Token::CDC => result.push_str("-->"),
Token::WhiteSpace(ref value) => {
result.push_str(value);
}
2020-04-22 09:37:02 +02:00
// div...
2020-03-29 09:54:20 +02:00
Token::Ident(ref value) => {
2020-04-22 09:37:02 +02:00
curr_rule = str!();
2020-03-29 09:54:20 +02:00
curr_prop = str!(value);
result.push_str(&escape(value));
2020-03-29 09:54:20 +02:00
}
2020-04-22 09:37:02 +02:00
// @import, @font-face, @charset, @media...
2020-03-29 09:54:20 +02:00
Token::AtKeyword(ref value) => {
curr_rule = str!(value);
2020-04-22 09:37:02 +02:00
if opt_no_fonts && curr_rule == "font-face" {
continue;
}
2020-03-29 09:54:20 +02:00
result.push_str("@");
result.push_str(value);
}
Token::Hash(ref value) => {
result.push_str("#");
result.push_str(value);
}
Token::QuotedString(ref value) => {
2020-04-22 09:37:02 +02:00
if curr_rule == "import" {
2020-03-29 09:54:20 +02:00
// Reset current at-rule value
curr_rule = str!();
// Skip empty import values
if value.len() < 1 {
result.push_str("''");
continue;
}
let import_full_url = resolve_url(&parent_url, value).unwrap_or_default();
let import_url_fragment = get_url_fragment(import_full_url.clone());
match retrieve_asset(cache, client, &parent_url, &import_full_url, opt_silent) {
Ok((import_contents, import_final_url, _import_media_type)) => {
let import_data_url = data_to_data_url(
"text/css",
embed_css(
cache,
client,
&import_final_url,
&String::from_utf8_lossy(&import_contents),
opt_no_fonts,
opt_no_images,
opt_silent,
2020-03-29 09:54:20 +02:00
)
.as_bytes(),
&import_final_url,
);
let assembled_url: String = url_with_fragment(
import_data_url.as_str(),
import_url_fragment.as_str(),
);
result.push_str(enquote(assembled_url, false).as_str());
}
Err(_) => {
// Keep remote reference if unable to retrieve the asset
if is_http_url(import_full_url.clone()) {
let assembled_url: String = url_with_fragment(
import_full_url.as_str(),
import_url_fragment.as_str(),
);
result.push_str(enquote(assembled_url, false).as_str());
}
}
}
2020-03-29 09:54:20 +02:00
} else {
if func_name == "url" {
// Skip empty url()'s
if value.len() < 1 {
continue;
}
if opt_no_images && is_image_url_prop(curr_prop.as_str()) {
2020-04-03 09:30:52 +02:00
result.push_str(enquote(str!(empty_image!()), false).as_str());
2020-03-29 09:54:20 +02:00
} else {
let resolved_url = resolve_url(&parent_url, value).unwrap_or_default();
let url_fragment = get_url_fragment(resolved_url.clone());
match retrieve_asset(
2020-03-29 09:54:20 +02:00
cache,
client,
&parent_url,
&resolved_url,
opt_silent,
) {
Ok((data, final_url, media_type)) => {
let data_url = data_to_data_url(&media_type, &data, &final_url);
let assembled_url: String =
url_with_fragment(data_url.as_str(), url_fragment.as_str());
result.push_str(enquote(assembled_url, false).as_str());
}
Err(_) => {
// Keep remote reference if unable to retrieve the asset
if is_http_url(resolved_url.clone()) {
let assembled_url: String = url_with_fragment(
resolved_url.as_str(),
url_fragment.as_str(),
);
result.push_str(enquote(assembled_url, false).as_str());
}
}
}
2020-03-29 09:54:20 +02:00
}
} else {
result.push_str(enquote(str!(value), false).as_str());
}
}
}
Token::Number {
ref has_sign,
ref value,
..
} => {
if *has_sign && *value >= 0. {
result.push_str("+");
}
result.push_str(&value.to_string())
}
Token::Percentage {
ref has_sign,
ref unit_value,
..
} => {
2020-04-09 00:07:39 +02:00
if *has_sign && *unit_value >= 0. {
result.push_str("+");
2020-03-29 09:54:20 +02:00
}
result.push_str(str!(unit_value * 100.).as_str());
result.push_str("%");
}
Token::Dimension {
2020-04-09 00:07:39 +02:00
ref has_sign,
2020-03-29 09:54:20 +02:00
ref value,
ref unit,
..
} => {
2020-04-09 00:07:39 +02:00
if *has_sign && *value >= 0. {
result.push_str("+");
}
2020-03-29 09:54:20 +02:00
result.push_str(str!(value).as_str());
result.push_str(str!(unit).as_str());
}
2020-04-22 09:37:02 +02:00
// #selector, #id...
2020-03-29 09:54:20 +02:00
Token::IDHash(ref value) => {
2020-04-22 09:37:02 +02:00
curr_rule = str!();
2020-03-29 09:54:20 +02:00
result.push_str("#");
result.push_str(&escape(value));
2020-03-29 09:54:20 +02:00
}
Token::UnquotedUrl(ref value) => {
let is_import: bool = curr_rule == "import";
2020-04-22 09:37:02 +02:00
2020-03-29 09:54:20 +02:00
if is_import {
// Reset current at-rule value
curr_rule = str!();
}
// Skip empty url()'s
if value.len() < 1 {
result.push_str("url()");
continue;
} else if value.starts_with("#") {
result.push_str("url(");
result.push_str(value);
result.push_str(")");
continue;
}
result.push_str("url(");
if is_import {
let full_url = resolve_url(&parent_url, value).unwrap_or_default();
let url_fragment = get_url_fragment(full_url.clone());
match retrieve_asset(cache, client, &parent_url, &full_url, opt_silent) {
Ok((css, final_url, _media_type)) => {
let data_url = data_to_data_url(
2020-03-29 09:54:20 +02:00
"text/css",
embed_css(
cache,
client,
&final_url,
&String::from_utf8_lossy(&css),
2020-04-22 09:37:02 +02:00
opt_no_fonts,
2020-03-29 09:54:20 +02:00
opt_no_images,
opt_silent,
)
.as_bytes(),
&final_url,
);
let assembled_url: String =
url_with_fragment(data_url.as_str(), url_fragment.as_str());
result.push_str(enquote(assembled_url, false).as_str());
}
Err(_) => {
// Keep remote reference if unable to retrieve the asset
if is_http_url(full_url.clone()) {
let assembled_url: String =
url_with_fragment(full_url.as_str(), url_fragment.as_str());
result.push_str(enquote(assembled_url, false).as_str());
}
}
}
2020-03-29 09:54:20 +02:00
} else {
if opt_no_images && is_image_url_prop(curr_prop.as_str()) {
2020-04-03 09:30:52 +02:00
result.push_str(enquote(str!(empty_image!()), false).as_str());
2020-03-29 09:54:20 +02:00
} else {
let full_url = resolve_url(&parent_url, value).unwrap_or_default();
let url_fragment = get_url_fragment(full_url.clone());
match retrieve_asset(cache, client, &parent_url, &full_url, opt_silent) {
Ok((data, final_url, media_type)) => {
let data_url = data_to_data_url(&media_type, &data, &final_url);
let assembled_url: String =
url_with_fragment(data_url.as_str(), url_fragment.as_str());
result.push_str(enquote(assembled_url, false).as_str());
}
Err(_) => {
// Keep remote reference if unable to retrieve the asset
if is_http_url(full_url.clone()) {
let assembled_url: String =
url_with_fragment(full_url.as_str(), url_fragment.as_str());
result.push_str(enquote(assembled_url, false).as_str());
}
}
}
2020-03-29 09:54:20 +02:00
}
}
result.push_str(")");
}
Token::Delim(ref value) => result.push_str(&value.to_string()),
Token::Function(ref name) => {
let function_name: &str = &name.clone();
result.push_str(function_name);
result.push_str("(");
let block_css: String = parser
.parse_nested_block(|parser| {
process_css(
cache,
client,
parent_url,
parser,
curr_rule.as_str(),
curr_prop.as_str(),
function_name,
2020-04-22 09:37:02 +02:00
opt_no_fonts,
2020-03-29 09:54:20 +02:00
opt_no_images,
opt_silent,
)
})
.unwrap();
result.push_str(block_css.as_str());
result.push_str(")");
}
Token::BadUrl(_) | Token::BadString(_) => {}
}
}
// Ensure empty CSS is really empty
if result.len() > 0 && result.trim().len() == 0 {
result = result.trim().to_string()
}
2020-03-29 09:54:20 +02:00
Ok(result)
}
pub fn embed_css(
cache: &mut HashMap<String, Vec<u8>>,
2020-03-29 09:54:20 +02:00
client: &Client,
parent_url: &str,
css: &str,
2020-04-22 09:37:02 +02:00
opt_no_fonts: bool,
2020-03-29 09:54:20 +02:00
opt_no_images: bool,
opt_silent: bool,
) -> String {
let mut input = ParserInput::new(&css);
let mut parser = Parser::new(&mut input);
process_css(
cache,
client,
parent_url,
&mut parser,
"",
"",
"",
2020-04-22 09:37:02 +02:00
opt_no_fonts,
2020-03-29 09:54:20 +02:00
opt_no_images,
opt_silent,
)
.unwrap()
}