2020-03-29 09:54:20 +02:00
|
|
|
use cssparser::{ParseError, Parser, ParserInput, SourcePosition, Token};
|
|
|
|
use reqwest::blocking::Client;
|
|
|
|
use std::collections::HashMap;
|
|
|
|
|
2020-06-28 07:36:41 +02:00
|
|
|
use crate::opts::Options;
|
2020-06-24 09:16:40 +02:00
|
|
|
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] = &[
|
2020-04-10 13:04:36 +02:00
|
|
|
// 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",
|
2020-04-10 13:04:36 +02:00
|
|
|
// Specific to @counter-style
|
|
|
|
"additive-symbols",
|
|
|
|
"negative",
|
|
|
|
"pad",
|
|
|
|
"prefix",
|
|
|
|
"suffix",
|
|
|
|
"symbols",
|
2020-03-29 09:54:20 +02:00
|
|
|
];
|
2020-04-11 23:50:23 +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("'", "\\'"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-11 23:50:23 +02:00
|
|
|
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>(
|
2020-04-11 02:43:29 +02:00
|
|
|
cache: &mut HashMap<String, Vec<u8>>,
|
2020-03-29 09:54:20 +02:00
|
|
|
client: &Client,
|
|
|
|
parent_url: &str,
|
|
|
|
parser: &mut Parser,
|
2020-06-28 07:36:41 +02:00
|
|
|
options: &Options,
|
2020-06-28 22:11:15 +02:00
|
|
|
depth: u32,
|
2020-03-29 09:54:20 +02:00
|
|
|
rule_name: &str,
|
|
|
|
prop_name: &str,
|
|
|
|
func_name: &str,
|
|
|
|
) -> 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-06-28 07:36:41 +02:00
|
|
|
if options.no_fonts && curr_rule == "font-face" {
|
2020-04-22 09:37:02 +02:00
|
|
|
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,
|
2020-06-28 07:36:41 +02:00
|
|
|
options,
|
2020-06-28 22:11:15 +02:00
|
|
|
depth,
|
2020-03-29 09:54:20 +02:00
|
|
|
rule_name,
|
|
|
|
curr_prop.as_str(),
|
|
|
|
func_name,
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.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);
|
2020-04-11 23:50:23 +02:00
|
|
|
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-06-28 07:36:41 +02:00
|
|
|
if options.no_fonts && curr_rule == "font-face" {
|
2020-04-22 09:37:02 +02:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-05-02 12:13:28 +02:00
|
|
|
let import_full_url = resolve_url(&parent_url, value).unwrap_or_default();
|
|
|
|
let import_url_fragment = get_url_fragment(import_full_url.clone());
|
2020-06-28 07:36:41 +02:00
|
|
|
match retrieve_asset(
|
|
|
|
cache,
|
|
|
|
client,
|
|
|
|
&parent_url,
|
|
|
|
&import_full_url,
|
|
|
|
options.silent,
|
2020-06-28 22:11:15 +02:00
|
|
|
depth + 1,
|
2020-06-28 07:36:41 +02:00
|
|
|
) {
|
2020-05-02 12:13:28 +02:00
|
|
|
Ok((import_contents, import_final_url, _import_media_type)) => {
|
2020-06-24 08:17:16 +02:00
|
|
|
let import_data_url = data_to_data_url(
|
|
|
|
"text/css",
|
|
|
|
embed_css(
|
|
|
|
cache,
|
|
|
|
client,
|
|
|
|
&import_final_url,
|
|
|
|
&String::from_utf8_lossy(&import_contents),
|
2020-06-28 07:36:41 +02:00
|
|
|
options,
|
2020-06-28 22:11:15 +02:00
|
|
|
depth + 1,
|
2020-03-29 09:54:20 +02:00
|
|
|
)
|
2020-06-24 08:17:16 +02:00
|
|
|
.as_bytes(),
|
|
|
|
&import_final_url,
|
|
|
|
);
|
|
|
|
let assembled_url: String = url_with_fragment(
|
|
|
|
import_data_url.as_str(),
|
|
|
|
import_url_fragment.as_str(),
|
2020-05-02 12:13:28 +02:00
|
|
|
);
|
2020-06-24 08:17:16 +02:00
|
|
|
result.push_str(enquote(assembled_url, false).as_str());
|
2020-05-02 12:13:28 +02:00
|
|
|
}
|
|
|
|
Err(_) => {
|
|
|
|
// Keep remote reference if unable to retrieve the asset
|
|
|
|
if is_http_url(import_full_url.clone()) {
|
2020-06-24 08:17:16 +02:00
|
|
|
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-05-02 12:13:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-03-29 09:54:20 +02:00
|
|
|
} else {
|
|
|
|
if func_name == "url" {
|
|
|
|
// Skip empty url()'s
|
|
|
|
if value.len() < 1 {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-06-28 07:36:41 +02:00
|
|
|
if options.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();
|
2020-05-02 12:13:28 +02:00
|
|
|
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,
|
2020-06-28 07:36:41 +02:00
|
|
|
options.silent,
|
2020-06-28 22:11:15 +02:00
|
|
|
depth + 1,
|
2020-05-02 12:13:28 +02:00
|
|
|
) {
|
|
|
|
Ok((data, final_url, media_type)) => {
|
2020-06-24 08:17:16 +02:00
|
|
|
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());
|
2020-05-02 12:13:28 +02:00
|
|
|
}
|
|
|
|
Err(_) => {
|
|
|
|
// Keep remote reference if unable to retrieve the asset
|
|
|
|
if is_http_url(resolved_url.clone()) {
|
2020-06-24 08:17:16 +02:00
|
|
|
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-05-02 12:13:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
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
|
|
|
}
|
2020-06-28 07:36:41 +02:00
|
|
|
result.push_str(str!(unit_value * 100.0).as_str());
|
2020-03-29 09:54:20 +02:00
|
|
|
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("#");
|
2020-04-11 23:50:23 +02:00
|
|
|
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());
|
2020-06-28 22:11:15 +02:00
|
|
|
match retrieve_asset(
|
|
|
|
cache,
|
|
|
|
client,
|
|
|
|
&parent_url,
|
|
|
|
&full_url,
|
|
|
|
options.silent,
|
|
|
|
depth + 1,
|
|
|
|
) {
|
2020-05-02 12:13:28 +02:00
|
|
|
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,
|
2020-05-02 12:13:28 +02:00
|
|
|
&final_url,
|
|
|
|
&String::from_utf8_lossy(&css),
|
2020-06-28 07:36:41 +02:00
|
|
|
options,
|
2020-06-28 22:11:15 +02:00
|
|
|
depth + 1,
|
2020-03-29 09:54:20 +02:00
|
|
|
)
|
|
|
|
.as_bytes(),
|
|
|
|
&final_url,
|
2020-05-02 12:13:28 +02:00
|
|
|
);
|
2020-06-24 08:17:16 +02:00
|
|
|
let assembled_url: String =
|
|
|
|
url_with_fragment(data_url.as_str(), url_fragment.as_str());
|
|
|
|
result.push_str(enquote(assembled_url, false).as_str());
|
2020-05-02 12:13:28 +02:00
|
|
|
}
|
|
|
|
Err(_) => {
|
|
|
|
// Keep remote reference if unable to retrieve the asset
|
|
|
|
if is_http_url(full_url.clone()) {
|
2020-06-24 08:17:16 +02:00
|
|
|
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-05-02 12:13:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-03-29 09:54:20 +02:00
|
|
|
} else {
|
2020-06-28 22:11:15 +02:00
|
|
|
if is_image_url_prop(curr_prop.as_str()) && options.no_images {
|
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();
|
2020-05-02 12:13:28 +02:00
|
|
|
let url_fragment = get_url_fragment(full_url.clone());
|
2020-06-28 22:11:15 +02:00
|
|
|
match retrieve_asset(
|
|
|
|
cache,
|
|
|
|
client,
|
|
|
|
&parent_url,
|
|
|
|
&full_url,
|
|
|
|
options.silent,
|
|
|
|
depth + 1,
|
|
|
|
) {
|
2020-05-02 12:13:28 +02:00
|
|
|
Ok((data, final_url, media_type)) => {
|
2020-06-24 08:17:16 +02:00
|
|
|
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());
|
2020-05-02 12:13:28 +02:00
|
|
|
}
|
|
|
|
Err(_) => {
|
|
|
|
// Keep remote reference if unable to retrieve the asset
|
|
|
|
if is_http_url(full_url.clone()) {
|
2020-06-24 08:17:16 +02:00
|
|
|
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-05-02 12:13:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
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,
|
2020-06-28 07:36:41 +02:00
|
|
|
options,
|
2020-06-28 22:11:15 +02:00
|
|
|
depth,
|
2020-03-29 09:54:20 +02:00
|
|
|
curr_rule.as_str(),
|
|
|
|
curr_prop.as_str(),
|
|
|
|
function_name,
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.unwrap();
|
|
|
|
result.push_str(block_css.as_str());
|
|
|
|
|
|
|
|
result.push_str(")");
|
|
|
|
}
|
|
|
|
Token::BadUrl(_) | Token::BadString(_) => {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-27 05:22:35 +02:00
|
|
|
// 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(
|
2020-04-11 02:43:29 +02:00
|
|
|
cache: &mut HashMap<String, Vec<u8>>,
|
2020-03-29 09:54:20 +02:00
|
|
|
client: &Client,
|
|
|
|
parent_url: &str,
|
|
|
|
css: &str,
|
2020-06-28 07:36:41 +02:00
|
|
|
options: &Options,
|
2020-06-28 22:11:15 +02:00
|
|
|
depth: u32,
|
2020-03-29 09:54:20 +02:00
|
|
|
) -> String {
|
|
|
|
let mut input = ParserInput::new(&css);
|
|
|
|
let mut parser = Parser::new(&mut input);
|
|
|
|
|
2020-06-28 22:11:15 +02:00
|
|
|
process_css(
|
|
|
|
cache,
|
|
|
|
client,
|
|
|
|
parent_url,
|
|
|
|
&mut parser,
|
|
|
|
options,
|
|
|
|
depth,
|
|
|
|
"",
|
|
|
|
"",
|
|
|
|
"",
|
|
|
|
)
|
|
|
|
.unwrap()
|
2020-03-29 09:54:20 +02:00
|
|
|
}
|