Skip to main content

node_html_parser/dom/element/
serialize.rs

1use super::main::HTMLElement; // temporarily rely on full struct defined in all.rs
2use super::normalize_attr_quotes; // make helper public in all.rs first
3
4impl HTMLElement {
5	pub fn outer_html(&self) -> String {
6		if self.is_root() {
7			return self.inner_html();
8		} // root container unwrap
9		let tag = self.name();
10		let attrs = if self.raw_attrs.is_empty() {
11			String::new()
12		} else {
13			format!(" {}", self.raw_attrs.trim())
14		};
15		let should_normalize = self.attrs_modified;
16		if self.is_void {
17			if self.void_add_slash {
18				// JS VoidTag.formatNode: 若 addClosingSlash 且存在属性且 attrs 不以空格结尾,补一个空格再加 '/'
19				// 这里 attrs 变量自身已经带前导空格(当非空)。需要确保 '/' 前存在恰当空格:
20				// 1. 无属性: <br/>
21				// 2. 有属性且最后不是空格: <img src="x.png" />
22				// 3. 若 attrs 末尾已经是空格(极少数构造),直接拼接 '/'
23				let mut norm_attrs = if attrs.is_empty() {
24					String::new()
25				} else if should_normalize {
26					normalize_attr_quotes(&attrs, true)
27				} else {
28					normalize_attr_quotes(&attrs, false)
29				};
30				// 局部策略:为 void 且需要加 '/' 的情况下,尽量将单引号属性值改成双引号,以匹配 JS 测试(img src="x.png" />)。
31				if !norm_attrs.is_empty() {
32					let mut converted = String::with_capacity(norm_attrs.len());
33					let nb = norm_attrs.as_bytes();
34					let mut i = 0;
35					while i < nb.len() {
36						let c = nb[i] as char;
37						if c == '\'' {
38							// attribute value shouldn't start with quote directly; safe fallback
39							converted.push(c);
40							i += 1;
41							continue;
42						}
43						// Detect pattern ='<value>' and convert
44						if c == '=' && i + 1 < nb.len() && nb[i + 1] as char == '\'' {
45							converted.push('=');
46							converted.push('"');
47							i += 2; // skip ='
48							let start = i;
49							while i < nb.len() && nb[i] as char != '\'' {
50								i += 1;
51							}
52							let val = &norm_attrs[start..i];
53							// if value contains double quotes, fallback to single quotes original
54							if val.contains('"') {
55								converted.push('\'');
56								converted.push_str(val);
57								converted.push('\'');
58							} else {
59								converted.push_str(val);
60								converted.push('"');
61							}
62							if i < nb.len() && nb[i] as char == '\'' {
63								i += 1;
64							}
65							continue;
66						}
67						converted.push(c);
68						i += 1;
69					}
70					norm_attrs = converted;
71				}
72				if norm_attrs.is_empty() {
73					format!("<{}{}{}>", tag, norm_attrs, "/")
74				} else if norm_attrs.ends_with(' ') {
75					format!("<{}{}{}>", tag, norm_attrs, "/")
76				} else {
77					format!("<{}{} {}>", tag, norm_attrs, "/")
78				}
79			} else {
80				let norm_attrs = if attrs.is_empty() {
81					String::new()
82				} else if should_normalize {
83					normalize_attr_quotes(&attrs, true)
84				} else {
85					normalize_attr_quotes(&attrs, false)
86				};
87				format!("<{}{}>", tag, norm_attrs)
88			}
89		} else {
90			// 对于非 void 元素,也需要规范化属性引号并确保属性间有空格
91			let norm_attrs = if attrs.is_empty() {
92				String::new()
93			} else if should_normalize {
94				normalize_attr_quotes(&attrs, true)
95			} else {
96				normalize_attr_quotes(&attrs, false)
97			};
98			// 兼容 JS 行为:如果该元素是被后续标签自动闭合,且本身没有子节点(空),原输入中不存在显式关闭标签,
99			// 则保留原样仅输出起始标签(如 <ul><li><li></ul> 中的两个 <li>)。
100			let auto_closed_empty = match self.range() {
101				Some((s, e)) if s == e && self.children.is_empty() => true,
102				_ => false,
103			};
104			if auto_closed_empty {
105				format!("<{}{}>", tag, norm_attrs)
106			} else {
107				format!("<{}{}>{}</{}>", tag, norm_attrs, self.inner_html(), tag)
108			}
109		}
110	}
111	/// JS API 等价:toString() => outerHTML (保留显式方法以便测试调用)
112	pub fn to_string(&self) -> String {
113		self.outer_html()
114	}
115}