use proc_macro::TokenTree::Literal;
use proc_macro::{Span, TokenStream};
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path::Path;
use std::str::FromStr;
#[proc_macro]
pub fn minify(input: TokenStream) -> TokenStream {
if let Some(input) = __ccm_read_input_to_string(input) {
let call_path = Span::call_site().local_file().unwrap();
let target_path = call_path.parent().unwrap().join(Path::new(&input));
let content = fs::read_to_string(target_path)
.unwrap_or_else(|_| panic!("could not find file {}", input));
__ccm_minify(content)
} else {
TokenStream::from_str("\"\"").unwrap()
}
}
#[proc_macro]
pub fn minify_str(input: TokenStream) -> TokenStream {
if let Some(input) = __ccm_read_input_to_string(input) {
__ccm_minify(input)
} else {
TokenStream::from_str("\"\"").unwrap()
}
}
fn __ccm_read_input_to_string(input: TokenStream) -> Option<String> {
let token_trees: Vec<_> = input.into_iter().collect();
if token_trees.len() != 1 {
panic!("const_css_minify requires a single str as input");
}
let Literal(literal) = token_trees.first().unwrap() else {
panic!("const_css_minify requires a literal str as input");
};
let mut literal = literal.to_string();
if let Some(c) = literal.get(0..=0)
&& c != "r"
{
literal = literal
.replace("\\\"", "\"")
.replace("\\n", "\n")
.replace("\\r", "\r")
.replace("\\t", "\t")
.replace("\\\\", "\\")
}
let start = &literal.find('\"').unwrap() + 1;
let end = &literal.rfind('\"').unwrap() - 1;
if start > end {
None
} else {
Some(literal[start..=end].to_string())
}
}
fn __ccm_minify(input: String) -> TokenStream {
let mut minified = input;
let mut minifier = __CcmMinifier::new();
minifier.minify_string(&minified);
minifier.emit_error_msgs();
minified = minifier.get_output();
minified = "r####\"".to_string() + &minified + "\"####";
TokenStream::from_str(&minified).unwrap()
}
struct __CcmParseError {
msg: String,
}
impl __CcmParseError {
pub fn from_msg(msg: &str) -> Self {
Self {
msg: msg.to_string(),
}
}
}
impl fmt::Display for __CcmParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.msg)
}
}
const __CCM_RGB_FUNC_DECODABLE: [u8; 15] = [
b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b' ', b',', b'%', b'.', b'/',
];
struct __CcmMinifier<'a> {
input: Option<&'a [u8]>,
output0: Vec<u8>,
output1: Vec<u8>,
quotes0: HashMap<usize, usize>,
errors: Vec<__CcmParseError>,
}
impl<'a> __CcmMinifier<'a> {
pub fn get_output(self) -> String {
String::from_utf8(self.output1).unwrap()
}
pub fn new() -> Self {
Self {
input: None,
output0: Vec::<u8>::with_capacity(0),
output1: Vec::<u8>::with_capacity(0),
quotes0: HashMap::<usize, usize>::new(),
errors: Vec::<__CcmParseError>::new(),
}
}
pub fn minify_string(&mut self, input: &'a String) {
self.input = Some(input.as_bytes());
self.pass0();
self.pass1();
}
fn add_error_msg(&mut self, msg: &str) {
self.errors.push(__CcmParseError::from_msg(msg));
}
fn emit_error_msgs(&self) {
for error in &self.errors {
eprintln!("WARN! const-css-minify parse error: {}", error);
}
}
fn pass0(&mut self) {
let input = self.input.unwrap();
let len = input.len();
let mut output = Vec::<u8>::with_capacity(len);
let mut read = 0;
loop {
match read {
i if i == len => break,
i if i > len => unreachable!(), _ => (),
}
match input[read] {
w if w.is_ascii_whitespace() => {
if let Some(last) = output.pop()
&& last != b' '
{
output.push(last);
}
read += 1;
while read < len && input[read].is_ascii_whitespace() {
read += 1;
}
if !output.is_empty() && read < len {
output.push(b' ');
}
}
b'/' if len > read + 1 && input[read + 1] == b'*' => {
let mut found_end = false;
read += 2;
if read < len {
read += 1;
}
while read < len {
let s = &input[read - 1..=read];
read += 1;
if s == [b'*', b'/'] {
found_end = true;
break;
}
}
if !found_end {
self.add_error_msg("reached end of input while inside comment");
}
}
q @ (b'"' | b'\'') => {
let start = output.len();
output.push(input[read]);
read += 1;
let mut found_end = false;
while read < len {
let b = input[read];
output.push(b);
read += 1;
if b == q {
found_end = true;
break;
}
}
if !found_end {
self.add_error_msg("reached end of input while inside quote string");
}
let end = output.len() - 1;
self.quotes0.insert(start, end);
}
_ => {
output.push(input[read]);
read += 1;
}
}
}
self.output0 = output;
}
fn pass1(&mut self) {
let input = &self.output0;
let len = input.len();
let mut output = Vec::<u8>::with_capacity(len);
let mut read = 0;
let mut peek;
let mut backreference = None;
loop {
match read {
i if i == len => break,
i if i > len => unreachable!(), _ => (),
}
match input[read] {
b'\'' | b'"' => {
let end = self.quotes0.get(&read).unwrap();
while read <= *end {
output.push(input[read]);
read += 1
}
}
b'{' => {
backreference = None;
if let Some(last) = output.pop()
&& last != b' '
{
output.push(last);
}
output.push(input[read]);
read += 1;
if read < len && input[read] == b' ' {
read += 1;
}
}
b'}' => {
if let Some(br) = backreference {
output.remove(br);
}
backreference = None;
if let Some(last) = output.pop()
&& last != b' '
{
output.push(last);
}
if let Some(last) = output.pop()
&& last != b';'
{
output.push(last);
}
output.push(input[read]);
read += 1;
if read < len && input[read] == b' ' {
read += 1;
}
}
b':' => {
backreference = None;
if len > read + 1 && input[read + 1] == b':' {
output.push(b':');
output.push(b':');
read += 2;
} else {
if let Some(last) = output.pop() {
if last == b' ' {
backreference = Some(output.len());
}
output.push(last);
}
output.push(input[read]);
read += 1;
if read < len && input[read] == b' ' {
read += 1;
}
}
}
b',' => {
if let Some(last) = output.pop()
&& last != b' '
{
output.push(last);
}
output.push(input[read]);
read += 1;
if read < len && input[read] == b' ' {
read += 1;
}
}
b';' => {
if let Some(br) = backreference {
output.remove(br);
}
backreference = None;
if let Some(last) = output.pop()
&& last != b' '
{
output.push(last);
}
output.push(input[read]);
read += 1;
if read < len && input[read] == b' ' {
read += 1;
}
}
b'#' if len > read + 3 => {
peek = read + 1;
while len > peek && input[peek].is_ascii_hexdigit() {
peek += 1;
}
if let Ok(mut hex_color) = __ccm_try_minify_hex_color(&input[read..peek]) {
output.append(&mut hex_color);
read = peek;
} else {
output.push(input[read]);
read += 1;
}
}
b'h' if len > read + 9
&& (input[read + 1..=read + 3] == [b's', b'l', b'(']
|| input[read + 1..=read + 4] == [b's', b'l', b'a', b'(']) =>
{
peek = read + 4;
if input[peek] == b'(' {
peek += 1;
}
while len > peek
&& input[peek] != b')'
&& __CCM_RGB_FUNC_DECODABLE.contains(&input[peek])
{
peek += 1
}
if input[peek] == b')'
&& let Ok(mut hex_color) = __ccm_try_decode_hsl_func(&input[read..=peek])
{
hex_color = __ccm_try_minify_hex_color(&hex_color).unwrap();
output.append(&mut hex_color);
read = peek + 1;
continue;
}
output.push(input[read]);
read += 1;
}
b'r' if len > read + 9
&& (input[read + 1..=read + 3] == [b'g', b'b', b'(']
|| input[read + 1..=read + 4] == [b'g', b'b', b'a', b'(']) =>
{
peek = read + 4;
if input[peek] == b'(' {
peek += 1;
}
while len > peek
&& input[peek] != b')'
&& __CCM_RGB_FUNC_DECODABLE.contains(&input[peek])
{
peek += 1
}
if input[peek] == b')'
&& let Ok(mut hex_color) = __ccm_try_decode_rgb_func(&input[read..=peek])
{
hex_color = __ccm_try_minify_hex_color(&hex_color).unwrap();
output.append(&mut hex_color);
read = peek + 1;
continue;
}
output.push(input[read]);
read += 1;
}
_ => {
output.push(input[read]);
read += 1;
}
}
}
self.output0.clear();
self.output0.shrink_to_fit();
self.output1 = output;
}
}
fn __ccm_try_decode_hsl_func(input: &[u8]) -> Result<Vec<u8>, ()> {
let mut v = vec![b'#'];
let mut read = 3;
if input[read] == b'a' {
read += 1;
}
if input[read] != b'(' {
return Err(());
}
read += 1;
let mut hsla_d = [
String::with_capacity(10),
String::with_capacity(10),
String::with_capacity(10),
String::with_capacity(10),
];
let mut percents = [false, false, false, false];
let mut i = 0;
while input[read] != b')' {
match input[read] {
x if !__CCM_RGB_FUNC_DECODABLE.contains(&x) => return Err(()),
d if d.is_ascii_digit() || d == b'.' => hsla_d[i].push(char::from(d)),
b'%' => percents[i] = true,
b' ' | b',' | b'/' => {
i += 1;
while [b' ', b',', b'/'].contains(&input[read + 1]) {
read += 1;
}
}
_ => unreachable!(), }
read += 1;
}
for digits in &hsla_d[0..=2] {
if digits.is_empty() {
return Err(());
}
}
let h = f32::from_str(&hsla_d[0]).or(Err(()))?;
if !(0.0..=360.0).contains(&h) {
return Err(());
}
let s = f32::from_str(&hsla_d[1]).or(Err(()))? / 100.0;
if !(0.0..=1.0).contains(&s) {
return Err(());
}
let l = f32::from_str(&hsla_d[2]).or(Err(()))? / 100.0;
if !(0.0..=1.0).contains(&l) {
return Err(());
}
let a = s * { if l <= 0.5 { l } else { 1_f32 - l } };
let ks = [
(h / 30_f32) % 12_f32,
(8_f32 + h / 30_f32) % 12_f32,
(4_f32 + h / 30_f32) % 12_f32,
];
for k in ks {
let c = match k {
..=2_f32 => -1_f32,
2_f32..=4_f32 => k - 3_f32,
4_f32..=8_f32 => 1_f32,
8_f32..=10_f32 => 9_f32 - k,
10_f32.. => -1_f32,
_ => unreachable!(),
};
let integer = ((l - a * c) * 255_f32).round();
if integer < u8::MIN.into() || integer > u8::MAX.into() {
return Err(());
}
let byte: u8 = unsafe { integer.to_int_unchecked() };
let hex = format!("{:04x}", byte).into_bytes();
v.push(hex[2]);
v.push(hex[3]);
}
if !hsla_d[3].is_empty() && !["1", "1.0", "100"].contains(&hsla_d[3].as_str()) {
let decimal = f32::from_str(&hsla_d[3]).or(Err(()))?;
let integer = if percents[3] {
(decimal * 255_f32 / 100_f32).round()
} else {
(decimal * 255_f32).round()
};
if integer < u8::MIN.into() || integer > u8::MAX.into() {
return Err(());
}
let byte: u8 = unsafe { integer.to_int_unchecked() };
let hex = format!("{:04x}", byte).into_bytes();
v.push(hex[2]);
v.push(hex[3]);
}
Ok(v)
}
fn __ccm_try_decode_rgb_func(input: &[u8]) -> Result<Vec<u8>, ()> {
let mut v = vec![b'#'];
let mut read = 3;
if input[read] == b'a' {
read += 1;
}
if input[read] != b'(' {
return Err(());
}
read += 1;
let mut rgba_d = [
String::with_capacity(10),
String::with_capacity(10),
String::with_capacity(10),
String::with_capacity(10),
];
let mut percents = [false, false, false, false];
let mut i = 0;
while input[read] != b')' {
match input[read] {
x if !__CCM_RGB_FUNC_DECODABLE.contains(&x) => return Err(()),
d if d.is_ascii_digit() || d == b'.' => rgba_d[i].push(char::from(d)),
b'%' => percents[i] = true,
b' ' | b',' | b'/' => {
i += 1;
while [b' ', b',', b'/'].contains(&input[read + 1]) {
read += 1;
}
}
_ => unreachable!(), }
read += 1;
}
for i in 0..=2 {
if rgba_d[i].is_empty() {
return Err(());
}
let byte: u8 = if percents[i] {
let decimal = f32::from_str(&rgba_d[i]).or(Err(()))?; let integer = (decimal * 255_f32 / 100_f32).round();
if integer < u8::MIN.into() || integer > u8::MAX.into() {
return Err(());
}
unsafe { integer.to_int_unchecked() }
} else {
u8::from_str(&rgba_d[i]).or(Err(()))?
};
let hex = format!("{:04x}", byte).into_bytes();
v.push(hex[2]);
v.push(hex[3]);
}
if !rgba_d[3].is_empty() && !["1", "1.0", "100"].contains(&rgba_d[3].as_str()) {
let decimal = f32::from_str(&rgba_d[3]).or(Err(()))?;
let integer = if percents[3] {
(decimal * 255_f32 / 100_f32).round()
} else {
(decimal * 255_f32).round()
};
if integer < u8::MIN.into() || integer > u8::MAX.into() {
return Err(());
}
let byte: u8 = unsafe { integer.to_int_unchecked() };
let hex = format!("{:04x}", byte).into_bytes();
v.push(hex[2]);
v.push(hex[3]);
}
Ok(v)
}
fn __ccm_try_minify_hex_color(input: &[u8]) -> Result<Vec<u8>, ()> {
let len = input.len();
if ![4, 5, 7, 9].contains(&len) || input[0] != b'#' {
return Err(());
}
let mut v = vec![b'#'];
for byte in &input[1..] {
if !byte.is_ascii_hexdigit() {
return Err(());
}
v.push(*byte);
}
if len == 9 && v[1] == v[2] && v[3] == v[4] && v[5] == v[6] && v[7] == v[8] {
v.remove(8);
v.remove(6);
v.remove(4);
v.remove(2);
}
if len == 7 && v[1] == v[2] && v[3] == v[4] && v[5] == v[6] {
v.remove(6);
v.remove(4);
v.remove(2);
}
Ok(v)
}