RTML - 在 Rust 中描述 HTML

rtml

在 Rust 中嘗試描述 html/css

在通常情況下要在 Rust 中生成 html 除了手動拼字符串外最常用的方式是使用模板引擎, 但這會帶來 2 個主要問題

  1. 1. 需要學習模板語法, 且不同模板引擎之間語法可能差異很大

  2. 2. 在模板中難以利用 Rust 類型系統, 以及在 IDE 中支持可能不佳

那有沒有可能在 Rust 實現 html 那樣可以比較方便地展示文檔樹形結構, 同時又能充分利用 Rust 的類型系統呢, rtml 正是這個想法的一次嘗試.

使用

先看一個簡單例子

use rtml::tags::*;

fn main() {
    let page = html(
        h1("hello world!")
    );
    println!("{}", page);
}

生成 html

<html>
    <h1>
        hello world!
    </h1>
</html>

可以看到在 rtml 中, html, h1 和 html 的 <html><h1> 標籤對應, 往 html 傳入的參數是 <html> </html> 的 children, html(h1(...)) 就如同往 <html> 標籤中間放入 h1 標籤一樣.

注意, 在 rtml 中, h1div, 和 title 等都是普通的函數 (以下簡稱標籤函數), 沒有使用任何過程宏, 對於有 LSP 支持的 IDE 來說, 懸浮顯示, 智能補全, 跳轉定義等都沒有問題. 而且 rtml 對於標準 html 標籤都提供了詳細的文檔, 方便使用.

靈活的 children 傳入

對於 rtml 中任意一個標籤函數, 其參數雖然只有一個, 但可傳入其中, 作爲 children 的類型非常多.

常見的字面類型

// 布爾值
span(true)

// 浮點數
span(1.1f64)

// 字符串
span("hello")

// format! 也OK
let name = "Rookie";
span(format!("hello {name}"))

// 整數
span(666usize)

更多類型

// 任意一個實現 Tag trait 的結構體
div(h1("hello"))

// 元組也OK
// div 傳入的參數是一個 (H1, Hr, P) 元組
div((
 h1("hello"),
 hr(()),
 p("this is some desc")
 ))

// Vec 也行
// ul 傳入的是一個 
ul(
    [1, 2, 3]
        .iter()
        .map(|i| li(format!("item {i}")))
        .collect::<Vec<_>>()
)

// const generic array 也可以, 如傳入 [Span; 2]
div([span(1), span(2)])

設置屬性, 樣式和綁定事件處理函數

在 html 標籤上, 用戶可以設置 id, class 樣式等. 爲了兼容 html 風格, 樣式設置格式, rtml 提供了 style!, 和 prop! 兩個宏以方便屬性設置. 而且 rtml 傳入屬性的方式也非常靈活.

如果查看 rtml 文檔裏某個標籤函數參數結構體 xxArgs 的文檔, 你會發現它實現很多 From<xxx> 方法.

正是這些實現讓 rtml 的構造函數很靈活.

對於 <meta> 等標籤, 通常我們不關係他的 children, 而是要傳入 charset 等屬性, 這時候你可以這樣傳參.

meta(prop! {
    charset = "utf-8"
})

對於某些標籤來說, 屬性, 樣式和 children 都需要設置, 除了要求 children 必須在最後一個位置外, 屬性和樣式可以亂序

// 注意, 傳入的一個三元素元組 
div((
    style!{color: "red"},
    prop! { id = "app" },
    p("hello")
))

下面是一個更復雜的例子

use rtml::prop;
use rtml::style;
use rtml::tags::*;

fn main() {
    let page = html((
        prop! { lang = "zh-cn" },
        (
            header((
                meta(prop! { charset = "utf-8" }),
                title("html file generated by Rust!"),
            )),
            body((
                style! {
                    color: "olive";
                    text-align: "center"
                },
                (
                    h1("WOW"),
                    hr(()),
                    h2("循環"),
                    pre((
                        style! {},
                        code(
                            r#"[1, 2, 3].iter().map(|i| p(format!("paragraph {}", i))).collect::<Vec<_>>()"#,
                        ),
                    )),
                    div([1, 2, 3]
                        .iter()
                        .map(|i| p(format!("paragraph {}", i)))
                        .collect::<Vec<_>>()),
                    hr(()),
                    h2("任意字面量"),
                    div((
                        style! { color: "black" },
                        (span(true), span(false), span(1u8), span(1.30f32)),
                    )),
                    hr(()),
                    footer(b("power by Rust!")),
                ),
            )),
        ),
    ));
    println!("{}", page);
}

生成的 html

<html lang=zh-cn>
    <header>
        <meta charset=utf-8>
        </meta>
        <title>
            html file generated by Rust!
        </title>
    </header>
    <body style="color: olive; text-align: center; ">
        <h1>
            WOW
        </h1>
        <hr>
        </hr>
        <h2>
            循環
        </h2>
        <pre>
            <code>
                [1, 2, 3].iter().map(|i| p(format!("paragraph {}", i))).collect::<Vec<_>>()
            </code>
        </pre>
        <div>
            <p>
                paragraph 1
            </p>
            <p>
                paragraph 2
            </p>
            <p>
                paragraph 3
            </p>
        </div>
        <hr>
        </hr>
        <h2>
            任意字面量
        </h2>
        <div style="color: black; ">
            <span>
                true
            </span>
            <span>
                false
            </span>
            <span>
                1
            </span>
            <span>
                1.3
            </span>
        </div>
        <hr>
        </hr>
        <footer>
            <b>
                power by Rust!
            </b>
        </footer>
    </body>
</html>

瀏覽器打開效果如下.

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/PegJF-cJ0ZRiZJti4O27Ew