const_css_minify/
lib.rs

1//! [<img alt="github" src="https://img.shields.io/badge/github-scpso%2Fconst--css--minify-7c72ff?logo=github">](https://github.com/scpso/const-css-minify)
2//! [<img alt="crates.io" src="https://img.shields.io/crates/v/const-css-minify.svg?logo=rust">](https://crates.io/crates/const-css-minify)
3//! [<img alt="docs.rs" src="https://img.shields.io/docsrs/const-css-minify/latest?logo=docs.rs">](https://docs.rs/const-css-minify)
4//!
5//! Include a minified css file as an inline const in your high-performance compiled web
6//! application.
7//!
8//! You can call it with a path to your source css file, just like you might use the built-in
9//! macro `include_str!()`:
10//!
11//! ```
12//! use const_css_minify::minify;
13//!
14//! // this is probably the pattern you want to use
15//! const CSS: &str = minify!("./path/to/style.css");
16//! ```
17//!
18//! <div class="warning">
19//!
20//! ***IMPORTANT!*** the current version of `const_css_minify` resolves paths relative to the
21//! source file where it is invoked. This behaviour is consistent with the rust built-in macros
22//! like `include_str()`. This behaviour is ***DIFFERENT*** from versions `0.1.8` and prior of
23//! this crate, which resolved paths relative to the crate root (i.e. the directory where your
24//! `Cargo.toml` is).
25//!
26//! </div>
27//!
28//! It's also possible to include a raw string with your css directly in your rust source:
29//! ```
30//! use const_css_minify::minify_str;
31//!
32//! const CSS: &str = minify_str!(r#"
33//!     input[type="radio"]:checked, .button:hover {
34//!         color: rgb(0 255 100% / 0.8);
35//!         margin: 10px 10px;
36//!     }
37//! "#);
38//! assert_eq!(CSS, r#"input[type="radio"]:checked,.button:hover{color:#0ffc;margin:10px 10px}"#);
39//! ```
40//!
41//! Note also that the current version of `const_css_minify` does not support passing in a variable.
42//! only the above two patterns of a path to an external file or a literal str will work.
43//!
44//! `const_css_minify` is not a good solution if your css changes out-of-step with your binary, as
45//! you will not be able to change the css without recompiling your application.
46//!
47//! #### `const_css_minify` ***will:***
48//! * remove unneeded whitespace and linebreaks
49//! * remove comments
50//! * remove unneeded trailing semicolon in each declaration block
51//! * opportunistically minify colors specified either by literal hex values or by `rgb()`,
52//!   `rgba()`, `hsl()` and `hsla()` functions (in either legacy syntax with commas or modern
53//!   syntax without commas) without changing the color. e.g. `#ffffff` will be substituted with
54//!   `#fff`, `hsl(180 50 50)` with `#40bfbf`, `rgba(20%, 40%, 60%, 0.8)` with `#369c`, etc.
55//!   `const-css-minify` will not attempt to calculate nested/complicated/relative rgb expressions
56//!   (which will be passed through unadulturated for the end user's browser to figure out for
57//!   itself) but many simple/literal expressions will be resolved and minified.
58//! * silently ignore css syntax errors originating in your source file*, and in so doing possibly
59//!   elicit slightly different failure modes from renderers by altering the placement of
60//!   whitespace around misplaced operators
61//!
62//! #### `const_css_minify` will ***not:***
63//! * compress your css using `gz`, `br` or `deflate`
64//! * change the semantic meaning of your semantically valid css
65//! * make any substitutions other than identical literal colors
66//! * alert you to invalid css* - it's not truly parsing the css, just scanning for and removing
67//!   characters it identifies as unnecessary
68//!
69//! note*: The current version of `const-css-minify` will emit compile-time warning messages for
70//! some syntax errors (specifically unclosed quote strings and comments) which indicate an error
71//! in the css (or a bug in `const-css-minify`), however these messages do not offer much help to
72//! the user to locate the source of the error. Internally, these error states are identifed and
73//! handled to avoid panicking due to indexing out-of-bounds, and so reporting the error message at
74//! compile time is in a sense 'for free', but this is a non-core feature of the library and may be
75//! removed in a future version if it turns out to do more harm than good. In any case,
76//! `const-css-minify` generally assumes it is being fed valid css as input and offers no
77//! guarantees about warnings. `const-css-minify` should not be relied upon for linting of css.
78//!
79//! `const_css_minify` is a lightweight solution - the current version of `const_css_minify` has
80//! zero dependencies outside rust's built-in std and proc_macro libraries.
81
82use proc_macro::TokenTree::Literal;
83use proc_macro::{Span, TokenStream};
84use std::collections::HashMap;
85use std::fmt;
86use std::fs;
87use std::path::Path;
88use std::str::FromStr;
89
90/// Takes a path to a .css file which will be minified as an inline const
91#[proc_macro]
92pub fn minify(input: TokenStream) -> TokenStream {
93    if let Some(input) = __ccm_read_input_to_string(input) {
94        let call_path = Span::call_site().local_file().unwrap();
95        let target_path = call_path.parent().unwrap().join(Path::new(&input));
96        let content = fs::read_to_string(target_path)
97            .unwrap_or_else(|_| panic!("could not find file {}", input));
98        __ccm_minify(content)
99    } else {
100        //bail if input is empty
101        TokenStream::from_str("\"\"").unwrap()
102    }
103}
104
105/// Takes an inline raw str as css which will be minified as an inline const
106#[proc_macro]
107pub fn minify_str(input: TokenStream) -> TokenStream {
108    if let Some(input) = __ccm_read_input_to_string(input) {
109        __ccm_minify(input)
110    } else {
111        //bail if input is empty
112        TokenStream::from_str("\"\"").unwrap()
113    }
114}
115
116fn __ccm_read_input_to_string(input: TokenStream) -> Option<String> {
117    let token_trees: Vec<_> = input.into_iter().collect();
118    if token_trees.len() != 1 {
119        panic!("const_css_minify requires a single str as input");
120    }
121    let Literal(literal) = token_trees.first().unwrap() else {
122        panic!("const_css_minify requires a literal str as input");
123    };
124    let mut literal = literal.to_string();
125
126    // not a raw string, so we must de-escape special chars
127    // this is not comprehensive but is anyone ever going to even notice?
128    // what weird and strange things might they even be trying to achieve?
129    if let Some(c) = literal.get(0..=0)
130        && c != "r"
131    {
132        literal = literal
133            .replace("\\\"", "\"")
134            .replace("\\n", "\n")
135            .replace("\\r", "\r")
136            .replace("\\t", "\t")
137            .replace("\\\\", "\\")
138    }
139
140    // trim leading and trailing ".." or r#".."# from string literal
141    let start = &literal.find('\"').unwrap() + 1;
142    let end = &literal.rfind('\"').unwrap() - 1;
143    //bail if literal is empty
144    if start > end {
145        None
146    } else {
147        Some(literal[start..=end].to_string())
148    }
149}
150
151fn __ccm_minify(input: String) -> TokenStream {
152    let mut minified = input;
153    let mut minifier = __CcmMinifier::new();
154    minifier.minify_string(&minified);
155    minifier.emit_error_msgs();
156    minified = minifier.get_output();
157
158    // wrap in quotes, ready to emit as rust raw str token
159    minified = "r####\"".to_string() + &minified + "\"####";
160
161    TokenStream::from_str(&minified).unwrap()
162}
163
164struct __CcmParseError {
165    msg: String,
166}
167
168impl __CcmParseError {
169    pub fn from_msg(msg: &str) -> Self {
170        Self {
171            msg: msg.to_string(),
172        }
173    }
174}
175
176impl fmt::Display for __CcmParseError {
177    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
178        write!(f, "{}", self.msg)
179    }
180}
181
182// we do not attempt to decode all valid rgb func expressions, but we do attempt simple expressions
183// that consist of purely literal numeric expressions.
184const __CCM_RGB_FUNC_DECODABLE: [u8; 15] = [
185    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'/',
186];
187
188/*
189 * css is relatively simple but there are a few gotchas. Nested classes basically means any
190 * property can be a selector, so we can't generically distinguish between the two without
191 * a lookup to known legal names, and also the fact that pseudo classes and elements are
192 * denoted with ':' which is also the value assignment operator means we need to scan ahead to
193 * decide if a particular ':' on the input is part of a selector and requires leading
194 * whitespace to be preserved, or if it's the assignment operator and doesn't require leading
195 * whitespace. To avoid re-implementing comment and quote handling while scanning forward, we
196 * instead mark the index as a backreference and remove it later if we can. This also has the
197 * conseqence that we also cannot generically identify if we are currently parsing a property
198 * or a value without a lookup to known legal names, which as far as I know shouldn't cause
199 * problems for handling correct css but eliminates some avenues for error tolerance. But
200 * intelligent handling of incorrect css is beyond this scope of this crate so this is
201 * acceptable.
202 */
203struct __CcmMinifier<'a> {
204    input: Option<&'a [u8]>,
205    output0: Vec<u8>,
206    output1: Vec<u8>,
207    // start and end indexes
208    quotes0: HashMap<usize, usize>,
209    errors: Vec<__CcmParseError>,
210}
211
212impl<'a> __CcmMinifier<'a> {
213    pub fn get_output(self) -> String {
214        String::from_utf8(self.output1).unwrap()
215    }
216
217    pub fn new() -> Self {
218        Self {
219            input: None,
220            output0: Vec::<u8>::with_capacity(0),
221            output1: Vec::<u8>::with_capacity(0),
222            quotes0: HashMap::<usize, usize>::new(),
223            errors: Vec::<__CcmParseError>::new(),
224        }
225    }
226
227    pub fn minify_string(&mut self, input: &'a String) {
228        self.input = Some(input.as_bytes());
229        self.pass0();
230        self.pass1();
231    }
232
233    fn add_error_msg(&mut self, msg: &str) {
234        self.errors.push(__CcmParseError::from_msg(msg));
235    }
236
237    fn emit_error_msgs(&self) {
238        for error in &self.errors {
239            eprintln!("WARN! const-css-minify parse error: {}", error);
240        }
241    }
242
243    //collapse all whitespace sequences into single ' ', remove comments,
244    //mark quotes in output stream
245    fn pass0(&mut self) {
246        let input = self.input.unwrap();
247        let len = input.len();
248        let mut output = Vec::<u8>::with_capacity(len);
249        let mut read = 0;
250        loop {
251            match read {
252                i if i == len => break,
253                i if i > len => unreachable!(), // to catch errors of reasoning in indexing
254                _ => (),
255            }
256            match input[read] {
257                // trim excess whitespace, convert to space
258                w if w.is_ascii_whitespace() => {
259                    // if the last element was a comment that was entirely ignored, and if the
260                    // comment was preceeded by whitespace, we might end up with two consecutive
261                    // whitespaces, which violates the promise of this method. Thus we explicitly
262                    // check and remove it if present.
263                    if let Some(last) = output.pop()
264                        && last != b' '
265                    {
266                        output.push(last);
267                    }
268                    read += 1;
269                    while read < len && input[read].is_ascii_whitespace() {
270                        read += 1;
271                    }
272                    // don't add whitespace to head or tail
273                    if !output.is_empty() && read < len {
274                        output.push(b' ');
275                    }
276                }
277                // css comments
278                b'/' if len > read + 1 && input[read + 1] == b'*' => {
279                    let mut found_end = false;
280                    // move read index to first char after '*' in the matched pattern, or possibly
281                    // past the end of input if '/*' are the last two chars.
282                    read += 2;
283                    // below we are comparing against a '*' at read - 1, and we explicitly want to
284                    // avoid opening and closing a comment on '/*/' - a correct comment consists of
285                    // '/**/ at a minimum. Therefore we must increment read once more, but we only
286                    // want to do this if we aren't already beyond the end of input
287                    if read < len {
288                        read += 1;
289                    }
290                    while read < len {
291                        let s = &input[read - 1..=read];
292                        read += 1;
293                        if s == [b'*', b'/'] {
294                            found_end = true;
295                            break;
296                        }
297                    }
298                    if !found_end {
299                        self.add_error_msg("reached end of input while inside comment");
300                    }
301                }
302                // quotes
303                q @ (b'"' | b'\'') => {
304                    let start = output.len();
305                    output.push(input[read]);
306                    read += 1;
307                    let mut found_end = false;
308                    while read < len {
309                        let b = input[read];
310                        output.push(b);
311                        read += 1;
312                        if b == q {
313                            found_end = true;
314                            break;
315                        }
316                    }
317                    if !found_end {
318                        self.add_error_msg("reached end of input while inside quote string");
319                    }
320                    let end = output.len() - 1;
321                    self.quotes0.insert(start, end);
322                }
323                _ => {
324                    output.push(input[read]);
325                    read += 1;
326                }
327            }
328        }
329        self.output0 = output;
330    }
331
332    fn pass1(&mut self) {
333        let input = &self.output0;
334        let len = input.len();
335        let mut output = Vec::<u8>::with_capacity(len);
336        let mut read = 0;
337        let mut peek;
338        let mut backreference = None;
339        loop {
340            match read {
341                i if i == len => break,
342                i if i > len => unreachable!(), // to catch errors of reasoning in indexing
343                _ => (),
344            }
345            match input[read] {
346                // copy quotes verbatim
347                b'\'' | b'"' => {
348                    let end = self.quotes0.get(&read).unwrap();
349                    while read <= *end {
350                        output.push(input[read]);
351                        read += 1
352                    }
353                }
354                // enter declaration block
355                b'{' => {
356                    backreference = None;
357                    if let Some(last) = output.pop()
358                        && last != b' '
359                    {
360                        output.push(last);
361                    }
362                    output.push(input[read]);
363                    read += 1;
364                    // drop trailing space
365                    if read < len && input[read] == b' ' {
366                        read += 1;
367                    }
368                }
369                // exit declaration block
370                b'}' => {
371                    if let Some(br) = backreference {
372                        output.remove(br);
373                    }
374                    backreference = None;
375                    if let Some(last) = output.pop()
376                        && last != b' '
377                    {
378                        output.push(last);
379                    }
380                    // drop final semicolon in declaration block
381                    if let Some(last) = output.pop()
382                        && last != b';'
383                    {
384                        output.push(last);
385                    }
386                    output.push(input[read]);
387                    read += 1;
388                    // drop trailing space
389                    if read < len && input[read] == b' ' {
390                        read += 1;
391                    }
392                }
393                // value assignement OR pseudo class/element
394                b':' => {
395                    backreference = None;
396                    // pseudo element
397                    if len > read + 1 && input[read + 1] == b':' {
398                        output.push(b':');
399                        output.push(b':');
400                        read += 2;
401                    } else {
402                        if let Some(last) = output.pop() {
403                            // mark backreference for possible future removal
404                            if last == b' ' {
405                                backreference = Some(output.len());
406                            }
407                            output.push(last);
408                        }
409                        output.push(input[read]);
410                        read += 1;
411                        // drop trailing space
412                        if read < len && input[read] == b' ' {
413                            read += 1;
414                        }
415                    }
416                }
417                // comma separator
418                b',' => {
419                    // drop spaces preceeding commas
420                    if let Some(last) = output.pop()
421                        && last != b' '
422                    {
423                        output.push(last);
424                    }
425                    output.push(input[read]);
426                    read += 1;
427                    // drop trailing space
428                    if read < len && input[read] == b' ' {
429                        read += 1;
430                    }
431                }
432                // semicolon separator
433                b';' => {
434                    if let Some(br) = backreference {
435                        output.remove(br);
436                    }
437                    backreference = None;
438                    // drop leading space
439                    if let Some(last) = output.pop()
440                        && last != b' '
441                    {
442                        output.push(last);
443                    }
444                    output.push(input[read]);
445                    read += 1;
446                    // drop trailing space
447                    if read < len && input[read] == b' ' {
448                        read += 1;
449                    }
450                }
451
452                // possible hex color
453                b'#' if len > read + 3 => {
454                    peek = read + 1;
455                    while len > peek && input[peek].is_ascii_hexdigit() {
456                        peek += 1;
457                    }
458                    if let Ok(mut hex_color) = __ccm_try_minify_hex_color(&input[read..peek]) {
459                        output.append(&mut hex_color);
460                        read = peek;
461                    } else {
462                        output.push(input[read]);
463                        read += 1;
464                    }
465                }
466                // possible hsl func
467                b'h' if len > read + 9
468                    && (input[read + 1..=read + 3] == [b's', b'l', b'(']
469                        || input[read + 1..=read + 4] == [b's', b'l', b'a', b'(']) =>
470                {
471                    peek = read + 4;
472                    if input[peek] == b'(' {
473                        peek += 1;
474                    }
475                    while len > peek
476                        && input[peek] != b')'
477                        && __CCM_RGB_FUNC_DECODABLE.contains(&input[peek])
478                    {
479                        peek += 1
480                    }
481                    if input[peek] == b')'
482                        && let Ok(mut hex_color) = __ccm_try_decode_hsl_func(&input[read..=peek])
483                    {
484                        hex_color = __ccm_try_minify_hex_color(&hex_color).unwrap();
485                        output.append(&mut hex_color);
486                        read = peek + 1;
487                        continue;
488                    }
489                    output.push(input[read]);
490                    read += 1;
491                }
492                // possible rgb func
493                b'r' if len > read + 9
494                    && (input[read + 1..=read + 3] == [b'g', b'b', b'(']
495                        || input[read + 1..=read + 4] == [b'g', b'b', b'a', b'(']) =>
496                {
497                    peek = read + 4;
498                    if input[peek] == b'(' {
499                        peek += 1;
500                    }
501                    while len > peek
502                        && input[peek] != b')'
503                        && __CCM_RGB_FUNC_DECODABLE.contains(&input[peek])
504                    {
505                        peek += 1
506                    }
507                    if input[peek] == b')'
508                        && let Ok(mut hex_color) = __ccm_try_decode_rgb_func(&input[read..=peek])
509                    {
510                        hex_color = __ccm_try_minify_hex_color(&hex_color).unwrap();
511                        output.append(&mut hex_color);
512                        read = peek + 1;
513                        continue;
514                    }
515                    output.push(input[read]);
516                    read += 1;
517                }
518                // all else copy verbatim
519                _ => {
520                    output.push(input[read]);
521                    read += 1;
522                }
523            }
524        }
525        self.output0.clear();
526        self.output0.shrink_to_fit();
527        self.output1 = output;
528    }
529}
530
531/*
532 * requires input to start with "hsl(" or "hsla(" and end with ")"
533 */
534fn __ccm_try_decode_hsl_func(input: &[u8]) -> Result<Vec<u8>, ()> {
535    let mut v = vec![b'#'];
536    let mut read = 3;
537    if input[read] == b'a' {
538        read += 1;
539    }
540    if input[read] != b'(' {
541        return Err(());
542    }
543    read += 1;
544    let mut hsla_d = [
545        String::with_capacity(10),
546        String::with_capacity(10),
547        String::with_capacity(10),
548        String::with_capacity(10),
549    ];
550    let mut percents = [false, false, false, false];
551    let mut i = 0;
552
553    while input[read] != b')' {
554        match input[read] {
555            x if !__CCM_RGB_FUNC_DECODABLE.contains(&x) => return Err(()),
556            d if d.is_ascii_digit() || d == b'.' => hsla_d[i].push(char::from(d)),
557            b'%' => percents[i] = true,
558            b' ' | b',' | b'/' => {
559                i += 1;
560                while [b' ', b',', b'/'].contains(&input[read + 1]) {
561                    read += 1;
562                }
563            }
564            _ => unreachable!(), // did we add chars to __CCM_RGB_FUNC_DECODABLE and not match here?
565        }
566        read += 1;
567    }
568
569    // check we got required input for h, s, l
570    for digits in &hsla_d[0..=2] {
571        if digits.is_empty() {
572            return Err(());
573        }
574    }
575
576    let h = f32::from_str(&hsla_d[0]).or(Err(()))?;
577    if !(0.0..=360.0).contains(&h) {
578        return Err(());
579    }
580    let s = f32::from_str(&hsla_d[1]).or(Err(()))? / 100.0;
581    if !(0.0..=1.0).contains(&s) {
582        return Err(());
583    }
584    let l = f32::from_str(&hsla_d[2]).or(Err(()))? / 100.0;
585    if !(0.0..=1.0).contains(&l) {
586        return Err(());
587    }
588
589    // weird algorithm from wikipedia...
590    let a = s * { if l <= 0.5 { l } else { 1_f32 - l } };
591    let ks = [
592        (h / 30_f32) % 12_f32,
593        (8_f32 + h / 30_f32) % 12_f32,
594        (4_f32 + h / 30_f32) % 12_f32,
595    ];
596    for k in ks {
597        let c = match k {
598            ..=2_f32 => -1_f32,
599            2_f32..=4_f32 => k - 3_f32,
600            4_f32..=8_f32 => 1_f32,
601            8_f32..=10_f32 => 9_f32 - k,
602            10_f32.. => -1_f32,
603            _ => unreachable!(),
604        };
605        let integer = ((l - a * c) * 255_f32).round();
606        if integer < u8::MIN.into() || integer > u8::MAX.into() {
607            return Err(());
608        }
609        let byte: u8 = unsafe { integer.to_int_unchecked() };
610        let hex = format!("{:04x}", byte).into_bytes();
611        //igore leading '0x' get only the actual hexadecimal digits
612        v.push(hex[2]);
613        v.push(hex[3]);
614    }
615
616    // alpha channel
617    if !hsla_d[3].is_empty() && !["1", "1.0", "100"].contains(&hsla_d[3].as_str()) {
618        let decimal = f32::from_str(&hsla_d[3]).or(Err(()))?;
619        let integer = if percents[3] {
620            (decimal * 255_f32 / 100_f32).round()
621        } else {
622            (decimal * 255_f32).round()
623        };
624        if integer < u8::MIN.into() || integer > u8::MAX.into() {
625            return Err(());
626        }
627        let byte: u8 = unsafe { integer.to_int_unchecked() };
628
629        //format as hexadecimal
630        let hex = format!("{:04x}", byte).into_bytes();
631        //igore leading '0x' get only the actual hexadecimal digits
632        v.push(hex[2]);
633        v.push(hex[3]);
634    }
635    Ok(v)
636}
637
638/*
639 * requires input to start with "rgb(" or "rgba(" and end with ")"
640 */
641fn __ccm_try_decode_rgb_func(input: &[u8]) -> Result<Vec<u8>, ()> {
642    let mut v = vec![b'#'];
643    let mut read = 3;
644    if input[read] == b'a' {
645        read += 1;
646    }
647    if input[read] != b'(' {
648        return Err(());
649    }
650    read += 1;
651    let mut rgba_d = [
652        String::with_capacity(10),
653        String::with_capacity(10),
654        String::with_capacity(10),
655        String::with_capacity(10),
656    ];
657    let mut percents = [false, false, false, false];
658    let mut i = 0;
659    while input[read] != b')' {
660        match input[read] {
661            x if !__CCM_RGB_FUNC_DECODABLE.contains(&x) => return Err(()),
662            d if d.is_ascii_digit() || d == b'.' => rgba_d[i].push(char::from(d)),
663            b'%' => percents[i] = true,
664            b' ' | b',' | b'/' => {
665                i += 1;
666                while [b' ', b',', b'/'].contains(&input[read + 1]) {
667                    read += 1;
668                }
669            }
670            _ => unreachable!(), // did we add chars to __CCM_RGB_FUNC_DECODABLE and not match here?
671        }
672        read += 1;
673    }
674    // check we got required input for r, g, b
675    for i in 0..=2 {
676        if rgba_d[i].is_empty() {
677            return Err(());
678        }
679        let byte: u8 = if percents[i] {
680            let decimal = f32::from_str(&rgba_d[i]).or(Err(()))?; // 👈 #unexpectedlisp
681            let integer = (decimal * 255_f32 / 100_f32).round();
682            if integer < u8::MIN.into() || integer > u8::MAX.into() {
683                return Err(());
684            }
685            unsafe { integer.to_int_unchecked() }
686        } else {
687            u8::from_str(&rgba_d[i]).or(Err(()))?
688        };
689        //format as hexadecimal
690        let hex = format!("{:04x}", byte).into_bytes();
691        //igore leading '0x' get only the actual hexadecimal digits
692        v.push(hex[2]);
693        v.push(hex[3]);
694    }
695    // alpha channel
696    if !rgba_d[3].is_empty() && !["1", "1.0", "100"].contains(&rgba_d[3].as_str()) {
697        let decimal = f32::from_str(&rgba_d[3]).or(Err(()))?;
698        let integer = if percents[3] {
699            (decimal * 255_f32 / 100_f32).round()
700        } else {
701            (decimal * 255_f32).round()
702        };
703        if integer < u8::MIN.into() || integer > u8::MAX.into() {
704            return Err(());
705        }
706        let byte: u8 = unsafe { integer.to_int_unchecked() };
707
708        //format as hexadecimal
709        let hex = format!("{:04x}", byte).into_bytes();
710        //igore leading '0x' get only the actual hexadecimal digits
711        v.push(hex[2]);
712        v.push(hex[3]);
713    }
714    Ok(v)
715}
716
717fn __ccm_try_minify_hex_color(input: &[u8]) -> Result<Vec<u8>, ()> {
718    let len = input.len();
719    if ![4, 5, 7, 9].contains(&len) || input[0] != b'#' {
720        return Err(());
721    }
722    let mut v = vec![b'#'];
723    for byte in &input[1..] {
724        if !byte.is_ascii_hexdigit() {
725            return Err(());
726        }
727        v.push(*byte);
728    }
729    if len == 9 && v[1] == v[2] && v[3] == v[4] && v[5] == v[6] && v[7] == v[8] {
730        v.remove(8);
731        v.remove(6);
732        v.remove(4);
733        v.remove(2);
734    }
735    if len == 7 && v[1] == v[2] && v[3] == v[4] && v[5] == v[6] {
736        v.remove(6);
737        v.remove(4);
738        v.remove(2);
739    }
740    Ok(v)
741}