Skip to main content

html_minifier/
html_minifier_helper.rs

1use std::str::from_utf8;
2
3use educe::Educe;
4
5use crate::{
6    HTMLMinifierError, HTMLWriter, css_minifier::CSSMinifier, foreign_content::ForeignContent,
7    functions::*, js_minifier::JSMinifier, script_data::ScriptData,
8};
9
10/// The longest `type` attribute value that is collected.
11/// Longer values are kept without minification.
12const MAX_TYPE_LENGTH: usize = 256;
13
14/// The longest tag or attribute name that is collected.
15/// A longer name never matches a name that is checked.
16const MAX_NAME_LENGTH: usize = 16;
17
18/// Check whether the `type` attribute value of a `script` or `style` element enables minification.
19/// Return `Some(true)` for a JavaScript module.
20fn embedded_type(tag: &[u8], value: &[u8]) -> Option<bool> {
21    // Every supported type is ASCII, and decoding entities never removes a non-ASCII byte.
22    if value.len() > MAX_TYPE_LENGTH || !value.is_ascii() {
23        return None;
24    }
25
26    let decoded;
27    let value = if value.contains(&b'&') {
28        decoded = html_escape::decode_html_entities(from_utf8(value).unwrap_or_default());
29
30        decoded.as_bytes()
31    } else {
32        value
33    };
34
35    if tag == b"script" {
36        if value.eq_ignore_ascii_case(b"module") {
37            Some(true)
38        } else {
39            (value.is_empty()
40                || [b"application/javascript".as_slice(), b"text/javascript"]
41                    .iter()
42                    .any(|t| value.trim_ascii().eq_ignore_ascii_case(t)))
43            .then_some(false)
44        }
45    } else {
46        (value.is_empty() || value.eq_ignore_ascii_case(b"text/css")).then_some(false)
47    }
48}
49
50#[derive(Educe, Debug, Copy, Clone, Eq, PartialEq)]
51#[educe(Default)]
52enum Step {
53    #[educe(Default)]
54    Initial,
55    InitialRemainOneWhitespace,
56    InitialIgnoreWhitespace,
57    StartTagInitial,
58    EndTagInitial,
59    StartTag,
60    StartTagIn,
61    StartTagAttributeName,
62    StartTagAttributeNameWaitingValue,
63    StartTagAttributeValueInitial,
64    StartTagUnquotedAttributeValue,
65    StartTagQuotedAttributeValue,
66    EndTag,
67    TagEnd,
68    Doctype,
69    Comment,
70    ScriptDefault,
71    ScriptJavaScript,
72    StyleDefault,
73    StyleCSS,
74    Pre,
75    Code,
76    Textarea,
77    RawText,
78    Foreign,
79    Plaintext,
80}
81
82impl Step {
83    #[inline]
84    const fn end_tag(self, raw_end: &'static [u8]) -> Option<&'static [u8]> {
85        match self {
86            Step::ScriptDefault | Step::ScriptJavaScript => Some(b"</script"),
87            Step::StyleDefault | Step::StyleCSS => Some(b"</style"),
88            Step::Textarea => Some(b"</textarea"),
89            Step::RawText => Some(raw_end),
90            _ => None,
91        }
92    }
93
94    #[inline]
95    const fn is_embedded(self) -> bool {
96        matches!(self, Step::ScriptJavaScript | Step::StyleCSS)
97    }
98
99    /// Check whether the step is in raw text or preserved content, such as `<script>` and `<pre>`.
100    #[inline]
101    const fn is_special(self) -> bool {
102        matches!(
103            self,
104            Step::ScriptDefault
105                | Step::ScriptJavaScript
106                | Step::StyleDefault
107                | Step::StyleCSS
108                | Step::Pre
109                | Step::Code
110                | Step::Textarea
111                | Step::RawText
112                | Step::Foreign
113                | Step::Plaintext
114        )
115    }
116}
117
118/// Check whether a byte of text content is written as it is.
119#[inline]
120const fn is_plain_text(e: u8) -> bool {
121    !(is_whitespace(e) || matches!(e, b'<' | b'&') || is_ascii_control(e))
122}
123
124/// Collect a byte of a tag or attribute name, up to one byte over the limit.
125#[inline]
126fn push_name_byte(name: &mut Vec<u8>, e: u8) {
127    if name.len() <= MAX_NAME_LENGTH {
128        name.push(e);
129    }
130}
131
132#[derive(Educe, Debug, Copy, Clone, Eq, PartialEq)]
133#[educe(Default)]
134enum PreservedStep {
135    #[educe(Default)]
136    Data,
137    TagOpen,
138    EndTagOpen,
139    TagName,
140    BeforeAttributeName,
141    AttributeName,
142    AfterAttributeName,
143    BeforeAttributeValue,
144    QuotedAttributeValue,
145    UnquotedAttributeValue,
146    Declaration,
147    BogusComment,
148    Comment,
149    Raw,
150}
151
152/// This struct helps you generate and minify your HTML code in the same time. The output destination is outside this struct.
153#[derive(Educe, Clone)]
154#[educe(Debug, Default(new))]
155pub struct HTMLMinifierHelper {
156    #[educe(Default = true)]
157    /// Remove HTML comments, keeping empty comments where text could form a character reference or a tag.
158    pub remove_comments: bool,
159    #[educe(Default = true)]
160    /// Minify the content in the `code` element.
161    pub minify_code:     bool,
162    #[educe(Default = true)]
163    /// Minify JavaScript in supported script elements.
164    pub minify_js:       bool,
165    #[educe(Default = true)]
166    /// Minify CSS in supported style elements.
167    pub minify_css:      bool,
168
169    // Buffers
170    #[educe(Debug(method = "str_bytes_fmt"))]
171    buffer:         Vec<u8>,
172    #[educe(Debug(method = "str_bytes_fmt"))]
173    tag:            Vec<u8>,
174    #[educe(Debug(method = "str_bytes_fmt"))]
175    attribute_type: Vec<u8>,
176    #[educe(Debug(method = "str_bytes_fmt"))]
177    preserved_tag:  Vec<u8>,
178    #[educe(Debug(method = "str_bytes_fmt"))]
179    held:           [u8; 8],
180
181    // Embedded code
182    css:         CSSMinifier,
183    js:          JSMinifier,
184    script_data: ScriptData,
185    foreign:     ForeignContent,
186
187    // Steps
188    step:                Step,
189    step_counter:        u8,
190    /// The name of the preserved element, such as `pre`, whose end tag ends the preserved content.
191    #[educe(Debug(method = "str_bytes_fmt"))]
192    preserved_root:      &'static [u8],
193    preserved_step:      PreservedStep,
194    preserved_depth:     usize,
195    preserved_counter:   u8,
196    preserved_end_tag:   bool,
197    preserved_plaintext: bool,
198    #[educe(Debug(method = "str_bytes_fmt"))]
199    raw_end:             &'static [u8],
200    preserved_quote:     u8,
201
202    // Temp
203    quote:      u8,
204    last_space: u8,
205    last_cr:    bool,
206
207    // Flags
208    /// Whether the text ends with an unfinished character reference or a literal `<`, which the text after a removed comment or `</>` could join.
209    reference_pending:    bool,
210    /// Whether the whitespace before removed comments has already been written.
211    text_space_written:   bool,
212    /// Whether the current tag or comment is at the start of the document.
213    at_start:             bool,
214    quoted_value_spacing: bool,
215    quoted_value_empty:   bool,
216    in_handled_attribute: bool,
217    in_attribute_type:    bool,
218    type_seen:            bool,
219    type_supported:       bool,
220    type_module:          bool,
221    in_end_tag:           bool,
222}
223
224impl HTMLMinifierHelper {
225    #[inline]
226    fn skip_plain_text(&mut self, text_bytes: &[u8], mut p: usize) -> usize {
227        let e = text_bytes[p];
228
229        self.reference_pending =
230            e == b'&' || (self.reference_pending && (e.is_ascii_alphanumeric() || e == b'#'));
231        self.text_space_written = false;
232
233        while let Some(&next) = text_bytes.get(p + 1) {
234            let skip = if self.reference_pending {
235                next.is_ascii_alphanumeric() || next == b'#'
236            } else {
237                is_plain_text(next)
238            };
239
240            if !skip {
241                break;
242            }
243
244            p += 1;
245        }
246
247        p
248    }
249
250    /// Reset the state for a new tag.
251    #[inline]
252    fn begin_tag(&mut self) {
253        self.in_end_tag = false;
254        self.at_start = false;
255        self.attribute_type.clear();
256        self.type_seen = false;
257        self.type_supported = true;
258        self.type_module = false;
259    }
260
261    #[inline]
262    fn set_flags_by_attribute(&mut self) {
263        let is_type = self.buffer == b"type" && matches!(self.tag.as_slice(), b"script" | b"style");
264
265        self.in_handled_attribute = self.buffer == b"class";
266
267        // Like browsers, only the first `type` attribute counts.
268        // Its value is collected only when it can enable minification.
269        self.in_attribute_type = is_type
270            && !self.type_seen
271            && if self.tag == b"script" { self.minify_js } else { self.minify_css };
272
273        if is_type {
274            self.type_seen = true;
275        }
276    }
277
278    /// Collect a byte of the `type` attribute value, up to one byte over the limit.
279    #[inline]
280    fn push_type_byte(&mut self, e: u8) {
281        if self.in_attribute_type && self.attribute_type.len() <= MAX_TYPE_LENGTH {
282            self.attribute_type.push(e);
283        }
284    }
285
286    #[inline]
287    fn finish_buffer(&mut self) {
288        if self.in_attribute_type {
289            let embedded_type = embedded_type(&self.tag, &self.attribute_type);
290
291            self.type_supported = embedded_type.is_some();
292            self.type_module = embedded_type == Some(true);
293        }
294    }
295
296    #[inline]
297    fn end_start_tag_and_get_next_step(
298        &mut self,
299        out: &mut impl HTMLWriter,
300        text_bytes: &[u8],
301        start: &mut usize,
302        p: usize,
303    ) -> Result<Step, HTMLMinifierError> {
304        let step = match self.tag.as_slice() {
305            b"script" => {
306                self.step_counter = 0;
307                self.script_data.reset();
308
309                if self.minify_js && self.type_supported {
310                    out.push_bytes(&text_bytes[*start..=p])?;
311                    *start = p + 1;
312
313                    self.js.reset(self.type_module);
314
315                    Step::ScriptJavaScript
316                } else {
317                    Step::ScriptDefault
318                }
319            },
320            b"style" => {
321                self.step_counter = 0;
322
323                if self.minify_css && self.type_supported {
324                    out.push_bytes(&text_bytes[*start..=p])?;
325                    *start = p + 1;
326
327                    self.css.reset();
328
329                    Step::StyleCSS
330                } else {
331                    Step::StyleDefault
332                }
333            },
334            b"pre" => {
335                self.start_preserved(b"pre");
336                Step::Pre
337            },
338            b"listing" => {
339                // Browsers keep the whitespace of `listing` like `pre`.
340                self.start_preserved(b"listing");
341                Step::Pre
342            },
343            b"code" => {
344                if self.minify_code {
345                    self.last_space = 0;
346
347                    Step::InitialRemainOneWhitespace
348                } else {
349                    self.start_preserved(b"code");
350                    Step::Code
351                }
352            },
353            b"textarea" => {
354                self.step_counter = 0;
355                Step::Textarea
356            },
357            b"plaintext" => Step::Plaintext,
358            _ => {
359                self.last_space = 0;
360
361                if let Some(end_tag) = raw_text_end_tag(&self.tag) {
362                    self.step_counter = 0;
363                    self.raw_end = end_tag;
364                    Step::RawText
365                } else {
366                    Step::InitialRemainOneWhitespace
367                }
368            },
369        };
370
371        Ok(step)
372    }
373
374    #[inline]
375    fn write_span(
376        &mut self,
377        bytes: &[u8],
378        out: &mut impl HTMLWriter,
379    ) -> Result<(), HTMLMinifierError> {
380        if self.step.is_embedded() { self.feed_embedded(bytes, out) } else { out.push_bytes(bytes) }
381    }
382
383    #[inline]
384    fn feed_embedded(
385        &mut self,
386        bytes: &[u8],
387        out: &mut impl HTMLWriter,
388    ) -> Result<(), HTMLMinifierError> {
389        if self.step == Step::ScriptJavaScript {
390            self.js.feed(bytes, out)
391        } else {
392            self.css.feed(bytes, out)
393        }
394    }
395
396    #[inline]
397    fn finish_embedded(&mut self, out: &mut impl HTMLWriter) -> Result<(), HTMLMinifierError> {
398        if self.step == Step::ScriptJavaScript { self.js.finish(out) } else { self.css.finish(out) }
399    }
400
401    /// Check whether the step is in a `title` element, whose whitespace is collapsed.
402    #[inline]
403    fn in_title(&self) -> bool {
404        self.step == Step::RawText && self.raw_end == b"</title"
405    }
406
407    /// Skip the bytes of raw text or preserved content that can neither start an end tag nor need a change.
408    #[inline]
409    fn skip_content(&self, text_bytes: &[u8], mut p: usize) -> usize {
410        // A LF after a CR is removed, so it is not skipped.
411        if self.follows_cr(text_bytes, p) {
412            return p;
413        }
414
415        if self.step == Step::Foreign
416            || (matches!(self.step, Step::Pre | Step::Code) && self.foreign.is_active())
417        {
418            return self.foreign.skip(text_bytes, p);
419        }
420
421        let script = matches!(self.step, Step::ScriptDefault | Step::ScriptJavaScript);
422        let plaintext = self.step == Step::Plaintext;
423        let title = self.in_title();
424        let plain = if plaintext {
425            true
426        } else if matches!(self.step, Step::Pre | Step::Code) {
427            self.preserved_step == PreservedStep::Data
428        } else {
429            self.step_counter == 0 && (!script || self.script_data.can_skip())
430        };
431
432        if plain {
433            while let Some(&e) = text_bytes.get(p) {
434                if (!plaintext && e == b'<')
435                    || e == b'\r'
436                    || is_ascii_control(e)
437                    || (script && e == b'-' && self.script_data.tracks_dash())
438                    || (title && is_whitespace(e))
439                {
440                    break;
441                }
442
443                p += 1;
444            }
445        }
446
447        p
448    }
449
450    /// Skip the rest of a quoted attribute value that needs no change after the byte at `p`.
451    #[inline]
452    fn skip_quoted_value(&self, text_bytes: &[u8], mut p: usize) -> usize {
453        // A `type` value is collected, and a LF after a CR is removed.
454        if self.in_attribute_type || text_bytes[p] == b'\r' {
455            return p;
456        }
457
458        while let Some(&e) = text_bytes.get(p + 1) {
459            if e == self.quote
460                || e == b'\r'
461                || is_ascii_control(e)
462                || (self.in_handled_attribute && is_whitespace(e))
463            {
464                break;
465            }
466
467            p += 1;
468        }
469
470        p
471    }
472
473    /// Leave raw text or preserved content after the name of its end tag.
474    #[inline]
475    fn end_raw_text(&mut self, e: u8) {
476        self.last_space = 0;
477
478        if e == b'>' {
479            self.step = Step::InitialRemainOneWhitespace;
480        } else {
481            self.in_end_tag = true;
482            self.begin_end_tag_attributes();
483        }
484    }
485
486    /// Start removing the attributes of an end tag.
487    /// The attribute steps of `PreservedStep` follow them, so that a `>` in a quoted value does not end the tag.
488    #[inline]
489    fn begin_end_tag_attributes(&mut self) {
490        self.preserved_step = PreservedStep::BeforeAttributeName;
491        self.step = Step::TagEnd;
492    }
493
494    /// Process a byte of embedded JavaScript or CSS.
495    /// The bytes that may start its end tag are held back until it is clear whether they are content.
496    fn digest_embedded(
497        &mut self,
498        e: u8,
499        out: &mut impl HTMLWriter,
500        text_bytes: &[u8],
501        start: &mut usize,
502        p: usize,
503        end_tag: &[u8],
504    ) -> Result<(), HTMLMinifierError> {
505        if is_ascii_control(e) {
506            self.feed_embedded(&text_bytes[*start..p], out)?;
507            *start = p + 1;
508
509            return Ok(());
510        }
511
512        let matched = usize::from(self.step_counter);
513
514        if self.script_data.ends(e, end_tag, &mut self.step_counter) {
515            self.finish_embedded(out)?;
516            out.push_bytes(&self.held[..matched])?;
517
518            // The whitespace after the end tag name is removed.
519            *start = if is_whitespace(e) { p + 1 } else { p };
520
521            self.end_raw_text(e);
522        } else if usize::from(self.step_counter) > matched {
523            if matched == 0 {
524                self.feed_embedded(&text_bytes[*start..p], out)?;
525            }
526
527            self.held[matched] = e;
528            *start = p + 1;
529        } else {
530            if matched > 0 {
531                let held = self.held;
532                self.feed_embedded(&held[..matched], out)?;
533            }
534
535            if self.step_counter == 1 {
536                // This `<` may start the end tag.
537                self.held[0] = e;
538                *start = p + 1;
539            } else {
540                self.normalize_line_break(e, out, text_bytes, start, p)?;
541            }
542        }
543
544        Ok(())
545    }
546
547    /// Check whether the byte before `p` is a CR, including the last byte of the previous chunk.
548    #[inline]
549    fn follows_cr(&self, text_bytes: &[u8], p: usize) -> bool {
550        if p == 0 { self.last_cr } else { text_bytes[p - 1] == b'\r' }
551    }
552
553    /// Write a CR as a LF and skip a LF that follows a CR, like the HTML input stream does.
554    #[inline]
555    fn normalize_line_break(
556        &mut self,
557        e: u8,
558        out: &mut impl HTMLWriter,
559        text_bytes: &[u8],
560        start: &mut usize,
561        p: usize,
562    ) -> Result<(), HTMLMinifierError> {
563        if e == b'\r' {
564            self.write_span(&text_bytes[*start..p], out)?;
565            self.write_span(b"\n", out)?;
566            *start = p + 1;
567        } else if e == b'\n' && self.follows_cr(text_bytes, p) {
568            self.write_span(&text_bytes[*start..p], out)?;
569            *start = p + 1;
570        }
571
572        Ok(())
573    }
574
575    /// Write the `<!` or `<!-` prefix that was held back while checking for a comment.
576    #[inline]
577    fn write_declaration_prefix(&self, out: &mut impl HTMLWriter) -> Result<(), HTMLMinifierError> {
578        match self.step_counter {
579            0 => out.push_bytes(b"<!"),
580            1 => out.push_bytes(b"<!-"),
581            _ => Ok(()),
582        }
583    }
584
585    #[inline]
586    fn push_quoted_value(
587        &mut self,
588        e: u8,
589        out: &mut impl HTMLWriter,
590        text_bytes: &[u8],
591        start: &mut usize,
592        p: usize,
593    ) -> Result<(), HTMLMinifierError> {
594        if self.quoted_value_empty {
595            self.quoted_value_empty = false;
596            out.push_bytes(&[b'=', self.quote])?;
597        } else if self.quoted_value_spacing {
598            out.push_bytes(&text_bytes[*start..p])?;
599            *start = p;
600            out.push(b' ')?;
601        }
602
603        if self.quoted_value_spacing {
604            self.push_type_byte(b' ');
605        }
606        self.push_type_byte(e);
607
608        self.quoted_value_spacing = false;
609        Ok(())
610    }
611
612    #[inline]
613    fn start_preserved(&mut self, root: &'static [u8]) {
614        self.preserved_root = root;
615        self.preserved_step = PreservedStep::Data;
616        self.preserved_depth = 1;
617        self.preserved_counter = 0;
618        self.preserved_end_tag = false;
619        self.preserved_plaintext = false;
620        self.preserved_quote = 0;
621        self.raw_end = b"";
622        self.preserved_tag.clear();
623    }
624
625    #[inline]
626    fn start_preserved_tag_name(&mut self, e: u8, end_tag: bool) {
627        self.preserved_end_tag = end_tag;
628        self.preserved_plaintext = false;
629        self.preserved_tag.clear();
630        self.preserved_tag.push(e.to_ascii_lowercase());
631        self.preserved_step = PreservedStep::TagName;
632    }
633
634    #[inline]
635    fn end_preserved_tag(&mut self) {
636        if self.preserved_plaintext {
637            self.preserved_plaintext = false;
638            self.step = Step::Plaintext;
639        }
640
641        self.preserved_step = if self.raw_end.is_empty() {
642            PreservedStep::Data
643        } else {
644            self.preserved_counter = 0;
645            PreservedStep::Raw
646        };
647    }
648
649    /// Follow the attributes of a tag, so that a `>` in a quoted value does not end the tag.
650    #[inline]
651    fn digest_tag_attribute(&mut self, e: u8) {
652        match self.preserved_step {
653            PreservedStep::BeforeAttributeName => match e {
654                b'>' => self.end_preserved_tag(),
655                _ if e == b'/' || is_whitespace(e) => (),
656                _ => self.preserved_step = PreservedStep::AttributeName,
657            },
658            PreservedStep::AttributeName => match e {
659                b'>' => self.end_preserved_tag(),
660                b'/' => self.preserved_step = PreservedStep::BeforeAttributeName,
661                b'=' => self.preserved_step = PreservedStep::BeforeAttributeValue,
662                _ if is_whitespace(e) => self.preserved_step = PreservedStep::AfterAttributeName,
663                _ => (),
664            },
665            PreservedStep::AfterAttributeName => match e {
666                b'>' => self.end_preserved_tag(),
667                b'/' => self.preserved_step = PreservedStep::BeforeAttributeName,
668                b'=' => self.preserved_step = PreservedStep::BeforeAttributeValue,
669                _ if is_whitespace(e) => (),
670                _ => self.preserved_step = PreservedStep::AttributeName,
671            },
672            PreservedStep::BeforeAttributeValue => match e {
673                b'>' => self.end_preserved_tag(),
674                b'"' | b'\'' => {
675                    self.preserved_quote = e;
676                    self.preserved_step = PreservedStep::QuotedAttributeValue;
677                },
678                _ if is_whitespace(e) => (),
679                _ => self.preserved_step = PreservedStep::UnquotedAttributeValue,
680            },
681            PreservedStep::QuotedAttributeValue => {
682                if e == self.preserved_quote {
683                    self.preserved_step = PreservedStep::BeforeAttributeName;
684                }
685            },
686            PreservedStep::UnquotedAttributeValue => match e {
687                b'>' => self.end_preserved_tag(),
688                _ if is_whitespace(e) => self.preserved_step = PreservedStep::BeforeAttributeName,
689                _ => (),
690            },
691            _ => (),
692        }
693    }
694
695    /// Follow the HTML tokenizer inside a preserved element.
696    /// Return `true` when the name of the closing tag of the preserved element ends.
697    fn digest_preserved(&mut self, e: u8) -> bool {
698        if self.foreign.is_active() {
699            self.foreign.feed(e);
700
701            if self.foreign.is_plaintext() {
702                self.step = Step::Plaintext;
703            } else if self.foreign.breakout_tag() == Some(self.preserved_root) {
704                // The HTML start tag that ends the region opens a nested element, such as `<pre>` in `<pre><svg><pre>`.
705                self.preserved_depth += 1;
706            }
707
708            return false;
709        }
710
711        match self.preserved_step {
712            PreservedStep::Data => {
713                if e == b'<' {
714                    self.preserved_step = PreservedStep::TagOpen;
715                }
716            },
717            PreservedStep::TagOpen => match e {
718                b'/' => self.preserved_step = PreservedStep::EndTagOpen,
719                b'!' => {
720                    self.preserved_counter = 0;
721                    self.preserved_step = PreservedStep::Declaration;
722                },
723                b'?' => self.preserved_step = PreservedStep::BogusComment,
724                b'<' => (),
725                _ if e.is_ascii_alphabetic() => self.start_preserved_tag_name(e, false),
726                _ => self.preserved_step = PreservedStep::Data,
727            },
728            PreservedStep::EndTagOpen => match e {
729                b'>' => self.preserved_step = PreservedStep::Data,
730                _ if e.is_ascii_alphabetic() => self.start_preserved_tag_name(e, true),
731                _ => self.preserved_step = PreservedStep::BogusComment,
732            },
733            PreservedStep::TagName => {
734                if e == b'>' || e == b'/' || is_whitespace(e) {
735                    if !self.preserved_end_tag {
736                        if matches!(self.preserved_tag.as_slice(), b"svg" | b"math") {
737                            self.foreign.start(&self.preserved_tag);
738                            self.foreign.feed(e);
739                            self.preserved_step = PreservedStep::Data;
740
741                            return false;
742                        }
743
744                        self.preserved_plaintext = self.preserved_tag == b"plaintext";
745                    }
746
747                    if self.preserved_tag == self.preserved_root {
748                        if self.preserved_end_tag {
749                            self.preserved_depth -= 1;
750                            if self.preserved_depth == 0 {
751                                return true;
752                            }
753                        } else {
754                            self.preserved_depth += 1;
755                        }
756                    }
757
758                    self.raw_end = if self.preserved_end_tag {
759                        b""
760                    } else {
761                        raw_text_end_tag(&self.preserved_tag).unwrap_or_default()
762                    };
763
764                    if self.raw_end == b"</script" {
765                        self.script_data.reset();
766                    }
767
768                    if e == b'>' {
769                        self.end_preserved_tag();
770                    } else {
771                        self.preserved_step = PreservedStep::BeforeAttributeName;
772                    }
773                } else {
774                    push_name_byte(&mut self.preserved_tag, e.to_ascii_lowercase());
775                }
776            },
777            PreservedStep::BeforeAttributeName
778            | PreservedStep::AttributeName
779            | PreservedStep::AfterAttributeName
780            | PreservedStep::BeforeAttributeValue
781            | PreservedStep::QuotedAttributeValue
782            | PreservedStep::UnquotedAttributeValue => self.digest_tag_attribute(e),
783            PreservedStep::Declaration => match e {
784                b'-' if self.preserved_counter == 0 => self.preserved_counter = 1,
785                b'-' => {
786                    self.preserved_counter = COMMENT_START;
787                    self.preserved_step = PreservedStep::Comment;
788                },
789                b'>' => self.preserved_step = PreservedStep::Data,
790                _ => self.preserved_step = PreservedStep::BogusComment,
791            },
792            PreservedStep::BogusComment => {
793                if e == b'>' {
794                    self.preserved_step = PreservedStep::Data;
795                }
796            },
797            PreservedStep::Comment => match next_comment_state(self.preserved_counter, e) {
798                Some(state) => self.preserved_counter = state,
799                None => self.preserved_step = PreservedStep::Data,
800            },
801            PreservedStep::Raw => {
802                if self.script_data.ends(e, self.raw_end, &mut self.preserved_counter) {
803                    self.raw_end = b"";
804                    self.preserved_step = if e == b'>' {
805                        PreservedStep::Data
806                    } else {
807                        PreservedStep::BeforeAttributeName
808                    };
809                }
810            },
811        }
812
813        false
814    }
815}
816
817impl HTMLMinifierHelper {
818    /// Reset this HTML minifier helper while keeping its options and allocated memory.
819    #[inline]
820    pub fn reset(&mut self) {
821        self.step = Step::default();
822        self.step_counter = 0;
823        self.buffer.clear();
824        self.tag.clear();
825        self.attribute_type.clear();
826        self.preserved_tag.clear();
827        self.preserved_root = b"";
828        self.preserved_step = PreservedStep::default();
829        self.css.reset();
830        self.js.reset(false);
831        self.script_data.reset();
832        self.foreign.reset();
833        self.preserved_depth = 0;
834        self.preserved_counter = 0;
835        self.preserved_end_tag = false;
836        self.preserved_plaintext = false;
837        self.raw_end = b"";
838        self.preserved_quote = 0;
839        self.quote = 0;
840        self.last_space = 0;
841        self.last_cr = false;
842        self.reference_pending = false;
843        self.text_space_written = false;
844        self.at_start = false;
845        self.quoted_value_spacing = false;
846        self.quoted_value_empty = false;
847        self.in_handled_attribute = false;
848        self.in_attribute_type = false;
849        self.in_end_tag = false;
850        self.type_seen = false;
851        self.type_supported = false;
852        self.type_module = false;
853    }
854
855    /// Finish the current input and report any incomplete tag or special element.
856    /// An error can leave partial output in the writer.
857    pub fn finish<W: HTMLWriter>(&mut self, out: &mut W) -> Result<(), HTMLMinifierError> {
858        match self.step {
859            Step::Initial
860            | Step::InitialRemainOneWhitespace
861            | Step::InitialIgnoreWhitespace
862            | Step::Plaintext => (),
863            Step::Foreign if self.foreign.can_finish() => (),
864            Step::StartTagInitial => out.push(b'<')?,
865            Step::EndTagInitial => out.push_bytes(b"</")?,
866            _ => return Err(HTMLMinifierError::IncompleteInput),
867        }
868
869        self.reset();
870        Ok(())
871    }
872
873    /// Input some text to generate HTML code. It is not necessary to input a full HTML text at once.
874    pub fn digest<S: AsRef<[u8]>, W: HTMLWriter>(
875        &mut self,
876        text: S,
877        out: &mut W,
878    ) -> Result<(), HTMLMinifierError> {
879        let text_bytes = text.as_ref();
880        let text_length = text_bytes.len();
881
882        let mut start = 0;
883        let mut p = 0;
884
885        while p < text_length {
886            if self.step.is_special() {
887                let next = self.skip_content(text_bytes, p);
888
889                // The bytes that are skipped in a title are not whitespace.
890                if next > p && self.in_title() {
891                    self.last_space = 0;
892                }
893
894                p = next;
895
896                if p == text_length {
897                    break;
898                }
899
900                let e = text_bytes[p];
901
902                if matches!(self.step, Step::Foreign | Step::Plaintext) {
903                    if is_ascii_control(e) {
904                        out.push_bytes(&text_bytes[start..p])?;
905                        start = p + 1;
906                    } else {
907                        self.normalize_line_break(e, out, text_bytes, &mut start, p)?;
908
909                        if self.step == Step::Foreign {
910                            // The bytes that need no change are fed in one run.
911                            p = self.foreign.feed_run(text_bytes, p);
912
913                            if self.foreign.is_plaintext() {
914                                self.step = Step::Plaintext;
915                            } else if let Some(tag) = self.foreign.breakout_tag() {
916                                // The HTML start tag that ends the region decides the next step, such as `<pre>` in `<svg><pre>`.
917                                self.tag.clear();
918                                self.tag.extend_from_slice(tag);
919
920                                self.step = self.end_start_tag_and_get_next_step(
921                                    out, text_bytes, &mut start, p,
922                                )?;
923                            } else if !self.foreign.is_active() {
924                                self.last_space = 0;
925                                self.step = Step::InitialRemainOneWhitespace;
926                            }
927                        }
928                    }
929                } else if matches!(self.step, Step::Pre | Step::Code) {
930                    if is_ascii_control(e) {
931                        out.push_bytes(&text_bytes[start..p])?;
932                        start = p + 1;
933                    } else if self.digest_preserved(e) {
934                        if is_whitespace(e) {
935                            out.push_bytes(&text_bytes[start..p])?;
936                            start = p + 1;
937                        }
938
939                        self.end_raw_text(e);
940                    } else {
941                        self.normalize_line_break(e, out, text_bytes, &mut start, p)?;
942                    }
943                } else if let Some(end_tag) = self.step.end_tag(self.raw_end) {
944                    if self.step.is_embedded() {
945                        self.digest_embedded(e, out, text_bytes, &mut start, p, end_tag)?;
946                    } else if is_ascii_control(e) {
947                        out.push_bytes(&text_bytes[start..p])?;
948                        start = p + 1;
949                    } else if self.script_data.ends(e, end_tag, &mut self.step_counter) {
950                        if is_whitespace(e) {
951                            out.push_bytes(&text_bytes[start..p])?;
952                            start = p + 1;
953                        }
954
955                        self.end_raw_text(e);
956                    } else if is_whitespace(e) && self.in_title() {
957                        if self.last_space > 0 {
958                            // Only the first whitespace of a run is kept in a title.
959                            out.push_bytes(&text_bytes[start..p])?;
960                            start = p + 1;
961                        } else {
962                            self.last_space = e;
963                            self.normalize_line_break(e, out, text_bytes, &mut start, p)?;
964                        }
965                    } else {
966                        self.last_space = 0;
967                        self.normalize_line_break(e, out, text_bytes, &mut start, p)?;
968                    }
969                }
970
971                p += 1;
972                continue;
973            }
974
975            let e = text_bytes[p];
976
977            if e <= 0x7F {
978                // ASCII
979                if is_ascii_control(e) {
980                    out.push_bytes(&text_bytes[start..p])?;
981                    start = p + 1;
982                } else {
983                    match self.step {
984                        Step::Initial => {
985                            // ?
986                            match e {
987                                b'<' => {
988                                    out.push_bytes(&text_bytes[start..p])?;
989                                    start = p + 1;
990
991                                    self.begin_tag();
992                                    self.at_start = true;
993                                    self.step = Step::StartTagInitial;
994                                },
995                                _ => {
996                                    if is_whitespace(e) {
997                                        debug_assert_eq!(start, p);
998                                        start = p + 1;
999                                        self.reference_pending = false;
1000                                    } else {
1001                                        self.last_space = 0;
1002                                        self.step = Step::InitialRemainOneWhitespace;
1003                                        p = self.skip_plain_text(text_bytes, p);
1004                                    }
1005                                },
1006                            }
1007                        },
1008                        Step::InitialRemainOneWhitespace => {
1009                            // a?
1010                            if is_whitespace(e) {
1011                                out.push_bytes(&text_bytes[start..p])?;
1012                                start = p + 1;
1013
1014                                self.last_space = e;
1015                                self.reference_pending = false;
1016
1017                                self.step = Step::InitialIgnoreWhitespace;
1018                            } else if e == b'<' {
1019                                out.push_bytes(&text_bytes[start..p])?;
1020                                start = p + 1;
1021
1022                                self.begin_tag();
1023                                self.step = Step::StartTagInitial;
1024                            } else {
1025                                self.last_space = 0;
1026
1027                                p = self.skip_plain_text(text_bytes, p);
1028                            }
1029                        },
1030                        Step::InitialIgnoreWhitespace => {
1031                            // a ?
1032                            match e {
1033                                _ if is_whitespace(e) => {
1034                                    debug_assert_eq!(start, p);
1035                                    self.reference_pending = false;
1036
1037                                    // A line feed in the whitespace makes the kept whitespace a line feed.
1038                                    loop {
1039                                        if text_bytes[p] == b'\n' && self.last_space > 0 {
1040                                            self.last_space = b'\n';
1041                                        }
1042
1043                                        match text_bytes.get(p + 1) {
1044                                            Some(&next) if is_whitespace(next) => p += 1,
1045                                            _ => break,
1046                                        }
1047                                    }
1048
1049                                    start = p + 1;
1050                                },
1051                                b'<' => {
1052                                    // This can just push ' ', but the minified HTML would be ugly
1053                                    if self.last_space == b'\n' {
1054                                        out.push(b'\n')?;
1055                                    } else if self.last_space > 0 {
1056                                        out.push(b' ')?;
1057                                    }
1058
1059                                    self.text_space_written |= self.last_space > 0;
1060                                    self.last_space = 0;
1061
1062                                    out.push_bytes(&text_bytes[start..p])?;
1063                                    start = p + 1;
1064
1065                                    self.begin_tag();
1066                                    self.step = Step::StartTagInitial;
1067                                },
1068                                _ => {
1069                                    if self.last_space == b'\n' {
1070                                        out.push(b'\n')?;
1071                                    } else if self.last_space > 0 {
1072                                        out.push(b' ')?;
1073                                    }
1074
1075                                    self.last_space = 0;
1076                                    self.step = Step::InitialRemainOneWhitespace;
1077                                    p = self.skip_plain_text(text_bytes, p);
1078                                },
1079                            }
1080                        },
1081                        Step::StartTagInitial => {
1082                            debug_assert_eq!(start, p);
1083
1084                            // <?
1085                            match e {
1086                                b'/' => {
1087                                    start = p + 1;
1088
1089                                    self.text_space_written = false;
1090
1091                                    self.in_end_tag = true;
1092                                    self.step = Step::EndTagInitial;
1093                                },
1094                                b'?' => {
1095                                    // `<?` starts a bogus comment, which ends at the first `>`.
1096                                    out.push(b'<')?;
1097
1098                                    self.reference_pending = false;
1099                                    self.text_space_written = false;
1100
1101                                    self.step_counter = 255;
1102                                    self.step = Step::Doctype;
1103                                },
1104                                b'!' => {
1105                                    // <!
1106                                    start = p + 1;
1107
1108                                    self.step_counter = 0;
1109                                    self.step = Step::Doctype;
1110                                },
1111                                _ => {
1112                                    out.push(b'<')?;
1113
1114                                    // A literal `<` before another `<` must not join the text after a removed comment or `</>`.
1115                                    self.reference_pending = e == b'<';
1116                                    self.text_space_written = false;
1117
1118                                    if is_whitespace(e) {
1119                                        out.push_bytes(&text_bytes[start..p])?;
1120                                        start = p + 1;
1121
1122                                        self.last_space = e;
1123
1124                                        self.step = Step::InitialIgnoreWhitespace;
1125                                    } else if e.is_ascii_alphabetic() {
1126                                        self.tag.clear();
1127                                        self.tag.push(e.to_ascii_lowercase());
1128
1129                                        self.step = Step::StartTag;
1130                                    } else {
1131                                        self.last_space = 0;
1132                                        self.step = Step::InitialRemainOneWhitespace;
1133                                        continue;
1134                                    }
1135                                },
1136                            }
1137                        },
1138                        Step::EndTagInitial => {
1139                            // </?
1140                            match e {
1141                                b'>' => {
1142                                    // </>
1143                                    if self.reference_pending {
1144                                        // Keep a boundary so that text on both sides cannot form a character reference or a tag.
1145                                        out.push_bytes(b"</>")?;
1146                                        self.reference_pending = false;
1147                                    }
1148
1149                                    start = p + 1;
1150
1151                                    self.last_space = 0;
1152                                    self.step = Step::InitialRemainOneWhitespace;
1153                                },
1154                                _ => {
1155                                    out.push_bytes(b"</")?;
1156
1157                                    self.reference_pending = false;
1158
1159                                    if e.is_ascii_alphabetic() {
1160                                        self.step = Step::EndTag;
1161                                    } else {
1162                                        // `</` that a letter does not follow starts a bogus comment, which ends at the first `>`.
1163                                        self.step_counter = 255;
1164                                        self.step = Step::Doctype;
1165                                        self.normalize_line_break(
1166                                            e, out, text_bytes, &mut start, p,
1167                                        )?;
1168                                    }
1169                                },
1170                            }
1171                        },
1172                        Step::StartTag => {
1173                            // <a?
1174                            if (is_whitespace(e) || matches!(e, b'/' | b'>'))
1175                                && matches!(self.tag.as_slice(), b"svg" | b"math")
1176                            {
1177                                self.foreign.start(&self.tag);
1178                                self.step = Step::Foreign;
1179
1180                                continue;
1181                            }
1182
1183                            if is_whitespace(e) {
1184                                out.push_bytes(&text_bytes[start..p])?;
1185                                start = p + 1;
1186
1187                                self.buffer.clear(); // the buffer may be used for the `type` attribute
1188
1189                                self.last_space = 0;
1190                                self.step = Step::StartTagIn;
1191                            } else {
1192                                match e {
1193                                    b'/' => self.step = Step::TagEnd,
1194                                    b'>' => {
1195                                        self.buffer.clear(); // the buffer may be used for the `type` attribute
1196
1197                                        self.step = self.end_start_tag_and_get_next_step(
1198                                            out, text_bytes, &mut start, p,
1199                                        )?;
1200                                    },
1201                                    _ => push_name_byte(&mut self.tag, e.to_ascii_lowercase()),
1202                                }
1203                            }
1204                        },
1205                        Step::StartTagIn => {
1206                            // <a ?
1207                            match e {
1208                                b'/' => {
1209                                    if self.last_space > 0 {
1210                                        out.push(b' ')?;
1211                                    }
1212
1213                                    self.step = Step::TagEnd;
1214                                },
1215                                b'>' => {
1216                                    self.step = self.end_start_tag_and_get_next_step(
1217                                        out, text_bytes, &mut start, p,
1218                                    )?;
1219                                },
1220                                _ => {
1221                                    if is_whitespace(e) {
1222                                        debug_assert_eq!(start, p);
1223
1224                                        while text_bytes
1225                                            .get(p + 1)
1226                                            .is_some_and(|&next| is_whitespace(next))
1227                                        {
1228                                            p += 1;
1229                                        }
1230
1231                                        start = p + 1;
1232                                    } else {
1233                                        out.push(b' ')?;
1234
1235                                        self.buffer.clear();
1236                                        self.buffer.push(e.to_ascii_lowercase());
1237
1238                                        self.step = Step::StartTagAttributeName;
1239                                    }
1240                                },
1241                            }
1242                        },
1243                        Step::StartTagAttributeName => {
1244                            // <a a?
1245                            if matches!(e, b'/' | b'>' | b'=') || is_whitespace(e) {
1246                                self.set_flags_by_attribute();
1247                            }
1248
1249                            match e {
1250                                b'/' => self.step = Step::TagEnd,
1251                                b'>' => {
1252                                    self.step = self.end_start_tag_and_get_next_step(
1253                                        out, text_bytes, &mut start, p,
1254                                    )?;
1255                                },
1256                                b'=' => {
1257                                    out.push_bytes(&text_bytes[start..p])?;
1258                                    start = p + 1;
1259
1260                                    self.step = Step::StartTagAttributeValueInitial;
1261                                },
1262                                _ => {
1263                                    if is_whitespace(e) {
1264                                        out.push_bytes(&text_bytes[start..p])?;
1265                                        start = p + 1;
1266
1267                                        self.step = Step::StartTagAttributeNameWaitingValue;
1268                                    } else {
1269                                        push_name_byte(&mut self.buffer, e.to_ascii_lowercase());
1270                                    }
1271                                },
1272                            }
1273                        },
1274                        Step::StartTagAttributeNameWaitingValue => {
1275                            // <a a ?
1276                            match e {
1277                                b'/' => self.step = Step::TagEnd,
1278                                b'>' => {
1279                                    self.step = self.end_start_tag_and_get_next_step(
1280                                        out, text_bytes, &mut start, p,
1281                                    )?;
1282                                },
1283                                b'=' => {
1284                                    out.push_bytes(&text_bytes[start..p])?;
1285                                    start = p + 1;
1286
1287                                    self.step = Step::StartTagAttributeValueInitial;
1288                                },
1289                                _ => {
1290                                    if is_whitespace(e) {
1291                                        debug_assert_eq!(start, p);
1292
1293                                        while text_bytes
1294                                            .get(p + 1)
1295                                            .is_some_and(|&next| is_whitespace(next))
1296                                        {
1297                                            p += 1;
1298                                        }
1299
1300                                        start = p + 1;
1301                                    } else {
1302                                        out.push(b' ')?;
1303
1304                                        self.buffer.clear();
1305                                        self.buffer.push(e.to_ascii_lowercase());
1306
1307                                        self.step = Step::StartTagAttributeName;
1308                                    }
1309                                },
1310                            }
1311                        },
1312                        Step::StartTagAttributeValueInitial => {
1313                            // <a a=?
1314                            debug_assert_eq!(start, p);
1315
1316                            match e {
1317                                b'>' => {
1318                                    self.step = self.end_start_tag_and_get_next_step(
1319                                        out, text_bytes, &mut start, p,
1320                                    )?;
1321                                },
1322                                b'"' | b'\'' => {
1323                                    self.quoted_value_spacing = false;
1324                                    self.quoted_value_empty = true;
1325
1326                                    start = p + 1;
1327
1328                                    self.quote = e;
1329                                    self.step = Step::StartTagQuotedAttributeValue;
1330                                },
1331                                _ => {
1332                                    if is_whitespace(e) {
1333                                        start = p + 1;
1334                                    } else {
1335                                        self.push_type_byte(e);
1336
1337                                        out.push(b'=')?;
1338
1339                                        self.step = Step::StartTagUnquotedAttributeValue;
1340                                    }
1341                                },
1342                            }
1343                        },
1344                        Step::StartTagQuotedAttributeValue => {
1345                            // <a a="?
1346                            // <a a='?
1347                            // NOTE: Backslashes cannot be used for escaping.
1348                            if e == self.quote {
1349                                if self.quoted_value_empty {
1350                                    start = p + 1;
1351                                }
1352
1353                                self.finish_buffer();
1354
1355                                out.push_bytes(&text_bytes[start..=p])?;
1356                                start = p + 1;
1357
1358                                self.last_space = 0;
1359                                self.step = Step::StartTagIn;
1360                            } else if self.in_handled_attribute && is_whitespace(e) {
1361                                if self.quoted_value_empty {
1362                                    start = p + 1;
1363                                } else if self.quoted_value_spacing {
1364                                    debug_assert_eq!(start, p);
1365                                    start = p + 1;
1366                                } else {
1367                                    out.push_bytes(&text_bytes[start..p])?;
1368                                    start = p + 1;
1369
1370                                    self.quoted_value_spacing = true;
1371                                    self.quoted_value_empty = false;
1372                                }
1373                            } else {
1374                                self.push_quoted_value(e, out, text_bytes, &mut start, p)?;
1375                                self.normalize_line_break(e, out, text_bytes, &mut start, p)?;
1376
1377                                p = self.skip_quoted_value(text_bytes, p);
1378                            }
1379                        },
1380                        Step::StartTagUnquotedAttributeValue => {
1381                            // <a a=v?
1382                            // <a a=v?
1383                            match e {
1384                                b'>' => {
1385                                    self.finish_buffer();
1386                                    self.step = self.end_start_tag_and_get_next_step(
1387                                        out, text_bytes, &mut start, p,
1388                                    )?;
1389                                },
1390                                _ => {
1391                                    if is_whitespace(e) {
1392                                        self.finish_buffer();
1393
1394                                        out.push_bytes(&text_bytes[start..p])?;
1395                                        start = p + 1;
1396
1397                                        self.last_space = e;
1398                                        self.step = Step::StartTagIn;
1399                                    } else {
1400                                        self.push_type_byte(e);
1401                                    }
1402                                },
1403                            }
1404                        },
1405                        Step::EndTag => {
1406                            // </a?
1407                            if is_whitespace(e) {
1408                                out.push_bytes(&text_bytes[start..p])?;
1409                                start = p + 1;
1410
1411                                self.begin_end_tag_attributes();
1412                            } else if e == b'>' {
1413                                self.last_space = 0;
1414                                self.step = Step::InitialRemainOneWhitespace;
1415                            }
1416                        },
1417                        Step::TagEnd => {
1418                            // <a/?
1419                            // </a ?
1420                            // A `>` in a quoted value of an end tag does not end the tag.
1421                            let quoted = self.in_end_tag
1422                                && self.preserved_step == PreservedStep::QuotedAttributeValue;
1423
1424                            match e {
1425                                b'>' if !quoted => {
1426                                    if self.in_end_tag {
1427                                        self.last_space = 0;
1428                                        self.step = Step::InitialRemainOneWhitespace;
1429                                    } else {
1430                                        self.step = self.end_start_tag_and_get_next_step(
1431                                            out, text_bytes, &mut start, p,
1432                                        )?;
1433                                    }
1434                                },
1435                                _ => {
1436                                    out.push_bytes(&text_bytes[start..p])?;
1437
1438                                    if self.in_end_tag {
1439                                        start = p + 1;
1440                                        self.digest_tag_attribute(e);
1441                                    } else {
1442                                        // A `/` that `>` does not follow is ignored, so attributes can follow it.
1443                                        start = p;
1444
1445                                        self.last_space = 0;
1446                                        self.step = Step::StartTagIn;
1447                                        continue;
1448                                    }
1449                                },
1450                            }
1451                        },
1452                        Step::Doctype => {
1453                            // <!?
1454                            if e == b'>' {
1455                                self.write_declaration_prefix(out)?;
1456
1457                                self.reference_pending = false;
1458                                self.text_space_written = false;
1459
1460                                self.last_space = 0;
1461                                self.step = Step::InitialRemainOneWhitespace;
1462                            } else {
1463                                match self.step_counter {
1464                                    0 => match e {
1465                                        b'-' => {
1466                                            start = p + 1;
1467
1468                                            self.step_counter = 1;
1469                                        },
1470                                        _ => {
1471                                            out.push_bytes(b"<!")?;
1472
1473                                            self.reference_pending = false;
1474                                            self.text_space_written = false;
1475
1476                                            self.step_counter = 255;
1477                                        },
1478                                    },
1479                                    1 => match e {
1480                                        b'-' => {
1481                                            if !self.remove_comments {
1482                                                out.push_bytes(b"<!--")?;
1483                                                self.text_space_written = false;
1484                                            } else if self.reference_pending {
1485                                                // Keep a boundary so that text on both sides cannot form a character reference or a tag.
1486                                                out.push_bytes(b"<!---->")?;
1487                                            }
1488
1489                                            self.reference_pending = false;
1490
1491                                            start = p + 1;
1492
1493                                            self.step_counter = COMMENT_START;
1494                                            self.step = Step::Comment;
1495                                        },
1496                                        _ => {
1497                                            out.push_bytes(b"<!-")?;
1498
1499                                            self.reference_pending = false;
1500                                            self.text_space_written = false;
1501
1502                                            self.step_counter = 255;
1503                                        },
1504                                    },
1505                                    255 => (),
1506                                    _ => unreachable!(),
1507                                }
1508
1509                                if self.step_counter == 255 {
1510                                    self.normalize_line_break(e, out, text_bytes, &mut start, p)?;
1511                                }
1512                            }
1513                        },
1514                        Step::Comment => {
1515                            // <!--?
1516                            if self.remove_comments {
1517                                debug_assert_eq!(start, p);
1518                                start = p + 1;
1519                            } else {
1520                                self.normalize_line_break(e, out, text_bytes, &mut start, p)?;
1521                            }
1522
1523                            match next_comment_state(self.step_counter, e) {
1524                                Some(state) => self.step_counter = state,
1525                                None => {
1526                                    self.step = if self.remove_comments && self.at_start {
1527                                        // The whitespace after a removed comment at the start of the document is removed too.
1528                                        Step::Initial
1529                                    } else if self.remove_comments && self.text_space_written {
1530                                        // The whitespace before a removed comment is already written, so the whitespace after it is skipped.
1531                                        Step::InitialIgnoreWhitespace
1532                                    } else {
1533                                        Step::InitialRemainOneWhitespace
1534                                    };
1535                                    self.last_space = 0;
1536                                },
1537                            }
1538                        },
1539                        Step::ScriptDefault
1540                        | Step::ScriptJavaScript
1541                        | Step::StyleDefault
1542                        | Step::StyleCSS
1543                        | Step::Pre
1544                        | Step::Code
1545                        | Step::Textarea
1546                        | Step::RawText
1547                        | Step::Foreign
1548                        | Step::Plaintext => unreachable!(),
1549                    }
1550                }
1551            } else {
1552                // non-ASCII
1553                match self.step {
1554                    Step::Initial => {
1555                        // ?
1556                        self.last_space = 0;
1557                        self.step = Step::InitialRemainOneWhitespace;
1558                        p = self.skip_plain_text(text_bytes, p);
1559                    },
1560                    Step::InitialRemainOneWhitespace => {
1561                        // a?
1562                        self.last_space = 0;
1563
1564                        p = self.skip_plain_text(text_bytes, p);
1565                    },
1566                    Step::InitialIgnoreWhitespace => {
1567                        // a ?
1568                        if self.last_space == b'\n' {
1569                            out.push(b'\n')?;
1570                        } else if self.last_space > 0 {
1571                            out.push(b' ')?;
1572                        }
1573
1574                        self.last_space = 0;
1575                        self.step = Step::InitialRemainOneWhitespace;
1576                        p = self.skip_plain_text(text_bytes, p);
1577                    },
1578                    Step::StartTagInitial => {
1579                        // <?
1580                        // To `InitialRemainOneWhitespace`.
1581                        debug_assert_eq!(start, p);
1582
1583                        out.push(b'<')?;
1584
1585                        self.reference_pending = false;
1586                        self.text_space_written = false;
1587
1588                        self.last_space = 0;
1589                        self.step = Step::InitialRemainOneWhitespace;
1590                    },
1591                    Step::EndTagInitial => {
1592                        // </?
1593                        // `</` that a letter does not follow starts a bogus comment.
1594                        out.push_bytes(b"</")?;
1595
1596                        self.reference_pending = false;
1597
1598                        self.step_counter = 255;
1599                        self.step = Step::Doctype;
1600                    },
1601                    Step::StartTag => push_name_byte(&mut self.tag, e),
1602                    Step::EndTag => (),
1603                    Step::StartTagIn => {
1604                        // <a ?
1605                        out.push(b' ')?;
1606
1607                        self.buffer.clear();
1608                        self.buffer.push(e);
1609
1610                        self.step = Step::StartTagAttributeName;
1611                    },
1612                    Step::StartTagAttributeName => {
1613                        // <a a?
1614                        push_name_byte(&mut self.buffer, e);
1615                    },
1616                    Step::StartTagAttributeNameWaitingValue => {
1617                        // <a a ?
1618                        out.push(b' ')?;
1619
1620                        self.buffer.clear();
1621                        self.buffer.push(e);
1622
1623                        self.step = Step::StartTagAttributeName;
1624                    },
1625                    Step::StartTagAttributeValueInitial => {
1626                        // <a a=?
1627                        debug_assert_eq!(start, p);
1628
1629                        self.push_type_byte(e);
1630
1631                        out.push(b'=')?;
1632
1633                        self.step = Step::StartTagUnquotedAttributeValue;
1634                    },
1635                    Step::StartTagQuotedAttributeValue => {
1636                        self.push_quoted_value(e, out, text_bytes, &mut start, p)?;
1637
1638                        p = self.skip_quoted_value(text_bytes, p);
1639                    },
1640                    Step::StartTagUnquotedAttributeValue => {
1641                        // <a a=v?
1642                        // <a a=v?
1643                        self.push_type_byte(e);
1644                    },
1645                    Step::TagEnd => {
1646                        // <a/?
1647                        // </a ?
1648                        out.push_bytes(&text_bytes[start..p])?;
1649
1650                        if self.in_end_tag {
1651                            start = p + 1;
1652                            self.digest_tag_attribute(e);
1653                        } else {
1654                            start = p;
1655
1656                            self.last_space = 0;
1657                            self.step = Step::StartTagIn;
1658                            continue;
1659                        }
1660                    },
1661                    Step::Doctype => {
1662                        // <!?
1663                        self.write_declaration_prefix(out)?;
1664
1665                        self.reference_pending = false;
1666                        self.text_space_written = false;
1667
1668                        self.step_counter = 255;
1669                    },
1670                    Step::Comment => {
1671                        // <!--?
1672                        if self.remove_comments {
1673                            debug_assert_eq!(start, p);
1674                            start = p + 1;
1675                        }
1676
1677                        self.step_counter = 0;
1678                    },
1679                    Step::ScriptDefault
1680                    | Step::StyleDefault
1681                    | Step::Pre
1682                    | Step::Code
1683                    | Step::Textarea
1684                    | Step::ScriptJavaScript
1685                    | Step::StyleCSS
1686                    | Step::RawText
1687                    | Step::Foreign
1688                    | Step::Plaintext => unreachable!(),
1689                }
1690            }
1691
1692            p += 1;
1693        }
1694
1695        self.write_span(&text_bytes[start..p], out)?;
1696
1697        if let Some(&last) = text_bytes.last() {
1698            self.last_cr = last == b'\r';
1699        }
1700
1701        Ok(())
1702    }
1703}