htmlbuilder - rust灵活构建html

htmlbuilder - rust灵活构建html

引言

rust中广泛运用来动态创建html的有build_html - Rusthtml - Rust,但是我实际使用来感觉不是很顺手(当然,我有我的需求,有些需求并非每个人都会用到)。

build_html一旦将标签结构into到内部节点元素后,本身信息退化,将不能再进行更改。即便用户通过Rc等方式保留HtmlElement,也与真正起作用的HtmlChild不是一个东西。这就使得html的"动态"构建只能够开始时动态,就算通过children也只能够获得HtmlChild克隆体,无法直接更改已经存入的节点元素。另外,build_html可用标签不全

html可以在build...Builder后继续添加,但是获取不了子元素或者亲节点,而且push之后仍然会存在跟build_html一样不能修改。具备更高动态性但不多。

两个库均将内部元素结构体作为子元素动态数组的类型,而暴露给用户的元素结构体,最终将被封装为内部元素结构体,同时还会拿走内部元素结构体所有权。特别是所有权机制,导致这两个动态生成库的行为没有"动态到底"。

改进计划

在我自己写的cpphtmlbuilder: 仅头文件的 c++ html构建器中,用指针构建的元素类有非常大的灵活性,这在rust中实现就需要RcRefCell了(我只考虑单线程)。显然,也是需要两个结构体,一个元素本身,还有一个元素的指针封装。

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&lt;&gt;" id="main">&amp;&lt;html&gt;&lt;div&gt;content内容&amp;
<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

相关推荐
星栈独行13 小时前
Makepad 应用如何读文件、调接口、保存数据
前端·程序人生·ui·rust·github
xinhuanjieyi14 小时前
html修复游戏种太阳错误
前端·游戏·html
guyoung15 小时前
BoxAgnts 工具系统(7)——Skill 模板、Agent 代理与 Cron 调度
rust·agent·ai编程
LaughingZhu17 小时前
Product Hunt 每日热榜 | 2026-06-11
人工智能·经验分享·神经网络·html·产品运营
分布式存储与RustFS17 小时前
基于Rust的国产开源对象存储RustFS:S3 Table对Iceberg数据湖的适配详解
rust·开源·iceberg·对象存储·rustfs·minio平替·s3 table
Jinkxs20 小时前
Rust 性能优化全流程:从 flamegraph 定位瓶颈到 unsafe 与 SIMD 加速,响应快 2 倍
开发语言·性能优化·rust
ShyanZh20 小时前
【skill】HTML PPT Skill:用 Claude Code 一句话生成专业演示文稿
前端·ai·html·powerpoint·skill
m0_547722921 天前
从零搭建乒乓球比赛管理系统——Spring Boot + 原生 HTML 实战
spring boot·后端·html
在水一缸1 天前
重塑前端开发认知:当 AI 遇见 HTML 的“不合理有效性”
前端·人工智能·html·ai编程·claude·前端开发
星栈独行1 天前
Rust + Makepad 应用怎么打包发布:Windows、macOS、Linux 全平台交付
windows·程序人生·macos·ui·rust