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}