htmlbuilder - rust灵活构建html
引言
rust中广泛运用来动态创建html的有build_html - Rust和html - Rust,但是我实际使用来感觉不是很顺手(当然,我有我的需求,有些需求并非每个人都会用到)。
build_html一旦将标签结构into到内部节点元素后,本身信息退化,将不能再进行更改。即便用户通过Rc等方式保留HtmlElement,也与真正起作用的HtmlChild不是一个东西。这就使得html的"动态"构建只能够开始时动态,就算通过children也只能够获得HtmlChild克隆体,无法直接更改已经存入的节点元素。另外,build_html的可用标签不全。
html可以在build为...Builder后继续添加,但是获取不了子元素或者亲节点,而且push之后仍然会存在跟build_html一样不能修改。具备更高动态性但不多。
两个库均将内部元素结构体作为子元素动态数组的类型,而暴露给用户的元素结构体,最终将被封装为内部元素结构体,同时还会拿走内部元素结构体所有权。特别是所有权机制,导致这两个动态生成库的行为没有"动态到底"。

改进计划
在我自己写的cpphtmlbuilder: 仅头文件的 c++ html构建器中,用指针构建的元素类有非常大的灵活性,这在rust中实现就需要Rc和RefCell了(我只考虑单线程)。显然,也是需要两个结构体,一个元素本身,还有一个元素的指针封装。
rust
#[derive(Clone)]
pub struct Element {
inner: Rc<RefCell<ElementInner>>,
}
struct ElementInner {
parent: Option<Weak<RefCell<ElementInner>>>,
children: Vec<Element>,
tag: String,
content: String,
kws: HashMap<&'static str, String>,
onetag: bool, // 是否为单标签
pre: bool, // 是否为原文本内容
}
但是,那两个库坏就坏在最终起作用的结构体跟用户操作的结构体是两个东西,所以不可能让用户去使用ElementInner然后转头去构建Element。所以用户只能去操作唯一暴露出来的Element。

初始化
rust
impl Element {
pub fn new(tag: impl Into<String>, content: impl Into<String>) -> Self {
Self {
inner: Rc::new(RefCell::new(ElementInner {
parent: None,
children: Vec::new(),
tag: tag.into(),
content: escape_ascii(&content.into()),
// 默认值
kws: HashMap::new(),
onetag: false,
pre: false,
}))
}
}
}
我一开始就直接创建引用计数,用户即便去clone,也是在克隆真正唯一的ElementInner的可变引用。
若干方法
rust
impl Element {
/// 添加子元素
pub fn add(&self, elem: Element) -> &Self {
{
let mut inner = self.inner.borrow_mut();
elem.inner.borrow_mut().parent = Some(Rc::downgrade(&self.inner));
inner.children.push(elem);
}
self
}
/// 添加子元素并返回Self
pub fn add_with(self, elem: Element) -> Self {
// ...
}
/// 获取父元素
pub fn parent(&self) -> Option<Element> {
// ...
}
/// 设置内容
pub fn configcnt(&self, content: impl Into<String>) -> &Self {
// ...
}
/// 设置属性
pub fn configkws(&self, mut kws: HashMap<&'static str, String>) -> &Self {
// ...
}
/// 获取子元素
pub fn children(&self) -> Vec<Element> {
self.inner.borrow().children.clone()
}
/// 移除指定位置子元素
pub fn remove_child(&self, index: usize) -> Option<Element> {
// ...
}
/// 渲染为html字符串
pub fn render(&self, split_s: &str) -> String {
// ...
}
}
Element的若干方法均从托管的ElementInner引用智能指针中获取内部元素,再对其进行操作。
测试
rust
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
fn write_file(filename: &str, content: &str) {
let mut file = File::create(filename).unwrap();
file.write_all(content.as_bytes()).unwrap();
}
#[test]
fn it_works() {
let root = Element::new("html", "");
// 短元素用add_with()方法添加
let head = Element::new("head", "")
.add_with(Element::new("title", "My Page"))
.add_with(
Element::new("meta", "")
.kws(HashMap::from([("charset", "utf-8".to_string())]))
);
root.add(head);
let body = Element::new("body", "");
// 这里克隆,因为后面还要用到body
root.add(body.clone());
let div = Element::new("div", "");
body.add(div.clone());
let mut attrs = HashMap::new();
attrs.insert("id", "main".to_string());
attrs.insert("class", "container<>".to_string());
div.configkws(attrs);
div.configcnt("&<html><div>content内容&");
// 输出父元素此刻的html代码
if let Some(parent) = div.parent() {
println!("{}", parent.render("\n"));
}
div.add(Element::new("h1", "cpphtmlbuilder"));
// 添加列表
let ul = Element::new("ul", "");
div.add(ul.clone());
for i in 0..10 {
ul.add(Element::new("li", &i.to_string()));
}
// 删除倒数第二个li
{
let children_count = ul.children().len();
if children_count >= 2 {
ul.remove_child(children_count - 2);
}
}
div.add(Element::new("", "content内容,只要标签名为空即可"));
let result = root.render("\n");
println!("{}", result);
write_file("test.html", &result);
}
}
观察代码,很容易发现元素本身的修改、下辖元素的增删改都是自由进行的,所有的构建与传递都只是计数指针,虽然实现上进行了一层封装,但使用上可以视为对同一个元素,始终在操作同一个东西。
输出结果:
html
<html>
<head>
<title>My Page</title>
<meta charset="utf-8"></meta>
</head>
<body>
<div class="container<>" id="main">&<html><div>content内容&
<h1>cpphtmlbuilder</h1>
<ul>
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>9</li>
</ul>
content内容,只要标签名为空即可
</div>
</body>
</html>
使用
项目地址:Smart-Space/rustHtmlBuilder: Build html flexibly and easily in Rust
crate库:htmlbuilder - crates.io: Rust Package Registry
自动生成的文档:htmlbuilder - Rust