1#![deny(clippy::all)]
2#![allow(clippy::needless_update)]
3#![allow(clippy::match_like_matches_macro)]
4#![allow(non_local_definitions)]
5
6pub use std::fmt::Result;
7use std::{borrow::Cow, iter::Peekable, str::Chars};
8
9use swc_atoms::Atom;
10use swc_common::Spanned;
11use swc_html_ast::*;
12use swc_html_codegen_macros::emitter;
13use swc_html_utils::HTML_ENTITIES;
14use writer::HtmlWriter;
15
16pub use self::emit::*;
17use self::{ctx::Ctx, list::ListFormat};
18
19#[macro_use]
20mod macros;
21mod ctx;
22mod emit;
23mod list;
24pub mod writer;
25
26#[derive(Debug, Clone, Default)]
27pub struct CodegenConfig<'a> {
28 pub minify: bool,
29 pub scripting_enabled: bool,
30 pub context_element: Option<&'a Element>,
32 pub tag_omission: Option<bool>,
35 pub self_closing_void_elements: Option<bool>,
39 pub quotes: Option<bool>,
42}
43
44enum TagOmissionParent<'a> {
45 Document(&'a Document),
46 DocumentFragment(&'a DocumentFragment),
47 Element(&'a Element),
48}
49
50#[derive(Debug)]
51pub struct CodeGenerator<'a, W>
52where
53 W: HtmlWriter,
54{
55 wr: W,
56 config: CodegenConfig<'a>,
57 ctx: Ctx,
58 is_plaintext: bool,
60 tag_omission: bool,
61 self_closing_void_elements: bool,
62 quotes: bool,
63}
64
65impl<'a, W> CodeGenerator<'a, W>
66where
67 W: HtmlWriter,
68{
69 pub fn new(wr: W, config: CodegenConfig<'a>) -> Self {
70 let tag_omission = config.tag_omission.unwrap_or(config.minify);
71 let self_closing_void_elements = config.tag_omission.unwrap_or(!config.minify);
72 let quotes = config.quotes.unwrap_or(!config.minify);
73
74 CodeGenerator {
75 wr,
76 config,
77 ctx: Default::default(),
78 is_plaintext: false,
79 tag_omission,
80 self_closing_void_elements,
81 quotes,
82 }
83 }
84
85 #[emitter]
86 fn emit_document(&mut self, n: &Document) -> Result {
87 if self.tag_omission {
88 self.emit_list_for_tag_omission(TagOmissionParent::Document(n))?;
89 } else {
90 self.emit_list(&n.children, ListFormat::NotDelimited)?;
91 }
92 }
93
94 #[emitter]
95 fn emit_document_fragment(&mut self, n: &DocumentFragment) -> Result {
96 let ctx = if let Some(context_element) = &self.config.context_element {
97 self.create_context_for_element(context_element)
98 } else {
99 Default::default()
100 };
101
102 if self.tag_omission {
103 self.with_ctx(ctx)
104 .emit_list_for_tag_omission(TagOmissionParent::DocumentFragment(n))?;
105 } else {
106 self.with_ctx(ctx)
107 .emit_list(&n.children, ListFormat::NotDelimited)?;
108 }
109 }
110
111 #[emitter]
112 fn emit_child(&mut self, n: &Child) -> Result {
113 match n {
114 Child::DocumentType(n) => emit!(self, n),
115 Child::Element(n) => emit!(self, n),
116 Child::Text(n) => emit!(self, n),
117 Child::Comment(n) => emit!(self, n),
118 }
119 }
120
121 #[emitter]
122 fn emit_document_doctype(&mut self, n: &DocumentType) -> Result {
123 let mut doctype = String::with_capacity(
124 10 + if let Some(name) = &n.name {
125 name.len() + 1
126 } else {
127 0
128 } + if let Some(public_id) = &n.public_id {
129 let mut len = public_id.len() + 10;
130
131 if let Some(system_id) = &n.system_id {
132 len += system_id.len() + 3
133 }
134
135 len
136 } else if let Some(system_id) = &n.system_id {
137 system_id.len() + 10
138 } else {
139 0
140 },
141 );
142
143 doctype.push('<');
144 doctype.push('!');
145
146 if self.config.minify {
147 doctype.push_str("doctype");
148 } else {
149 doctype.push_str("DOCTYPE");
150 }
151
152 if let Some(name) = &n.name {
153 doctype.push(' ');
154 doctype.push_str(name);
155 }
156
157 if let Some(public_id) = &n.public_id {
158 doctype.push(' ');
159
160 if self.config.minify {
161 doctype.push_str("public");
162 } else {
163 doctype.push_str("PUBLIC");
164 }
165
166 doctype.push(' ');
167
168 let public_id_quote = if public_id.contains('"') { '\'' } else { '"' };
169
170 doctype.push(public_id_quote);
171 doctype.push_str(public_id);
172 doctype.push(public_id_quote);
173
174 if let Some(system_id) = &n.system_id {
175 doctype.push(' ');
176
177 let system_id_quote = if system_id.contains('"') { '\'' } else { '"' };
178
179 doctype.push(system_id_quote);
180 doctype.push_str(system_id);
181 doctype.push(system_id_quote);
182 }
183 } else if let Some(system_id) = &n.system_id {
184 doctype.push(' ');
185
186 if self.config.minify {
187 doctype.push_str("system");
188 } else {
189 doctype.push_str("SYSTEM");
190 }
191
192 doctype.push(' ');
193
194 let system_id_quote = if system_id.contains('"') { '\'' } else { '"' };
195
196 doctype.push(system_id_quote);
197 doctype.push_str(system_id);
198 doctype.push(system_id_quote);
199 }
200
201 doctype.push('>');
202
203 write_multiline_raw!(self, n.span, &doctype);
204 formatting_newline!(self);
205 }
206
207 fn basic_emit_element(
208 &mut self,
209 n: &Element,
210 parent: Option<&Element>,
211 prev: Option<&Child>,
212 next: Option<&Child>,
213 ) -> Result {
214 if self.is_plaintext {
215 return Ok(());
216 }
217
218 let has_attributes = !n.attributes.is_empty();
219 let can_omit_start_tag = self.tag_omission
220 && !has_attributes
221 && n.namespace == Namespace::HTML
222 && match &*n.tag_name {
223 "html" if !matches!(n.children.first(), Some(Child::Comment(..))) => true,
227 "head"
230 if n.children.is_empty()
231 || matches!(n.children.first(), Some(Child::Element(..))) =>
232 {
233 true
234 }
235 "body"
239 if n.children.is_empty()
240 || (match n.children.first() {
241 Some(Child::Text(text))
242 if !text.data.is_empty()
243 && text.data.chars().next().unwrap().is_ascii_whitespace() =>
244 {
245 false
246 }
247 Some(Child::Comment(..)) => false,
248 Some(Child::Element(Element {
249 namespace,
250 tag_name,
251 ..
252 })) if *namespace == Namespace::HTML
253 && matches!(
254 &**tag_name,
255 "base"
256 | "basefont"
257 | "bgsound"
258 | "frameset"
259 | "link"
260 | "meta"
261 | "noframes"
262 | "noscript"
263 | "script"
264 | "style"
265 | "template"
266 | "title"
267 ) =>
268 {
269 false
270 }
271 _ => true,
272 }) =>
273 {
274 true
275 }
276 "colgroup"
281 if match n.children.first() {
282 Some(Child::Element(element))
283 if element.namespace == Namespace::HTML
284 && element.tag_name == "col" =>
285 {
286 !matches!(prev, Some(Child::Element(element)) if element.namespace == Namespace::HTML
287 && element.tag_name == "colgroup")
288 }
289 _ => false,
290 } =>
291 {
292 true
293 }
294 "tbody"
299 if match n.children.first() {
300 Some(Child::Element(element))
301 if element.namespace == Namespace::HTML && element.tag_name == "tr" =>
302 {
303 !matches!(prev, Some(Child::Element(element)) if element.namespace == Namespace::HTML
304 && matches!(
305 &*element.tag_name,
306 "tbody" | "thead" | "tfoot"
307 ))
308 }
309 _ => false,
310 } =>
311 {
312 true
313 }
314 _ => false,
315 };
316
317 let is_void_element = match n.namespace {
318 Namespace::HTML => matches!(
319 &*n.tag_name,
320 "area"
321 | "base"
322 | "basefont"
323 | "bgsound"
324 | "br"
325 | "col"
326 | "embed"
327 | "frame"
328 | "hr"
329 | "img"
330 | "input"
331 | "keygen"
332 | "link"
333 | "meta"
334 | "param"
335 | "source"
336 | "track"
337 | "wbr"
338 ),
339 Namespace::SVG => n.children.is_empty(),
340 Namespace::MATHML => n.children.is_empty(),
341 _ => false,
342 };
343
344 if !can_omit_start_tag {
345 write_raw!(self, "<");
346 write_raw!(self, &n.tag_name);
347
348 if has_attributes {
349 space!(self);
350
351 self.emit_list(&n.attributes, ListFormat::SpaceDelimited)?;
352 }
353
354 if (matches!(n.namespace, Namespace::SVG | Namespace::MATHML) && is_void_element)
355 || (self.self_closing_void_elements
356 && n.is_self_closing
357 && is_void_element
358 && matches!(n.namespace, Namespace::HTML))
359 {
360 if self.config.minify {
361 let need_space = match n.attributes.last() {
362 Some(Attribute {
363 value: Some(value), ..
364 }) => !value.chars().any(|c| match c {
365 c if c.is_ascii_whitespace() => true,
366 '`' | '=' | '<' | '>' | '"' | '\'' => true,
367 _ => false,
368 }),
369 _ => false,
370 };
371
372 if need_space {
373 write_raw!(self, " ");
374 }
375 } else {
376 write_raw!(self, " ");
377 }
378
379 write_raw!(self, "/");
380 }
381
382 write_raw!(self, ">");
383
384 if !self.config.minify && n.namespace == Namespace::HTML && n.tag_name == "html" {
385 newline!(self);
386 }
387 }
388
389 if is_void_element {
390 return Ok(());
391 }
392
393 if !self.is_plaintext {
394 self.is_plaintext = matches!(&*n.tag_name, "plaintext");
395 }
396
397 if let Some(content) = &n.content {
398 emit!(self, content);
399 } else if !n.children.is_empty() {
400 let ctx = self.create_context_for_element(n);
401
402 let need_extra_newline =
403 n.namespace == Namespace::HTML && matches!(&*n.tag_name, "textarea" | "pre");
404
405 if need_extra_newline {
406 if let Some(Child::Text(Text { data, .. })) = &n.children.first() {
407 if data.contains('\n') {
408 newline!(self);
409 } else {
410 formatting_newline!(self);
411 }
412 }
413 }
414
415 if self.tag_omission {
416 self.with_ctx(ctx)
417 .emit_list_for_tag_omission(TagOmissionParent::Element(n))?;
418 } else {
419 self.with_ctx(ctx)
420 .emit_list(&n.children, ListFormat::NotDelimited)?;
421 }
422 }
423
424 let can_omit_end_tag = self.is_plaintext
425 || (self.tag_omission
426 && n.namespace == Namespace::HTML
427 && match &*n.tag_name {
428 "html" | "body" => !matches!(next, Some(Child::Comment(..))),
436 "head" => match next {
439 Some(Child::Text(text))
440 if text.data.chars().next().unwrap().is_ascii_whitespace() =>
441 {
442 false
443 }
444 Some(Child::Comment(..)) => false,
445 _ => true,
446 },
447 "p" => match next {
455 Some(Child::Element(Element {
456 namespace,
457 tag_name,
458 ..
459 })) if *namespace == Namespace::HTML
460 && matches!(
461 &**tag_name,
462 "address"
463 | "article"
464 | "aside"
465 | "blockquote"
466 | "details"
467 | "div"
468 | "dl"
469 | "fieldset"
470 | "figcaption"
471 | "figure"
472 | "footer"
473 | "form"
474 | "h1"
475 | "h2"
476 | "h3"
477 | "h4"
478 | "h5"
479 | "h6"
480 | "header"
481 | "hgroup"
482 | "hr"
483 | "main"
484 | "menu"
485 | "nav"
486 | "ol"
487 | "p"
488 | "pre"
489 | "section"
490 | "table"
491 | "ul"
492 ) =>
493 {
494 true
495 }
496 None if match parent {
497 Some(Element {
498 namespace,
499 tag_name,
500 ..
501 }) if is_html_tag_name(*namespace, tag_name)
502 && !matches!(
503 &**tag_name,
504 "a" | "audio"
505 | "acronym"
506 | "big"
507 | "del"
508 | "font"
509 | "ins"
510 | "tt"
511 | "strike"
512 | "map"
513 | "noscript"
514 | "video"
515 | "kbd"
516 | "rbc"
517 ) =>
518 {
519 true
520 }
521 _ => false,
522 } =>
523 {
524 true
525 }
526 _ => false,
527 },
528 "li" if match parent {
532 Some(Element {
533 namespace,
534 tag_name,
535 ..
536 }) if *namespace == Namespace::HTML
537 && matches!(&**tag_name, "ul" | "ol" | "menu") =>
538 {
539 true
540 }
541 _ => false,
542 } =>
543 {
544 match next {
545 Some(Child::Element(Element {
546 namespace,
547 tag_name,
548 ..
549 })) if *namespace == Namespace::HTML && *tag_name == "li" => true,
550 None => true,
551 _ => false,
552 }
553 }
554 "dt" => match next {
557 Some(Child::Element(Element {
558 namespace,
559 tag_name,
560 ..
561 })) if *namespace == Namespace::HTML
562 && (*tag_name == "dt" || *tag_name == "dd") =>
563 {
564 true
565 }
566 _ => false,
567 },
568 "dd" => match next {
572 Some(Child::Element(Element {
573 namespace,
574 tag_name,
575 ..
576 })) if *namespace == Namespace::HTML
577 && (*tag_name == "dd" || *tag_name == "dt") =>
578 {
579 true
580 }
581 None => true,
582 _ => false,
583 },
584 "rt" | "rp" => match next {
592 Some(Child::Element(Element {
593 namespace,
594 tag_name,
595 ..
596 })) if *namespace == Namespace::HTML
597 && (*tag_name == "rt" || *tag_name == "rp") =>
598 {
599 true
600 }
601 None => true,
602 _ => false,
603 },
604 "rb" => match next {
608 Some(Child::Element(Element {
609 namespace,
610 tag_name,
611 ..
612 })) if *namespace == Namespace::HTML
613 && (*tag_name == "rt"
614 || *tag_name == "rtc"
615 || *tag_name == "rp"
616 || *tag_name == "rb") =>
617 {
618 true
619 }
620 None => true,
621 _ => false,
622 },
623 "rtc" => match next {
627 Some(Child::Element(Element {
628 namespace,
629 tag_name,
630 ..
631 })) if *namespace == Namespace::HTML
632 && (*tag_name == "rb" || *tag_name == "rtc" || *tag_name == "rt") =>
633 {
634 true
635 }
636 None => true,
637 _ => false,
638 },
639 "optgroup" => match next {
643 Some(Child::Element(Element {
644 namespace,
645 tag_name,
646 ..
647 })) if *namespace == Namespace::HTML && *tag_name == "optgroup" => true,
648 None => true,
649 _ => false,
650 },
651 "option" => match next {
656 Some(Child::Element(Element {
657 namespace,
658 tag_name,
659 ..
660 })) if *namespace == Namespace::HTML
661 && (*tag_name == "option" || *tag_name == "optgroup") =>
662 {
663 true
664 }
665 None => true,
666 _ => false,
667 },
668 "caption" | "colgroup" => match next {
674 Some(Child::Text(text))
675 if text.data.chars().next().unwrap().is_ascii_whitespace() =>
676 {
677 false
678 }
679 Some(Child::Comment(..)) => false,
680 _ => true,
681 },
682 "tbody" => match next {
686 Some(Child::Element(Element {
687 namespace,
688 tag_name,
689 ..
690 })) if *namespace == Namespace::HTML
691 && (*tag_name == "tbody" || *tag_name == "tfoot") =>
692 {
693 true
694 }
695 None => true,
696 _ => false,
697 },
698 "thead" => match next {
701 Some(Child::Element(Element {
702 namespace,
703 tag_name,
704 ..
705 })) if *namespace == Namespace::HTML
706 && (*tag_name == "tbody" || *tag_name == "tfoot") =>
707 {
708 true
709 }
710 _ => false,
711 },
712 "tfoot" => next.is_none(),
715 "tr" => match next {
719 Some(Child::Element(Element {
720 namespace,
721 tag_name,
722 ..
723 })) if *namespace == Namespace::HTML && *tag_name == "tr" => true,
724 None => true,
725 _ => false,
726 },
727 "td" | "th" => match next {
731 Some(Child::Element(Element {
732 namespace,
733 tag_name,
734 ..
735 })) if *namespace == Namespace::HTML
736 && (*tag_name == "td" || *tag_name == "th") =>
737 {
738 true
739 }
740 None => true,
741 _ => false,
742 },
743 _ => false,
744 });
745
746 if can_omit_end_tag {
747 return Ok(());
748 }
749
750 write_raw!(self, "<");
751 write_raw!(self, "/");
752 write_raw!(self, &n.tag_name);
753 write_raw!(self, ">");
754
755 Ok(())
756 }
757
758 #[emitter]
759 fn emit_element(&mut self, n: &Element) -> Result {
760 self.basic_emit_element(n, None, None, None)?;
761 }
762
763 #[emitter]
764 fn emit_attribute(&mut self, n: &Attribute) -> Result {
765 let mut attribute = String::with_capacity(
766 if let Some(prefix) = &n.prefix {
767 prefix.len() + 1
768 } else {
769 0
770 } + n.name.len()
771 + if let Some(value) = &n.value {
772 value.len() + 1
773 } else {
774 0
775 },
776 );
777
778 if let Some(prefix) = &n.prefix {
779 attribute.push_str(prefix);
780 attribute.push(':');
781 }
782
783 attribute.push_str(&n.name);
784
785 if let Some(value) = &n.value {
786 attribute.push('=');
787
788 if self.config.minify {
789 let (minifier, quote) = minify_attribute_value(value, self.quotes);
790
791 if let Some(quote) = quote {
792 attribute.push(quote);
793 }
794
795 attribute.push_str(&minifier);
796
797 if let Some(quote) = quote {
798 attribute.push(quote);
799 }
800 } else {
801 let normalized = escape_string(value, true);
802
803 attribute.push('"');
804 attribute.push_str(&normalized);
805 attribute.push('"');
806 }
807 }
808
809 write_multiline_raw!(self, n.span, &attribute);
810 }
811
812 #[emitter]
813 fn emit_text(&mut self, n: &Text) -> Result {
814 if self.ctx.need_escape_text {
815 if self.config.minify {
816 write_multiline_raw!(self, n.span, &minify_text(&n.data));
817 } else {
818 write_multiline_raw!(self, n.span, &escape_string(&n.data, false));
819 }
820 } else {
821 write_multiline_raw!(self, n.span, &n.data);
822 }
823 }
824
825 #[emitter]
826 fn emit_comment(&mut self, n: &Comment) -> Result {
827 let mut comment = String::with_capacity(n.data.len() + 7);
828
829 comment.push_str("<!--");
830 comment.push_str(&n.data);
831 comment.push_str("-->");
832
833 write_multiline_raw!(self, n.span, &comment);
834 }
835
836 fn create_context_for_element(&self, n: &Element) -> Ctx {
837 let need_escape_text = match &*n.tag_name {
838 "style" | "script" | "xmp" | "iframe" | "noembed" | "noframes" | "plaintext" => false,
839 "noscript" => !self.config.scripting_enabled,
840 _ if self.is_plaintext => false,
841 _ => true,
842 };
843
844 Ctx {
845 need_escape_text,
846 ..self.ctx
847 }
848 }
849
850 fn emit_list_for_tag_omission(&mut self, parent: TagOmissionParent) -> Result {
851 let nodes = match &parent {
852 TagOmissionParent::Document(document) => &document.children,
853 TagOmissionParent::DocumentFragment(document_fragment) => &document_fragment.children,
854 TagOmissionParent::Element(element) => &element.children,
855 };
856 let parent = match parent {
857 TagOmissionParent::Element(element) => Some(element),
858 _ => None,
859 };
860
861 for (idx, node) in nodes.iter().enumerate() {
862 match node {
863 Child::Element(element) => {
864 let prev = if idx > 0 { nodes.get(idx - 1) } else { None };
865 let next = nodes.get(idx + 1);
866
867 self.basic_emit_element(element, parent, prev, next)?;
868 }
869 _ => {
870 emit!(self, node)
871 }
872 }
873 }
874
875 Ok(())
876 }
877
878 fn emit_list<N>(&mut self, nodes: &[N], format: ListFormat) -> Result
879 where
880 Self: Emit<N>,
881 N: Spanned,
882 {
883 for (idx, node) in nodes.iter().enumerate() {
884 if idx != 0 {
885 self.write_delim(format)?;
886
887 if format & ListFormat::LinesMask == ListFormat::MultiLine {
888 formatting_newline!(self);
889 }
890 }
891
892 emit!(self, node)
893 }
894
895 Ok(())
896 }
897
898 fn write_delim(&mut self, f: ListFormat) -> Result {
899 match f & ListFormat::DelimitersMask {
900 ListFormat::None => {}
901 ListFormat::SpaceDelimited => {
902 space!(self)
903 }
904 _ => unreachable!(),
905 }
906
907 Ok(())
908 }
909}
910
911#[allow(clippy::unused_peekable)]
912fn minify_attribute_value(value: &str, quotes: bool) -> (Cow<'_, str>, Option<char>) {
913 if value.is_empty() {
914 return (Cow::Borrowed(value), Some('"'));
915 }
916
917 if !quotes
919 && value.chars().all(|c| match c {
920 '&' | '`' | '=' | '<' | '>' | '"' | '\'' => false,
921 c if c.is_ascii_whitespace() => false,
922 _ => true,
923 })
924 {
925 return (Cow::Borrowed(value), None);
926 }
927
928 let mut minified = String::with_capacity(value.len());
929
930 let mut unquoted = true;
931 let mut dq = 0;
932 let mut sq = 0;
933
934 let mut chars = value.chars().peekable();
935
936 while let Some(c) = chars.next() {
937 match c {
938 '&' => {
939 let next = chars.next();
940
941 if let Some(next) = next {
942 if matches!(next, '#' | 'a'..='z' | 'A'..='Z') {
943 minified.push_str(&minify_amp(next, &mut chars));
944 } else {
945 minified.push('&');
946 minified.push(next);
947 }
948 } else {
949 minified.push('&');
950 }
951
952 continue;
953 }
954 c if c.is_ascii_whitespace() => {
955 unquoted = false;
956 }
957 '`' | '=' | '<' | '>' => {
958 unquoted = false;
959 }
960 '"' => {
961 unquoted = false;
962 dq += 1;
963 }
964 '\'' => {
965 unquoted = false;
966 sq += 1;
967 }
968
969 _ => {}
970 };
971
972 minified.push(c);
973 }
974
975 if !quotes && unquoted {
976 return (Cow::Owned(minified), None);
977 }
978
979 if dq > sq {
980 (Cow::Owned(minified.replace('\'', "'")), Some('\''))
981 } else {
982 (Cow::Owned(minified.replace('"', """)), Some('"'))
983 }
984}
985
986#[allow(clippy::unused_peekable)]
987fn minify_text(value: &str) -> Cow<'_, str> {
988 if value.is_empty() {
990 return Cow::Borrowed(value);
991 }
992
993 if value.chars().all(|c| match c {
995 '&' | '<' => false,
996 _ => true,
997 }) {
998 return Cow::Borrowed(value);
999 }
1000
1001 let mut result = String::with_capacity(value.len());
1002 let mut chars = value.chars().peekable();
1003
1004 while let Some(c) = chars.next() {
1005 match c {
1006 '&' => {
1007 let next = chars.next();
1008
1009 if let Some(next) = next {
1010 if matches!(next, '#' | 'a'..='z' | 'A'..='Z') {
1011 result.push_str(&minify_amp(next, &mut chars));
1012 } else {
1013 result.push('&');
1014 result.push(next);
1015 }
1016 } else {
1017 result.push('&');
1018 }
1019 }
1020 '<' => {
1021 result.push_str("<");
1022 }
1023 _ => result.push(c),
1024 }
1025 }
1026
1027 Cow::Owned(result)
1028}
1029
1030fn minify_amp(next: char, chars: &mut Peekable<Chars>) -> String {
1031 let mut result = String::with_capacity(7);
1032
1033 match next {
1034 hash @ '#' => {
1035 match chars.next() {
1036 Some(number @ '0'..='9') => {
1039 result.push_str("&");
1040 result.push(hash);
1041 result.push(number);
1042 }
1043 Some(x @ 'x' | x @ 'X') => {
1044 match chars.peek() {
1045 Some(c) if c.is_ascii_hexdigit() => {
1048 result.push_str("&");
1049 result.push(hash);
1050 result.push(x);
1051 }
1052 _ => {
1053 result.push('&');
1054 result.push(hash);
1055 result.push(x);
1056 }
1057 }
1058 }
1059 any => {
1060 result.push('&');
1061 result.push(hash);
1062
1063 if let Some(any) = any {
1064 result.push(any);
1065 }
1066 }
1067 }
1068 }
1069 c @ 'a'..='z' | c @ 'A'..='Z' => {
1072 let mut entity_temporary_buffer = String::with_capacity(33);
1073
1074 entity_temporary_buffer.push('&');
1075 entity_temporary_buffer.push(c);
1076
1077 let mut found_entity = false;
1078
1079 for c in chars {
1081 entity_temporary_buffer.push(c);
1082
1083 if HTML_ENTITIES.get(&entity_temporary_buffer).is_some() {
1084 found_entity = true;
1085
1086 break;
1087 } else {
1088 if !c.is_ascii_alphanumeric() || entity_temporary_buffer.len() > 32 {
1093 break;
1094 }
1095 }
1096 }
1097
1098 if found_entity {
1099 result.push_str("&");
1100 result.push_str(&entity_temporary_buffer[1..]);
1101 } else {
1102 result.push('&');
1103 result.push_str(&entity_temporary_buffer[1..]);
1104 }
1105 }
1106 any => {
1107 result.push('&');
1108 result.push(any);
1109 }
1110 }
1111
1112 result
1113}
1114
1115fn escape_string(value: &str, is_attribute_mode: bool) -> Cow<'_, str> {
1130 if value.is_empty() {
1132 return Cow::Borrowed(value);
1133 }
1134
1135 if value.chars().all(|c| match c {
1136 '&' | '\u{00A0}' => false,
1137 '"' if is_attribute_mode => false,
1138 '<' if !is_attribute_mode => false,
1139 '>' if !is_attribute_mode => false,
1140 _ => true,
1141 }) {
1142 return Cow::Borrowed(value);
1143 }
1144
1145 let mut result = String::with_capacity(value.len());
1146
1147 for c in value.chars() {
1148 match c {
1149 '&' => {
1150 result.push_str("&");
1151 }
1152 '\u{00A0}' => result.push_str(" "),
1153 '"' if is_attribute_mode => result.push_str("""),
1154 '<' if !is_attribute_mode => {
1155 result.push_str("<");
1156 }
1157 '>' if !is_attribute_mode => {
1158 result.push_str(">");
1159 }
1160 _ => result.push(c),
1161 }
1162 }
1163
1164 Cow::Owned(result)
1165}
1166
1167fn is_html_tag_name(namespace: Namespace, tag_name: &Atom) -> bool {
1168 if namespace != Namespace::HTML {
1169 return false;
1170 }
1171
1172 matches!(
1173 &**tag_name,
1174 "a" | "abbr"
1175 | "acronym"
1176 | "address"
1177 | "applet"
1178 | "area"
1179 | "article"
1180 | "aside"
1181 | "audio"
1182 | "b"
1183 | "base"
1184 | "basefont"
1185 | "bdi"
1186 | "bdo"
1187 | "big"
1188 | "blockquote"
1189 | "body"
1190 | "br"
1191 | "button"
1192 | "canvas"
1193 | "caption"
1194 | "center"
1195 | "cite"
1196 | "code"
1197 | "col"
1198 | "colgroup"
1199 | "data"
1200 | "datalist"
1201 | "dd"
1202 | "del"
1203 | "details"
1204 | "dfn"
1205 | "dialog"
1206 | "dir"
1207 | "div"
1208 | "dl"
1209 | "dt"
1210 | "em"
1211 | "embed"
1212 | "fieldset"
1213 | "figcaption"
1214 | "figure"
1215 | "font"
1216 | "footer"
1217 | "form"
1218 | "frame"
1219 | "frameset"
1220 | "h1"
1221 | "h2"
1222 | "h3"
1223 | "h4"
1224 | "h5"
1225 | "h6"
1226 | "head"
1227 | "header"
1228 | "hgroup"
1229 | "hr"
1230 | "html"
1231 | "i"
1232 | "iframe"
1233 | "image"
1234 | "img"
1235 | "input"
1236 | "ins"
1237 | "isindex"
1238 | "kbd"
1239 | "keygen"
1240 | "label"
1241 | "legend"
1242 | "li"
1243 | "link"
1244 | "listing"
1245 | "main"
1246 | "map"
1247 | "mark"
1248 | "marquee"
1249 | "menu"
1250 | "meta"
1253 | "meter"
1254 | "nav"
1255 | "nobr"
1256 | "noembed"
1257 | "noframes"
1258 | "noscript"
1259 | "object"
1260 | "ol"
1261 | "optgroup"
1262 | "option"
1263 | "output"
1264 | "p"
1265 | "param"
1266 | "picture"
1267 | "plaintext"
1268 | "pre"
1269 | "progress"
1270 | "q"
1271 | "rb"
1272 | "rbc"
1273 | "rp"
1274 | "rt"
1275 | "rtc"
1276 | "ruby"
1277 | "s"
1278 | "samp"
1279 | "script"
1280 | "section"
1281 | "select"
1282 | "small"
1283 | "source"
1284 | "span"
1285 | "strike"
1286 | "strong"
1287 | "style"
1288 | "sub"
1289 | "summary"
1290 | "sup"
1291 | "table"
1292 | "tbody"
1293 | "td"
1294 | "template"
1295 | "textarea"
1296 | "tfoot"
1297 | "th"
1298 | "thead"
1299 | "time"
1300 | "title"
1301 | "tr"
1302 | "track"
1303 | "tt"
1304 | "u"
1305 | "ul"
1306 | "var"
1307 | "video"
1308 | "wbr"
1309 | "xmp"
1310 )
1311}