Refactor to support custom escaping schemes

This commit is contained in:
Michael Pfaff 2024-03-15 18:05:31 -04:00
parent d97885392b
commit 6e09ca7464
68 changed files with 753 additions and 2204 deletions

432
Cargo.lock generated
View File

@ -1,432 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "basic-toml"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bfc506e7a2370ec239e1d072507b2a80c833083699d3c6fa176fbb4de8448c6"
dependencies = [
"serde",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "filetime"
version = "0.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"windows-sys",
]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "hashbrown"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12"
[[package]]
name = "home"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
dependencies = [
"windows-sys",
]
[[package]]
name = "indexmap"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "integration-tests"
version = "0.8.3"
dependencies = [
"pretty_assertions",
"sailfish",
"sailfish-compiler",
"sailfish-macros",
"serde_json",
"trybuild",
]
[[package]]
name = "itoa"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "itoap"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9028f49264629065d057f340a86acb84867925865f73bbf8d47b4d149a7e88b8"
[[package]]
name = "libc"
version = "0.2.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b"
[[package]]
name = "memchr"
version = "2.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
[[package]]
name = "once_cell"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "pretty_assertions"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro2"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
"bitflags",
]
[[package]]
name = "ryu"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
[[package]]
name = "sailfish"
version = "0.8.3"
dependencies = [
"itoap",
"ryu",
"sailfish-macros",
"serde",
"serde_json",
"version_check",
]
[[package]]
name = "sailfish-compiler"
version = "0.8.3"
dependencies = [
"filetime",
"home",
"memchr",
"pretty_assertions",
"proc-macro2",
"quote",
"serde",
"syn",
"toml",
]
[[package]]
name = "sailfish-macros"
version = "0.8.3"
dependencies = [
"proc-macro2",
"sailfish-compiler",
]
[[package]]
name = "serde"
version = "1.0.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
[[package]]
name = "syn"
version = "2.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termcolor"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
dependencies = [
"winapi-util",
]
[[package]]
name = "toml"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "trybuild"
version = "1.0.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "196a58260a906cedb9bf6d8034b6379d0c11f552416960452f267402ceeddff1"
dependencies = [
"basic-toml",
"glob",
"once_cell",
"serde",
"serde_derive",
"serde_json",
"termcolor",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "winnow"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c"
dependencies = [
"memchr",
]
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"

View File

@ -40,7 +40,7 @@ Template file (templates/hello.stpl):
<html>
<body>
<% for msg in &messages { %>
<div><%= msg %></div>
<div><%\html msg %></div>
<% } %>
</body>
</html>

View File

@ -8,7 +8,7 @@ Create a new directory named `templates` in the same directory as `Cargo.toml`.
<html>
<body>
<% for msg in &messages { %>
<div><%= msg %></div>
<div><%\html msg %></div>
<% } %>
</body>
</html>

View File

@ -6,7 +6,7 @@ You can control the rendering behaviour via `template` attribute.
```rust
#[derive(RenderOnce)]
#[template(path = "template.stpl", escape = false)]
#[template(path = "template.stpl", rm_whitespace = true)]
struct TemplateStruct {
...
}
@ -15,7 +15,6 @@ struct TemplateStruct {
`template` attribute accepts the following options.
- `path`: path to template file. This options is always required.
- `escape`: Enable HTML escaping (default: `true`)
- `delimiter`: Replace the '%' character used for the tag delimiter (default: '%')
- `rm_whitespace`: try to strip whitespaces as much as possible without collapsing HTML structure (default: `false`). This option might not work correctly if your templates have inline `script` tag.
@ -51,7 +50,6 @@ Configuration files are written in the TOML 0.5 format. Here is the default conf
```toml
template_dirs = ["templates"]
escape = true
delimiter = "%"
[optimizations]

View File

@ -7,7 +7,7 @@ Example:
=== "Template"
``` rhtml
message: <%= "foo\nbar" | dbg %>
message: <%\html "foo\nbar" | dbg %>
```
=== "Result"
@ -17,23 +17,22 @@ Example:
```
!!! Note
Since `dbg` filter accepts `<T: std::fmt::Debug>` types, that type isn't required to implement [`Render`](https://docs.rs/sailfish/latest/sailfish/runtime/trait.Render.html) trait. That means you can pass the type which doesn't implement `Render` trait.
Since `dbg` filter accepts `<T: std::fmt::Debug>` types, that type isn't required to implement [`Render`](https://docs.rs/sailfish/latest/sailfish/runtime/trait.Render.html) trait. That means you can pass the type which doesn't implement `Render` trait.
## Syntax
- Apply filter and HTML escaping
- Apply filter and HTML escaping
``` rhtml
<%= expression | filter %>
```rhtml
<%\html expression | filter %>
```
- Apply filter only
- Apply filter only
``` rhtml
```rhtml
<%- expression | filter %>
```
## Built-In Filters
Built-In filters can be found in [`sailfish::runtime::filter`](https://docs.rs/sailfish/latest/sailfish/runtime/filter/index.html) module.
Built-In filters can be found in [`sailfish::filter`](https://docs.rs/sailfish/latest/sailfish/runtime/filter/index.html) module.

View File

@ -3,7 +3,7 @@
## Tags
- `<% %>`: Inline tag, you can write Rust code inside this tag
- `<%= %>`: Evaluate the Rust expression and outputs the value into the template (HTML escaped)
- `<%\mode %>`: Evaluate the Rust expression and outputs the value into the template, escaped according to `mode`
- `<%- %>`: Evaluate the Rust expression and outputs the unescaped value into the template
- `<%# %>`: Comment tag
- `<%%`: Outputs a literal '<%'
@ -20,7 +20,7 @@
```rhtml
<% for (i, msg) in messages.iter().enumerate() { %>
<div><%= i %>: <%= msg %></div>
<div><%\html i %>: <%\html msg %></div>
<% } %>
```
@ -33,12 +33,12 @@
## Filters
```rhtml
<%= message | upper %>
<%- message | upper %>
```
```rhtml
```json
{
"id": <%= id %>
"comment": <%- comment | json %>
"id": <%- id %>,
"comment": "<%\json comment %>"
}
```

View File

@ -16,7 +16,7 @@ You can write Rust statement inside `<% %>` tag.
}
}
%>
<div>total = <%= total %></div>
<div>total = <%\html total %></div>
```
=== "Result"
@ -65,12 +65,12 @@ Although almost all Rust statement is supported, the following statements inside
## Evaluation block
Rust expression inside `<%= %>` tag is evaluated and the result will be rendered.
Rust expression inside `<%\mode %>` tag is evaluated and the result will be rendered.
=== "Template"
``` rhtml
<% let a = 1; %><%= a + 2 %>
<% let a = 1; %><%\html a + 2 %>
```
=== "Result"
@ -81,7 +81,7 @@ Rust expression inside `<%= %>` tag is evaluated and the result will be rendered
If the result contains `&"'<>` characters, sailfish replaces these characters with the equivalent html.
If you want to render the results without escaping, you can use `<%- %>` tag or [configure sailfish to not escape by default](../options.md).
If you want to render the results without escaping, you can use `<%- %>` tag.
=== "Template"
@ -103,5 +103,5 @@ If you want to render the results without escaping, you can use `<%- %>` tag or
Evaluation block does not return any value, so you cannot use the block to pass the render result to another code block. The following code is invalid.
``` rhtml
<% let result = %><%= 1 %><% ; %>
<% let result = %><%\html 1 %><% ; %>
```

File diff suppressed because one or more lines are too long

View File

@ -75,10 +75,10 @@ However, since it is a corner case, It may be better if we provide `no_std=false
We must ensure that all of the data passed to templates should satisfy the following restrictions.
- completely immutable
- does not allocate/deallocate memory
- can be serialized to/deserialized from byte array (All data is serialized to byte array, and then decoded inside templates)
- can be defined inside `#![no_std]` crate
- completely immutable
- does not allocate/deallocate memory
- can be serialized to/deserialized from byte array (All data is serialized to byte array, and then decoded inside templates)
- can be defined inside `#![no_std]` crate
Sailfish provide `TemplateData` trait which satisfies the above restrictions.
@ -94,12 +94,12 @@ pub unsafe trait TemplateData {
This trait can be implemented to the following types
- String,
- Primitive integers (bool, char, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, isize, usize)
- [T; N] where T: TemplateData
- (T1, T2, T3, ...) where T1, T2, T3, ... : TemplateData
- Option\<T\> where T: TemplateData
- Vec\<T\> where T: TemplateData
- String,
- Primitive integers (bool, char, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, isize, usize)
- [T; N] where T: TemplateData
- (T1, T2, T3, ...) where T1, T2, T3, ... : TemplateData
- Option\<T\> where T: TemplateData
- Vec\<T\> where T: TemplateData
### `#[derive(TemplateData)]` attribute
@ -116,7 +116,7 @@ Template file contents is transformed into Rust code when `sailfish::dynamic::co
For example, if we have a template
```html
<h1><%= msg %></h1>
<h1><%\html msg %></h1>
```
and Rust code
@ -154,7 +154,7 @@ pub extern fn sf_message(version: u64, data: *const [u8], data_len: usize, vtabl
let Message { msg } = deserialize(&mut data);
let mut buf = VBuffer::from_vtable(vtable);
static SIZE_HINT = SizeHint::new();
let size_hint = SIZE_HINT.get();
buf.reserve(size_hint);
@ -178,11 +178,11 @@ pub extern fn sf_message(version: u64, data: *const [u8], data_len: usize, vtabl
Template:
```html
<!DOCTYPE html>
<!doctype html>
<html>
<body>
<b><%= name %></b>: <%= score %>
</body>
<body>
<b><%\html name %></b>: <%\html score %>
</body>
</html>
```

View File

@ -29,7 +29,9 @@ impl<T: Template> Display for T {
If you derive this trait, you cannot move out the struct fields. For example, the following template
```html
<% for msg in messages { %><div><%= msg %></div><% } %>
<% for msg in messages { %>
<div><%\html msg %></div>
<% } %>
```
will be transformed into the Rust code like

1246
examples/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ struct Greet<'a> {
async fn greet(req: HttpRequest) -> actix_web::Result<HttpResponse> {
let name = req.match_info().get("name").unwrap_or("World");
let body = Greet { name }
.render_once()
.render_once_to_string()
.map_err(|e| InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?;
Ok(HttpResponse::Ok()

View File

@ -12,5 +12,5 @@ fn main() {
title: "Website".to_owned(),
name: "Hanako".to_owned(),
};
println!("{}", ctx.render_once().unwrap());
println!("{}", ctx.render_once_to_string().unwrap());
}

View File

@ -8,5 +8,5 @@ struct Simple {
fn main() {
let messages = vec![String::from("Message 1"), String::from("<Message 2>")];
println!("{}", Simple { messages }.render_once().unwrap());
println!("{}", Simple { messages }.render_once_to_string().unwrap());
}

View File

@ -1,5 +1,5 @@
<html>
<body>
Hello <%= name %>!
Hello <%\html name %>!
</body>
</html>

View File

@ -2,4 +2,4 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="format-detection" content="telephone=no">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<title><%= title %></title>
<title><%\html &title %></title>

View File

@ -4,7 +4,7 @@
<% include!("header.stpl"); %>
</head>
<body>
<h1><%= title %></h1>
Hello, <%= name %>!
<h1><%\html title %></h1>
Hello, <%\html name %>!
</body>
</html>

View File

@ -6,7 +6,7 @@
<% if i == 0 { %>
<h1>Hello, world!</h1>
<% } %>
<div><%= *msg %></div>
<div><%\html msg %></div>
<% } %>
</body>
</html>

View File

@ -34,7 +34,7 @@ impl Compiler {
fn translate_file_contents(&self, input: &Path) -> Result<TranslatedSource, Error> {
let parser = Parser::new().delimiter(self.config.delimiter);
let translator = Translator::new().escape(self.config.escape);
let translator = Translator::new();
let content = read_to_string(input)
.chain_err(|| format!("Failed to open template file: {:?}", input))?;
@ -114,7 +114,7 @@ impl Compiler {
});
let parser = Parser::new().delimiter(self.config.delimiter);
let translator = Translator::new().escape(self.config.escape);
let translator = Translator::new();
let resolver = Resolver::new().include_handler(include_handler);
let optimizer = Optimizer::new().rm_whitespace(self.config.rm_whitespace);

View File

@ -3,7 +3,6 @@ use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Hash)]
pub struct Config {
pub delimiter: char,
pub escape: bool,
pub rm_whitespace: bool,
pub template_dirs: Vec<PathBuf>,
#[doc(hidden)]
@ -17,7 +16,6 @@ impl Default for Config {
Self {
template_dirs: Vec::new(),
delimiter: '%',
escape: true,
cache_dir: Path::new(env!("OUT_DIR")).join("cache"),
rm_whitespace: false,
_non_exhaustive: (),
@ -74,10 +72,6 @@ mod imp {
config.delimiter = delimiter;
}
if let Some(escape) = config_file.escape {
config.escape = escape;
}
if let Some(optimizations) = config_file.optimizations {
if let Some(rm_whitespace) = optimizations.rm_whitespace {
config.rm_whitespace = rm_whitespace;
@ -103,7 +97,6 @@ mod imp {
struct ConfigFile {
template_dirs: Option<Vec<String>>,
delimiter: Option<char>,
escape: Option<bool>,
optimizations: Option<Optimizations>,
}

View File

@ -115,24 +115,32 @@ impl fmt::Display for Error {
writeln!(f, "file: {}", source_file.display())?;
}
if let (Some(ref source), Some(offset)) = (source, self.offset) {
let (lineno, colno) = into_line_column(source, offset);
writeln!(f, "position: line {}, column {}\n", lineno, colno)?;
if let Some(ref source) = source {
if let Some(offset) = self.offset {
let (lineno, colno) = into_line_column(source, offset);
writeln!(f, "position: line {}, column {}\n", lineno, colno)?;
// TODO: display adjacent lines
let line = source.lines().nth(lineno - 1).unwrap();
let lpad = count_digits(lineno);
// TODO: display adjacent lines
let line = source.lines().nth(lineno - 1).unwrap();
let lpad = count_digits(lineno);
writeln!(f, "{:<lpad$} |", "", lpad = lpad)?;
writeln!(f, "{} | {}", lineno, line)?;
writeln!(
f,
"{:<lpad$} | {:<rpad$}^",
"",
"",
lpad = lpad,
rpad = colno - 1
)?;
writeln!(f, "{:<lpad$} |", "", lpad = lpad)?;
writeln!(f, "{} | {}", lineno, line)?;
writeln!(
f,
"{:<lpad$} | {:<rpad$}^",
"",
"",
lpad = lpad,
rpad = colno - 1
)?;
} else {
let line_count = source.lines().count();
let lpad = count_digits(line_count);
for (i, line) in source.lines().enumerate() {
writeln!(f, "{:>lpad$} | {line}", i + 1, lpad = lpad)?;
}
}
}
Ok(())

View File

@ -1,12 +1,11 @@
use proc_macro2::Span;
use quote::{quote, ToTokens};
use syn::parse::{Parse, ParseStream, Result as ParseResult};
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::visit_mut::VisitMut;
use syn::{
Block, Expr, ExprBreak, ExprCall, ExprContinue, ExprLit, ExprPath, Ident, Lit,
LitStr, Stmt, Token,
LitStr, Stmt,
};
pub struct Optimizer {

View File

@ -55,8 +55,8 @@ impl Default for Parser {
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TokenKind {
BufferedCode { escape: bool },
pub enum TokenKind<'a> {
BufferedCode { escape: Option<&'a str> },
Code,
Comment,
Text,
@ -66,12 +66,12 @@ pub enum TokenKind {
pub struct Token<'a> {
content: &'a str,
offset: usize,
kind: TokenKind,
kind: TokenKind<'a>,
}
impl<'a> Token<'a> {
#[inline]
pub fn new(content: &'a str, offset: usize, kind: TokenKind) -> Token<'a> {
pub fn new(content: &'a str, offset: usize, kind: TokenKind<'a>) -> Token<'a> {
Token {
content,
offset,
@ -153,15 +153,21 @@ impl<'a> ParseStream<'a> {
token_kind = TokenKind::Comment;
start += 1;
}
Some(b'=') => {
token_kind = TokenKind::BufferedCode { escape: true };
Some(b'\\') => {
start += 1;
let (mode, _) = self.source[start..]
.split_once(' ')
.ok_or_else(|| self.error("Invalid syntax for escaped render"))?;
start += mode.len();
token_kind = TokenKind::BufferedCode { escape: Some(mode) };
}
Some(b'-') => {
token_kind = TokenKind::BufferedCode { escape: false };
token_kind = TokenKind::BufferedCode { escape: None };
start += 1;
}
_ => {}
Some(b' ') => {}
Some(b'%') if self.source[start..] == self.block_delimiter.1 => {}
_ => return Err(self.error("Invalid block syntax")),
}
// skip whitespaces
@ -424,7 +430,7 @@ mod tests {
Token {
content: "inner | upper",
offset: 10,
kind: TokenKind::BufferedCode { escape: false },
kind: TokenKind::BufferedCode { escape: None },
},
Token {
content: " outer",
@ -437,7 +443,8 @@ mod tests {
#[test]
fn non_ascii_delimiter() {
let src = r##"foo <🍣# This is a comment #🍣> bar <🍣= r"🍣>" 🍣> baz <🍣🍣"##;
let src =
r##"foo <🍣# This is a comment #🍣> bar <🍣\html r"🍣>" 🍣> baz <🍣🍣"##;
let parser = Parser::new().delimiter('🍣');
let tokens = parser.parse(src).into_vec().unwrap();
assert_eq!(
@ -460,17 +467,19 @@ mod tests {
},
Token {
content: "r\"🍣>\"",
offset: 47,
kind: TokenKind::BufferedCode { escape: true }
offset: 51,
kind: TokenKind::BufferedCode {
escape: Some("html")
}
},
Token {
content: " baz ",
offset: 61,
offset: 65,
kind: TokenKind::Text
},
Token {
content: "<🍣",
offset: 66,
offset: 70,
kind: TokenKind::Text
},
]
@ -479,7 +488,7 @@ mod tests {
#[test]
fn comment_inside_block() {
let src = "<% // %>\n %><%= /* %%>*/ 1 %>";
let src = "<% // %>\n %><%\\html /* %%>*/ 1 %>";
let parser = Parser::new();
let tokens = parser.parse(src).into_vec().unwrap();
assert_eq!(
@ -492,8 +501,10 @@ mod tests {
},
Token {
content: "/* %%>*/ 1",
offset: 16,
kind: TokenKind::BufferedCode { escape: true }
offset: 20,
kind: TokenKind::BufferedCode {
escape: Some("html")
}
},
]
);

View File

@ -22,7 +22,6 @@ struct DeriveTemplateOptions {
found_keys: Vec<Ident>,
path: Option<LitStr>,
delimiter: Option<LitChar>,
escape: Option<LitBool>,
rm_whitespace: Option<LitBool>,
}
@ -45,8 +44,6 @@ impl DeriveTemplateOptions {
self.path = Some(s.parse::<LitStr>()?);
} else if key == "delimiter" {
self.delimiter = Some(s.parse::<LitChar>()?);
} else if key == "escape" {
self.escape = Some(s.parse::<LitBool>()?);
} else if key == "rm_whitespace" {
self.rm_whitespace = Some(s.parse::<LitBool>()?);
} else {
@ -75,9 +72,6 @@ fn merge_config_options(config: &mut Config, options: &DeriveTemplateOptions) {
if let Some(ref delimiter) = options.delimiter {
config.delimiter = delimiter.value();
}
if let Some(ref escape) = options.escape {
config.escape = escape.value;
}
if let Some(ref rm_whitespace) = options.rm_whitespace {
config.rm_whitespace = rm_whitespace.value;
}
@ -358,7 +352,7 @@ fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error>
let tokens = quote! {
impl #impl_generics sailfish::RenderOnce for #name #ty_generics #where_clause {
#inline
fn render_once(self, __sf_buf: &mut sailfish::runtime::Buffer) -> std::result::Result<(), sailfish::runtime::RenderError> {
fn render_once(self, __sf_buf: &mut sailfish::Buffer) -> std::result::Result<(), sailfish::RenderError> {
// This line is required for cargo to track child templates
#include_bytes_seq;
use sailfish::runtime as __sf_rt;
@ -369,7 +363,7 @@ fn derive_template_impl(tokens: TokenStream) -> Result<TokenStream, syn::Error>
}
#[inline]
fn render_once_escaped(self, b: &mut sailfish::runtime::Buffer) -> std::result::Result<(), sailfish::runtime::RenderError> {
fn render_once_escaped<E: sailfish::Escape>(self, b: &mut sailfish::Buffer, _: &E) -> std::result::Result<(), sailfish::RenderError> {
self.render_once(b)
}
}

View File

@ -7,21 +7,20 @@ use crate::error::*;
use crate::parser::{ParseStream, Token, TokenKind};
// translate tokens into Rust code
#[derive(Clone, Debug, Default)]
pub struct Translator {
escape: bool,
#[derive(Clone, Debug)]
pub struct Translator {}
impl Default for Translator {
#[inline]
fn default() -> Self {
Self::new()
}
}
impl Translator {
#[inline]
pub fn new() -> Self {
Self { escape: true }
}
#[inline]
pub fn escape(mut self, new: bool) -> Self {
self.escape = new;
self
Self {}
}
pub fn translate<'a>(
@ -30,7 +29,7 @@ impl Translator {
) -> Result<TranslatedSource, Error> {
let original_source = token_iter.original_source;
let mut ps = SourceBuilder::new(self.escape);
let mut ps = SourceBuilder::new();
ps.reserve(original_source.len());
ps.feed_tokens(token_iter)?;
@ -77,15 +76,13 @@ impl SourceMap {
}
struct SourceBuilder {
escape: bool,
source: String,
source_map: SourceMap,
}
impl SourceBuilder {
fn new(escape: bool) -> SourceBuilder {
fn new() -> SourceBuilder {
SourceBuilder {
escape,
source: String::from("{\n"),
source_map: SourceMap::default(),
}
@ -133,7 +130,7 @@ impl SourceBuilder {
fn write_buffered_code<'a>(
&mut self,
token: &Token<'a>,
escape: bool,
escape: Option<&str>,
) -> Result<(), Error> {
self.write_buffered_code_with_suffix(token, escape, "")
}
@ -141,7 +138,7 @@ impl SourceBuilder {
fn write_buffered_code_with_suffix<'a>(
&mut self,
token: &Token<'a>,
escape: bool,
escape: Option<&str>,
suffix: &str,
) -> Result<(), Error> {
// parse and split off filter
@ -151,7 +148,7 @@ impl SourceBuilder {
err.offset = into_offset(token.as_str(), span).map(|p| token.offset() + p);
err
})?;
let method = if self.escape && escape {
let method = if escape.is_some() {
"render_escaped"
} else {
"render"
@ -171,7 +168,7 @@ impl SourceBuilder {
),
};
self.source.push_str("sailfish::runtime::filter::");
self.source.push_str("__sf_rt::filter::");
self.source.push_str(&*name);
self.source.push('(');
@ -199,6 +196,12 @@ impl SourceBuilder {
self.source.push_str(suffix);
}
if let Some(escape) = escape {
self.source.push_str(", &__sf_rt::esc_");
self.source.push_str(escape);
self.source.push_str("()");
}
self.source.push_str(")?;\n");
Ok(())
@ -257,7 +260,7 @@ impl SourceBuilder {
let mut err =
make_error!(ErrorKind::RustSyntaxError(synerr), source = self.source);
err.offset = original_offset;
// err.offset = original_offset;
Err(err)
}
@ -365,11 +368,10 @@ mod tests {
#[test]
#[ignore]
fn translate() {
let src = "<% pub fn sample() { %> <%% <%=//%>\n1%><% } %>";
let src = "<% pub fn sample() { %> <%% <%\\html //%>\n1%><% } %>";
let lexer = Parser::new();
let token_iter = lexer.parse(src);
let mut ps = SourceBuilder {
escape: true,
source: String::with_capacity(token_iter.original_source.len()),
source_map: SourceMap::default(),
};
@ -383,7 +385,6 @@ mod tests {
let lexer = Parser::new();
let token_iter = lexer.parse(src);
let mut ps = SourceBuilder {
escape: true,
source: String::with_capacity(token_iter.original_source.len()),
source_map: SourceMap::default(),
};
@ -405,7 +406,6 @@ mod tests {
let lexer = Parser::new();
let token_iter = lexer.parse(src);
let mut ps = SourceBuilder {
escape: true,
source: String::with_capacity(token_iter.original_source.len()),
source_map: SourceMap::default(),
};
@ -417,7 +417,7 @@ mod tests {
.ast
.into_token_stream()
.to_string(),
r#"{ __sf_rt :: render_text (__sf_buf , "outer ") ; __sf_rt :: render (__sf_buf , sailfish :: runtime :: filter :: upper ((inner))) ? ; __sf_rt :: render_text (__sf_buf , " outer") ; }"#
r#"{ __sf_rt :: render_text (__sf_buf , "outer ") ; __sf_rt :: render (__sf_buf , __sf_rt :: filter :: upper ((inner))) ? ; __sf_rt :: render_text (__sf_buf , " outer") ; }"#
);
}
}

View File

@ -1 +1 @@
<h1><%= message %></h1>
<h1><%\html message %></h1>

View File

@ -2,7 +2,7 @@
<% for i in 1..=9 %>
<tr>
<% for j in 1..=9 %>
<td><%= i * j %></td>
<td><%\html i * j %></td>
<% } %>
</td>
<% } %>

View File

@ -1,6 +1,5 @@
template_dirs = ["../templates"]
escape = true
delimiter = "%"
[optimizations]
rm_whitespace = false
rm_whitespace = false

View File

@ -1 +1 @@
<%= 1 + /* 10 + %> /* 100 + */ 1000 %> + */ 10000 %>
<%\html 1 + /* 10 + %> /* 100 + */ 1000 %> + */ 10000 %>

View File

@ -2,7 +2,7 @@
<% for i in 0..10 { %>
<div>head</div>
<% if i < 2 { continue; } %>
<div><%= i %></div>
<div><%\html i %></div>
<% if i > 5 { break; } %>
<div>tail</div>
<% } %>

View File

@ -1 +1 @@
<🍣 let i = 10; 🍣><div>i: <🍣= i 🍣></div>
<🍣 let i = 10; 🍣><div>i: <🍣\html i 🍣></div>

View File

@ -1,4 +1,4 @@
disp: <%- message | disp %>
dbg: <%- message | dbg %>
disp escaped: <%= message | disp %>
dbg escaped: <%= message | dbg %>
disp escaped: <%\html message | disp %>
dbg escaped: <%\html message | dbg %>

View File

@ -1,3 +1,3 @@
trim: <%= " <html> " | trim %>
lower: <%= "aBc" | lower %>
upper: <%= "aBc" | upper %>
trim: <%\html " <html> " | trim %>
lower: <%\html "aBc" | lower %>
upper: <%\html "aBc" | upper %>

View File

@ -1 +1 @@
<% for i in 0..10 { %><%= format!("{:02}", i) %><% } %>
<% for i in 0..10 { %><%\html format_args!("{:02}", i) %><% } %>

View File

@ -1 +1 @@
<% let a = include!("includes/rust.rs"); %><%= a %>
<% let a = include!("includes/rust.rs"); %><%\html a %>

View File

@ -1 +1 @@
INCLUDED: <%= s %>
INCLUDED: <%\html s %>

View File

@ -1,4 +1,4 @@
{
"name": <%- name | dbg %>,
"value": <%= value %>
"value": <%- value %>
}

View File

@ -5,9 +5,9 @@
<span>3</span>
</div>
<div>
trailing spaces
trailing spaces
This line should be appeared under the previous line
</div>
<% for msg in messages { %>
<div><%= msg %></div>
<div><%\html msg %></div>
<% } %>

View File

@ -4,7 +4,7 @@
<body>
<table>
<tr><th>id</th><th>message</th></tr>
<% for item in items { %><tr><td><%= item.id %></td><td><%= item.message %></td></tr><% } %>
<% for item in items { %><tr><td><%\html item.id %></td><td><%\html item.message %></td></tr><% } %>
</table>
</body>
</html>

View File

@ -7,7 +7,6 @@ fn read_config() {
let config = Config::search_file_and_read(&*path).unwrap();
assert_eq!(config.delimiter, '%');
assert_eq!(config.escape, true);
assert_eq!(config.rm_whitespace, false);
assert_eq!(config.template_dirs.len(), 1);
}

View File

@ -2,7 +2,7 @@ use sailfish::RenderOnce;
use sailfish_macros::RenderOnce;
#[derive(RenderOnce)]
#[template(path = "foo.stpl", escape = 1)]
#[template(path = "foo.stpl", rm_whitespace = 1)]
struct InvalidOptionValue {
name: String,
}

View File

@ -1,8 +1,8 @@
error: expected boolean literal
--> $DIR/invalid_option_value.rs:5:38
|
5 | #[template(path = "foo.stpl", escape=1)]
| ^
5 | #[template(path = "foo.stpl", rm_whitespace = 1)]
| ^
error[E0599]: no method named `render_once` found for struct `InvalidOptionValue` in the current scope
--> $DIR/invalid_option_value.rs:11:69

View File

@ -2,8 +2,8 @@ use sailfish::RenderOnce;
use sailfish_macros::RenderOnce;
#[derive(RenderOnce)]
#[template(path = "foo.stpl", escape = true)]
#[template(escape = false)]
#[template(path = "foo.stpl", rm_whitespace = true)]
#[template(rm_whitespace = false)]
struct InvalidOptionValue {
name: String,
}

View File

@ -1,8 +1,8 @@
error: Argument `escape` was repeated.
--> $DIR/repeated_arguments.rs:6:12
|
6 | #[template(escape = false)]
| ^^^^^^
6 | #[template(rm_whitespace = false)]
| ^^^^^^^^^^^^^
error[E0599]: no method named `render_once` found for struct `InvalidOptionValue` in the current scope
--> $DIR/repeated_arguments.rs:12:69

View File

@ -1,3 +1,3 @@
<% for player in &players %>
<div><%= player.name %>: <%= player.score %></div>
<div><%\html player.name %>: <%\html player.score %></div>
<% } %>

View File

@ -1,5 +1,5 @@
<html>
<body>
<%= content
<%\html content
</body>
</html>

View File

@ -1,4 +1,4 @@
{
"name": "<%= name %>",
"content": <% =content %>
"name": "<%\json name %>",
"content": <% \jsoncontent %>
}

View File

@ -5,7 +5,7 @@ file: unclosed_delimiter.stpl
position: line 3, column 5
|
3 | <%= content
3 | <%\html content
| ^
--> $DIR/unclosed_delimter.rs:4:10

View File

@ -9,7 +9,6 @@ struct Content<'a> {
#[derive(RenderOnce)]
#[template(path = "unexpected_token.stpl")]
#[template(escape = false)]
struct UnexpectedToken<'a> {
name: &'a str,
content: Content<'a>,

View File

@ -1,9 +1,5 @@
#[macro_use]
extern crate sailfish_macros;
use integration_tests::assert_string_eq;
use sailfish::runtime::RenderResult;
use sailfish::RenderOnce;
use sailfish::{RenderOnce, RenderResult};
use std::path::PathBuf;
fn assert_render_result(name: &str, result: RenderResult) {
@ -220,7 +216,7 @@ fn test_rust_macro() {
}
#[derive(RenderOnce)]
#[template(path = "formatting.stpl", escape = false)]
#[template(path = "formatting.stpl")]
struct Formatting;
#[test]

View File

@ -24,6 +24,7 @@ json = ["serde", "serde_json"]
perf-inline = []
[dependencies]
array-vec = { git = "https://git.pfaff.dev/michael/array-vec.rs" }
itoap = "1.0.1"
ryu = "1.0.13"
serde = { version = "1.0.159", optional = true }

89
sailfish/src/escape.rs Normal file
View File

@ -0,0 +1,89 @@
use array_vec::ArrayStr;
use super::Buffer;
/// A scheme for escaping strings.
pub trait Escape {
/// The type of an escaped character.
type Escaped: AsRef<str>;
/// True if `true` and `false` will never need escaping.
const IDENT_BOOLS: bool = false;
/// True if unsigned integers will never need escaping.
const IDENT_UINTS: bool = false;
/// True if signed integers will never need escaping.
const IDENT_INTS: bool = false;
/// True if floats (using [`ryu`]'s formatting) will never need escaping.
const IDENT_FLOATS: bool = false;
/// If the character needs to be escaped, does so and returns it as a string. Otherwise,
/// returns `None`.
fn escape(&self, c: char) -> Option<Self::Escaped>;
/// Writes the `string` to the `buffer`, applying any necessary escaping.
#[inline]
fn escape_to_buf(&self, buffer: &mut Buffer, string: &str) {
buffer.reserve(string.len());
let mut i = 0;
for (j, c) in string.char_indices() {
if let Some(rep) = self.escape(c) {
buffer.push_str(&string[i..j]);
buffer.push_str(rep.as_ref());
i = j + c.len_utf8();
}
}
}
/// Writes the `string` to the `buffer`, applying any necessary escaping.
///
/// # Examples
///
/// ```
/// use sailfish::{Escape, EscapeHtml};
///
/// let mut buf = String::new();
/// EscapeHtml.escape_to_string(&mut buf, "<h1>Hello, world!</h1>");
/// assert_eq!(buf, "&lt;h1&gt;Hello, world!&lt;/h1&gt;");
/// ```
#[inline]
fn escape_to_string(&self, buffer: &mut String, string: &str) {
let mut buf = Buffer::from(std::mem::take(buffer));
self.escape_to_buf(&mut buf, string);
*buffer = buf.into_string();
}
}
/// A scheme for escaping strings for safe insertion into JSON strings.
pub struct EscapeJsonString;
impl Escape for EscapeJsonString {
type Escaped = ArrayStr<4>;
const IDENT_BOOLS: bool = true;
const IDENT_UINTS: bool = true;
const IDENT_INTS: bool = true;
const IDENT_FLOATS: bool = true;
#[inline]
fn escape(&self, c: char) -> Option<Self::Escaped> {
match c {
'"' => Some(ArrayStr::try_from(r#"\""#).unwrap()),
'\\' => Some(ArrayStr::try_from(r"\\").unwrap()),
'\u{0000}'..='\u{001F}' => {
let c = c as u8;
let mut s = ArrayStr::try_from(r"\u").unwrap();
unsafe {
const HEX_DIGITS: [u8; 16] = *b"0123456789ABCDEF";
// SAFETY: we only write valid UTF-8
let arr = s.data_mut();
arr.unused_mut()[0].write(HEX_DIGITS[usize::from(c >> 4)]);
arr.unused_mut()[1].write(HEX_DIGITS[usize::from(c & 0xF)]);
// SAFETY: we just initialized the last 2 bytes
arr.set_len(arr.len() + 2);
}
Some(s)
}
_ => None,
}
}
}

View File

@ -3,9 +3,7 @@
use std::fmt;
use std::ptr;
use super::escape;
use super::render::RenderOnce;
use super::{Buffer, Render, RenderError};
use crate::{Buffer, Escape, Render, RenderError, RenderOnce};
/// Helper struct for 'display' filter
#[derive(Clone, Copy)]
@ -15,7 +13,7 @@ impl<T: fmt::Display> Render for Display<T> {
fn render(&self, b: &mut Buffer) -> Result<(), RenderError> {
use fmt::Write;
write!(b, "{}", self.0).map_err(|e| RenderError::from(e))
write!(b, "{}", self.0).map_err(RenderError::from)
}
}
@ -24,7 +22,7 @@ impl<T: fmt::Display> Render for Display<T> {
/// # Examples
///
/// ```text
/// filename: <%= filename.display() | disp %>
/// filename: <%\html filename.display() | disp %>
/// ```
#[inline]
pub fn disp<T: fmt::Display>(expr: T) -> Display<T> {
@ -39,7 +37,7 @@ impl<T: fmt::Debug> Render for Debug<T> {
fn render(&self, b: &mut Buffer) -> Result<(), RenderError> {
use fmt::Write;
write!(b, "{:?}", self.0).map_err(|e| RenderError::from(e))
write!(b, "{:?}", self.0).map_err(RenderError::from)
}
}
@ -47,14 +45,14 @@ impl<T: fmt::Debug> Render for Debug<T> {
///
/// # Examples
///
/// The following examples produce exactly same results, but former is a bit faster
/// The following examples produce exactly same results, but former is faster
///
/// ```text
/// table content: <%= table | dbg %>
/// table content: <%\html table | dbg %>
/// ```
///
/// ```text
/// table content: <%= format!("{:?}", table) %>
/// table content: <%\html format!("{:?}", table) %>
/// ```
#[inline]
pub fn dbg<T: fmt::Debug>(expr: T) -> Debug<T> {
@ -66,6 +64,7 @@ pub fn dbg<T: fmt::Debug>(expr: T) -> Debug<T> {
pub struct Upper<T>(T);
impl<T: RenderOnce> RenderOnce for Upper<T> {
#[inline]
fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> {
let mut tmp = Buffer::new();
self.0.render_once(&mut tmp)?;
@ -77,7 +76,12 @@ impl<T: RenderOnce> RenderOnce for Upper<T> {
Ok(())
}
fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> {
#[inline]
fn render_once_escaped<E: Escape>(
self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
let old_len = b.len();
self.0.render_once(b)?;
let mut tmp = Buffer::new();
@ -88,7 +92,7 @@ impl<T: RenderOnce> RenderOnce for Upper<T> {
tmp.push(c);
}
unsafe { b._set_len(old_len) };
escape::escape_to_buf(tmp.as_str(), b);
e.escape_to_buf(b, tmp.as_str());
Ok(())
}
}
@ -98,7 +102,7 @@ impl<T: RenderOnce> RenderOnce for Upper<T> {
/// # Examples
///
/// ```text
/// <%= "tschüß" | upper %>
/// <%\html "tschüß" | upper %>
/// ```
///
/// result:
@ -116,6 +120,7 @@ pub fn upper<T: RenderOnce>(expr: T) -> Upper<T> {
pub struct Lower<T>(T);
impl<T: RenderOnce> RenderOnce for Lower<T> {
#[inline]
fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> {
let mut tmp = Buffer::new();
self.0.render_once(&mut tmp)?;
@ -123,8 +128,10 @@ impl<T: RenderOnce> RenderOnce for Lower<T> {
b.reserve(tmp.len());
let old_len = b.len();
for c in tmp.as_str().chars() {
// see comments in str::to_lowercase
// See comments in str::to_lowercase
if c == 'Σ' {
// This is very inefficient, but it seems unlikely to be worth the effort
// duplicating what already exists in the standard library for unicode support.
let lower = tmp.as_str().to_lowercase();
unsafe { b._set_len(old_len) };
b.push_str(&lower);
@ -137,7 +144,12 @@ impl<T: RenderOnce> RenderOnce for Lower<T> {
Ok(())
}
fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> {
#[inline]
fn render_once_escaped<E: Escape>(
self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
let old_len = b.len();
self.0.render_once(b)?;
let mut tmp = Buffer::new();
@ -149,7 +161,7 @@ impl<T: RenderOnce> RenderOnce for Lower<T> {
if c == 'Σ' {
let lower = s.to_lowercase();
unsafe { b._set_len(old_len) };
b.push_str(&lower);
e.escape_to_buf(b, &lower);
return Ok(());
}
for c in c.to_lowercase() {
@ -157,7 +169,7 @@ impl<T: RenderOnce> RenderOnce for Lower<T> {
}
}
unsafe { b._set_len(old_len) };
escape::escape_to_buf(tmp.as_str(), b);
e.escape_to_buf(b, tmp.as_str());
Ok(())
}
}
@ -167,7 +179,7 @@ impl<T: RenderOnce> RenderOnce for Lower<T> {
/// # Examples
///
/// ```text
/// <%= "ὈΔΥΣΣΕΎΣ" | lower %>
/// <%\html "ὈΔΥΣΣΕΎΣ" | lower %>
/// ```
///
/// result:
@ -193,9 +205,13 @@ impl<T: RenderOnce> RenderOnce for Trim<T> {
}
#[inline]
fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> {
fn render_once_escaped<E: Escape>(
self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
let old_len = b.len();
self.0.render_once_escaped(b)?;
self.0.render_once_escaped(b, e)?;
trim_impl(b, old_len)
}
}
@ -238,7 +254,7 @@ fn trim_impl(b: &mut Buffer, old_len: usize) -> Result<(), RenderError> {
/// # Examples
///
/// ```text
/// <%= " Hello world\n" | trim %>
/// <%\html " Hello world\n" | trim %>
/// ```
///
/// result:
@ -264,23 +280,29 @@ impl<T: RenderOnce> RenderOnce for Truncate<T> {
}
#[inline]
fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> {
fn render_once_escaped<E: Escape>(
self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
let old_len = b.len();
self.0.render_once_escaped(b)?;
self.0.render_once_escaped(b, e)?;
truncate_impl(b, old_len, self.1)
}
}
#[cfg_attr(feature = "perf-inline", inline)]
fn truncate_impl(
b: &mut Buffer,
old_len: usize,
limit: usize,
) -> Result<(), RenderError> {
let new_contents = b.as_str().get(old_len..).ok_or(RenderError::BufSize)?;
if let Some(idx) = new_contents.char_indices().nth(limit).map(|(i, _)| i) {
unsafe { b._set_len(old_len + idx) };
b.push_str("...");
if new_contents.len() > limit {
if let Some(idx) = new_contents.char_indices().nth(limit).map(|(i, _)| i) {
unsafe { b._set_len(old_len + idx) };
b.push_str("...");
}
}
Ok(())
@ -293,7 +315,7 @@ fn truncate_impl(
/// The following example renders the first 20 characters of `message`
///
/// ```test
/// <%= "Hello, world!" | truncate(5) %>
/// <%\html "Hello, world!" | truncate(5) %>
/// ```
///
/// result:
@ -340,16 +362,18 @@ cfg_json! {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
use super::escape::escape_to_buf;
fn render_escaped<E: Escape>(
&self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
struct Writer<'a, E: Escape>(&'a mut Buffer, &'a E);
struct Writer<'a>(&'a mut Buffer);
impl<'a> std::io::Write for Writer<'a> {
impl<'a, E: Escape> std::io::Write for Writer<'a, E> {
#[inline]
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let buf = unsafe { std::str::from_utf8_unchecked(buf) };
escape_to_buf(buf, self.0);
self.1.escape_to_buf(self.0, buf);
Ok(buf.len())
}
@ -364,7 +388,7 @@ cfg_json! {
}
}
serde_json::to_writer(Writer(b), &self.0)
serde_json::to_writer(Writer(b, e), &self.0)
.map_err(|e| RenderError::new(&e.to_string()))
}
}
@ -387,6 +411,8 @@ cfg_json! {
#[cfg(test)]
mod tests {
use crate::EscapeHtml;
use super::*;
fn assert_render<T: RenderOnce>(expr: T, expected: &str) {
@ -397,7 +423,7 @@ mod tests {
fn assert_render_escaped<T: RenderOnce>(expr: T, expected: &str) {
let mut buf = Buffer::new();
RenderOnce::render_once_escaped(expr, &mut buf).unwrap();
RenderOnce::render_once_escaped(expr, &mut buf, &EscapeHtml).unwrap();
assert_eq!(buf.as_str(), expected);
}

View File

@ -6,9 +6,9 @@ use std::arch::x86::*;
use std::arch::x86_64::*;
use std::slice;
use super::super::Buffer;
use super::naive::push_escaped_str;
use super::{ESCAPED, ESCAPED_LEN, ESCAPE_LUT};
use crate::Buffer;
const VECTOR_BYTES: usize = std::mem::size_of::<__m256i>();

View File

@ -1,7 +1,7 @@
#![allow(clippy::cast_ptr_alignment)]
use super::super::Buffer;
use super::naive;
use crate::Buffer;
#[cfg(target_pointer_width = "16")]
const USIZE_BYTES: usize = 2;

View File

@ -34,94 +34,96 @@ static ESCAPE_LUT: [u8; 256] = [
const ESCAPED: [&str; 5] = ["&quot;", "&amp;", "&#039;", "&lt;", "&gt;"];
const ESCAPED_LEN: usize = 5;
use super::buffer::Buffer;
use super::{Buffer, Escape};
/// write the escaped contents into `Buffer`
#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), not(miri)))]
#[cfg_attr(feature = "perf-inline", inline)]
pub fn escape_to_buf(feed: &str, buf: &mut Buffer) {
#[cfg(not(target_feature = "avx2"))]
{
use std::sync::atomic::{AtomicPtr, Ordering};
/// A scheme for escaping strings for safe insertion into HTML.
#[derive(Debug, Clone, Copy)]
pub struct EscapeHtml;
type FnRaw = *mut ();
static FN: AtomicPtr<()> = AtomicPtr::new(detect as FnRaw);
impl Escape for EscapeHtml {
type Escaped = &'static str;
fn detect(feed: &str, buf: &mut Buffer) {
debug_assert!(feed.len() >= 16);
let fun = if is_x86_feature_detected!("avx2") {
avx2::escape
} else if is_x86_feature_detected!("sse2") {
sse2::escape
} else {
fallback::escape
};
const IDENT_BOOLS: bool = true;
const IDENT_UINTS: bool = true;
const IDENT_INTS: bool = true;
const IDENT_FLOATS: bool = true;
FN.store(fun as FnRaw, Ordering::Relaxed);
unsafe { fun(feed, buf) };
#[inline(always)]
fn escape(&self, c: char) -> Option<Self::Escaped> {
match c {
'\"' => Some("&quot;"),
'&' => Some("&amp;"),
'<' => Some("&lt;"),
'>' => Some("&gt;"),
'\'' => Some("&#039;"),
_ => None,
}
}
#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), not(miri)))]
#[cfg_attr(feature = "perf-inline", inline)]
fn escape_to_buf(&self, buf: &mut Buffer, string: &str) {
unsafe {
if feed.len() < 16 {
buf.reserve_small(feed.len() * 6);
let l = naive::escape_small(feed, buf.as_mut_ptr().add(buf.len()));
if string.len() < 16 {
buf.reserve_small(string.len() * 6);
let l = naive::escape_small(string, buf.as_mut_ptr().add(buf.len()));
buf.advance(l);
} else {
let fun = FN.load(Ordering::Relaxed);
std::mem::transmute::<FnRaw, fn(&str, &mut Buffer)>(fun)(feed, buf);
#[cfg(target_feature = "avx2")]
avx2::escape(string, buf);
#[cfg(all(not(target_feature = "avx2"), target_feature = "sse2"))]
sse2::escape(string, buf);
#[cfg(not(any(target_feature = "avx2", target_feature = "sse2")))]
{
use std::sync::atomic::{AtomicPtr, Ordering};
type FnRaw = *mut ();
static FN: AtomicPtr<()> = AtomicPtr::new(detect as FnRaw);
fn detect(string: &str, buf: &mut Buffer) {
debug_assert!(string.len() >= 16);
let fun = if is_x86_feature_detected!("avx2") {
avx2::escape
} else if is_x86_feature_detected!("sse2") {
sse2::escape
} else {
fallback::escape
};
FN.store(fun as FnRaw, Ordering::Relaxed);
unsafe { fun(string, buf) };
}
let fun = FN.load(Ordering::Relaxed);
std::mem::transmute::<FnRaw, fn(&str, &mut Buffer)>(fun)(string, buf);
};
}
}
}
#[cfg(target_feature = "avx2")]
unsafe {
if feed.len() < 16 {
buf.reserve_small(feed.len() * 6);
let l = naive::escape_small(feed, buf.as_mut_ptr().add(buf.len()));
buf.advance(l);
} else if cfg!(target_feature = "avx2") {
avx2::escape(feed, buf);
#[cfg(not(all(any(target_arch = "x86", target_arch = "x86_64"), not(miri))))]
#[cfg_attr(feature = "perf-inline", inline)]
fn escape_to_buf(&self, buffer: &mut Buffer, string: &str) {
unsafe {
if cfg!(miri) {
let bp = feed.as_ptr();
naive::escape(buf, bp, bp, bp.add(feed.len()))
} else if feed.len() < 16 {
buf.reserve_small(feed.len() * 6);
let l = naive::escape_small(feed, buf.as_mut_ptr().add(buf.len()));
buf.advance(l);
} else {
fallback::escape(feed, buf)
}
}
}
}
/// write the escaped contents into `Buffer`
#[cfg(not(all(any(target_arch = "x86", target_arch = "x86_64"), not(miri))))]
#[cfg_attr(feature = "perf-inline", inline)]
pub fn escape_to_buf(feed: &str, buf: &mut Buffer) {
unsafe {
if cfg!(miri) {
let bp = feed.as_ptr();
naive::escape(buf, bp, bp, bp.add(feed.len()))
} else if feed.len() < 16 {
buf.reserve_small(feed.len() * 6);
let l = naive::escape_small(feed, buf.as_mut_ptr().add(buf.len()));
buf.advance(l);
} else {
fallback::escape(feed, buf)
}
}
}
/// write the escaped contents into `String`
///
/// # Examples
///
/// ```
/// use sailfish::runtime::escape::escape_to_string;
///
/// let mut buf = String::new();
/// escape_to_string("<h1>Hello, world!</h1>", &mut buf);
/// assert_eq!(buf, "&lt;h1&gt;Hello, world!&lt;/h1&gt;");
/// ```
#[inline]
pub fn escape_to_string(feed: &str, s: &mut String) {
let mut s2 = String::new();
std::mem::swap(s, &mut s2);
let mut buf = Buffer::from(s2);
escape_to_buf(feed, &mut buf);
let mut s2 = buf.into_string();
std::mem::swap(s, &mut s2);
#[deprecated = "Use [`EscapeHtml::escape_to_buf`] instead"]
#[inline(always)]
fn escape_to_buf(feed: &str, buf: &mut Buffer) {
EscapeHtml.escape_to_buf(buf, feed)
}
#[cfg(test)]
@ -130,7 +132,7 @@ mod tests {
fn escape(feed: &str) -> String {
let mut s = String::new();
escape_to_string(feed, &mut s);
EscapeHtml.escape_to_string(&mut s, feed);
s
}

View File

@ -1,9 +1,9 @@
use std::ptr;
use std::slice;
use super::super::utils::memcpy_16;
use super::super::Buffer;
use super::{ESCAPED, ESCAPED_LEN, ESCAPE_LUT};
use crate::utils::memcpy_16;
use crate::Buffer;
#[inline]
pub(super) unsafe fn escape(

View File

@ -6,9 +6,9 @@ use std::arch::x86::*;
use std::arch::x86_64::*;
use std::slice;
use super::super::Buffer;
use super::naive::push_escaped_str;
use super::{ESCAPED, ESCAPED_LEN, ESCAPE_LUT};
use crate::Buffer;
const VECTOR_BYTES: usize = std::mem::size_of::<__m128i>();

View File

@ -8,7 +8,7 @@
//!
//! In most cases you don't need to care about the `runtime` module in this crate, but
//! if you want to render custom data inside templates, you must implement
//! `runtime::Render` trait for that type.
//! [`Render`] or [`RenderOnce`] for that type.
//!
//! ```ignore
//! use sailfish::RenderOnce;
@ -33,12 +33,29 @@
)]
#![cfg_attr(sailfish_nightly, feature(core_intrinsics))]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![feature(maybe_uninit_slice)]
#![feature(specialization)]
#![allow(clippy::redundant_closure)]
#![deny(missing_docs)]
pub mod runtime;
#[macro_use]
mod utils;
mod buffer;
mod escape;
pub mod filter;
mod html_escape;
mod render;
#[doc(hidden)]
pub mod runtime;
mod size_hint;
pub use buffer::Buffer;
pub use escape::{Escape, EscapeJsonString};
pub use html_escape::EscapeHtml;
pub use render::{Render, RenderError, RenderOnce, RenderResult};
pub use size_hint::SizeHint;
pub use runtime::{Buffer, Render, RenderError, RenderOnce, RenderResult};
#[cfg(feature = "derive")]
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
pub use sailfish_macros::{Render, RenderOnce};

View File

@ -1,6 +1,6 @@
use std::borrow::Cow;
use std::cell::{Ref, RefMut};
use std::fmt;
use std::fmt::{self, Arguments, Write};
use std::num::{
NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize,
NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, Wrapping,
@ -9,12 +9,9 @@ use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::{Arc, MutexGuard, RwLockReadGuard, RwLockWriteGuard};
use crate::runtime::SizeHint;
use super::{Buffer, Escape, SizeHint};
use super::buffer::Buffer;
use super::escape;
/// types which can be rendered inside buffer block (`<%= %>`) by reference
/// types which can be rendered inside buffer block (`<%- %>`) by reference
///
/// If you want to render the custom data, you must implement this trait and specify
/// the behaviour.
@ -29,7 +26,7 @@ use super::escape;
/// # Examples
///
/// ```
/// use sailfish::runtime::{Buffer, Render, RenderError};
/// use sailfish::{Buffer, Render, RenderError};
///
/// struct MyU64(u64);
///
@ -46,15 +43,16 @@ pub trait Render {
/// render to `Buffer` with HTML escaping
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
let mut tmp = Buffer::new();
self.render(&mut tmp)?;
escape::escape_to_buf(tmp.as_str(), b);
Ok(())
fn render_escaped<E: Escape>(
&self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
default_render_escaped(self, b, e)
}
}
/// types which can be rendered inside buffer block (`<%= %>`)
/// types which can be rendered inside buffer block (`<%- %>`)
///
/// See [`Render`] for more information.
pub trait RenderOnce: Sized {
@ -90,11 +88,12 @@ pub trait RenderOnce: Sized {
/// render to `Buffer` with HTML escaping
#[inline]
fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> {
let mut tmp = Buffer::new();
self.render_once(&mut tmp)?;
escape::escape_to_buf(tmp.as_str(), b);
Ok(())
fn render_once_escaped<E: Escape>(
self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
default_render_once_escaped(self, b, e)
}
/// Render the template and return the rendering result as `RenderResult`
@ -118,6 +117,30 @@ pub trait RenderOnce: Sized {
}
}
#[inline]
pub fn default_render_escaped<T: Render + ?Sized, E: Escape>(
t: &T,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
let mut tmp = Buffer::new();
t.render(&mut tmp)?;
e.escape_to_buf(b, tmp.as_str());
Ok(())
}
#[inline]
pub fn default_render_once_escaped<T: RenderOnce, E: Escape>(
t: T,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
let mut tmp = Buffer::new();
t.render_once(&mut tmp)?;
e.escape_to_buf(b, tmp.as_str());
Ok(())
}
// impl<'a, T: ?Sized> Render for &'a T
// where
// T: Render,
@ -126,8 +149,8 @@ pub trait RenderOnce: Sized {
// T::render(self, b)
// }
// fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
// T::render_escaped(self, b)
// fn render_escaped<E: Escape>(&self, b: &mut Buffer, e: &E) -> Result<(), RenderError> {
// T::render_escaped(self, b, e)
// }
// }
@ -138,34 +161,15 @@ impl<T: Render> RenderOnce for T {
}
#[inline]
fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> {
self.render_escaped(b)
fn render_once_escaped<E: Escape>(
self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
self.render_escaped(b, e)
}
}
// /// Autoref-based stable specialization
// ///
// /// Explanation can be found [here](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md)
// impl<T: Display> Render for &T {
// fn render(&self, b: &mut Buffer) -> Result<(), RenderError> {
// fmt::write(b, format_args!("{}", self))
// }
//
// fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
// struct Wrapper<'a>(&'a mut Buffer);
//
// impl<'a> fmt::Write for Wrapper<'a> {
// #[inline]
// fn push_str(&mut self, s: &str) -> Result<(), RenderError> {
// escape::escape_to_buf(s, self.0);
// Ok(())
// }
// }
//
// fmt::write(&mut Wrapper(b), format_args!("{}", self))
// }
// }
impl Render for String {
#[inline]
fn render(&self, b: &mut Buffer) -> Result<(), RenderError> {
@ -174,8 +178,12 @@ impl Render for String {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
escape::escape_to_buf(self, b);
fn render_escaped<E: Escape>(
&self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
e.escape_to_buf(b, self);
Ok(())
}
}
@ -188,8 +196,12 @@ impl Render for str {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
escape::escape_to_buf(self, b);
fn render_escaped<E: Escape>(
&self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
e.escape_to_buf(b, self);
Ok(())
}
}
@ -202,14 +214,14 @@ impl Render for char {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
match *self {
'\"' => b.push_str("&quot;"),
'&' => b.push_str("&amp;"),
'<' => b.push_str("&lt;"),
'>' => b.push_str("&gt;"),
'\'' => b.push_str("&#039;"),
_ => b.push(*self),
fn render_escaped<E: Escape>(
&self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
match e.escape(*self) {
Some(s) => b.push_str(s.as_ref()),
None => b.push(*self),
}
Ok(())
}
@ -224,8 +236,12 @@ impl Render for PathBuf {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
escape::escape_to_buf(self.to_str().ok_or(RenderError::Fmt(fmt::Error))?, b);
fn render_escaped<E: Escape>(
&self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
e.escape_to_buf(b, self.to_str().ok_or(RenderError::Fmt(fmt::Error))?);
Ok(())
}
}
@ -239,8 +255,12 @@ impl Render for Path {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
escape::escape_to_buf(self.to_str().ok_or(RenderError::Fmt(fmt::Error))?, b);
fn render_escaped<E: Escape>(
&self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
e.escape_to_buf(b, self.to_str().ok_or(RenderError::Fmt(fmt::Error))?);
Ok(())
}
}
@ -278,13 +298,26 @@ impl Render for bool {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
self.render(b)
fn render_escaped<E: Escape>(
&self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
if E::IDENT_BOOLS {
self.render(b)
} else {
if *self {
e.escape_to_buf(b, "true");
} else {
e.escape_to_buf(b, "false");
}
Ok(())
}
}
}
macro_rules! render_int {
($($int:ty),*) => {
($ident_tag:ident, $($int:ty),*) => {
$(
impl Render for $int {
#[cfg_attr(feature = "perf-inline", inline)]
@ -307,16 +340,21 @@ macro_rules! render_int {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
// push_str without escape
self.render(b)
fn render_escaped<E: Escape>(&self, b: &mut Buffer, e: &E) -> Result<(), RenderError> {
if E::$ident_tag {
// push_str without escape
self.render(b)
} else {
default_render_once_escaped(self, b, e)
}
}
}
)*
}
}
render_int!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, usize, isize);
render_int!(IDENT_UINTS, u8, u16, u32, u64, u128, usize);
render_int!(IDENT_INTS, i8, i16, i32, i64, i128, isize);
impl Render for f32 {
#[cfg_attr(feature = "perf-inline", inline)]
@ -341,9 +379,16 @@ impl Render for f32 {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
// escape string
self.render(b)
fn render_escaped<E: Escape>(
&self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
if E::IDENT_FLOATS {
self.render(b)
} else {
default_render_escaped(self, b, e)
}
}
}
@ -370,9 +415,42 @@ impl Render for f64 {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
// escape string
self.render(b)
fn render_escaped<E: Escape>(
&self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
if E::IDENT_FLOATS {
self.render(b)
} else {
default_render_escaped(self, b, e)
}
}
}
impl Render for Arguments<'_> {
#[inline]
fn render(&self, b: &mut Buffer) -> Result<(), RenderError> {
if let Some(s) = self.as_str() {
b.push_str(s);
Ok(())
} else {
b.write_fmt(*self).map_err(RenderError::Fmt)
}
}
#[inline]
fn render_escaped<E: Escape>(
&self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
if let Some(s) = self.as_str() {
e.escape_to_buf(b, s);
Ok(())
} else {
default_render_escaped(self, b, e)
}
}
}
@ -389,8 +467,8 @@ macro_rules! render_deref {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
(**self).render_escaped(b)
fn render_escaped<E: Escape>(&self, b: &mut Buffer, e: &E) -> Result<(), RenderError> {
(**self).render_escaped(b, e)
}
}
};
@ -399,17 +477,17 @@ macro_rules! render_deref {
// render_ref!(['a, T] [&'a T: Render] T);
// render_ref!(['a] [] String);
render_deref!(['a, T: Render + ?Sized] [] &'a T);
render_deref!(['a, T: Render + ?Sized] [] &'a mut T);
render_deref!([T: Render + ?Sized] [] &T);
render_deref!([T: Render + ?Sized] [] &mut T);
render_deref!([T: Render + ?Sized] [] Box<T>);
render_deref!([T: Render + ?Sized] [] Rc<T>);
render_deref!([T: Render + ?Sized] [] Arc<T>);
render_deref!(['a, T: Render + ToOwned + ?Sized] [] Cow<'a, T>);
render_deref!(['a, T: Render + ?Sized] [] Ref<'a, T>, [*]);
render_deref!(['a, T: Render + ?Sized] [] RefMut<'a, T>, [*]);
render_deref!(['a, T: Render + ?Sized] [] MutexGuard<'a, T>, [*]);
render_deref!(['a, T: Render + ?Sized] [] RwLockReadGuard<'a, T>, [*]);
render_deref!(['a, T: Render + ?Sized] [] RwLockWriteGuard<'a, T>, [*]);
render_deref!([T: Render + ToOwned + ?Sized] [] Cow<'_, T>);
render_deref!([T: Render + ?Sized] [] Ref<'_, T>, [*]);
render_deref!([T: Render + ?Sized] [] RefMut<'_, T>, [*]);
render_deref!([T: Render + ?Sized] [] MutexGuard<'_, T>, [*]);
render_deref!([T: Render + ?Sized] [] RwLockReadGuard<'_, T>, [*]);
render_deref!([T: Render + ?Sized] [] RwLockWriteGuard<'_, T>, [*]);
macro_rules! render_nonzero {
($($type:ty,)*) => {
@ -421,8 +499,8 @@ macro_rules! render_nonzero {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
self.get().render_escaped(b)
fn render_escaped<E: Escape>(&self, b: &mut Buffer, e: &E) -> Result<(), RenderError> {
self.get().render_escaped(b, e)
}
}
)*
@ -451,8 +529,12 @@ impl<T: Render> Render for Wrapping<T> {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
self.0.render_escaped(b)
fn render_escaped<E: Escape>(
&self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
self.0.render_escaped(b, e)
}
}
@ -476,10 +558,10 @@ macro_rules! render_tuple {
}
#[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> {
fn render_escaped<Esc: Escape>(&self, b: &mut Buffer, e: &Esc) -> Result<(), RenderError> {
#[allow(non_snake_case)]
let ($($T,)+) = self;
$($T.render_escaped(b)?;)+
$($T.render_escaped(b, e)?;)+
Ok(())
}
}
@ -505,10 +587,10 @@ macro_rules! render_once_tuple {
}
#[inline]
fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> {
fn render_once_escaped<Esc: Escape>(self, b: &mut Buffer, e: &Esc) -> Result<(), RenderError> {
#[allow(non_snake_case)]
let ($($T,)+) = self;
$($T.render_once_escaped(b)?;)+
$($T.render_once_escaped(b, e)?;)+
Ok(())
}
}
@ -574,9 +656,12 @@ pub type RenderResult = Result<String, RenderError>;
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
use crate::EscapeHtml;
use super::*;
#[test]
fn receiver_coercion() {
let mut b = Buffer::new();
@ -589,24 +674,24 @@ mod tests {
RenderOnce::render_once(&true, &mut b).unwrap();
RenderOnce::render_once(&&false, &mut b).unwrap();
RenderOnce::render_once_escaped(&&&true, &mut b).unwrap();
RenderOnce::render_once_escaped(&&&&false, &mut b).unwrap();
RenderOnce::render_once_escaped(&&&true, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(&&&&false, &mut b, &EscapeHtml).unwrap();
assert_eq!(b.as_str(), "truefalsetruefalse");
b.clear();
let s = "apple";
RenderOnce::render_once_escaped(&s, &mut b).unwrap();
RenderOnce::render_once_escaped(&s, &mut b).unwrap();
RenderOnce::render_once_escaped(&&s, &mut b).unwrap();
RenderOnce::render_once_escaped(&&&s, &mut b).unwrap();
RenderOnce::render_once_escaped(&&&&s, &mut b).unwrap();
RenderOnce::render_once_escaped(&s, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(&s, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(&&s, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(&&&s, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(&&&&s, &mut b, &EscapeHtml).unwrap();
assert_eq!(b.as_str(), "appleappleappleappleapple");
b.clear();
RenderOnce::render_once_escaped(&'c', &mut b).unwrap();
RenderOnce::render_once_escaped(&&'<', &mut b).unwrap();
RenderOnce::render_once_escaped(&&&'&', &mut b).unwrap();
RenderOnce::render_once_escaped(&&&&' ', &mut b).unwrap();
RenderOnce::render_once_escaped(&'c', &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(&&'<', &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(&&&'&', &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(&&&&' ', &mut b, &EscapeHtml).unwrap();
assert_eq!(b.as_str(), "c&lt;&amp; ");
b.clear();
}
@ -619,10 +704,10 @@ mod tests {
let mut b = Buffer::new();
Render::render(&String::from("a"), &mut b).unwrap();
Render::render(&PathBuf::from("b"), &mut b).unwrap();
Render::render_escaped(&Rc::new(4u32), &mut b).unwrap();
Render::render_escaped(&Rc::new(2.3f32), &mut b).unwrap();
Render::render_escaped(Path::new("<"), &mut b).unwrap();
Render::render_escaped(&Path::new("d"), &mut b).unwrap();
Render::render_escaped(&Rc::new(4u32), &mut b, &EscapeHtml).unwrap();
Render::render_escaped(&Rc::new(2.3f32), &mut b, &EscapeHtml).unwrap();
Render::render_escaped(Path::new("<"), &mut b, &EscapeHtml).unwrap();
Render::render_escaped(&Path::new("d"), &mut b, &EscapeHtml).unwrap();
assert_eq!(b.as_str(), "ab42.3&lt;d");
}
@ -631,17 +716,19 @@ mod tests {
fn float() {
let mut b = Buffer::new();
RenderOnce::render_once_escaped(0.0f64, &mut b).unwrap();
RenderOnce::render_once_escaped(std::f64::INFINITY, &mut b).unwrap();
RenderOnce::render_once_escaped(std::f64::NEG_INFINITY, &mut b).unwrap();
RenderOnce::render_once_escaped(std::f64::NAN, &mut b).unwrap();
RenderOnce::render_once_escaped(0.0f64, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(std::f64::INFINITY, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(std::f64::NEG_INFINITY, &mut b, &EscapeHtml)
.unwrap();
RenderOnce::render_once_escaped(std::f64::NAN, &mut b, &EscapeHtml).unwrap();
assert_eq!(b.as_str(), "0.0inf-infNaN");
b.clear();
RenderOnce::render_once_escaped(0.0f32, &mut b).unwrap();
RenderOnce::render_once_escaped(std::f32::INFINITY, &mut b).unwrap();
RenderOnce::render_once_escaped(std::f32::NEG_INFINITY, &mut b).unwrap();
RenderOnce::render_once_escaped(std::f32::NAN, &mut b).unwrap();
RenderOnce::render_once_escaped(0.0f32, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(std::f32::INFINITY, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(std::f32::NEG_INFINITY, &mut b, &EscapeHtml)
.unwrap();
RenderOnce::render_once_escaped(std::f32::NAN, &mut b, &EscapeHtml).unwrap();
assert_eq!(b.as_str(), "0.0inf-infNaN");
}
@ -650,7 +737,9 @@ mod tests {
let mut b = Buffer::new();
let funcs: Vec<fn(char, &mut Buffer) -> Result<(), RenderError>> =
vec![RenderOnce::render_once, RenderOnce::render_once_escaped];
vec![RenderOnce::render_once, |c, b| {
RenderOnce::render_once_escaped(c, b, &EscapeHtml)
}];
for func in funcs {
func('a', &mut b).unwrap();
@ -675,7 +764,12 @@ mod tests {
fn test_nonzero() {
let mut b = Buffer::with_capacity(2);
RenderOnce::render_once(NonZeroU8::new(10).unwrap(), &mut b).unwrap();
RenderOnce::render_once_escaped(NonZeroI16::new(-20).unwrap(), &mut b).unwrap();
RenderOnce::render_once_escaped(
NonZeroI16::new(-20).unwrap(),
&mut b,
&EscapeHtml,
)
.unwrap();
assert_eq!(b.as_str(), "10-20");
}

34
sailfish/src/runtime.rs Normal file
View File

@ -0,0 +1,34 @@
use crate::RenderError;
use crate::{Buffer, Escape, EscapeHtml, EscapeJsonString, RenderOnce};
pub use crate::filter;
#[inline(always)]
pub fn esc_html() -> EscapeHtml {
EscapeHtml
}
#[inline(always)]
pub fn esc_json() -> EscapeJsonString {
EscapeJsonString
}
#[inline(always)]
pub fn render<T: RenderOnce>(buf: &mut Buffer, value: T) -> Result<(), RenderError> {
value.render_once(buf)
}
#[inline(always)]
pub fn render_escaped<T: RenderOnce, E: Escape>(
buf: &mut Buffer,
value: T,
escape: &E,
) -> Result<(), RenderError> {
value.render_once_escaped(buf, escape)
}
#[inline(always)]
pub fn render_text(buf: &mut Buffer, value: &str) {
buf.push_str(value)
}

View File

@ -1,24 +0,0 @@
use crate::RenderError;
use super::{Buffer, RenderOnce};
#[doc(hidden)]
#[inline(always)]
pub fn render<T: RenderOnce>(buf: &mut Buffer, value: T) -> Result<(), RenderError> {
value.render_once(buf)
}
#[doc(hidden)]
#[inline(always)]
pub fn render_escaped<T: RenderOnce>(
buf: &mut Buffer,
value: T,
) -> Result<(), RenderError> {
value.render_once_escaped(buf)
}
#[doc(hidden)]
#[inline(always)]
pub fn render_text(buf: &mut Buffer, value: &str) {
buf.push_str(value)
}

View File

@ -1,18 +0,0 @@
//! Sailfish runtime
#[macro_use]
mod utils;
mod alias_funcs;
mod buffer;
pub mod escape;
pub mod filter;
mod render;
mod size_hint;
pub use buffer::Buffer;
pub use render::{Render, RenderError, RenderOnce, RenderResult};
pub use size_hint::SizeHint;
#[doc(hidden)]
pub use alias_funcs::{render, render_escaped, render_text};

View File

@ -9,7 +9,7 @@ unlet b:current_syntax
syn include @rustSyntax syntax/rust.vim
syn region sailfishCodeBlock matchgroup=sailfishTag start=/<%/ keepend end=/%>/ contains=@rustSyntax
syn region sailfishBufferBlock matchgroup=sailfishTag start=/<%=/ keepend end=/%>/ contains=@rustSyntax
syn region sailfishBufferBlock matchgroup=sailfishTag start=/<%-/ keepend end=/%>/ contains=@rustSyntax
syn region sailfishCommentBlock start=/<%#/ end=/%>/
" Redefine htmlTag so that it can contain jspExpr

View File

@ -1,50 +1,59 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "sailfish",
"patterns": [
{
"include": "#commentblock"
},
{
"include": "#codeblock"
},
{
"include": "text.html.basic"
}
],
"repository": {
"commentblock": {
"patterns": [{
"name": "comment.block.embedded.html",
"begin": "<(%|\\?)#",
"end": "(%|\\?)>",
"captures": {
"0": {
"name": "punctuation.definition.comment.html"
}
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "sailfish",
"patterns": [
{
"include": "#commentblock"
},
{
"include": "#codeblock"
},
{
"include": "text.html.basic"
}
],
"repository": {
"commentblock": {
"patterns": [
{
"name": "comment.block.embedded.html",
"begin": "<(%|\\?)#",
"end": "(%|\\?)>",
"captures": {
"0": {
"name": "punctuation.definition.comment.html"
}
}
}
]
},
"codeblock": {
"patterns": [
{
"name": "source.rust.embedded.html",
"begin": "<(%|\\?)(\\(\\w+)|-)?",
"beginCaptures": {
"0": {
"name": "punctuation.definition.tag.begin.html"
},
"2": {
"name": "entity.name.function.sailfish"
}
},
"end": "(%|\\?)>",
"endCaptures": {
"0": {
"name": "punctuation.definition.tag.end.html"
}
},
"patterns": [
{
"include": "source.rust"
}
]
}
]
}
}]
},
"codeblock": {
"patterns": [{
"name": "source.rust.embedded.html",
"begin": "<(%|\\?)(=|-)?",
"beginCaptures": {
"0": {
"name": "punctuation.definition.tag.begin.html"
}
},
"end": "(%|\\?)>",
"endCaptures": {
"0": {
"name": "punctuation.definition.tag.end.html"
}
},
"patterns": [{
"include": "source.rust"
}]
}]
}
},
"scopeName": "source.sailfish"
"scopeName": "source.sailfish"
}