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
10const MAX_TYPE_LENGTH: usize = 256;
13
14const MAX_NAME_LENGTH: usize = 16;
17
18fn embedded_type(tag: &[u8], value: &[u8]) -> Option<bool> {
21 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 #[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#[inline]
120const fn is_plain_text(e: u8) -> bool {
121 !(is_whitespace(e) || matches!(e, b'<' | b'&') || is_ascii_control(e))
122}
123
124#[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#[derive(Educe, Clone)]
154#[educe(Debug, Default(new))]
155pub struct HTMLMinifierHelper {
156 #[educe(Default = true)]
157 pub remove_comments: bool,
159 #[educe(Default = true)]
160 pub minify_code: bool,
162 #[educe(Default = true)]
163 pub minify_js: bool,
165 #[educe(Default = true)]
166 pub minify_css: bool,
168
169 #[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 css: CSSMinifier,
183 js: JSMinifier,
184 script_data: ScriptData,
185 foreign: ForeignContent,
186
187 step: Step,
189 step_counter: u8,
190 #[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 quote: u8,
204 last_space: u8,
205 last_cr: bool,
206
207 reference_pending: bool,
210 text_space_written: bool,
212 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 #[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 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 #[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 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 #[inline]
403 fn in_title(&self) -> bool {
404 self.step == Step::RawText && self.raw_end == b"</title"
405 }
406
407 #[inline]
409 fn skip_content(&self, text_bytes: &[u8], mut p: usize) -> usize {
410 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 #[inline]
452 fn skip_quoted_value(&self, text_bytes: &[u8], mut p: usize) -> usize {
453 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 #[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 #[inline]
489 fn begin_end_tag_attributes(&mut self) {
490 self.preserved_step = PreservedStep::BeforeAttributeName;
491 self.step = Step::TagEnd;
492 }
493
494 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 *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 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 #[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 #[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 #[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 #[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 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 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 #[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 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 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 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 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 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 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 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 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 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 match e {
1033 _ if is_whitespace(e) => {
1034 debug_assert_eq!(start, p);
1035 self.reference_pending = false;
1036
1037 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 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 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 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 start = p + 1;
1107
1108 self.step_counter = 0;
1109 self.step = Step::Doctype;
1110 },
1111 _ => {
1112 out.push(b'<')?;
1113
1114 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 match e {
1141 b'>' => {
1142 if self.reference_pending {
1144 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 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 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(); 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(); 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 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 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 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 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 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 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 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 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 start = p;
1444
1445 self.last_space = 0;
1446 self.step = Step::StartTagIn;
1447 continue;
1448 }
1449 },
1450 }
1451 },
1452 Step::Doctype => {
1453 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 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 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 Step::Initial
1529 } else if self.remove_comments && self.text_space_written {
1530 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 match self.step {
1554 Step::Initial => {
1555 self.last_space = 0;
1557 self.step = Step::InitialRemainOneWhitespace;
1558 p = self.skip_plain_text(text_bytes, p);
1559 },
1560 Step::InitialRemainOneWhitespace => {
1561 self.last_space = 0;
1563
1564 p = self.skip_plain_text(text_bytes, p);
1565 },
1566 Step::InitialIgnoreWhitespace => {
1567 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 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 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 out.push(b' ')?;
1606
1607 self.buffer.clear();
1608 self.buffer.push(e);
1609
1610 self.step = Step::StartTagAttributeName;
1611 },
1612 Step::StartTagAttributeName => {
1613 push_name_byte(&mut self.buffer, e);
1615 },
1616 Step::StartTagAttributeNameWaitingValue => {
1617 out.push(b' ')?;
1619
1620 self.buffer.clear();
1621 self.buffer.push(e);
1622
1623 self.step = Step::StartTagAttributeName;
1624 },
1625 Step::StartTagAttributeValueInitial => {
1626 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 self.push_type_byte(e);
1644 },
1645 Step::TagEnd => {
1646 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 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 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}