apace_dom/node/
serialize.rs1use html_escape::{encode_quoted_attribute_to_string, encode_text_to_string};
4use markup5ever::{QualName, ns};
5use style_traits::values::ToCss;
6
7use super::{Node, NodeData};
8
9#[derive(Clone, Copy)]
10enum OutputStyle {
11 Normal,
12 Pretty,
13}
14
15#[derive(Clone, Copy)]
16struct SerializeOpts {
17 style: OutputStyle,
18 resolve_current_color: bool,
20 declare_xlink_ns: bool,
22}
23
24impl SerializeOpts {
25 const fn html(style: OutputStyle) -> Self {
26 Self {
27 style,
28 resolve_current_color: false,
29 declare_xlink_ns: false,
30 }
31 }
32}
33
34const XLINK_NS: &str = "http://www.w3.org/1999/xlink";
35
36fn write_attr_name(name: &QualName, writer: &mut String) {
38 match name.ns {
39 ns!() | ns!(html) => {}
40 ns!(xml) => writer.push_str("xml:"),
41 ns!(xmlns) if name.local.as_str() == "xmlns" => {}
42 ns!(xmlns) => writer.push_str("xmlns:"),
43 ns!(xlink) => writer.push_str("xlink:"),
44 _ => {
45 if let Some(prefix) = &name.prefix {
46 writer.push_str(prefix);
47 writer.push(':');
48 }
49 }
50 }
51 writer.push_str(&name.local);
52}
53
54fn is_xlink_ns_decl(name: &QualName) -> bool {
55 (name.ns == ns!(xmlns) && name.local.as_str() == "xlink")
56 || name.local.as_str() == "xmlns:xlink"
57}
58
59impl Node {
60 pub fn outer_html(&self) -> String {
68 let mut output = String::new();
69 self.write_outer_html(&mut output);
70 output
71 }
72
73 pub fn outer_html_pretty(&self) -> String {
89 let mut output = String::new();
90 self.write_outer_html_pretty(&mut output);
91 output
92 }
93
94 pub fn write_outer_html(&self, writer: &mut String) {
95 self.write_outer_html_in_style(writer, SerializeOpts::html(OutputStyle::Normal), 0);
96 }
97
98 pub fn write_outer_html_pretty(&self, writer: &mut String) {
99 self.write_outer_html_in_style(writer, SerializeOpts::html(OutputStyle::Pretty), 0);
100 }
101
102 #[cfg(feature = "svg")]
105 pub(crate) fn svg_source(&self) -> String {
106 let mut output = String::new();
107 let opts = SerializeOpts {
108 style: OutputStyle::Normal,
109 resolve_current_color: true,
110 declare_xlink_ns: true,
111 };
112 self.write_outer_html_in_style(&mut output, opts, 0);
113 output
114 }
115
116 fn write_outer_html_in_style(&self, writer: &mut String, opts: SerializeOpts, nesting: usize) {
117 const INDENT: &str = " ";
118 let style = opts.style;
119 let has_children = !self.children.is_empty();
120 let current_color = opts
121 .resolve_current_color
122 .then(|| self.primary_styles())
123 .flatten()
124 .map(|style| style.clone_color())
125 .map(|color| color.to_css_string());
126
127 match &self.data {
128 NodeData::Document(_) => {}
129 NodeData::Comment { .. } => {}
130 NodeData::AnonymousBlock(_) => {}
131 NodeData::Text(data) => {
133 if matches!(style, OutputStyle::Pretty) {
134 for _ in 0..nesting {
135 writer.push_str(INDENT);
136 }
137 }
138 let in_raw_text_element = self
139 .parent
140 .and_then(|id| self.tree()[id].data.downcast_element())
141 .is_some_and(|el| {
142 el.name.ns == ns!(html)
144 && matches!(
145 el.name.local.as_ref(),
146 "style"
147 | "script"
148 | "xmp"
149 | "iframe"
150 | "noembed"
151 | "noframes"
152 | "plaintext"
153 )
154 });
155 if in_raw_text_element {
156 writer.push_str(data.content.as_str());
157 } else {
158 encode_text_to_string(data.content.as_str(), writer);
159 }
160 if matches!(style, OutputStyle::Pretty) {
161 writer.push('\n');
162 }
163 }
164 NodeData::Element(data) => {
165 if matches!(style, OutputStyle::Pretty) {
166 for _ in 0..nesting {
167 writer.push_str(INDENT);
168 }
169 }
170 writer.push('<');
171 writer.push_str(&data.name.local);
172
173 if opts.declare_xlink_ns
174 && nesting == 0
175 && !data.attrs().iter().any(|attr| is_xlink_ns_decl(&attr.name))
176 {
177 writer.push_str(" xmlns:xlink=\"");
178 writer.push_str(XLINK_NS);
179 writer.push('"');
180 }
181
182 for attr in data.attrs() {
183 writer.push(' ');
184 write_attr_name(&attr.name, writer);
185 writer.push_str("=\"");
186 #[allow(clippy::unnecessary_unwrap)] if current_color.is_some() && attr.value.contains("currentColor") {
188 let value = attr
189 .value
190 .replace("currentColor", current_color.as_ref().unwrap());
191 encode_quoted_attribute_to_string(&value, writer);
192 } else {
193 encode_quoted_attribute_to_string(&attr.value, writer);
194 }
195 writer.push('"');
196 }
197 if !has_children {
198 writer.push_str(" /");
199 }
200 writer.push('>');
201 if matches!(style, OutputStyle::Pretty) {
202 writer.push('\n');
203 }
204
205 if has_children {
206 for &child_id in &self.children {
207 self.tree()[child_id].write_outer_html_in_style(writer, opts, nesting + 1);
208 }
209
210 if matches!(style, OutputStyle::Pretty) {
211 for _ in 0..nesting {
212 writer.push_str(INDENT);
213 }
214 }
215 writer.push_str("</");
216 writer.push_str(&data.name.local);
217 writer.push('>');
218 if matches!(style, OutputStyle::Pretty) {
219 writer.push('\n');
220 }
221 }
222 }
223 }
224 }
225}