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> <html>
<body> <body>
<% for msg in &messages { %> <% for msg in &messages { %>
<div><%= msg %></div> <div><%\html msg %></div>
<% } %> <% } %>
</body> </body>
</html> </html>

View File

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

View File

@ -6,7 +6,7 @@ You can control the rendering behaviour via `template` attribute.
```rust ```rust
#[derive(RenderOnce)] #[derive(RenderOnce)]
#[template(path = "template.stpl", escape = false)] #[template(path = "template.stpl", rm_whitespace = true)]
struct TemplateStruct { struct TemplateStruct {
... ...
} }
@ -15,7 +15,6 @@ struct TemplateStruct {
`template` attribute accepts the following options. `template` attribute accepts the following options.
- `path`: path to template file. This options is always required. - `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: '%') - `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. - `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 ```toml
template_dirs = ["templates"] template_dirs = ["templates"]
escape = true
delimiter = "%" delimiter = "%"
[optimizations] [optimizations]

View File

@ -7,7 +7,7 @@ Example:
=== "Template" === "Template"
``` rhtml ``` rhtml
message: <%= "foo\nbar" | dbg %> message: <%\html "foo\nbar" | dbg %>
``` ```
=== "Result" === "Result"
@ -17,23 +17,22 @@ Example:
``` ```
!!! Note !!! 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 ## Syntax
- Apply filter and HTML escaping - Apply filter and HTML escaping
``` rhtml ```rhtml
<%= expression | filter %> <%\html expression | filter %>
``` ```
- Apply filter only - Apply filter only
``` rhtml ```rhtml
<%- expression | filter %> <%- expression | filter %>
``` ```
## Built-In Filters ## 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 ## Tags
- `<% %>`: Inline tag, you can write Rust code inside this tag - `<% %>`: 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 - `<%- %>`: Evaluate the Rust expression and outputs the unescaped value into the template
- `<%# %>`: Comment tag - `<%# %>`: Comment tag
- `<%%`: Outputs a literal '<%' - `<%%`: Outputs a literal '<%'
@ -20,7 +20,7 @@
```rhtml ```rhtml
<% for (i, msg) in messages.iter().enumerate() { %> <% for (i, msg) in messages.iter().enumerate() { %>
<div><%= i %>: <%= msg %></div> <div><%\html i %>: <%\html msg %></div>
<% } %> <% } %>
``` ```
@ -33,12 +33,12 @@
## Filters ## Filters
```rhtml ```rhtml
<%= message | upper %> <%- message | upper %>
``` ```
```rhtml ```json
{ {
"id": <%= id %> "id": <%- id %>,
"comment": <%- comment | json %> "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" === "Result"
@ -65,12 +65,12 @@ Although almost all Rust statement is supported, the following statements inside
## Evaluation block ## 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" === "Template"
``` rhtml ``` rhtml
<% let a = 1; %><%= a + 2 %> <% let a = 1; %><%\html a + 2 %>
``` ```
=== "Result" === "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 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" === "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. 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 ``` 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. We must ensure that all of the data passed to templates should satisfy the following restrictions.
- completely immutable - completely immutable
- does not allocate/deallocate memory - 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 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 - can be defined inside `#![no_std]` crate
Sailfish provide `TemplateData` trait which satisfies the above restrictions. 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 This trait can be implemented to the following types
- String, - String,
- Primitive integers (bool, char, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, isize, usize) - Primitive integers (bool, char, u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, isize, usize)
- [T; N] where T: TemplateData - [T; N] where T: TemplateData
- (T1, T2, T3, ...) where T1, T2, T3, ... : TemplateData - (T1, T2, T3, ...) where T1, T2, T3, ... : TemplateData
- Option\<T\> where T: TemplateData - Option\<T\> where T: TemplateData
- Vec\<T\> where T: TemplateData - Vec\<T\> where T: TemplateData
### `#[derive(TemplateData)]` attribute ### `#[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 For example, if we have a template
```html ```html
<h1><%= msg %></h1> <h1><%\html msg %></h1>
``` ```
and Rust code 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 Message { msg } = deserialize(&mut data);
let mut buf = VBuffer::from_vtable(vtable); let mut buf = VBuffer::from_vtable(vtable);
static SIZE_HINT = SizeHint::new(); static SIZE_HINT = SizeHint::new();
let size_hint = SIZE_HINT.get(); let size_hint = SIZE_HINT.get();
buf.reserve(size_hint); buf.reserve(size_hint);
@ -178,11 +178,11 @@ pub extern fn sf_message(version: u64, data: *const [u8], data_len: usize, vtabl
Template: Template:
```html ```html
<!DOCTYPE html> <!doctype html>
<html> <html>
<body> <body>
<b><%= name %></b>: <%= score %> <b><%\html name %></b>: <%\html score %>
</body> </body>
</html> </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 If you derive this trait, you cannot move out the struct fields. For example, the following template
```html ```html
<% for msg in messages { %><div><%= msg %></div><% } %> <% for msg in messages { %>
<div><%\html msg %></div>
<% } %>
``` ```
will be transformed into the Rust code like 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> { async fn greet(req: HttpRequest) -> actix_web::Result<HttpResponse> {
let name = req.match_info().get("name").unwrap_or("World"); let name = req.match_info().get("name").unwrap_or("World");
let body = Greet { name } let body = Greet { name }
.render_once() .render_once_to_string()
.map_err(|e| InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?; .map_err(|e| InternalError::new(e, StatusCode::INTERNAL_SERVER_ERROR))?;
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()

View File

@ -12,5 +12,5 @@ fn main() {
title: "Website".to_owned(), title: "Website".to_owned(),
name: "Hanako".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() { fn main() {
let messages = vec![String::from("Message 1"), String::from("<Message 2>")]; 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> <html>
<body> <body>
Hello <%= name %>! Hello <%\html name %>!
</body> </body>
</html> </html>

View File

@ -2,4 +2,4 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="format-detection" content="telephone=no"> <meta name="format-detection" content="telephone=no">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <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"); %> <% include!("header.stpl"); %>
</head> </head>
<body> <body>
<h1><%= title %></h1> <h1><%\html title %></h1>
Hello, <%= name %>! Hello, <%\html name %>!
</body> </body>
</html> </html>

View File

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

View File

@ -34,7 +34,7 @@ impl Compiler {
fn translate_file_contents(&self, input: &Path) -> Result<TranslatedSource, Error> { fn translate_file_contents(&self, input: &Path) -> Result<TranslatedSource, Error> {
let parser = Parser::new().delimiter(self.config.delimiter); 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) let content = read_to_string(input)
.chain_err(|| format!("Failed to open template file: {:?}", 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 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 resolver = Resolver::new().include_handler(include_handler);
let optimizer = Optimizer::new().rm_whitespace(self.config.rm_whitespace); 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)] #[derive(Clone, Debug, Hash)]
pub struct Config { pub struct Config {
pub delimiter: char, pub delimiter: char,
pub escape: bool,
pub rm_whitespace: bool, pub rm_whitespace: bool,
pub template_dirs: Vec<PathBuf>, pub template_dirs: Vec<PathBuf>,
#[doc(hidden)] #[doc(hidden)]
@ -17,7 +16,6 @@ impl Default for Config {
Self { Self {
template_dirs: Vec::new(), template_dirs: Vec::new(),
delimiter: '%', delimiter: '%',
escape: true,
cache_dir: Path::new(env!("OUT_DIR")).join("cache"), cache_dir: Path::new(env!("OUT_DIR")).join("cache"),
rm_whitespace: false, rm_whitespace: false,
_non_exhaustive: (), _non_exhaustive: (),
@ -74,10 +72,6 @@ mod imp {
config.delimiter = delimiter; config.delimiter = delimiter;
} }
if let Some(escape) = config_file.escape {
config.escape = escape;
}
if let Some(optimizations) = config_file.optimizations { if let Some(optimizations) = config_file.optimizations {
if let Some(rm_whitespace) = optimizations.rm_whitespace { if let Some(rm_whitespace) = optimizations.rm_whitespace {
config.rm_whitespace = rm_whitespace; config.rm_whitespace = rm_whitespace;
@ -103,7 +97,6 @@ mod imp {
struct ConfigFile { struct ConfigFile {
template_dirs: Option<Vec<String>>, template_dirs: Option<Vec<String>>,
delimiter: Option<char>, delimiter: Option<char>,
escape: Option<bool>,
optimizations: Option<Optimizations>, optimizations: Option<Optimizations>,
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
template_dirs = ["../templates"] template_dirs = ["../templates"]
escape = true
delimiter = "%" delimiter = "%"
[optimizations] [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 { %> <% for i in 0..10 { %>
<div>head</div> <div>head</div>
<% if i < 2 { continue; } %> <% if i < 2 { continue; } %>
<div><%= i %></div> <div><%\html i %></div>
<% if i > 5 { break; } %> <% if i > 5 { break; } %>
<div>tail</div> <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 %> disp: <%- message | disp %>
dbg: <%- message | dbg %> dbg: <%- message | dbg %>
disp escaped: <%= message | disp %> disp escaped: <%\html message | disp %>
dbg escaped: <%= message | dbg %> dbg escaped: <%\html message | dbg %>

View File

@ -1,3 +1,3 @@
trim: <%= " <html> " | trim %> trim: <%\html " <html> " | trim %>
lower: <%= "aBc" | lower %> lower: <%\html "aBc" | lower %>
upper: <%= "aBc" | upper %> 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 %>, "name": <%- name | dbg %>,
"value": <%= value %> "value": <%- value %>
} }

View File

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

View File

@ -4,7 +4,7 @@
<body> <body>
<table> <table>
<tr><th>id</th><th>message</th></tr> <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> </table>
</body> </body>
</html> </html>

View File

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

View File

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

View File

@ -1,8 +1,8 @@
error: expected boolean literal error: expected boolean literal
--> $DIR/invalid_option_value.rs:5:38 --> $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 error[E0599]: no method named `render_once` found for struct `InvalidOptionValue` in the current scope
--> $DIR/invalid_option_value.rs:11:69 --> $DIR/invalid_option_value.rs:11:69

View File

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

View File

@ -1,8 +1,8 @@
error: Argument `escape` was repeated. error: Argument `escape` was repeated.
--> $DIR/repeated_arguments.rs:6:12 --> $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 error[E0599]: no method named `render_once` found for struct `InvalidOptionValue` in the current scope
--> $DIR/repeated_arguments.rs:12:69 --> $DIR/repeated_arguments.rs:12:69

View File

@ -1,3 +1,3 @@
<% for player in &players %> <% 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> <html>
<body> <body>
<%= content <%\html content
</body> </body>
</html> </html>

View File

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

View File

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

View File

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

View File

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

View File

@ -24,6 +24,7 @@ json = ["serde", "serde_json"]
perf-inline = [] perf-inline = []
[dependencies] [dependencies]
array-vec = { git = "https://git.pfaff.dev/michael/array-vec.rs" }
itoap = "1.0.1" itoap = "1.0.1"
ryu = "1.0.13" ryu = "1.0.13"
serde = { version = "1.0.159", optional = true } 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::fmt;
use std::ptr; use std::ptr;
use super::escape; use crate::{Buffer, Escape, Render, RenderError, RenderOnce};
use super::render::RenderOnce;
use super::{Buffer, Render, RenderError};
/// Helper struct for 'display' filter /// Helper struct for 'display' filter
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -15,7 +13,7 @@ impl<T: fmt::Display> Render for Display<T> {
fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render(&self, b: &mut Buffer) -> Result<(), RenderError> {
use fmt::Write; 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 /// # Examples
/// ///
/// ```text /// ```text
/// filename: <%= filename.display() | disp %> /// filename: <%\html filename.display() | disp %>
/// ``` /// ```
#[inline] #[inline]
pub fn disp<T: fmt::Display>(expr: T) -> Display<T> { 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> { fn render(&self, b: &mut Buffer) -> Result<(), RenderError> {
use fmt::Write; 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 /// # 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 /// ```text
/// table content: <%= table | dbg %> /// table content: <%\html table | dbg %>
/// ``` /// ```
/// ///
/// ```text /// ```text
/// table content: <%= format!("{:?}", table) %> /// table content: <%\html format!("{:?}", table) %>
/// ``` /// ```
#[inline] #[inline]
pub fn dbg<T: fmt::Debug>(expr: T) -> Debug<T> { 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); pub struct Upper<T>(T);
impl<T: RenderOnce> RenderOnce for Upper<T> { impl<T: RenderOnce> RenderOnce for Upper<T> {
#[inline]
fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> { fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> {
let mut tmp = Buffer::new(); let mut tmp = Buffer::new();
self.0.render_once(&mut tmp)?; self.0.render_once(&mut tmp)?;
@ -77,7 +76,12 @@ impl<T: RenderOnce> RenderOnce for Upper<T> {
Ok(()) 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(); let old_len = b.len();
self.0.render_once(b)?; self.0.render_once(b)?;
let mut tmp = Buffer::new(); let mut tmp = Buffer::new();
@ -88,7 +92,7 @@ impl<T: RenderOnce> RenderOnce for Upper<T> {
tmp.push(c); tmp.push(c);
} }
unsafe { b._set_len(old_len) }; unsafe { b._set_len(old_len) };
escape::escape_to_buf(tmp.as_str(), b); e.escape_to_buf(b, tmp.as_str());
Ok(()) Ok(())
} }
} }
@ -98,7 +102,7 @@ impl<T: RenderOnce> RenderOnce for Upper<T> {
/// # Examples /// # Examples
/// ///
/// ```text /// ```text
/// <%= "tschüß" | upper %> /// <%\html "tschüß" | upper %>
/// ``` /// ```
/// ///
/// result: /// result:
@ -116,6 +120,7 @@ pub fn upper<T: RenderOnce>(expr: T) -> Upper<T> {
pub struct Lower<T>(T); pub struct Lower<T>(T);
impl<T: RenderOnce> RenderOnce for Lower<T> { impl<T: RenderOnce> RenderOnce for Lower<T> {
#[inline]
fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> { fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> {
let mut tmp = Buffer::new(); let mut tmp = Buffer::new();
self.0.render_once(&mut tmp)?; self.0.render_once(&mut tmp)?;
@ -123,8 +128,10 @@ impl<T: RenderOnce> RenderOnce for Lower<T> {
b.reserve(tmp.len()); b.reserve(tmp.len());
let old_len = b.len(); let old_len = b.len();
for c in tmp.as_str().chars() { for c in tmp.as_str().chars() {
// see comments in str::to_lowercase // See comments in str::to_lowercase
if c == 'Σ' { 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(); let lower = tmp.as_str().to_lowercase();
unsafe { b._set_len(old_len) }; unsafe { b._set_len(old_len) };
b.push_str(&lower); b.push_str(&lower);
@ -137,7 +144,12 @@ impl<T: RenderOnce> RenderOnce for Lower<T> {
Ok(()) 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(); let old_len = b.len();
self.0.render_once(b)?; self.0.render_once(b)?;
let mut tmp = Buffer::new(); let mut tmp = Buffer::new();
@ -149,7 +161,7 @@ impl<T: RenderOnce> RenderOnce for Lower<T> {
if c == 'Σ' { if c == 'Σ' {
let lower = s.to_lowercase(); let lower = s.to_lowercase();
unsafe { b._set_len(old_len) }; unsafe { b._set_len(old_len) };
b.push_str(&lower); e.escape_to_buf(b, &lower);
return Ok(()); return Ok(());
} }
for c in c.to_lowercase() { for c in c.to_lowercase() {
@ -157,7 +169,7 @@ impl<T: RenderOnce> RenderOnce for Lower<T> {
} }
} }
unsafe { b._set_len(old_len) }; unsafe { b._set_len(old_len) };
escape::escape_to_buf(tmp.as_str(), b); e.escape_to_buf(b, tmp.as_str());
Ok(()) Ok(())
} }
} }
@ -167,7 +179,7 @@ impl<T: RenderOnce> RenderOnce for Lower<T> {
/// # Examples /// # Examples
/// ///
/// ```text /// ```text
/// <%= "ὈΔΥΣΣΕΎΣ" | lower %> /// <%\html "ὈΔΥΣΣΕΎΣ" | lower %>
/// ``` /// ```
/// ///
/// result: /// result:
@ -193,9 +205,13 @@ impl<T: RenderOnce> RenderOnce for Trim<T> {
} }
#[inline] #[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(); let old_len = b.len();
self.0.render_once_escaped(b)?; self.0.render_once_escaped(b, e)?;
trim_impl(b, old_len) trim_impl(b, old_len)
} }
} }
@ -238,7 +254,7 @@ fn trim_impl(b: &mut Buffer, old_len: usize) -> Result<(), RenderError> {
/// # Examples /// # Examples
/// ///
/// ```text /// ```text
/// <%= " Hello world\n" | trim %> /// <%\html " Hello world\n" | trim %>
/// ``` /// ```
/// ///
/// result: /// result:
@ -264,23 +280,29 @@ impl<T: RenderOnce> RenderOnce for Truncate<T> {
} }
#[inline] #[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(); 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) truncate_impl(b, old_len, self.1)
} }
} }
#[cfg_attr(feature = "perf-inline", inline)]
fn truncate_impl( fn truncate_impl(
b: &mut Buffer, b: &mut Buffer,
old_len: usize, old_len: usize,
limit: usize, limit: usize,
) -> Result<(), RenderError> { ) -> Result<(), RenderError> {
let new_contents = b.as_str().get(old_len..).ok_or(RenderError::BufSize)?; let new_contents = b.as_str().get(old_len..).ok_or(RenderError::BufSize)?;
if new_contents.len() > limit {
if let Some(idx) = new_contents.char_indices().nth(limit).map(|(i, _)| i) { if let Some(idx) = new_contents.char_indices().nth(limit).map(|(i, _)| i) {
unsafe { b._set_len(old_len + idx) }; unsafe { b._set_len(old_len + idx) };
b.push_str("..."); b.push_str("...");
}
} }
Ok(()) Ok(())
@ -293,7 +315,7 @@ fn truncate_impl(
/// The following example renders the first 20 characters of `message` /// The following example renders the first 20 characters of `message`
/// ///
/// ```test /// ```test
/// <%= "Hello, world!" | truncate(5) %> /// <%\html "Hello, world!" | truncate(5) %>
/// ``` /// ```
/// ///
/// result: /// result:
@ -340,16 +362,18 @@ cfg_json! {
} }
#[inline] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(
use super::escape::escape_to_buf; &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, E: Escape> std::io::Write for Writer<'a, E> {
impl<'a> std::io::Write for Writer<'a> {
#[inline] #[inline]
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> { fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let buf = unsafe { std::str::from_utf8_unchecked(buf) }; 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()) 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())) .map_err(|e| RenderError::new(&e.to_string()))
} }
} }
@ -387,6 +411,8 @@ cfg_json! {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::EscapeHtml;
use super::*; use super::*;
fn assert_render<T: RenderOnce>(expr: T, expected: &str) { 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) { fn assert_render_escaped<T: RenderOnce>(expr: T, expected: &str) {
let mut buf = Buffer::new(); 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); assert_eq!(buf.as_str(), expected);
} }

View File

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

View File

@ -1,7 +1,7 @@
#![allow(clippy::cast_ptr_alignment)] #![allow(clippy::cast_ptr_alignment)]
use super::super::Buffer;
use super::naive; use super::naive;
use crate::Buffer;
#[cfg(target_pointer_width = "16")] #[cfg(target_pointer_width = "16")]
const USIZE_BYTES: usize = 2; 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: [&str; 5] = ["&quot;", "&amp;", "&#039;", "&lt;", "&gt;"];
const ESCAPED_LEN: usize = 5; const ESCAPED_LEN: usize = 5;
use super::buffer::Buffer; use super::{Buffer, Escape};
/// write the escaped contents into `Buffer` /// A scheme for escaping strings for safe insertion into HTML.
#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), not(miri)))] #[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "perf-inline", inline)] pub struct EscapeHtml;
pub fn escape_to_buf(feed: &str, buf: &mut Buffer) {
#[cfg(not(target_feature = "avx2"))]
{
use std::sync::atomic::{AtomicPtr, Ordering};
type FnRaw = *mut (); impl Escape for EscapeHtml {
static FN: AtomicPtr<()> = AtomicPtr::new(detect as FnRaw); type Escaped = &'static str;
fn detect(feed: &str, buf: &mut Buffer) { const IDENT_BOOLS: bool = true;
debug_assert!(feed.len() >= 16); const IDENT_UINTS: bool = true;
let fun = if is_x86_feature_detected!("avx2") { const IDENT_INTS: bool = true;
avx2::escape const IDENT_FLOATS: bool = true;
} else if is_x86_feature_detected!("sse2") {
sse2::escape
} else {
fallback::escape
};
FN.store(fun as FnRaw, Ordering::Relaxed); #[inline(always)]
unsafe { fun(feed, buf) }; 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 { unsafe {
if feed.len() < 16 { if string.len() < 16 {
buf.reserve_small(feed.len() * 6); buf.reserve_small(string.len() * 6);
let l = naive::escape_small(feed, buf.as_mut_ptr().add(buf.len())); let l = naive::escape_small(string, buf.as_mut_ptr().add(buf.len()));
buf.advance(l); buf.advance(l);
} else { } else {
let fun = FN.load(Ordering::Relaxed); #[cfg(target_feature = "avx2")]
std::mem::transmute::<FnRaw, fn(&str, &mut Buffer)>(fun)(feed, buf); 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")] #[cfg(not(all(any(target_arch = "x86", target_arch = "x86_64"), not(miri))))]
unsafe { #[cfg_attr(feature = "perf-inline", inline)]
if feed.len() < 16 { fn escape_to_buf(&self, buffer: &mut Buffer, string: &str) {
buf.reserve_small(feed.len() * 6); unsafe {
let l = naive::escape_small(feed, buf.as_mut_ptr().add(buf.len())); if cfg!(miri) {
buf.advance(l); let bp = feed.as_ptr();
} else if cfg!(target_feature = "avx2") { naive::escape(buf, bp, bp, bp.add(feed.len()))
avx2::escape(feed, buf); } 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` /// write the escaped contents into `Buffer`
#[cfg(not(all(any(target_arch = "x86", target_arch = "x86_64"), not(miri))))] #[deprecated = "Use [`EscapeHtml::escape_to_buf`] instead"]
#[cfg_attr(feature = "perf-inline", inline)] #[inline(always)]
pub fn escape_to_buf(feed: &str, buf: &mut Buffer) { fn escape_to_buf(feed: &str, buf: &mut Buffer) {
unsafe { EscapeHtml.escape_to_buf(buf, feed)
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);
} }
#[cfg(test)] #[cfg(test)]
@ -130,7 +132,7 @@ mod tests {
fn escape(feed: &str) -> String { fn escape(feed: &str) -> String {
let mut s = String::new(); let mut s = String::new();
escape_to_string(feed, &mut s); EscapeHtml.escape_to_string(&mut s, feed);
s s
} }

View File

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

View File

@ -6,9 +6,9 @@ use std::arch::x86::*;
use std::arch::x86_64::*; use std::arch::x86_64::*;
use std::slice; use std::slice;
use super::super::Buffer;
use super::naive::push_escaped_str; use super::naive::push_escaped_str;
use super::{ESCAPED, ESCAPED_LEN, ESCAPE_LUT}; use super::{ESCAPED, ESCAPED_LEN, ESCAPE_LUT};
use crate::Buffer;
const VECTOR_BYTES: usize = std::mem::size_of::<__m128i>(); 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 //! 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 //! 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 //! ```ignore
//! use sailfish::RenderOnce; //! use sailfish::RenderOnce;
@ -33,12 +33,29 @@
)] )]
#![cfg_attr(sailfish_nightly, feature(core_intrinsics))] #![cfg_attr(sailfish_nightly, feature(core_intrinsics))]
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
#![feature(maybe_uninit_slice)]
#![feature(specialization)]
#![allow(clippy::redundant_closure)] #![allow(clippy::redundant_closure)]
#![deny(missing_docs)] #![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(feature = "derive")]
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))] #[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
pub use sailfish_macros::{Render, RenderOnce}; pub use sailfish_macros::{Render, RenderOnce};

View File

@ -1,6 +1,6 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::{Ref, RefMut}; use std::cell::{Ref, RefMut};
use std::fmt; use std::fmt::{self, Arguments, Write};
use std::num::{ use std::num::{
NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize,
NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, Wrapping, NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, Wrapping,
@ -9,12 +9,9 @@ use std::path::{Path, PathBuf};
use std::rc::Rc; use std::rc::Rc;
use std::sync::{Arc, MutexGuard, RwLockReadGuard, RwLockWriteGuard}; use std::sync::{Arc, MutexGuard, RwLockReadGuard, RwLockWriteGuard};
use crate::runtime::SizeHint; use super::{Buffer, Escape, SizeHint};
use super::buffer::Buffer; /// types which can be rendered inside buffer block (`<%- %>`) by reference
use super::escape;
/// 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 /// If you want to render the custom data, you must implement this trait and specify
/// the behaviour. /// the behaviour.
@ -29,7 +26,7 @@ use super::escape;
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// use sailfish::runtime::{Buffer, Render, RenderError}; /// use sailfish::{Buffer, Render, RenderError};
/// ///
/// struct MyU64(u64); /// struct MyU64(u64);
/// ///
@ -46,15 +43,16 @@ pub trait Render {
/// render to `Buffer` with HTML escaping /// render to `Buffer` with HTML escaping
#[inline] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(
let mut tmp = Buffer::new(); &self,
self.render(&mut tmp)?; b: &mut Buffer,
escape::escape_to_buf(tmp.as_str(), b); e: &E,
Ok(()) ) -> 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. /// See [`Render`] for more information.
pub trait RenderOnce: Sized { pub trait RenderOnce: Sized {
@ -90,11 +88,12 @@ pub trait RenderOnce: Sized {
/// render to `Buffer` with HTML escaping /// render to `Buffer` with HTML escaping
#[inline] #[inline]
fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> { fn render_once_escaped<E: Escape>(
let mut tmp = Buffer::new(); self,
self.render_once(&mut tmp)?; b: &mut Buffer,
escape::escape_to_buf(tmp.as_str(), b); e: &E,
Ok(()) ) -> Result<(), RenderError> {
default_render_once_escaped(self, b, e)
} }
/// Render the template and return the rendering result as `RenderResult` /// 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 // impl<'a, T: ?Sized> Render for &'a T
// where // where
// T: Render, // T: Render,
@ -126,8 +149,8 @@ pub trait RenderOnce: Sized {
// T::render(self, b) // T::render(self, b)
// } // }
// fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { // fn render_escaped<E: Escape>(&self, b: &mut Buffer, e: &E) -> Result<(), RenderError> {
// T::render_escaped(self, b) // T::render_escaped(self, b, e)
// } // }
// } // }
@ -138,34 +161,15 @@ impl<T: Render> RenderOnce for T {
} }
#[inline] #[inline]
fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> { fn render_once_escaped<E: Escape>(
self.render_escaped(b) 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 { impl Render for String {
#[inline] #[inline]
fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render(&self, b: &mut Buffer) -> Result<(), RenderError> {
@ -174,8 +178,12 @@ impl Render for String {
} }
#[inline] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(
escape::escape_to_buf(self, b); &self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
e.escape_to_buf(b, self);
Ok(()) Ok(())
} }
} }
@ -188,8 +196,12 @@ impl Render for str {
} }
#[inline] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(
escape::escape_to_buf(self, b); &self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
e.escape_to_buf(b, self);
Ok(()) Ok(())
} }
} }
@ -202,14 +214,14 @@ impl Render for char {
} }
#[inline] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(
match *self { &self,
'\"' => b.push_str("&quot;"), b: &mut Buffer,
'&' => b.push_str("&amp;"), e: &E,
'<' => b.push_str("&lt;"), ) -> Result<(), RenderError> {
'>' => b.push_str("&gt;"), match e.escape(*self) {
'\'' => b.push_str("&#039;"), Some(s) => b.push_str(s.as_ref()),
_ => b.push(*self), None => b.push(*self),
} }
Ok(()) Ok(())
} }
@ -224,8 +236,12 @@ impl Render for PathBuf {
} }
#[inline] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(
escape::escape_to_buf(self.to_str().ok_or(RenderError::Fmt(fmt::Error))?, b); &self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
e.escape_to_buf(b, self.to_str().ok_or(RenderError::Fmt(fmt::Error))?);
Ok(()) Ok(())
} }
} }
@ -239,8 +255,12 @@ impl Render for Path {
} }
#[inline] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(
escape::escape_to_buf(self.to_str().ok_or(RenderError::Fmt(fmt::Error))?, b); &self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
e.escape_to_buf(b, self.to_str().ok_or(RenderError::Fmt(fmt::Error))?);
Ok(()) Ok(())
} }
} }
@ -278,13 +298,26 @@ impl Render for bool {
} }
#[inline] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(
self.render(b) &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 { macro_rules! render_int {
($($int:ty),*) => { ($ident_tag:ident, $($int:ty),*) => {
$( $(
impl Render for $int { impl Render for $int {
#[cfg_attr(feature = "perf-inline", inline)] #[cfg_attr(feature = "perf-inline", inline)]
@ -307,16 +340,21 @@ macro_rules! render_int {
} }
#[inline] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(&self, b: &mut Buffer, e: &E) -> Result<(), RenderError> {
// push_str without escape if E::$ident_tag {
self.render(b) // 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 { impl Render for f32 {
#[cfg_attr(feature = "perf-inline", inline)] #[cfg_attr(feature = "perf-inline", inline)]
@ -341,9 +379,16 @@ impl Render for f32 {
} }
#[inline] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(
// escape string &self,
self.render(b) 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] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(
// escape string &self,
self.render(b) 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] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(&self, b: &mut Buffer, e: &E) -> Result<(), RenderError> {
(**self).render_escaped(b) (**self).render_escaped(b, e)
} }
} }
}; };
@ -399,17 +477,17 @@ macro_rules! render_deref {
// render_ref!(['a, T] [&'a T: Render] T); // render_ref!(['a, T] [&'a T: Render] T);
// render_ref!(['a] [] String); // render_ref!(['a] [] String);
render_deref!(['a, T: Render + ?Sized] [] &'a T); render_deref!([T: Render + ?Sized] [] &T);
render_deref!(['a, T: Render + ?Sized] [] &'a mut T); render_deref!([T: Render + ?Sized] [] &mut T);
render_deref!([T: Render + ?Sized] [] Box<T>); render_deref!([T: Render + ?Sized] [] Box<T>);
render_deref!([T: Render + ?Sized] [] Rc<T>); render_deref!([T: Render + ?Sized] [] Rc<T>);
render_deref!([T: Render + ?Sized] [] Arc<T>); render_deref!([T: Render + ?Sized] [] Arc<T>);
render_deref!(['a, T: Render + ToOwned + ?Sized] [] Cow<'a, T>); render_deref!([T: Render + ToOwned + ?Sized] [] Cow<'_, T>);
render_deref!(['a, T: Render + ?Sized] [] Ref<'a, T>, [*]); render_deref!([T: Render + ?Sized] [] Ref<'_, T>, [*]);
render_deref!(['a, T: Render + ?Sized] [] RefMut<'a, T>, [*]); render_deref!([T: Render + ?Sized] [] RefMut<'_, T>, [*]);
render_deref!(['a, T: Render + ?Sized] [] MutexGuard<'a, T>, [*]); render_deref!([T: Render + ?Sized] [] MutexGuard<'_, T>, [*]);
render_deref!(['a, T: Render + ?Sized] [] RwLockReadGuard<'a, T>, [*]); render_deref!([T: Render + ?Sized] [] RwLockReadGuard<'_, T>, [*]);
render_deref!(['a, T: Render + ?Sized] [] RwLockWriteGuard<'a, T>, [*]); render_deref!([T: Render + ?Sized] [] RwLockWriteGuard<'_, T>, [*]);
macro_rules! render_nonzero { macro_rules! render_nonzero {
($($type:ty,)*) => { ($($type:ty,)*) => {
@ -421,8 +499,8 @@ macro_rules! render_nonzero {
} }
#[inline] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(&self, b: &mut Buffer, e: &E) -> Result<(), RenderError> {
self.get().render_escaped(b) self.get().render_escaped(b, e)
} }
} }
)* )*
@ -451,8 +529,12 @@ impl<T: Render> Render for Wrapping<T> {
} }
#[inline] #[inline]
fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { fn render_escaped<E: Escape>(
self.0.render_escaped(b) &self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
self.0.render_escaped(b, e)
} }
} }
@ -476,10 +558,10 @@ macro_rules! render_tuple {
} }
#[inline] #[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)] #[allow(non_snake_case)]
let ($($T,)+) = self; let ($($T,)+) = self;
$($T.render_escaped(b)?;)+ $($T.render_escaped(b, e)?;)+
Ok(()) Ok(())
} }
} }
@ -505,10 +587,10 @@ macro_rules! render_once_tuple {
} }
#[inline] #[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)] #[allow(non_snake_case)]
let ($($T,)+) = self; let ($($T,)+) = self;
$($T.render_once_escaped(b)?;)+ $($T.render_once_escaped(b, e)?;)+
Ok(()) Ok(())
} }
} }
@ -574,9 +656,12 @@ pub type RenderResult = Result<String, RenderError>;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use std::error::Error; use std::error::Error;
use crate::EscapeHtml;
use super::*;
#[test] #[test]
fn receiver_coercion() { fn receiver_coercion() {
let mut b = Buffer::new(); let mut b = Buffer::new();
@ -589,24 +674,24 @@ mod tests {
RenderOnce::render_once(&true, &mut b).unwrap(); RenderOnce::render_once(&true, &mut b).unwrap();
RenderOnce::render_once(&&false, &mut b).unwrap(); RenderOnce::render_once(&&false, &mut b).unwrap();
RenderOnce::render_once_escaped(&&&true, &mut b).unwrap(); RenderOnce::render_once_escaped(&&&true, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(&&&&false, &mut b).unwrap(); RenderOnce::render_once_escaped(&&&&false, &mut b, &EscapeHtml).unwrap();
assert_eq!(b.as_str(), "truefalsetruefalse"); assert_eq!(b.as_str(), "truefalsetruefalse");
b.clear(); b.clear();
let s = "apple"; let s = "apple";
RenderOnce::render_once_escaped(&s, &mut b).unwrap(); RenderOnce::render_once_escaped(&s, &mut b, &EscapeHtml).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).unwrap(); RenderOnce::render_once_escaped(&&s, &mut b, &EscapeHtml).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).unwrap(); RenderOnce::render_once_escaped(&&&&s, &mut b, &EscapeHtml).unwrap();
assert_eq!(b.as_str(), "appleappleappleappleapple"); assert_eq!(b.as_str(), "appleappleappleappleapple");
b.clear(); b.clear();
RenderOnce::render_once_escaped(&'c', &mut b).unwrap(); RenderOnce::render_once_escaped(&'c', &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(&&'<', &mut b).unwrap(); RenderOnce::render_once_escaped(&&'<', &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(&&&'&', &mut b).unwrap(); RenderOnce::render_once_escaped(&&&'&', &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(&&&&' ', &mut b).unwrap(); RenderOnce::render_once_escaped(&&&&' ', &mut b, &EscapeHtml).unwrap();
assert_eq!(b.as_str(), "c&lt;&amp; "); assert_eq!(b.as_str(), "c&lt;&amp; ");
b.clear(); b.clear();
} }
@ -619,10 +704,10 @@ mod tests {
let mut b = Buffer::new(); let mut b = Buffer::new();
Render::render(&String::from("a"), &mut b).unwrap(); Render::render(&String::from("a"), &mut b).unwrap();
Render::render(&PathBuf::from("b"), &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(4u32), &mut b, &EscapeHtml).unwrap();
Render::render_escaped(&Rc::new(2.3f32), &mut b).unwrap(); Render::render_escaped(&Rc::new(2.3f32), &mut b, &EscapeHtml).unwrap();
Render::render_escaped(Path::new("<"), &mut b).unwrap(); Render::render_escaped(Path::new("<"), &mut b, &EscapeHtml).unwrap();
Render::render_escaped(&Path::new("d"), &mut b).unwrap(); Render::render_escaped(&Path::new("d"), &mut b, &EscapeHtml).unwrap();
assert_eq!(b.as_str(), "ab42.3&lt;d"); assert_eq!(b.as_str(), "ab42.3&lt;d");
} }
@ -631,17 +716,19 @@ mod tests {
fn float() { fn float() {
let mut b = Buffer::new(); let mut b = Buffer::new();
RenderOnce::render_once_escaped(0.0f64, &mut b).unwrap(); RenderOnce::render_once_escaped(0.0f64, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(std::f64::INFINITY, &mut b).unwrap(); RenderOnce::render_once_escaped(std::f64::INFINITY, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(std::f64::NEG_INFINITY, &mut b).unwrap(); RenderOnce::render_once_escaped(std::f64::NEG_INFINITY, &mut b, &EscapeHtml)
RenderOnce::render_once_escaped(std::f64::NAN, &mut b).unwrap(); .unwrap();
RenderOnce::render_once_escaped(std::f64::NAN, &mut b, &EscapeHtml).unwrap();
assert_eq!(b.as_str(), "0.0inf-infNaN"); assert_eq!(b.as_str(), "0.0inf-infNaN");
b.clear(); b.clear();
RenderOnce::render_once_escaped(0.0f32, &mut b).unwrap(); RenderOnce::render_once_escaped(0.0f32, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(std::f32::INFINITY, &mut b).unwrap(); RenderOnce::render_once_escaped(std::f32::INFINITY, &mut b, &EscapeHtml).unwrap();
RenderOnce::render_once_escaped(std::f32::NEG_INFINITY, &mut b).unwrap(); RenderOnce::render_once_escaped(std::f32::NEG_INFINITY, &mut b, &EscapeHtml)
RenderOnce::render_once_escaped(std::f32::NAN, &mut b).unwrap(); .unwrap();
RenderOnce::render_once_escaped(std::f32::NAN, &mut b, &EscapeHtml).unwrap();
assert_eq!(b.as_str(), "0.0inf-infNaN"); assert_eq!(b.as_str(), "0.0inf-infNaN");
} }
@ -650,7 +737,9 @@ mod tests {
let mut b = Buffer::new(); let mut b = Buffer::new();
let funcs: Vec<fn(char, &mut Buffer) -> Result<(), RenderError>> = 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 { for func in funcs {
func('a', &mut b).unwrap(); func('a', &mut b).unwrap();
@ -675,7 +764,12 @@ mod tests {
fn test_nonzero() { fn test_nonzero() {
let mut b = Buffer::with_capacity(2); let mut b = Buffer::with_capacity(2);
RenderOnce::render_once(NonZeroU8::new(10).unwrap(), &mut b).unwrap(); 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"); 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 include @rustSyntax syntax/rust.vim
syn region sailfishCodeBlock matchgroup=sailfishTag start=/<%/ keepend end=/%>/ contains=@rustSyntax 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=/%>/ syn region sailfishCommentBlock start=/<%#/ end=/%>/
" Redefine htmlTag so that it can contain jspExpr " Redefine htmlTag so that it can contain jspExpr

View File

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