Refactor to support custom escaping schemes

This commit is contained in:
Michael Pfaff 2024-03-15 18:05:31 -04:00
commit 2f8f1010f8
62 changed files with 745 additions and 459 deletions

23
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "basic-toml"
@ -29,6 +29,17 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "equivalent"
version = "1.0.1"
@ -172,6 +183,7 @@ dependencies = [
"sailfish-macros",
"serde",
"serde_json",
"tinystr",
"version_check",
]
@ -258,6 +270,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "tinystr"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
dependencies = [
"displaydoc",
]
[[package]]
name = "toml"
version = "0.8.2"

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

@ -4,9 +4,9 @@
You can control the rendering behaviour via `template` attribute.
``` rust
#[derive(TemplateSimple)]
#[template(path = "template.stpl", escape = false)]
```rust
#[derive(Render)]
#[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,20 +17,19 @@ 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 %>
```

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 %><% ; %>
```

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

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

@ -104,7 +104,7 @@ impl fmt::Display for Error {
if let Some(ref source_file) = self.source_file {
let source_file =
if env::var("SAILFISH_INTEGRATION_TESTS").map_or(false, |s| s == "1") {
if env::var("SAILFISH_INTEGRATION_TESTS").is_ok_and(|s| s == "1") {
match source_file.file_name() {
Some(f) => Path::new(f),
None => Path::new(""),
@ -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

@ -122,7 +122,7 @@ impl VisitMut for OptmizerImpl {
fn visit_expr_call_mut(&mut self, i: &mut ExprCall) {
if self.rm_whitespace {
if let Some(v) = get_rendertext_value(&i) {
if let Some(v) = get_rendertext_value(i) {
let ts = match remove_whitespace(v) {
Some(value) => value,
None => return,

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
@ -176,8 +182,7 @@ impl<'a> ParseStream<'a> {
if token_kind == TokenKind::Comment {
let block_delim_end = self.block_delimiter.1.as_bytes();
let pos = self.source[start..]
.as_bytes()
let pos = self.source.as_bytes()[start..]
.windows(1 + block_delim_end.len())
.enumerate()
.position(|(_, window)| {
@ -201,9 +206,8 @@ impl<'a> ParseStream<'a> {
{
// closing bracket was found
self.take_n(start);
let s = &self.source[..pos - self.block_delimiter.1.len()].trim_end_matches(
|c| matches!(c, ' ' | '\t' | '\r' | '\u{000B}' | '\u{000C}'),
);
let s = &self.source[..pos - self.block_delimiter.1.len()]
.trim_end_matches([' ', '\t', '\r', '\u{000B}', '\u{000C}']);
let token = Token {
content: s,
offset: self.offset(),
@ -299,7 +303,7 @@ fn find_block_end(haystack: &str, delimiter: &str) -> Option<usize> {
},
b'\"' => {
// check if the literal is a raw string
for (i, byte) in remain[..pos].as_bytes().iter().enumerate().rev() {
for (i, byte) in remain.as_bytes()[..pos].iter().enumerate().rev() {
match byte {
b'#' => {}
b'r' => {
@ -424,7 +428,7 @@ mod tests {
Token {
content: "inner | upper",
offset: 10,
kind: TokenKind::BufferedCode { escape: false },
kind: TokenKind::BufferedCode { escape: None },
},
Token {
content: " outer",
@ -437,7 +441,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 +465,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 +486,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 +499,10 @@ mod tests {
},
Token {
content: "/* %%>*/ 1",
offset: 16,
kind: TokenKind::BufferedCode { escape: true }
offset: 20,
kind: TokenKind::BufferedCode {
escape: Some("html")
}
},
]
);

View file

@ -21,7 +21,6 @@ struct DeriveTemplateOptions {
found_keys: Vec<Ident>,
path: Option<LitStr>,
delimiter: Option<LitChar>,
escape: Option<LitBool>,
rm_whitespace: Option<LitBool>,
}
@ -33,7 +32,7 @@ impl DeriveTemplateOptions {
s.parse::<Token![=]>()?;
// check if argument is repeated
if self.found_keys.iter().any(|e| *e == key) {
if self.found_keys.contains(&key) {
return Err(syn::Error::new(
key.span(),
format!("Argument `{}` was repeated.", key),
@ -44,8 +43,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 {
@ -74,9 +71,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;
}
@ -169,7 +163,7 @@ fn derive_template_common_impl(
#[cfg(not(feature = "config"))]
let mut config = Config::default();
if env::var("SAILFISH_INTEGRATION_TESTS").map_or(false, |s| s == "1") {
if env::var("SAILFISH_INTEGRATION_TESTS").is_ok_and(|s| s == "1") {
let template_dir = env::current_dir()
.unwrap()
.ancestors()
@ -341,7 +335,7 @@ fn derive_template_once_only_impl(
// drops when the implementation is written in `sailfish` crate.
quote! {
impl #impl_generics sailfish::RenderOnce for #name #ty_generics #where_clause {
fn render_once(mut 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;
@ -352,7 +346,7 @@ fn derive_template_once_only_impl(
}
fn render_once_to_string(mut self) -> sailfish::RenderResult {
use sailfish::runtime::{Buffer, SizeHint};
use sailfish::{Buffer, SizeHint};
static SIZE_HINT: SizeHint = SizeHint::new();
let mut buf = Buffer::with_capacity(SIZE_HINT.get());
@ -377,7 +371,7 @@ fn derive_template_mut_only_impl(
// drops when the implementation is written in `sailfish` crate.
quote! {
impl #impl_generics sailfish::RenderMut for #name #ty_generics #where_clause {
fn render_mut(&mut self, __sf_buf: &mut sailfish::runtime::Buffer) -> std::result::Result<(), sailfish::runtime::RenderError> {
fn render_mut(&mut self, __sf_buf: &mut sailfish::Buffer) -> std::result::Result<(), sailfish::RenderError> {
// This line is required for cargo to track child templates
#include_bytes_seq;
@ -388,7 +382,7 @@ fn derive_template_mut_only_impl(
}
fn render_mut_to_string(&mut self) -> sailfish::RenderResult {
use sailfish::runtime::{Buffer, SizeHint};
use sailfish::{Buffer, SizeHint};
static SIZE_HINT: SizeHint = SizeHint::new();
let mut buf = Buffer::with_capacity(SIZE_HINT.get());
@ -413,7 +407,7 @@ fn derive_template_only_impl(
// drops when the implementation is written in `sailfish` crate.
quote! {
impl #impl_generics sailfish::Render for #name #ty_generics #where_clause {
fn render(&self, __sf_buf: &mut sailfish::runtime::Buffer) -> std::result::Result<(), sailfish::runtime::RenderError> {
fn render(&self, __sf_buf: &mut sailfish::Buffer) -> std::result::Result<(), sailfish::RenderError> {
// This line is required for cargo to track child templates
#include_bytes_seq;
@ -424,7 +418,7 @@ fn derive_template_only_impl(
}
fn render_to_string(&self) -> sailfish::RenderResult {
use sailfish::runtime::{Buffer, SizeHint};
use sailfish::{Buffer, SizeHint};
static SIZE_HINT: SizeHint = SizeHint::new();
let mut buf = Buffer::with_capacity(SIZE_HINT.get());

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(
@ -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(
&mut self,
token: &Token<'_>,
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(
&mut self,
token: &Token<'_>,
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,13 +168,13 @@ impl SourceBuilder {
),
};
self.source.push_str("sailfish::runtime::filter::");
self.source.push_str("__sf_rt::filter::");
self.source.push_str(&name);
self.source.push('(');
// arguments to filter function
{
self.source.push_str("(");
self.source.push('(');
let entry = SourceMapEntry {
original: token.offset(),
new: self.source.len(),
@ -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(())
@ -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

@ -70,10 +70,7 @@ pub fn rustfmt_block(source: &str) -> io::Result<String> {
s.replace_range(..brace_offset, "");
Ok(s)
} else {
Err(io::Error::new(
io::ErrorKind::Other,
"rustfmt command failed",
))
Err(io::Error::other("rustfmt command failed"))
}
}

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: <%- self.message | disp %>
dbg: <%- self.message | dbg %>
disp escaped: <%= self.message | disp %>
dbg escaped: <%= self.message | dbg %>
disp: <%- &self.message | disp %>
dbg: <%- &self.message | dbg %>
disp escaped: <%\html &self.message | disp %>
dbg escaped: <%\html &self.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 { %><%- 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": <%- &self.name | dbg %>,
"value": <%= self.value %>
"name": <%- &self.name | json %>,
"value": <%- self.value %>
}

View file

@ -1,9 +1,9 @@
ESCAPED: <%= self.uppercase() %>
ESCAPED: <%\html self.uppercase() %>
NON-ESCAPED: <%- self.uppercase() %>
PASS-VALUE: <%= Self::uppercase_val(self.s) %>
PASS-REF: <%= Self::multiply_ref(&self.i) %>
MUTABLE: <% self.mutate(); %><%= &self.mutate %>
MATCH: <%= match self.uppercase().as_str() {
PASS-VALUE: <%\html Self::uppercase_val(self.s) %>
PASS-REF: <%\html Self::multiply_ref(&self.i) %>
MUTABLE: <% self.mutate(); %><%\html &self.mutate %>
MATCH: <%\html match self.uppercase().as_str() {
"<TEST>" => "Cool",
_ => "Not cool"
} %>

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 self.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 &self.items { %><tr><td><%= item.id %></td><td><%= item.message %></td></tr><% } %>
<% for item in &self.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,8 @@
<<<<<<< HEAD
<% for player in &self.players %>
<div><%= player.name %>: <%= player.score %></div>
=======
<% for player in &self.players %>
<div><%\html player.name %>: <%\html player.score %></div>
>>>>>>> 6e09ca7 (Refactor to support custom escaping schemes)
<% } %>

View file

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

View file

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

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,8 +1,5 @@
extern crate sailfish_macros;
use integration_tests::assert_string_eq;
use sailfish::runtime::RenderResult;
use sailfish::{RenderOnce, RenderMut, Render};
use sailfish::{RenderOnce, RenderMut, Render, RenderResult};
use std::path::PathBuf;
fn assert_render_result(name: &str, result: RenderResult) {
@ -231,7 +228,7 @@ fn test_rust_macro() {
}
#[derive(Render)]
#[template(path = "formatting.stpl", escape = false)]
#[template(path = "formatting.stpl")]
struct Formatting;
#[test]

View file

@ -28,6 +28,7 @@ itoap = "1.0.1"
ryu = "1.0.13"
serde = { version = "1.0.159", optional = true }
serde_json = { version = "1.0.95", optional = true }
tinystr = { version = "0.8.1", default-features = false }
[dependencies.sailfish-macros]
path = "../sailfish-macros"
@ -41,3 +42,6 @@ version_check = "0.9.4"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints.rust]
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(sailfish_nightly)'] }

View file

@ -107,7 +107,7 @@ impl Buffer {
/// overflows `isize::MAX`.
#[inline]
pub(crate) unsafe fn reserve_small(&mut self, size: usize) {
debug_assert!(size <= std::isize::MAX as usize);
debug_assert!(size <= isize::MAX as usize);
if likely!(self.len + size <= self.capacity) {
return;
}
@ -163,7 +163,7 @@ impl Buffer {
#[cfg_attr(feature = "perf-inline", inline)]
fn reserve_internal(&mut self, size: usize) {
debug_assert!(size <= std::isize::MAX as usize);
debug_assert!(size <= isize::MAX as usize);
let new_capacity = std::cmp::max(self.capacity * 2, self.capacity + size);
debug_assert!(new_capacity > self.capacity);
@ -179,7 +179,7 @@ impl Buffer {
fn safe_alloc(capacity: usize) -> *mut u8 {
assert!(capacity > 0);
assert!(
capacity <= std::isize::MAX as usize,
capacity <= isize::MAX as usize,
"capacity is too large"
);
@ -198,13 +198,13 @@ fn safe_alloc(capacity: usize) -> *mut u8 {
/// # Safety
///
/// - if `capacity > 0`, `capacity` is the same value that was used to allocate the block
/// of memory pointed by `ptr`.
/// of memory pointed by `ptr`.
#[cold]
#[inline(never)]
unsafe fn safe_realloc(ptr: *mut u8, capacity: usize, new_capacity: usize) -> *mut u8 {
assert!(new_capacity > 0);
assert!(
new_capacity <= std::isize::MAX as usize,
new_capacity <= isize::MAX as usize,
"capacity is too large"
);

98
sailfish/src/escape.rs Normal file
View file

@ -0,0 +1,98 @@
use tinystr::{tinystr, TinyAsciiStr};
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 = TinyAsciiStrWrapper<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(TinyAsciiStrWrapper(tinystr!(4, r#"\""#))),
'\\' => Some(TinyAsciiStrWrapper(tinystr!(4, r"\\"))),
'\u{0000}'..='\u{001F}' => {
let c = c as u8;
const HEX_DIGITS: &[u8; 16] = b"0123456789ABCDEF";
let s = [
b'\\',
b'u',
HEX_DIGITS[usize::from(c >> 4)],
HEX_DIGITS[usize::from(c & 0xF)],
];
Some(TinyAsciiStrWrapper(unsafe {
// SAFETY: we only write valid UTF-8, and never NUL bytes
TinyAsciiStr::from_utf8_unchecked(s)
}))
}
_ => None,
}
}
}
pub struct TinyAsciiStrWrapper<const N: usize>(TinyAsciiStr<N>);
impl<const N: usize> AsRef<str> for TinyAsciiStrWrapper<N> {
#[inline]
fn as_ref(&self) -> &str {
self.0.as_str()
}
}

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 old_len = b.len();
self.0.render_once(b)?;
@ -78,15 +77,20 @@ 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 content = b.as_str().get(old_len..).ok_or(RenderError::BufSize)?;
// TODO: don't allocate unless a character expands to multiple characters
let s = content.to_uppercase();
unsafe { b._set_len(old_len) };
escape::escape_to_buf(s.as_str(), b);
e.escape_to_buf(b, s.as_str());
Ok(())
}
}
@ -96,7 +100,7 @@ impl<T: RenderOnce> RenderOnce for Upper<T> {
/// # Examples
///
/// ```text
/// <%= "tschüß" | upper %>
/// <%\html "tschüß" | upper %>
/// ```
///
/// result:
@ -114,11 +118,13 @@ 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 old_len = b.len();
self.0.render_once(b)?;
let content = b.as_str().get(old_len..).ok_or(RenderError::BufSize)?;
// TODO: don't allocate unless a character expands to multiple characters
let s = content.to_lowercase();
unsafe { b._set_len(old_len) };
@ -126,7 +132,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)?;
@ -134,7 +145,7 @@ impl<T: RenderOnce> RenderOnce for Lower<T> {
let s = content.to_lowercase();
unsafe { b._set_len(old_len) };
escape::escape_to_buf(s.as_str(), b);
e.escape_to_buf(b, s.as_str());
Ok(())
}
}
@ -144,7 +155,7 @@ impl<T: RenderOnce> RenderOnce for Lower<T> {
/// # Examples
///
/// ```text
/// <%= "ὈΔΥΣΣΕΎΣ" | lower %>
/// <%\html "ὈΔΥΣΣΕΎΣ" | lower %>
/// ```
///
/// result:
@ -170,9 +181,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)
}
}
@ -215,7 +230,7 @@ fn trim_impl(b: &mut Buffer, old_len: usize) -> Result<(), RenderError> {
/// # Examples
///
/// ```text
/// <%= " Hello world\n" | trim %>
/// <%\html " Hello world\n" | trim %>
/// ```
///
/// result:
@ -241,23 +256,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(())
@ -270,7 +291,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:
@ -296,14 +317,16 @@ cfg_json! {
impl<'a> std::io::Write for Writer<'a> {
#[inline]
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let buf = unsafe { std::str::from_utf8_unchecked(buf) };
self.0.push_str(buf);
self.write_all(buf)?;
Ok(buf.len())
}
#[inline]
fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
self.write(buf).map(|_| {})
// SAFETY: serde_json only emits valid UTF-8
let buf = unsafe { std::str::from_utf8_unchecked(buf) };
self.0.push_str(buf);
Ok(())
}
#[inline]
@ -317,16 +340,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())
}
@ -341,7 +366,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()))
}
}
@ -364,6 +389,8 @@ cfg_json! {
#[cfg(test)]
mod tests {
use crate::EscapeHtml;
use super::*;
fn assert_render<T: RenderOnce>(expr: T, expected: &str) {
@ -374,7 +401,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;
@ -31,14 +31,29 @@
#![doc(
html_logo_url = "https://raw.githubusercontent.com/rust-sailfish/sailfish/master/resources/icon.png"
)]
#![cfg_attr(sailfish_nightly, feature(core_intrinsics))]
#![cfg_attr(sailfish_nightly, feature(likely_unlikely))]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![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, RenderMut, RenderOnce, RenderResult};
pub use size_hint::SizeHint;
pub use runtime::{Buffer, Render, RenderError, RenderMut, RenderOnce, RenderResult};
#[cfg(feature = "derive")]
#[cfg_attr(docsrs, doc(cfg(feature = "derive")))]
pub use sailfish_macros::{Render, RenderMut, 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.
@ -24,24 +21,23 @@ use super::escape;
/// This trait allows modifying the previously-rendered contents or even decreasing the
/// buffer size. However, such an operation easily cause unexpected rendering results.
/// In order to avoid this, implementors should ensure that the contents which is already
/// rendered won't be changed during `render` or `render_escaped` method is called.
/// rendered won't be changed during [`render_once`] or [`render_once_escaped`] method is
/// called.
///
/// # Examples
///
/// ```
/// use sailfish::runtime::{Buffer, Render, RenderError};
/// use sailfish::{Buffer, RenderOnce, RenderError};
///
/// struct MyU64(u64);
///
/// impl Render for MyU64 {
/// impl RenderOnce for MyU64 {
/// #[inline]
/// fn render(&self, b: &mut Buffer) -> Result<(), RenderError> {
/// self.0.render(b)
/// fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> {
/// self.0.render_once(b)
/// }
/// }
/// ```
/// types which can be rendered inside buffer block (`<%- %>`)
pub trait RenderOnce: Sized {
/// render to `Buffer` without escaping
///
@ -75,11 +71,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`
@ -110,11 +107,12 @@ pub trait RenderMut {
/// See [`RenderOnce::render_once_escaped`] for more information.
#[inline]
fn render_mut_escaped(&mut self, b: &mut Buffer) -> Result<(), RenderError> {
let mut tmp = Buffer::new();
self.render_mut(&mut tmp)?;
escape::escape_to_buf(tmp.as_str(), b);
Ok(())
fn render_mut_escaped<E: Escape>(
&mut self,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
default_render_mut_escaped(self, b, e)
}
/// See [`RenderOnce::render_once_to_string`] for more information.
@ -137,11 +135,12 @@ pub trait Render {
/// See [`RenderOnce::render_once_escaped`] for more information.
#[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)
}
/// See [`RenderOnce::render_once_to_string`] for more information.
@ -161,9 +160,9 @@ impl<T: Render + ?Sized> RenderMut for T {
fn render_mut(&mut self, b: &mut Buffer) -> Result<(), RenderError> {
self.render(b)
}
fn render_mut_escaped(&mut self, b: &mut Buffer) -> Result<(), RenderError> {
self.render_escaped(b)
fn render_mut_escaped<E: Escape>(&mut self, b: &mut Buffer, e: &E) -> Result<(), RenderError> {
self.render_escaped(b, e)
}
fn render_mut_to_string(&mut self) -> RenderResult {
@ -176,8 +175,8 @@ impl<T: RenderMut> RenderOnce for T {
self.render_mut(b)
}
fn render_once_escaped(mut self, b: &mut Buffer) -> Result<(), RenderError> {
self.render_mut_escaped(b)
fn render_once_escaped<E: Escape>(mut self, b: &mut Buffer, e: &E) -> Result<(), RenderError> {
self.render_mut_escaped(b, e)
}
fn render_once_to_string(mut self) -> RenderResult {
@ -185,6 +184,42 @@ impl<T: RenderMut> RenderOnce for T {
}
}
#[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(())
}
#[inline]
pub fn default_render_mut_escaped<T: RenderMut + ?Sized, E: Escape>(
t: &mut T,
b: &mut Buffer,
e: &E,
) -> Result<(), RenderError> {
let mut tmp = Buffer::new();
t.render_mut(&mut tmp)?;
e.escape_to_buf(b, tmp.as_str());
Ok(())
}
#[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(())
}
impl Render for String {
#[inline]
fn render(&self, b: &mut Buffer) -> Result<(), RenderError> {
@ -193,8 +228,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(())
}
}
@ -207,8 +246,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(())
}
}
@ -221,14 +264,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(())
}
@ -243,8 +286,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(())
}
}
@ -258,8 +305,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(())
}
}
@ -297,13 +348,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)]
@ -326,16 +390,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)]
@ -360,9 +429,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)
}
}
}
@ -389,9 +465,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)
}
}
}
@ -408,8 +517,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)
}
}
};
@ -418,17 +527,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,)*) => {
@ -440,8 +549,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)
}
}
)*
@ -470,8 +579,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)
}
}
@ -495,10 +608,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(())
}
}
@ -524,10 +637,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(())
}
}
@ -593,9 +706,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();
@ -608,24 +724,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();
}
@ -638,10 +754,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");
}
@ -650,17 +766,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");
}
@ -669,7 +787,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();
@ -694,7 +814,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");
}
@ -713,6 +838,6 @@ mod tests {
let err = RenderError::BufSize;
assert!(err.source().is_none());
format!("{}", err);
_ = format!("{}", err);
}
}

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, RenderMut, RenderOnce, RenderResult};
pub use size_hint::SizeHint;
#[doc(hidden)]
pub use alias_funcs::{render, render_escaped, render_text};

View file

@ -13,7 +13,7 @@ macro_rules! cfg_json {
#[cfg(sailfish_nightly)]
macro_rules! likely {
($val:expr) => {
std::intrinsics::likely($val)
std::hint::likely($val)
};
}
@ -27,7 +27,7 @@ macro_rules! likely {
#[cfg(sailfish_nightly)]
macro_rules! unlikely {
($val:expr) => {
std::intrinsics::unlikely($val)
std::hint::unlikely($val)
};
}

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

@ -385,10 +385,13 @@
"patterns": [
{
"name": "source.rust.embedded.html",
"begin": "<(%|\\?)(=|-)?",
"begin": "<(%|\\?)(\\(\\w+)|-)?",
"beginCaptures": {
"0": {
"name": "punctuation.definition.tag.begin.html"
},
"2": {
"name": "entity.name.function.sailfish"
}
},
"end": "(%|\\?)>",
@ -407,4 +410,4 @@
}
},
"scopeName": "source.sailfish"
}
}