# 模板
Deft 的服务端模板构建在块字面量(block literal)之上 —— `[html ... \html]`、`[sql ... \sql]`、`[codeblock ... \codeblock]`。块是一种多行字面量,其内部会被词法化:字面文本、`$var` 插值和嵌套的 `[...]` 调用可以自由混合。编译器把内部内容打包成一个带标签的映射,并调用与块同名的函数,由它应用相应领域的转义并返回渲染结果。
## 块如何工作
`[name ... \name]` 脱糖为一次普通的调用:
[name %{ :parts @[ part1 part2 ... ] }]
- 块名通过常规命名空间链解析 —— 它就是一个函数。`html` 函数来自 `from "deft/stdext/html" import @{ html html-raw }`;你可以用同样的方式定义自己的块处理器。
- `:parts` 是一个由已求值部分组成的向量:
| 部分 | 形态 | 含义 |
|---|---|---|
| string | `"text"` | 字面块文本 —— 原样输出 |
| `$var` | `%__escape{:value ...}` | 插值 —— 应用领域转义 |
| trusted | `%__HtmlSafe{...}` / `%__SqlSafe{...}` | 已预转义 —— 直接通过 |
| list | `@[ ... ]` | 收集到的结果(例如 `for` 循环体)—— 递归并拼接 |
处理器的职责:遍历各部分,按各自领域转义并拼接。这就是全部契约 —— 参见定义自己的模板。
## `[html ... \html]`
主力工具。返回一个 `%__HtmlSafe{:value ...}` 带标签映射(一种知道自身可安全嵌入的普通字符串)。
from "deft/stdext/html" import @{ html html-raw html-escape } def render-user {| user^map | [html <div class="user"> <h1>$user~name</h1> <p>Age: $user~age</p> </div> \html] } def handler {|req res| http/html $res [render-user $alice] }
### 插值形式
字符串插值中的任何形式都可以在块内使用:
| 形式 | 示例 | 说明 |
|---|---|---|
| `$var` | `$name` | 变量或 `$user~field` 链 |
| `$var(args)` | `$xs(len)` | 后缀调用 |
| `$(expr)` | `$($price * $quantity)` | 中缀表达式 |
| `$[cmd]` | `$[upper $name]` | 前缀调用 —— 结果原样使用 |
| `[cmd]` | `[upper $name]` | 裸方括号调用同样会插值 |
字面量 `[` 需转义为 `\[`。
### 转义是显式的
块是原始模板:插值的 `$var` 值不转义直接通过。请显式转义动态文本:
- `[html-escape $text]` —— 实体转义(`&` `<` `>` `"` `'`),用于安全的文本插值。
- `[html-raw $markup]` —— 将字符串标记为受信任的 HTML(不转义)。
set bio "<b>bold</b>" [html <p>[html-escape $bio]</p> # <b>bold</b> <p>[html-raw $bio]</p> # <b>bold</b> \html]
任何被 `__HtmlSafe` 包装的内容(`[html ... \html]` 或 `[html-raw ...]` 的结果)都会原样通过嵌套块 —— 片段正是这样组合的。
### 片段与嵌套
返回 `[html ... \html]` 的函数可以直接嵌入外层块中:
def render-user {| user^map | [html <h1>$user~name</h1> \html] } [html <div class="card"> [render-user $user] </div> \html]
### 循环
块中带方括号的 `[for ...]` 会把每次迭代的循环体结果收集为一个列表部分,由处理器拼接。为每一项输出一个嵌套的 `[html ... \html]` 块:
def user-list {| users^list | [html <ul> [for u $users { [html <li>[html-escape $u~name]</li> \html] }] </ul> \html] }
### 原始块
`[*name ... \name]` 逐字捕获内部内容 —— 完全不做 `$var` 或 `[cmd]` 插值。适用于源码列表:
[*codeblock
set x $who
[str $x]
\codeblock] # $who stays literal; the block is syntax-highlighted
## 定义自己的模板
任何接受一个参数的函数都可以作为块处理器。块映射以带块名标签的 `%{ :parts @[...] }` 形式传入;按类型解包每个部分并应用你自己的转义规则。
def block-part-str {| part | if ([type $part] == "__escape") { # $var interpolation — apply your own escaping here [str [get $part :value]] } else if ([type $part] == "__HtmlSafe") or ([type $part] == "__SqlSafe") { # pre-escaped content — trust it [str [get $part :value]] } else if [is? "list" $part] { # collected loop bodies — recurse and join [join [collect [map $part {[block-part-str $it]}]] ""] } else { # literal text [str $part] } } def my-pill {| block^map | set body [join [collect [map [get $block :parts] {[block-part-str $it]}]] ""] [html <span class="pill">$body</span> \html] } [my-pill hi $who \my-pill] # → <span class="pill">hi world</span>
返回 `__HtmlSafe` 值(通过 `[html ... \html]` 或 `[html-raw ...]`),这样你的模板就能安全地嵌套在其他块中。stdext 包的 `html.dft` 和 `sql.dft` 是参考实现:
- `packages/stdext/src/html.dft` — `html`, `html-raw`, `html-escape`
- `packages/stdext/src/sql.dft` — `sql`, `sql-raw`, `sql-escape-value`
## `[sql ... \sql]` —— 另一个示例
同样的块机制,但带有 SQL 感知的转义。`html` 基于字符串,而 `sql` 按 Deft 类型转义值:
| 值 | SQL 字面量 |
|---|---|
| string | `'O''Brien'`(单引号包围,引号翻倍) |
| number | `42`, `3.14` |
| boolean | `TRUE` / `FALSE` |
| nil | `NULL` |
| list | `(1, 2, 'text')` |
| map | `"col" = 'val' AND ...`(WHERE 风格) |
from "deft/stdext/sql" import @{ sql sql-raw } set name "O'Brien" set id 42 set active true set query [sql SELECT * FROM users WHERE name = $name AND id = $id AND active = $active \sql] # SELECT * FROM users # WHERE name = 'O''Brien' AND id = 42 AND active = TRUE
对于类型感知转义器会破坏的受信任片段 —— 函数调用、标识符、运算符 —— 请使用 `[sql-raw ...]`:
[sql SELECT * FROM users WHERE updated_at > [sql-raw "NOW() - INTERVAL '1 day'"] \sql]
与 `sqlite/query` 自然搭配:
set rows [sqlite/query $db [sql SELECT * FROM users WHERE name = $name \sql]]
## 快速参考
| 形式 | 用途 |
|---|---|
| `[html ... \html]` | 渲染 HTML 片段;返回 `__HtmlSafe` |
| `[html-raw $x]` | 将字符串标记为受信任的 HTML |
| `[html-escape $x]` | 为安全插值对文本做实体转义 |
| `[sql ... \sql]` | 构建带类型感知值转义的 SQL 字符串 |
| `[sql-raw $x]` | 将片段标记为受信任的 SQL |
| `[*name ... \name]` | 任意块,逐字捕获(无插值) |
| `[myname ... \myname]` | 你自己的块处理器 —— 任意单参数函数 |