11个不稳定版本 (3个破坏性更新)

0.4.1 2023年4月6日
0.4.0 2023年4月6日
0.3.1 2023年4月2日
0.2.5 2023年4月2日
0.1.0 2021年11月26日

#170模板引擎

Download history 10/week @ 2024-03-10 32/week @ 2024-03-31 3/week @ 2024-04-14

每月67次 下载

MIT 许可证

90KB
1K SLoC

laby

Crate Docs Maintainer License Issues Contributors

laby是一个小型宏库,用于在Rust中编写HTML模板。 文档

let n = html!(
  head!(
    title!("laby"),
  ),
  body!(
    p!("Hello, world!"),
  ),
);

let s = render!(DocType::HTML5, n);
<!DOCTYPE html><html><head><title>laby</title></head><body><p>Hello, world!</p></body></html>

lib.rs:

laby是一个小型 库,用于在Rust中编写快速HTML模板。它关注以下三个方面

  • 简单性:laby具有最少的依赖项,无需任何配置即可直接运行,并且可以根据需要轻松扩展以添加额外功能。
  • 性能:laby生成专用代码以生成HTML。在运行时不需要进行堆分配,除了将生成的HTML渲染到的缓冲区外。任何涉及额外堆分配的操作都是可选的。所有渲染代码都在编译时进行静态类型检查并内联以优化性能。
  • 熟悉性:laby提供可以接受任何有效Rust代码并扩展为常规Rust代码的宏;学习新的领域特定语言进行HTML模板编写不是必需的。宏可以嵌套、组合、格式化,并通过rustfmt进行格式化,像常规Rust代码一样分离成组件并由函数返回。

laby的大部分高性能代码是从sailfish继承的,这是一个为Rust设计的极快的HTML模板引擎。然而,sailfish使用特殊的语法处理HTML模板文件,而laby提供直接嵌入到您的代码中的宏。选择哪个库取决于您的编码风格和个人偏好。

laby针对 Rust稳定版,并支持带无_std的嵌入式环境。无需进行配置。

安装

在您的项目中,将以下行添加到 Cargo.toml 文件中的 [dependencies] 部分

[dependencies]
laby = "0.4"

此外,您可能希望像这样将laby导入到您的代码中

use laby::*;

这只是为了方便,因为laby导出大量的宏,每个宏代表一个HTML标签。当然,也可以单独导入你使用的宏。本指南的其余部分假设您已经导入了必要的宏。

laby不提供对流行Web框架的集成支持。它返回一个普通的String作为渲染结果,因此鼓励您编写自己的宏,将那个String写入响应流。大多数Web框架都可以开箱即用完成这项任务。

基础

laby提供在编译时生成专用Rust代码的过程宏,这些代码在运行时渲染时生成HTML代码。为了有效地使用laby,理解它是如何转换您的代码的非常重要。考虑以下示例。

# use laby::*;
// construct a tree of html nodes
let n = html!(
    head!(
        title!("laby"),
    ),
    body!(
        class = "dark",
        p!("hello, world"),
    ),
);

// convert the tree into a string
let s = render!(n);

// check the result
assert_eq!(s, "\
    <html>\
        <head>\
            <title>laby</title>\
        </head>\
        <body class=\"dark\">\
            <p>hello, world</p>\
        </body>\
    </html>\
");

上面的代码使用宏html!head!title!body!和[p!]来构建基本的HTML结构。然后,使用render!宏将树转换为String表示形式。结果与另一个字符串进行比较,该字符串被扩展到多行以提高可读性。

注意,节点子代作为常规位置参数传递,而节点属性作为赋值表达式指定。这是完全有效的Rust语法,这意味着可以使用rustfmt进行格式化。

在底层,laby将上述代码转换为类似以下代码

# use laby::*;
let n = {
    struct _html {}
    impl Render for _html {
        #[inline]
        fn render(self, buf: &mut laby::internal::Buffer) {
            buf.push_str("<html><head><title>laby</title></head><body class=\"dark\"><p>hello, world</p></body></html>");
        }
    }
    _html {}
};

let s = render!(n);
// assert_eq!(s, ...);

本质上,这就是laby宏所做的一切;它们只是为节点树声明一个新的专用结构体,为该结构体实现Render trait,构建该结构体,并返回构建的值。

当此代码编译为发布版本时,所有包装代码都会被剥离,渲染代码将被内联,执行时将类似于以下内容

# use laby::*;
let mut buf = laby::internal::Buffer::new();
buf.push_str("<html><head><title>laby</title></head><body class=\"dark\"><p>hello, world</p></body></html>");

let s = buf.into_string();
// assert_eq!(s, ...);

模板化

laby接受任何有效的表达式作为属性名称和值以及子代节点,并且可以像常规代码一样访问局部作用域中的变量。它不仅限于只接受字符串字面量。

唯一的要求是表达式评估的结果必须实现Render trait。请参阅外部实现列表以了解哪些类型实现了此trait。评估后的值存储在专用结构体中,并在render!宏被调用时进行渲染。考虑以下示例。

// retrieve an article from a database
let title = "laby";
let content = "hello, 'world'";
let date = "2030-01-01";

// construct a tree of nodes, with templated expressions
let n = article!(
    class = format!("date-{}", date),
    h1!({
        let mut title = title.to_owned();
        title.truncate(30);
        title
    }),
    p!(content),
);

// convert the tree into a string
let s = render!(n);

// check the result
assert_eq!(s, "\
    <article class=\"date-2030-01-01\">\
        <h1>laby</h1>\
        <p>hello, &#39;world&#39;</p>\
    </article>\
");

上面的代码构建了一个带有标题、内容和类属性的模板基本HTML结构。

  • class属性:展开和评估format!宏表达式。
  • <h1> 节点:评估一个截断标题至最多三十个字符的表达式。
  • <p> 节点:评估一个简单的局部变量表达式。

注意,这些表达式是在节点构建的地方(即 let n = ...)进行评估的,而不是在调用 render! 宏的地方。

此外,文章内容中的撇号被HTML实体 &#39; 转义。默认情况下,laby会对所有模板表达式进行转义,除非使用 raw! 宏。

在底层,laby将上述代码转换为类似以下代码

let title = "laby";
let content = "hello, 'world'";
let date = "2030-01-01";

let n = {
    struct _article<T1, T2, T3> { t1: T1, t2: T2, t3: T3 }
    impl<T1, T2, T3> Render for _article<T1, T2, T3>
        where T1: Render, T2: Render, T3: Render {
        #[inline]
        fn render(self, buf: &mut laby::internal::Buffer) {
            buf.push_str("<article class=\"");
            self.t1.render(buf); // date
            buf.push_str("\"><h1>");
            self.t2.render(buf); // title
            buf.push_str("</h1><p>");
            self.t3.render(buf); // content
            buf.push_str("</p></article>");
        }
    }
    _article {
        t1: format!("date-{}", date),
        t2: {
            let mut title = title.to_owned();
            title.truncate(30);
            title
        },
        t3: content
    }
};

let s = render!(n);
// assert_eq!(s, ...);

注意,生成的专用结构的字段是泛型化的,它们覆盖了模板表达式。当该结构被构建时(即 _article { ... }),编译器能够从字段赋值中推断泛型类型参数并对结构进行单态化。如果所有字段表达式都评估为实现了 Render 特性的值,那么该特性也将为生成的结构实现,允许它通过 render! 进行渲染。

组件化

编写用于渲染整个HTML文档的大模板很快就会变得难以管理且难以维护,因此通常有必要将文档拆分成几个较小的组件。针对这个问题有两种流行的技术:包含继承。laby支持这两种模式,使用Rust提供的高级语言功能。

在实践中,这些模式经常混合使用,以形成一个完整且一致的文档。以下将探讨这两种方法的示例。

模板继承

这是一个 自顶向下 的方法,它将大文档分解为小组件。这导致了一种结构一致但僵化,难以轻松扩展或更改的结构。

# use laby::*;
// a large template that takes small components
fn page(title: impl Render, header: impl Render, body: impl Render) -> impl Render {
    html!(
        head!(
            title!(title),
        ),
        body!(
            header!(header),
            main!(body),
        ),
    )
}

// a component that *inherits* a large template
fn home() -> impl Render {
    page(
        "Home",
        h1!("About laby"),
        p!("laby is an HTML macro library for Rust."),
    )
}

assert_eq!(render!(home()), "\
    <html>\
        <head>\
            <title>Home</title>\
        </head>\
        <body>\
            <header>\
                <h1>About laby</h1>\
            </header>\
            <main>\
                <p>laby is an HTML macro library for Rust.</p>\
            </main>\
        </body>\
    </html>\
");

模板包含

这是一个 自底向上 的方法,它将小组件合并成大文档。这导致了一种灵活但可能不一致的结构,也可能导致更多的样板代码。

# use laby::*;
// small individual components
fn title() -> impl Render {
    "Home"
}

fn header() -> impl Render {
    h1!("About laby")
}

fn body() -> impl Render {
    p!("laby is an HTML macro library for Rust.")
}

// a large component that *includes* the small components
fn home() -> impl Render {
    html!(
        head!(
            title!(title()),
        ),
        body!(
            header!(header()),
            main!(body()),
        ),
    )
}

assert_eq!(render!(home()), "\
    <html>\
        <head>\
            <title>Home</title>\
        </head>\
        <body>\
            <header>\
                <h1>About laby</h1>\
            </header>\
            <main>\
                <p>laby is an HTML macro library for Rust.</p>\
            </main>\
        </body>\
    </html>\
");

命名参数

有时组件会变得很大,接受一个很长的位置参数列表,这会损害可读性。laby提供了一个名为 #[laby] 的属性宏,它允许您使用显式命名的参数和可选值调用任意函数,类似于HTML宏。

要启用支持,只需在组件函数之前添加此属性,然后使用生成的宏调用它。

#[laby]
fn page(title: impl Render, header: impl Render, body: impl Render) -> impl Render {
    html!(
        head!(
            title!(title),
        ),
        body!(
            header!(header),
            main!(body),
        ),
    )
}

#[laby]
fn home() -> impl Render {
    // `page` function called using the generated `page!` macro
    page!(
        title = "Home",
        header = h1!("About laby"),
        body = p!("laby is an HTML macro library for Rust."),
    )
}

扩展

laby 可以通过简单地实现 Render 特性来扩展,这是一个表示渲染操作最小单位的低级特性。如果 laby 提供的现成功能不能满足您的特定需求,或者 laby 没有为所需的类型提供 Render 实现,那么自己实现这个特性可能是一个可行的解决方案。

创建扩展的一般模式如下

  1. 编写一个结构体,存储您的渲染操作所需的所有必要数据。
  2. 为该结构体实现 Render 特性。
  3. 提供一个简单、简短的宏,方便地构建该结构体。

事实上,iter!raw!disp! 宏就是以这种方式实现的。它们并不是魔法;它们仅仅是 laby 核心渲染系统的扩展。您甚至可以忽略 laby 的 HTML 宏,并编写自己的转换来实现 Render 特性。

许可证

laby 由 chiya.dev 编写,许可协议为 MIT 许可证。部分代码来自 sailfish,由 Ryohei Machida 编写,同样采用 MIT 许可证。HTML 标签的文档来自 MDN,许可协议为 CC-BY-SA 2.5

依赖项

~2MB
~49K SLoC