1mod tag_parse;
12mod text_content;
13
14use tag_parse::{
15 ParsedAttribute, ParsedAttributeValue, ParsedTag, canonical_group_key, parse_tag,
16 reorder_attributes,
17};
18use text_content::{
19 collapse_whitespace, decode_xml_entities, dedent_block_with_offset, encode_xml_entities,
20 is_text_content_element, normalize_text_content_with_entities, strip_cdata_wrapper,
21};
22use tree_sitter::{Node, Parser};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
36#[cfg_attr(feature = "cli", value(rename_all = "kebab-case"))]
37pub enum AttributeSort {
38 None,
40 #[default]
42 Canonical,
43 Alphabetical,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
59#[cfg_attr(feature = "cli", value(rename_all = "kebab-case"))]
60pub enum AttributeLayout {
61 #[default]
63 Auto,
64 SingleLine,
66 MultiLine,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
82#[cfg_attr(feature = "cli", value(rename_all = "kebab-case"))]
83pub enum QuoteStyle {
84 #[default]
86 Preserve,
87 Double,
89 Single,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
106#[cfg_attr(feature = "cli", value(rename_all = "kebab-case"))]
107pub enum WrappedAttributeIndent {
108 OneLevel,
110 #[default]
112 AlignToTagName,
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
126#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
127#[cfg_attr(feature = "cli", value(rename_all = "kebab-case"))]
128pub enum BlankLines {
129 Remove,
131 Preserve,
133 #[default]
135 Truncate,
136 Insert,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
151#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
152#[cfg_attr(feature = "cli", value(rename_all = "kebab-case"))]
153pub enum TextContentMode {
154 Collapse,
156 #[default]
158 Maintain,
159 Prettify,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum EmbeddedLanguage {
173 Css,
175 JavaScript,
177 Html,
179}
180
181const fn is_script_or_style_language(language: EmbeddedLanguage) -> bool {
182 matches!(
183 language,
184 EmbeddedLanguage::Css | EmbeddedLanguage::JavaScript
185 )
186}
187
188pub struct EmbeddedContent<'a> {
202 pub language: EmbeddedLanguage,
204 pub content: &'a str,
206 pub indent_depth: usize,
208 pub file_byte_offset: usize,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq)]
232pub struct FormatOptions {
233 pub indent_width: usize,
235 pub insert_spaces: bool,
237 pub max_inline_tag_width: usize,
239 pub attribute_sort: AttributeSort,
241 pub attribute_layout: AttributeLayout,
243 pub attributes_per_line: usize,
245 pub space_before_self_close: bool,
247 pub quote_style: QuoteStyle,
249 pub wrapped_attribute_indent: WrappedAttributeIndent,
251 pub text_content: TextContentMode,
253 pub blank_lines: BlankLines,
255 pub ignore_prefixes: Vec<String>,
264}
265
266impl Default for FormatOptions {
267 fn default() -> Self {
268 Self {
269 indent_width: 2,
270 insert_spaces: true,
271 max_inline_tag_width: 100,
272 attribute_sort: AttributeSort::Canonical,
273 attribute_layout: AttributeLayout::Auto,
274 attributes_per_line: 1,
275 space_before_self_close: true,
276 quote_style: QuoteStyle::Preserve,
277 wrapped_attribute_indent: WrappedAttributeIndent::AlignToTagName,
278 text_content: TextContentMode::Maintain,
279 blank_lines: BlankLines::Truncate,
280 ignore_prefixes: vec!["svg-format".to_string()],
281 }
282 }
283}
284
285#[must_use]
294pub fn format(source: &str) -> String {
295 format_with_options(source, FormatOptions::default())
296}
297
298#[must_use]
309pub fn format_with_options(source: &str, options: FormatOptions) -> String {
310 format_with_host(source, options, &mut |_| None)
311}
312
313#[must_use]
335pub fn format_with_host(
336 source: &str,
337 options: FormatOptions,
338 format_embedded: &mut dyn FnMut(EmbeddedContent<'_>) -> Option<String>,
339) -> String {
340 let mut parser = Parser::new();
341 if parser
342 .set_language(&tree_sitter_svg::LANGUAGE.into())
343 .is_err()
344 {
345 return normalize_line_endings(source);
346 }
347
348 let Some(tree) = parser.parse(source.as_bytes(), None) else {
349 return normalize_line_endings(source);
350 };
351
352 if tree.root_node().has_error() {
353 return normalize_line_endings(source);
354 }
355
356 if has_ignore_file_comment(
358 tree.root_node(),
359 source.as_bytes(),
360 &options.ignore_prefixes,
361 ) {
362 return normalize_line_endings(source);
363 }
364
365 let mut formatter = Formatter::new(source.as_bytes(), options);
366 formatter.format_node(tree.root_node(), 0, format_embedded);
367 formatter.finish(source)
368}
369
370fn normalize_line_endings(source: &str) -> String {
376 if !source.contains('\r') {
377 return source.to_owned();
378 }
379 source.replace("\r\n", "\n").replace('\r', "\n")
380}
381
382fn has_ignore_file_comment(node: Node<'_>, source: &[u8], prefixes: &[String]) -> bool {
384 if node.kind() == "comment" {
385 let inner = node
386 .child_by_field_name("text")
387 .and_then(|t| std::str::from_utf8(&source[t.byte_range()]).ok())
388 .map_or("", str::trim);
389 if prefixes.iter().any(|p| inner == format!("{p}-ignore-file")) {
390 return true;
391 }
392 }
393 let mut cursor = node.walk();
394 node.named_children(&mut cursor)
395 .any(|child| has_ignore_file_comment(child, source, prefixes))
396}
397
398fn wrap_path_data(name: &str, raw: &str, budget: usize) -> Option<String> {
410 if budget == 0 || raw.chars().count() <= budget {
411 return None;
412 }
413 if raw.trim().is_empty() {
414 return None;
415 }
416 if raw.contains('\n') {
417 return None;
420 }
421 match name {
422 "d" => wrap_d_value(raw, budget),
423 "points" => wrap_points_value(raw, budget),
424 _ => None,
425 }
426}
427
428fn wrap_d_value(raw: &str, budget: usize) -> Option<String> {
429 let mut parser = Parser::new();
436 parser
437 .set_language(&tree_sitter_svg_path::LANGUAGE.into())
438 .ok()?;
439 let tree = parser.parse(raw.as_bytes(), None)?;
440 if tree.root_node().has_error() {
441 return None;
442 }
443
444 let mut segments: Vec<(usize, usize, &str)> = Vec::new();
445 collect_path_segments(tree.root_node(), raw.as_bytes(), &mut segments);
446 if segments.is_empty() {
447 return None;
448 }
449
450 let segment_strs: Vec<&str> = segments
451 .iter()
452 .map(|&(start, end, _kind)| &raw[start..end])
453 .collect();
454 let segment_kinds: Vec<&str> = segments.iter().map(|&(_, _, kind)| kind).collect();
455
456 pack_segments(&segment_strs, &segment_kinds, budget, true)
457}
458
459fn wrap_points_value(raw: &str, budget: usize) -> Option<String> {
460 let tokens: Vec<&str> = raw.split_ascii_whitespace().collect();
466 if tokens.is_empty() {
467 return None;
468 }
469 let mut pairs: Vec<String> = Vec::new();
473 let mut i = 0;
474 while i < tokens.len() {
475 let t = tokens[i];
476 if t.contains(',') {
477 pairs.push(t.to_string());
478 i += 1;
479 } else if i + 1 < tokens.len() {
480 pairs.push(format!("{t},{}", tokens[i + 1]));
481 i += 2;
482 } else {
483 pairs.push(t.to_string());
484 i += 1;
485 }
486 }
487 if pairs.len() <= 1 {
488 return None;
489 }
490 let pair_refs: Vec<&str> = pairs.iter().map(String::as_str).collect();
491 let kinds = vec!["pair"; pair_refs.len()];
492 pack_segments(&pair_refs, &kinds, budget, false)
493}
494
495fn pack_segments(
501 segments: &[&str],
502 kinds: &[&str],
503 budget: usize,
504 prefer_subpath_breaks: bool,
505) -> Option<String> {
506 if segments.is_empty() {
507 return None;
508 }
509 let mut out = String::with_capacity(segments.iter().map(|s| s.len() + 1).sum::<usize>());
510 let mut line_width = 0usize;
511 let half_budget = budget / 2;
512 for (i, seg) in segments.iter().enumerate() {
513 let w = seg.chars().count();
514 let is_moveto_split = prefer_subpath_breaks
515 && i > 0
516 && kinds[i] == "moveto_segment"
517 && line_width >= half_budget;
518 if line_width == 0 {
519 out.push_str(seg);
520 line_width = w;
521 } else if !is_moveto_split && line_width + 1 + w <= budget {
522 out.push(' ');
523 out.push_str(seg);
524 line_width += 1 + w;
525 } else {
526 out.push('\n');
527 out.push_str(seg);
528 line_width = w;
529 }
530 }
531 if out.contains('\n') { Some(out) } else { None }
532}
533
534fn collect_path_segments(
535 node: Node<'_>,
536 source: &[u8],
537 segments: &mut Vec<(usize, usize, &'static str)>,
538) {
539 const SEGMENT_KINDS: &[&str] = &[
540 "moveto_segment",
541 "lineto_segment",
542 "closepath_segment",
543 "curveto_segment",
544 "smooth_curveto_segment",
545 "quadratic_bezier_curveto_segment",
546 "smooth_quadratic_bezier_curveto_segment",
547 "elliptical_arc_segment",
548 "horizontal_lineto_segment",
549 "vertical_lineto_segment",
550 "implicit_lineto_segment",
551 ];
552 let kind = node.kind();
553 if let Some(&matched) = SEGMENT_KINDS.iter().find(|k| **k == kind) {
554 let range = node.byte_range();
555 let text = std::str::from_utf8(&source[range.clone()])
558 .unwrap_or("")
559 .trim_end();
560 let end = range.start + text.len();
561 segments.push((range.start, end, matched));
562 return;
563 }
564 let mut cursor = node.walk();
565 for child in node.named_children(&mut cursor) {
566 collect_path_segments(child, source, segments);
567 }
568}
569
570fn partition_by_canonical_group(attributes: &[ParsedAttribute]) -> Vec<Vec<usize>> {
574 if attributes.is_empty() {
575 return Vec::new();
576 }
577 let mut groups: Vec<Vec<usize>> = Vec::new();
578 let mut current: Vec<usize> = vec![0];
579 let mut current_key = canonical_group_key(&attributes[0].name);
580 for (i, attr) in attributes.iter().enumerate().skip(1) {
581 let key = canonical_group_key(&attr.name);
582 if key == current_key {
583 current.push(i);
584 } else {
585 groups.push(std::mem::take(&mut current));
586 current.push(i);
587 current_key = key;
588 }
589 }
590 if !current.is_empty() {
591 groups.push(current);
592 }
593 groups
594}
595
596fn split_group_if_overflow(
606 attributes: &[ParsedAttribute],
607 group: Vec<usize>,
608 budget: usize,
609) -> Vec<Vec<usize>> {
610 let width: usize = group
611 .iter()
612 .map(|&i| approximate_attribute_width(&attributes[i]))
613 .sum::<usize>()
614 + group.len().saturating_sub(1);
615 if width <= budget || group.len() <= 1 {
616 vec![group]
617 } else {
618 group.into_iter().map(|i| vec![i]).collect()
619 }
620}
621
622fn approximate_attribute_width(attribute: &ParsedAttribute) -> usize {
626 let name_width = attribute.name.chars().count();
627 attribute.value.as_ref().map_or(name_width, |v| {
628 let quote_width = if v.original_quote.is_some() { 2 } else { 0 };
629 name_width + 1 + quote_width + v.raw.chars().count()
630 })
631}
632
633fn split_first_chunk_for_tag_line(
644 attributes: &[ParsedAttribute],
645 chunks: &mut Vec<Vec<usize>>,
646 tag_line_prefix_width: usize,
647 line_budget: usize,
648 mut render: impl FnMut(&ParsedAttribute) -> String,
649) -> (Vec<usize>, Vec<Vec<usize>>) {
650 if chunks.is_empty() {
651 return (Vec::new(), Vec::new());
652 }
653 let first = chunks.remove(0);
654 let mut kept: Vec<usize> = Vec::with_capacity(first.len());
655 let mut popped: Vec<usize> = Vec::new();
656 for &idx in &first {
657 let candidate = render(&attributes[idx]);
658 let tentative = if kept.is_empty() {
659 tag_line_prefix_width + candidate.chars().count()
660 } else {
661 let existing: usize = tag_line_prefix_width
663 + kept
664 .iter()
665 .map(|&i| render(&attributes[i]).chars().count() + 1)
666 .sum::<usize>()
667 - 1;
668 existing + 1 + candidate.chars().count()
669 };
670 if !kept.is_empty() && tentative > line_budget {
671 popped.push(idx);
672 } else {
673 kept.push(idx);
674 }
675 }
676 for &idx in &first {
679 if !kept.contains(&idx) && !popped.contains(&idx) {
680 popped.push(idx);
681 }
682 }
683 let rest: Vec<Vec<usize>> = if popped.is_empty() {
684 std::mem::take(chunks)
685 } else {
686 std::iter::once(popped)
687 .chain(std::mem::take(chunks))
688 .collect()
689 };
690 (kept, rest)
691}
692
693struct Formatter<'a> {
694 source: &'a [u8],
695 options: FormatOptions,
696 out: String,
697}
698
699impl<'a> Formatter<'a> {
700 const fn new(source: &'a [u8], options: FormatOptions) -> Self {
701 Self {
702 source,
703 options,
704 out: String::new(),
705 }
706 }
707
708 fn finish(mut self, original: &str) -> String {
709 while self.out.ends_with('\n') {
710 self.out.pop();
711 }
712 if original.ends_with('\n') {
713 self.out.push('\n');
714 }
715 self.out
716 }
717
718 fn format_node(
719 &mut self,
720 node: Node<'_>,
721 depth: usize,
722 fmt: &mut dyn FnMut(EmbeddedContent<'_>) -> Option<String>,
723 ) {
724 match node.kind() {
725 "svg_root_element" | "element" => self.format_element_like(node, depth, fmt),
726 "start_tag" => self.write_tag_node(node, depth, false),
727 "self_closing_tag" => self.write_tag_node(node, depth, true),
728 "style_text_double" | "style_text_single" | "script_text_double"
729 | "script_text_single" => {
730 self.write_preserved_block_text(node, depth);
731 }
732 "text" | "raw_text" => {
733 self.write_text_node(node, depth);
734 }
735 "end_tag"
736 | "comment"
737 | "cdata_section"
738 | "doctype"
739 | "processing_instruction"
740 | "xml_declaration"
741 | "entity_reference"
742 | "erroneous_end_tag" => {
743 let text = self.node_text(node).trim().to_string();
744 self.write_line(depth, &text);
745 }
746 _ => self.format_children(node, depth, fmt),
747 }
748 }
749
750 fn format_children(
751 &mut self,
752 node: Node<'_>,
753 depth: usize,
754 fmt: &mut dyn FnMut(EmbeddedContent<'_>) -> Option<String>,
755 ) {
756 let mut cursor = node.walk();
757 let mut prev_end: Option<usize> = None;
758 let mut prev_was_comment = false;
759 let mut ignore_next = false;
760 let mut in_ignore_range = false;
761 for child in node.named_children(&mut cursor) {
762 if self.handle_ignore(
763 child,
764 &mut in_ignore_range,
765 &mut ignore_next,
766 &mut prev_was_comment,
767 &mut prev_end,
768 ) {
769 continue;
770 }
771
772 if let Some(end) = prev_end {
773 self.emit_gap(end, child.start_byte(), prev_was_comment);
774 }
775 self.format_node(child, depth, fmt);
776 prev_was_comment = child.kind() == "comment";
777 prev_end = Some(child.end_byte());
778 }
779 }
780
781 fn format_element_like(
782 &mut self,
783 node: Node<'_>,
784 depth: usize,
785 fmt: &mut dyn FnMut(EmbeddedContent<'_>) -> Option<String>,
786 ) {
787 let mut cursor = node.walk();
788 let children: Vec<Node<'_>> = node.named_children(&mut cursor).collect();
789 if children.is_empty() {
790 return;
791 }
792
793 if children.len() == 1 && children[0].kind() == "self_closing_tag" {
794 self.format_node(children[0], depth, fmt);
795 return;
796 }
797
798 let tag_name: String = children
799 .iter()
800 .find(|c| c.kind() == "start_tag")
801 .and_then(|tag| {
802 let text = self.node_text(*tag).trim();
803 text.strip_prefix('<')
804 .and_then(|s| s.split(|c: char| c.is_whitespace() || c == '>').next())
805 .map(str::to_string)
806 })
807 .unwrap_or_default();
808
809 let embedded_lang = match tag_name.as_str() {
810 "style" => Some(EmbeddedLanguage::Css),
811 "script" => Some(EmbeddedLanguage::JavaScript),
812 "foreignObject" => Some(EmbeddedLanguage::Html),
813 _ => None,
814 };
815
816 if embedded_lang == Some(EmbeddedLanguage::Html)
817 && self
818 .try_format_foreign_object(&children, depth, fmt)
819 .is_some()
820 {
821 return;
822 }
823
824 if let Some((start, end)) = text_content_entity_bounds(&children, &tag_name) {
825 self.format_text_content_element(start, end, depth);
826 return;
827 }
828
829 self.format_element_children(&children, embedded_lang, depth, fmt);
830 }
831
832 fn format_element_children(
833 &mut self,
834 children: &[Node<'_>],
835 embedded_lang: Option<EmbeddedLanguage>,
836 depth: usize,
837 fmt: &mut dyn FnMut(EmbeddedContent<'_>) -> Option<String>,
838 ) {
839 let mut prev_end: Option<usize> = None;
840 let mut prev_was_comment = false;
841 let mut ignore_next = false;
842 let mut in_ignore_range = false;
843 for &child in children {
844 if self.handle_ignore(
845 child,
846 &mut in_ignore_range,
847 &mut ignore_next,
848 &mut prev_was_comment,
849 &mut prev_end,
850 ) {
851 continue;
852 }
853
854 match child.kind() {
855 "start_tag" | "end_tag" => {
856 self.format_node(child, depth, fmt);
857 }
858 "style_text_double" | "style_text_single" | "script_text_double"
859 | "script_text_single" => {
860 if !self.node_text(child).trim().is_empty() {
861 self.write_preserved_block_text(child, depth + 1);
862 }
863 prev_was_comment = false;
864 prev_end = Some(child.end_byte());
865 }
866 "cdata_section" if embedded_lang.is_some_and(is_script_or_style_language) => {
867 let Some(lang) = embedded_lang else { continue };
868 self.format_embedded_child(
869 child,
870 lang,
871 depth + 1,
872 fmt,
873 (&mut prev_end, &mut prev_was_comment),
874 true,
875 );
876 }
877 "text" | "raw_text" => {
878 if let Some(lang) = embedded_lang
879 && lang != EmbeddedLanguage::Html
880 {
881 self.format_embedded_child(
882 child,
883 lang,
884 depth + 1,
885 fmt,
886 (&mut prev_end, &mut prev_was_comment),
887 false,
888 );
889 continue;
890 }
891 self.format_plain_text_child(
892 child,
893 depth + 1,
894 &mut prev_end,
895 &mut prev_was_comment,
896 );
897 }
898 _ => {
899 if let Some(end) = prev_end {
900 self.emit_gap(end, child.start_byte(), prev_was_comment);
901 }
902 self.format_node(child, depth + 1, fmt);
903 prev_was_comment = child.kind() == "comment";
904 prev_end = Some(child.end_byte());
905 }
906 }
907 }
908 }
909
910 fn format_plain_text_child(
911 &mut self,
912 child: Node<'_>,
913 depth: usize,
914 prev_end: &mut Option<usize>,
915 prev_was_comment: &mut bool,
916 ) {
917 if self.node_text(child).trim().is_empty() {
918 return;
919 }
920 if let Some(end) = *prev_end {
921 self.emit_gap(end, child.start_byte(), *prev_was_comment);
922 }
923 self.write_text_node(child, depth);
924 *prev_was_comment = false;
925 *prev_end = Some(child.end_byte());
926 }
927
928 fn format_embedded_child(
929 &mut self,
930 child: Node<'_>,
931 language: EmbeddedLanguage,
932 depth: usize,
933 fmt: &mut dyn FnMut(EmbeddedContent<'_>) -> Option<String>,
934 previous: (&mut Option<usize>, &mut bool),
935 force_preserve: bool,
936 ) {
937 let (prev_end, prev_was_comment) = previous;
938 if self.node_text(child).trim().is_empty() {
939 return;
940 }
941 if let Some(end) = *prev_end {
942 self.emit_gap(end, child.start_byte(), *prev_was_comment);
943 }
944 if !self.try_format_embedded_text(child, language, depth, fmt) {
945 if force_preserve || self.options.text_content == TextContentMode::Maintain {
946 self.write_preserved_embedded_text(child, depth);
947 } else {
948 self.write_text_node(child, depth);
949 }
950 }
951 *prev_was_comment = false;
952 *prev_end = Some(child.end_byte());
953 }
954
955 fn format_text_content_element(&mut self, start: Node<'_>, end: Node<'_>, depth: usize) {
964 let raw = std::str::from_utf8(&self.source[start.end_byte()..end.start_byte()])
965 .unwrap_or_default();
966 let normalized = normalize_text_content_with_entities(raw);
967 let end_text = self.node_text(end).trim().to_string();
968
969 let out_before = self.out.len();
971 self.write_tag_node(start, depth, false);
972 let tag_output = self.out[out_before..].to_string();
973
974 if normalized.is_empty() {
975 self.write_line(depth, &end_text);
976 return;
977 }
978
979 let tag_str = tag_output.trim_end_matches('\n');
981 if !tag_str.contains('\n') {
982 let tag_inline = tag_str.trim_start();
983 let candidate = format!("{tag_inline}{normalized}{end_text}");
984 if self.indent(depth).len() + candidate.len() <= self.options.max_inline_tag_width {
985 self.out.truncate(out_before);
986 self.write_line(depth, &candidate);
987 return;
988 }
989 }
990
991 self.write_line(depth + 1, &normalized);
993 self.write_line(depth, &end_text);
994 }
995
996 fn write_tag_node(&mut self, node: Node<'_>, depth: usize, self_closing: bool) {
997 let raw = self.node_text(node).trim().to_string();
998 let Some(mut tag) = parse_tag(&raw, self_closing) else {
999 self.write_line(depth, &raw);
1000 return;
1001 };
1002 reorder_attributes(&mut tag.attributes, self.options.attribute_sort);
1003 let rendered_attributes: Vec<String> = tag
1004 .attributes
1005 .iter()
1006 .map(|attribute| self.render_attribute(attribute))
1007 .collect();
1008 let inline = self.render_inline_tag(&tag.name, &rendered_attributes, self_closing);
1009
1010 if !self.should_break_into_multiline(&raw, &inline, &rendered_attributes) {
1011 self.write_line(depth, &inline);
1012 return;
1013 }
1014 if rendered_attributes.is_empty() {
1015 self.emit_attributeless_multiline(depth, &tag.name, self_closing);
1016 return;
1017 }
1018 self.emit_multiline_tag(depth, &mut tag, self_closing);
1019 }
1020
1021 fn render_inline_tag(
1025 &self,
1026 name: &str,
1027 rendered_attributes: &[String],
1028 self_closing: bool,
1029 ) -> String {
1030 let mut inline = format!("<{name}");
1031 if !rendered_attributes.is_empty() {
1032 inline.push(' ');
1033 inline.push_str(&rendered_attributes.join(" "));
1034 }
1035 if self_closing {
1036 inline.push_str(self.self_closing_suffix());
1037 } else {
1038 inline.push('>');
1039 }
1040 inline
1041 }
1042
1043 fn should_break_into_multiline(
1048 &self,
1049 raw: &str,
1050 inline: &str,
1051 rendered_attributes: &[String],
1052 ) -> bool {
1053 match self.options.attribute_layout {
1054 AttributeLayout::SingleLine => false,
1055 AttributeLayout::MultiLine => !rendered_attributes.is_empty(),
1056 AttributeLayout::Auto => {
1057 raw.contains('\n') || inline.len() > self.options.max_inline_tag_width
1058 }
1059 }
1060 }
1061
1062 fn emit_attributeless_multiline(&mut self, depth: usize, tag_name: &str, self_closing: bool) {
1066 self.write_line(depth, &format!("<{tag_name}"));
1067 if self_closing {
1068 self.write_line(depth, self.self_closing_suffix());
1069 } else {
1070 self.write_line(depth, ">");
1071 }
1072 }
1073
1074 fn emit_multiline_tag(&mut self, depth: usize, tag: &mut ParsedTag, self_closing: bool) {
1081 let wrapped_prefix = self.wrapped_attribute_prefix(depth, &tag.name);
1082 self.auto_wrap_path_data_values(&mut tag.attributes, &wrapped_prefix);
1083
1084 let has_multiline_value = tag
1090 .attributes
1091 .iter()
1092 .any(|a| a.value.as_ref().is_some_and(|v| v.raw.contains('\n')));
1093
1094 let first_inline = matches!(
1103 self.options.wrapped_attribute_indent,
1104 WrappedAttributeIndent::AlignToTagName,
1105 );
1106 let closer = if self_closing {
1107 self.self_closing_suffix()
1108 } else {
1109 ">"
1110 };
1111
1112 let chunks = self.build_wrap_chunks(&tag.attributes, &wrapped_prefix, has_multiline_value);
1113
1114 if first_inline {
1115 self.emit_first_inline_layout(depth, tag, chunks, &wrapped_prefix, closer);
1116 } else {
1117 self.emit_one_level_layout(depth, tag, &chunks, &wrapped_prefix, closer);
1118 }
1119 }
1120
1121 fn auto_wrap_path_data_values(&self, attributes: &mut [ParsedAttribute], wrapped_prefix: &str) {
1127 for attribute in attributes {
1128 let Some(value) = attribute.value.as_mut() else {
1129 continue;
1130 };
1131 let name_width = attribute.name.chars().count();
1132 let available = self
1133 .options
1134 .max_inline_tag_width
1135 .saturating_sub(wrapped_prefix.len() + name_width + 2);
1136 if let Some(wrapped) = wrap_path_data(&attribute.name, &value.raw, available) {
1137 value.raw = wrapped;
1138 }
1139 }
1140 }
1141
1142 fn build_wrap_chunks(
1154 &self,
1155 attributes: &[ParsedAttribute],
1156 wrapped_prefix: &str,
1157 has_multiline_value: bool,
1158 ) -> Vec<Vec<usize>> {
1159 let budget = self
1160 .options
1161 .max_inline_tag_width
1162 .saturating_sub(wrapped_prefix.len());
1163 if matches!(self.options.attribute_sort, AttributeSort::Canonical) && !has_multiline_value {
1164 partition_by_canonical_group(attributes)
1165 .into_iter()
1166 .flat_map(|group| split_group_if_overflow(attributes, group, budget))
1167 .collect()
1168 } else {
1169 let per_line = if has_multiline_value {
1170 1
1171 } else {
1172 self.options.attributes_per_line.max(1)
1173 };
1174 (0..attributes.len())
1175 .collect::<Vec<_>>()
1176 .chunks(per_line)
1177 .map(<[usize]>::to_vec)
1178 .collect()
1179 }
1180 }
1181
1182 fn emit_first_inline_layout(
1189 &mut self,
1190 depth: usize,
1191 tag: &ParsedTag,
1192 mut chunks: Vec<Vec<usize>>,
1193 wrapped_prefix: &str,
1194 closer: &str,
1195 ) {
1196 let (first_chunk_indices, rest_chunks) = split_first_chunk_for_tag_line(
1197 &tag.attributes,
1198 &mut chunks,
1199 self.indent(depth).len() + tag.name.chars().count() + 2, self.options.max_inline_tag_width,
1201 |attr| self.render_attribute_aligned(attr, wrapped_prefix),
1202 );
1203 let first_rendered: Vec<String> = first_chunk_indices
1204 .iter()
1205 .map(|&i| self.render_attribute_aligned(&tag.attributes[i], wrapped_prefix))
1206 .collect();
1207 let mut tag_line = format!(
1208 "{}<{} {}",
1209 self.indent(depth),
1210 tag.name,
1211 first_rendered.join(" ")
1212 );
1213 if rest_chunks.is_empty() {
1214 tag_line.push_str(closer);
1215 }
1216 tag_line.push('\n');
1217 self.out.push_str(&tag_line);
1218 self.emit_wrapped_chunks(tag, &rest_chunks, wrapped_prefix, closer);
1219 }
1220
1221 fn emit_one_level_layout(
1225 &mut self,
1226 depth: usize,
1227 tag: &ParsedTag,
1228 chunks: &[Vec<usize>],
1229 wrapped_prefix: &str,
1230 closer: &str,
1231 ) {
1232 self.write_line(depth, &format!("<{}", tag.name));
1233 self.emit_wrapped_chunks(tag, chunks, wrapped_prefix, closer);
1234 }
1235
1236 fn emit_wrapped_chunks(
1239 &mut self,
1240 tag: &ParsedTag,
1241 chunks: &[Vec<usize>],
1242 wrapped_prefix: &str,
1243 closer: &str,
1244 ) {
1245 for (index, chunk) in chunks.iter().enumerate() {
1246 let rendered: Vec<String> = chunk
1247 .iter()
1248 .map(|&i| self.render_attribute_aligned(&tag.attributes[i], wrapped_prefix))
1249 .collect();
1250 let mut line = rendered.join(" ");
1251 if index == chunks.len() - 1 {
1252 line.push_str(closer);
1253 }
1254 self.write_prefixed_line(wrapped_prefix, &line);
1255 }
1256 }
1257
1258 const fn self_closing_suffix(&self) -> &'static str {
1259 if self.options.space_before_self_close {
1260 " />"
1261 } else {
1262 "/>"
1263 }
1264 }
1265
1266 fn render_attribute(&self, attribute: &ParsedAttribute) -> String {
1267 attribute.value.as_ref().map_or_else(
1268 || attribute.name.clone(),
1269 |value| format!("{}={}", attribute.name, self.render_attribute_value(value)),
1270 )
1271 }
1272
1273 fn render_attribute_value(&self, value: &ParsedAttributeValue) -> String {
1274 match self.options.quote_style {
1275 QuoteStyle::Preserve => match value.original_quote {
1276 Some('\'') => format!("'{}'", value.raw),
1277 Some('"') => format!("\"{}\"", value.raw),
1278 Some(other) => format!("{other}{}{other}", value.raw),
1279 None => value.raw.clone(),
1280 },
1281 QuoteStyle::Double => format!("\"{}\"", value.raw.replace('"', """)),
1282 QuoteStyle::Single => format!("'{}'", value.raw.replace('\'', "'")),
1283 }
1284 }
1285
1286 fn render_attribute_aligned(&self, attribute: &ParsedAttribute, prefix: &str) -> String {
1304 let Some(value) = attribute.value.as_ref() else {
1305 return attribute.name.clone();
1306 };
1307 if !value.raw.contains('\n') {
1308 return format!("{}={}", attribute.name, self.render_attribute_value(value));
1309 }
1310
1311 let quote = match self.options.quote_style {
1312 QuoteStyle::Preserve => value.original_quote.unwrap_or('"'),
1313 QuoteStyle::Double => '"',
1314 QuoteStyle::Single => '\'',
1315 };
1316 let name_width = attribute.name.chars().count();
1317 let mut pad = String::with_capacity(prefix.len() + name_width + 2);
1318 pad.push_str(prefix);
1319 for _ in 0..name_width + 2 {
1321 pad.push(' ');
1322 }
1323
1324 let mut result = String::with_capacity(value.raw.len() + pad.len() * 2 + 8);
1325 let mut lines = value.raw.split('\n');
1326 if let Some(first) = lines.next() {
1327 result.push_str(&attribute.name);
1328 result.push('=');
1329 result.push(quote);
1330 result.push_str(first);
1331 }
1332 for line in lines {
1333 result.push('\n');
1334 result.push_str(&pad);
1335 result.push_str(line.trim_start());
1336 }
1337 result.push(quote);
1338 result
1339 }
1340
1341 fn wrapped_attribute_prefix(&self, depth: usize, tag_name: &str) -> String {
1342 match self.options.wrapped_attribute_indent {
1343 WrappedAttributeIndent::OneLevel => self.indent(depth + 1),
1344 WrappedAttributeIndent::AlignToTagName => {
1345 let mut prefix = self.indent(depth);
1346 prefix.push_str(&" ".repeat(tag_name.chars().count() + 2));
1348 prefix
1349 }
1350 }
1351 }
1352
1353 fn write_prefixed_line(&mut self, prefix: &str, text: &str) {
1354 self.out.push_str(prefix);
1355 self.out.push_str(text);
1356 self.out.push('\n');
1357 }
1358
1359 fn write_text_node(&mut self, node: Node<'_>, depth: usize) {
1360 let text = self.node_text(node).to_string();
1361 self.write_text_str(&text, depth);
1362 }
1363
1364 fn write_text_str(&mut self, text: &str, depth: usize) {
1365 if text.trim().is_empty() {
1366 return;
1367 }
1368
1369 match self.options.text_content {
1370 TextContentMode::Collapse => {
1371 for line in text.lines() {
1372 let collapsed = collapse_whitespace(line);
1373 if collapsed.is_empty() {
1374 continue;
1375 }
1376 self.write_line(depth, &collapsed);
1377 }
1378 }
1379 TextContentMode::Maintain => {
1380 self.write_preserved_str(text, depth);
1381 }
1382 TextContentMode::Prettify => {
1383 for line in text.lines() {
1384 let trimmed = line.trim();
1385 if trimmed.is_empty() {
1386 continue;
1387 }
1388 self.write_line(depth, trimmed);
1389 }
1390 }
1391 }
1392 }
1393
1394 fn write_preserved_block_text(&mut self, node: Node<'_>, depth: usize) {
1395 let text = self.node_text(node).to_string();
1396 self.write_preserved_str(&text, depth);
1397 }
1398
1399 fn write_preserved_embedded_text(&mut self, node: Node<'_>, depth: usize) {
1400 let text = self.node_text(node).to_string();
1401 self.write_embedded_preserved_str(&text, depth);
1402 }
1403
1404 fn write_preserved_str(&mut self, text: &str, depth: usize) {
1405 if text.trim().is_empty() {
1406 return;
1407 }
1408
1409 let lines: Vec<&str> = text.lines().collect();
1410 let first_non_empty = lines.iter().position(|line| !line.trim().is_empty());
1411 let last_non_empty = lines.iter().rposition(|line| !line.trim().is_empty());
1412 let (Some(start), Some(end)) = (first_non_empty, last_non_empty) else {
1413 return;
1414 };
1415
1416 let block = &lines[start..=end];
1417 let min_leading = block
1418 .iter()
1419 .filter(|line| !line.trim().is_empty())
1420 .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
1421 .min()
1422 .unwrap_or(0);
1423
1424 for line in block {
1425 let without_common_indent = line.chars().skip(min_leading).collect::<String>();
1426 self.write_line(depth, without_common_indent.trim_end());
1427 }
1428 }
1429
1430 fn write_embedded_preserved_str(&mut self, text: &str, depth: usize) {
1431 if text.trim().is_empty() {
1432 return;
1433 }
1434
1435 let lines: Vec<&str> = text.lines().collect();
1436 let first_non_empty = lines.iter().position(|line| !line.trim().is_empty());
1437 let last_non_empty = lines.iter().rposition(|line| !line.trim().is_empty());
1438 let (Some(start), Some(end)) = (first_non_empty, last_non_empty) else {
1439 return;
1440 };
1441
1442 let block = &lines[start..=end];
1443 let min_leading = block
1444 .iter()
1445 .filter(|line| !line.trim().is_empty())
1446 .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
1447 .min()
1448 .unwrap_or(0);
1449
1450 let mut consecutive_blank = 0usize;
1451 for line in block {
1452 let without_common_indent = line.chars().skip(min_leading).collect::<String>();
1453 let trimmed = without_common_indent.trim_end();
1454 if trimmed.is_empty() {
1455 consecutive_blank += 1;
1456 if self.should_emit_embedded_blank(consecutive_blank) {
1457 self.out.push('\n');
1458 }
1459 } else {
1460 consecutive_blank = 0;
1461 self.write_line(depth, trimmed);
1462 }
1463 }
1464 }
1465
1466 fn try_format_embedded_text(
1477 &mut self,
1478 node: Node<'_>,
1479 language: EmbeddedLanguage,
1480 depth: usize,
1481 fmt: &mut dyn FnMut(EmbeddedContent<'_>) -> Option<String>,
1482 ) -> bool {
1483 let raw = self.node_text(node);
1484 let raw_start = node.start_byte();
1485 let (payload, cdata_wrapped, payload_offset) = strip_cdata_wrapper(raw).map_or_else(
1486 || {
1487 let (dedented, offset) = dedent_block_with_offset(raw);
1488 (decode_xml_entities(dedented), false, offset)
1489 },
1490 |(inner_offset, inner)| {
1491 let (dedented, offset) = dedent_block_with_offset(inner);
1495 (dedented, true, offset.map(|value| inner_offset + value))
1496 },
1497 );
1498 let Some(payload_offset) = payload_offset else {
1503 return false;
1504 };
1505 let file_byte_offset = raw_start + payload_offset;
1506 let req = EmbeddedContent {
1507 language,
1508 content: &payload,
1509 indent_depth: depth,
1510 file_byte_offset,
1511 };
1512 fmt(req).is_some_and(|formatted| {
1513 if cdata_wrapped {
1514 self.write_cdata_block(&formatted, depth);
1515 } else {
1516 let encoded = encode_xml_entities(&formatted);
1517 self.write_indented_block(&encoded, depth);
1518 }
1519 true
1520 })
1521 }
1522
1523 fn write_cdata_block(&mut self, text: &str, depth: usize) {
1530 self.write_line(depth, "<![CDATA[");
1531 self.write_indented_block(text, depth + 1);
1532 self.write_line(depth, "]]>");
1533 }
1534
1535 fn try_format_foreign_object(
1538 &mut self,
1539 children: &[Node<'_>],
1540 depth: usize,
1541 fmt: &mut dyn FnMut(EmbeddedContent<'_>) -> Option<String>,
1542 ) -> Option<()> {
1543 let start_tag = children.iter().find(|c| c.kind() == "start_tag")?;
1544 let end_tag = children.iter().find(|c| c.kind() == "end_tag")?;
1545
1546 let content_start = start_tag.end_byte();
1547 let content_end = end_tag.start_byte();
1548 if content_start >= content_end {
1549 return None;
1550 }
1551
1552 let raw = std::str::from_utf8(&self.source[content_start..content_end]).ok()?;
1553 let (content, offset) = dedent_block_with_offset(raw);
1554 let offset = offset?;
1558 let file_byte_offset = content_start + offset;
1559
1560 let req = EmbeddedContent {
1561 language: EmbeddedLanguage::Html,
1562 content: &content,
1563 indent_depth: depth + 1,
1564 file_byte_offset,
1565 };
1566 let formatted = fmt(req)?;
1567
1568 self.write_tag_node(*start_tag, depth, false);
1570 self.write_indented_block(&formatted, depth + 1);
1571 let end_text = self.node_text(*end_tag).trim().to_string();
1572 self.write_line(depth, &end_text);
1573 Some(())
1574 }
1575
1576 fn write_indented_block(&mut self, text: &str, depth: usize) {
1581 let lines: Vec<&str> = text.lines().collect();
1582 let Some(first) = lines.iter().position(|l| !l.trim().is_empty()) else {
1583 return;
1584 };
1585 let last = lines
1586 .iter()
1587 .rposition(|l| !l.trim().is_empty())
1588 .unwrap_or(first);
1589 let block = &lines[first..=last];
1590
1591 let indent = self.indent(depth);
1592 let mut consecutive_blank = 0usize;
1593 for line in block {
1594 if line.trim().is_empty() {
1595 consecutive_blank += 1;
1596 if self.should_emit_embedded_blank(consecutive_blank) {
1597 self.out.push('\n');
1598 }
1599 } else {
1600 consecutive_blank = 0;
1601 self.out.push_str(&indent);
1602 self.out.push_str(line);
1603 self.out.push('\n');
1604 }
1605 }
1606 }
1607
1608 fn handle_ignore(
1612 &mut self,
1613 child: Node<'_>,
1614 in_ignore_range: &mut bool,
1615 ignore_next: &mut bool,
1616 prev_was_comment: &mut bool,
1617 prev_end: &mut Option<usize>,
1618 ) -> bool {
1619 let mut skip_ignore_self = false;
1620
1621 if child.kind() == "comment" {
1622 if *in_ignore_range {
1625 if self.is_ignore_directive(child, "ignore-end") {
1626 self.write_source_span(*prev_end, child.end_byte());
1627 *in_ignore_range = false;
1628 *prev_was_comment = true;
1629 *prev_end = Some(child.end_byte());
1630 return true;
1631 }
1632 } else {
1635 if self.is_ignore_directive(child, "ignore-start") {
1636 *in_ignore_range = true;
1637 if let Some(end) = *prev_end {
1638 self.emit_gap(end, child.start_byte(), *prev_was_comment);
1639 }
1640 self.write_source_span(Some(child.start_byte()), child.end_byte());
1641 *prev_was_comment = true;
1642 *prev_end = Some(child.end_byte());
1643 return true;
1644 }
1645 if self.is_ignore_directive(child, "ignore") {
1646 *ignore_next = true;
1647 skip_ignore_self = true;
1648 }
1649 }
1650 }
1651
1652 if !*in_ignore_range
1655 && matches!(child.kind(), "text" | "raw_text")
1656 && self.node_text(child).trim().is_empty()
1657 {
1658 return true;
1659 }
1660
1661 if !skip_ignore_self && *in_ignore_range {
1662 self.write_source_span(*prev_end, child.end_byte());
1665 *prev_was_comment = child.kind() == "comment";
1666 *prev_end = Some(child.end_byte());
1667 return true;
1668 }
1669
1670 if !skip_ignore_self && *ignore_next {
1671 self.write_source_span(Some(child.start_byte()), child.end_byte());
1675 if !self.out.ends_with('\n') {
1676 self.out.push('\n');
1677 }
1678 *ignore_next = false;
1679 *prev_was_comment = child.kind() == "comment";
1680 *prev_end = Some(child.end_byte());
1681 return true;
1682 }
1683
1684 false
1685 }
1686
1687 fn is_ignore_directive(&self, node: Node<'_>, suffix: &str) -> bool {
1692 let inner = node
1693 .child_by_field_name("text")
1694 .map_or("", |t| self.node_text(t).trim());
1695 self.options
1696 .ignore_prefixes
1697 .iter()
1698 .any(|prefix| inner == format!("{prefix}-{suffix}"))
1699 }
1700
1701 fn write_source_span(&mut self, from: Option<usize>, to: usize) {
1704 let start = from.unwrap_or(to);
1705 if start < to {
1706 self.out
1707 .push_str(std::str::from_utf8(&self.source[start..to]).unwrap_or_default());
1708 }
1709 }
1710
1711 fn source_blank_lines(&self, from: usize, to: usize) -> usize {
1713 if from >= to {
1714 return 0;
1715 }
1716 let gap = std::str::from_utf8(&self.source[from..to]).unwrap_or_default();
1717 let newlines = gap.chars().filter(|&c| c == '\n').count();
1718 newlines.saturating_sub(1)
1719 }
1720
1721 fn emit_gap(&mut self, prev_end: usize, next_start: usize, prev_was_comment: bool) {
1726 let source_gaps = self.source_blank_lines(prev_end, next_start);
1727 let count = match self.options.blank_lines {
1728 BlankLines::Remove => 0,
1729 BlankLines::Preserve => source_gaps,
1730 BlankLines::Truncate => source_gaps.min(1),
1731 BlankLines::Insert => usize::from(!prev_was_comment),
1732 };
1733 for _ in 0..count {
1734 self.out.push('\n');
1735 }
1736 }
1737
1738 const fn should_emit_embedded_blank(&self, consecutive: usize) -> bool {
1745 match self.options.blank_lines {
1746 BlankLines::Remove => false,
1747 BlankLines::Truncate => consecutive <= 1,
1748 BlankLines::Preserve | BlankLines::Insert => true,
1749 }
1750 }
1751
1752 fn node_text(&self, node: Node<'_>) -> &str {
1753 std::str::from_utf8(&self.source[node.byte_range()]).unwrap_or_default()
1754 }
1755
1756 fn write_line(&mut self, depth: usize, text: &str) {
1757 self.out.push_str(&self.indent(depth));
1758 self.out.push_str(text);
1759 self.out.push('\n');
1760 }
1761
1762 fn indent(&self, depth: usize) -> String {
1763 if self.options.insert_spaces {
1764 " ".repeat(depth.saturating_mul(self.options.indent_width))
1765 } else {
1766 "\t".repeat(depth)
1767 }
1768 }
1769}
1770
1771fn text_content_entity_bounds<'a>(
1772 children: &[Node<'a>],
1773 tag_name: &str,
1774) -> Option<(Node<'a>, Node<'a>)> {
1775 if !is_text_content_element(tag_name)
1776 || !children
1777 .iter()
1778 .any(|child| child.kind() == "entity_reference")
1779 {
1780 return None;
1781 }
1782
1783 let start = children
1784 .iter()
1785 .find(|child| child.kind() == "start_tag")
1786 .copied()?;
1787 let end = children
1788 .iter()
1789 .find(|child| child.kind() == "end_tag")
1790 .copied()?;
1791 let all_inline = children
1792 .iter()
1793 .filter(|child| !matches!(child.kind(), "start_tag" | "end_tag"))
1794 .all(|child| matches!(child.kind(), "text" | "raw_text" | "entity_reference"));
1795
1796 all_inline.then_some((start, end))
1797}