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