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