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

相关推荐
魔力军6 小时前
Rust学习Day2: 变量与可变性、数据类型和函数和控制流
开发语言·学习·rust
暴躁小师兄数据学院20 小时前
【WEB3.0零基础转行笔记】Rust编程篇-第一讲:课程简介
rust·web3·区块链·智能合约
anOnion1 天前
构建无障碍组件之Alert Dialog Pattern
前端·html·交互设计
一个懒人懒人1 天前
Promise async/await与fetch的概念
前端·javascript·html
Hello.Reader1 天前
Rocket Fairings 实战把全局能力做成“结构化中间件”
中间件·rust·rocket
Andrew_Ryan1 天前
rust arena 内存分配
rust
Andrew_Ryan1 天前
深入理解 Rust 内存管理:基于 typed_arena 的指针操作实践
rust
晚烛2 天前
CANN + 物理信息神经网络(PINNs):求解偏微分方程的新范式
javascript·人工智能·flutter·html·零售
ۓ明哲ڪ2 天前
网页视频倍速播放.
html