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