diff --git a/sailfish-compiler/src/optimizer.rs b/sailfish-compiler/src/optimizer.rs index 8e5a39b..9ba8a6c 100644 --- a/sailfish-compiler/src/optimizer.rs +++ b/sailfish-compiler/src/optimizer.rs @@ -1,10 +1,12 @@ -use proc_macro2::TokenStream; -use quote::quote; +use proc_macro2::Span; +use quote::{quote, ToTokens}; use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::punctuated::Punctuated; +use syn::token::Comma; use syn::visit_mut::VisitMut; use syn::{ - Block, Expr, ExprBreak, ExprContinue, ExprMacro, Ident, LitStr, Macro, Stmt, - StmtMacro, Token, + Block, Expr, ExprBreak, ExprCall, ExprContinue, ExprLit, ExprPath, Ident, Lit, + LitStr, Stmt, Token, }; pub struct Optimizer { @@ -102,12 +104,12 @@ impl VisitMut for OptmizerImpl { fl.body.stmts.remove(0); *fl.body.stmts.last_mut().unwrap() = syn::parse2(quote! { - __sf_rt::render_text!(__sf_buf, #concat); + __sf_rt::render_text(__sf_buf, #concat); }) .unwrap(); let mut new_stmts = syn::parse2::(quote! {{ - __sf_rt::render_text!(__sf_buf, #previous); + __sf_rt::render_text(__sf_buf, #previous); #stmt unsafe { __sf_buf._set_len(__sf_buf.len() - #sf_len); } }}) @@ -119,38 +121,23 @@ impl VisitMut for OptmizerImpl { i.stmts = results; } - fn visit_stmt_macro_mut(&mut self, i: &mut StmtMacro) { + fn visit_expr_call_mut(&mut self, i: &mut ExprCall) { if self.rm_whitespace { - if let Some(v) = get_rendertext_value(&i.mac) { + if let Some(v) = get_rendertext_value(&i) { let ts = match remove_whitespace(v) { Some(value) => value, None => return, }; - i.mac.tokens = ts; + i.args[1] = ts; return; } } - syn::visit_mut::visit_stmt_macro_mut(self, i); - } - - fn visit_expr_macro_mut(&mut self, i: &mut ExprMacro) { - if self.rm_whitespace { - if let Some(v) = get_rendertext_value(&i.mac) { - let ts = match remove_whitespace(v) { - Some(value) => value, - None => return, - }; - i.mac.tokens = ts; - return; - } - } - - syn::visit_mut::visit_expr_macro_mut(self, i); + syn::visit_mut::visit_expr_call_mut(self, i); } } -fn remove_whitespace(v: String) -> Option { +fn remove_whitespace(v: String) -> Option { let mut buffer = String::new(); let mut it = v.lines().peekable(); if let Some(line) = it.next() { @@ -175,48 +162,64 @@ fn remove_whitespace(v: String) -> Option { } } - Some(quote! { __sf_buf, #buffer }) + Some(Expr::Lit(ExprLit { + attrs: vec![], + lit: LitStr::new(&buffer, Span::call_site()).into(), + })) } -fn get_rendertext_value(mac: &Macro) -> Option { - struct RenderTextMacroArgument { +fn get_rendertext_value(call: &ExprCall) -> Option { + struct RenderTextArguments<'a> { #[allow(dead_code)] - context: Ident, - arg: LitStr, + buf: &'a Ident, + text: &'a LitStr, } - impl Parse for RenderTextMacroArgument { - fn parse(s: ParseStream) -> ParseResult { - let context = s.parse()?; - s.parse::()?; - let arg = s.parse()?; - - Ok(Self { context, arg }) + impl<'a> RenderTextArguments<'a> { + pub fn parse(expr: &'a Punctuated) -> Option { + if expr.len() != 2 { + panic!("bad arguments: {:?}", expr.to_token_stream()); + return None; + } + let Expr::Path(ExprPath { path: buf, .. }) = &expr[0] else { + panic!("bad arguments: {:?}", expr.to_token_stream()); + return None; + }; + let Some(buf) = buf.get_ident() else { + panic!("bad arguments: {:?}", expr.to_token_stream()); + return None; + }; + let Expr::Lit(ExprLit { + lit: Lit::Str(text), + .. + }) = &expr[1] + else { + panic!("bad arguments: {:?}", expr.to_token_stream()); + return None; + }; + Some(Self { buf, text }) } } - let mut it = mac.path.segments.iter(); - - if it.next().map_or(false, |s| s.ident == "__sf_rt") - && it.next().map_or(false, |s| s.ident == "render_text") - && it.next().is_none() + let Expr::Path(ExprPath { path, .. }) = &*call.func else { + return None; + }; + if path.segments.len() != 2 + || path.segments[0].ident != "__sf_rt" + || path.segments[1].ident != "render_text" { - let tokens = mac.tokens.clone(); - if let Ok(macro_arg) = syn::parse2::(tokens) { - return Some(macro_arg.arg.value()); - } + return None; } - None + let args = RenderTextArguments::parse(&call.args)?; + Some(args.text.value()) } fn get_rendertext_value_from_stmt(stmt: &Stmt) -> Option { - let em = match stmt { - Stmt::Expr(Expr::Macro(ref mac), Some(_)) => mac, - _ => return None, - }; - - get_rendertext_value(&em.mac) + match stmt { + Stmt::Expr(Expr::Call(ref ec), Some(_)) => get_rendertext_value(ec), + _ => None, + } } fn block_has_continue_or_break(i: &mut Block) -> bool { diff --git a/sailfish-compiler/src/procmacro.rs b/sailfish-compiler/src/procmacro.rs index 983c1c5..fc864b3 100644 --- a/sailfish-compiler/src/procmacro.rs +++ b/sailfish-compiler/src/procmacro.rs @@ -368,7 +368,6 @@ fn derive_template_impl(tokens: TokenStream) -> Result fn render_once_to(self, __sf_buf: &mut sailfish::runtime::Buffer) -> std::result::Result<(), sailfish::runtime::RenderError> { // This line is required for cargo to track child templates #include_bytes_seq; - use sailfish::runtime as __sf_rt; let #name { #field_names } = self; include!(#output_file_string); diff --git a/sailfish-compiler/src/translator.rs b/sailfish-compiler/src/translator.rs index 86047f0..50aa549 100644 --- a/sailfish-compiler/src/translator.rs +++ b/sailfish-compiler/src/translator.rs @@ -123,7 +123,7 @@ impl SourceBuilder { length: 1, }); - self.source.push_str("__sf_rt::render_text!(__sf_buf, "); + self.source.push_str("__sf_rt::render_text(__sf_buf, "); // write text token with Debug::fmt write!(self.source, "{:?}", token.as_str()).unwrap(); self.source.push_str(");\n"); @@ -159,7 +159,7 @@ impl SourceBuilder { self.source.push_str("__sf_rt::"); self.source.push_str(method); - self.source.push_str("!(__sf_buf, "); + self.source.push_str("(__sf_buf, "); if let Some(filter) = code_block.filter { let expr_str = format!("{}{}", code_block.expr.into_token_stream(), suffix); @@ -177,7 +177,7 @@ impl SourceBuilder { // arguments to filter function { - self.source.push_str("&("); + self.source.push_str("("); let entry = SourceMapEntry { original: token.offset(), new: self.source.len(), @@ -199,7 +199,7 @@ impl SourceBuilder { self.source.push_str(suffix); } - self.source.push_str(");\n"); + self.source.push_str(")?;\n"); Ok(()) } @@ -400,7 +400,7 @@ mod tests { .ast .into_token_stream() .to_string(), - r#"{ __sf_rt :: render_text ! (__sf_buf , "outer ") ; __sf_rt :: render ! (__sf_buf , inner . render_once () ?) ; __sf_rt :: render_text ! (__sf_buf , " outer") ; }"# + r#"{ __sf_rt :: render_text (__sf_buf , "outer ") ; __sf_rt :: render (__sf_buf , inner . render_once () ?) ? ; __sf_rt :: render_text (__sf_buf , " outer") ; }"# ); } @@ -422,7 +422,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 . render_once () ?))) ; __sf_rt :: render_text ! (__sf_buf , " outer") ; }"# + 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") ; }"# ); } } diff --git a/sailfish/src/lib.rs b/sailfish/src/lib.rs index ec9ea85..8289052 100644 --- a/sailfish/src/lib.rs +++ b/sailfish/src/lib.rs @@ -35,6 +35,7 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![allow(clippy::redundant_closure)] #![deny(missing_docs)] +#![feature(specialization)] pub mod runtime; @@ -45,7 +46,7 @@ pub use runtime::{RenderError, RenderResult}; pub use sailfish_macros::TemplateOnce; /// Template that can be rendered with consuming itself. -pub trait TemplateOnce: Sized + private::Sealed { +pub trait TemplateOnce: Sized { /// Render the template and return the rendering result as `RenderResult` /// /// This method never returns `Err`, unless you explicitly return RenderError @@ -95,9 +96,28 @@ pub trait TemplateOnce: Sized + private::Sealed { } /// Work in Progress -pub trait Template: private::Sealed { +pub trait Template +where + for<'a> &'a Self: TemplateOnce, +{ /// Work in progress fn render(&self) -> runtime::RenderResult; + + /// Work in progress + fn render_to(&self, buf: &mut Buffer) -> Result<(), RenderError>; +} + +impl Template for T +where + for<'a> &'a T: TemplateOnce, +{ + fn render(&self) -> runtime::RenderResult { + TemplateOnce::render_once(self) + } + + fn render_to(&self, buf: &mut Buffer) -> Result<(), RenderError> { + TemplateOnce::render_once_to(self, buf) + } } #[doc(hidden)] diff --git a/sailfish/src/runtime/alias_funcs.rs b/sailfish/src/runtime/alias_funcs.rs new file mode 100644 index 0000000..c9187a0 --- /dev/null +++ b/sailfish/src/runtime/alias_funcs.rs @@ -0,0 +1,24 @@ +use crate::RenderError; + +use super::{Buffer, RenderOnce}; + +#[doc(hidden)] +#[inline(always)] +pub fn render(buf: &mut Buffer, value: T) -> Result<(), RenderError> { + value.render_once(buf) +} + +#[doc(hidden)] +#[inline(always)] +pub fn render_escaped( + 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) +} diff --git a/sailfish/src/runtime/filter.rs b/sailfish/src/runtime/filter.rs index e151948..c22ba64 100644 --- a/sailfish/src/runtime/filter.rs +++ b/sailfish/src/runtime/filter.rs @@ -3,12 +3,15 @@ use std::fmt; use std::ptr; +use super::escape; +use super::render::RenderOnce; use super::{Buffer, Render, RenderError}; /// Helper struct for 'display' filter -pub struct Display<'a, T: ?Sized>(&'a T); +#[derive(Clone, Copy)] +pub struct Display(T); -impl<'a, T: fmt::Display + ?Sized> Render for Display<'a, T> { +impl Render for Display { fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { use fmt::Write; @@ -24,14 +27,15 @@ impl<'a, T: fmt::Display + ?Sized> Render for Display<'a, T> { /// filename: <%= filename.display() | disp %> /// ``` #[inline] -pub fn disp(expr: &T) -> Display { +pub fn disp(expr: T) -> Display { Display(expr) } /// Helper struct for 'dbg' filter -pub struct Debug<'a, T: ?Sized>(&'a T); +#[derive(Clone, Copy)] +pub struct Debug(T); -impl<'a, T: fmt::Debug + ?Sized> Render for Debug<'a, T> { +impl Render for Debug { fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { use fmt::Write; @@ -53,32 +57,38 @@ impl<'a, T: fmt::Debug + ?Sized> Render for Debug<'a, T> { /// table content: <%= format!("{:?}", table) %> /// ``` #[inline] -pub fn dbg(expr: &T) -> Debug { +pub fn dbg(expr: T) -> Debug { Debug(expr) } /// Helper struct for 'upper' filter -pub struct Upper<'a, T: ?Sized>(&'a T); +#[derive(Clone, Copy)] +pub struct Upper(T); -impl<'a, T: Render + ?Sized> Render for Upper<'a, T> { - fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { - let old_len = b.len(); - self.0.render(b)?; - - let content = b.as_str().get(old_len..).ok_or(RenderError::BufSize)?; - let s = content.to_uppercase(); - unsafe { b._set_len(old_len) }; - b.push_str(&*s); +impl RenderOnce for Upper { + fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> { + let mut tmp = Buffer::new(); + self.0.render_once(&mut tmp)?; + // Estimate assuming ASCII and non-convertible UTF-8 are the most common. + b.reserve(tmp.len()); + for c in tmp.as_str().chars().flat_map(|c| c.to_uppercase()) { + b.push(c); + } Ok(()) } - fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { + fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> { let old_len = b.len(); - self.0.render_escaped(b)?; - - let s = b.as_str()[old_len..].to_uppercase(); + self.0.render_once(b)?; + let mut tmp = Buffer::new(); + let s = &b.as_str()[old_len..]; + // Estimate assuming ASCII and non-convertible UTF-8 are the most common. + tmp.reserve(s.len()); + for c in s.chars().flat_map(|c| c.to_uppercase()) { + tmp.push(c); + } unsafe { b._set_len(old_len) }; - b.push_str(&*s); + escape::escape_to_buf(tmp.as_str(), b); Ok(()) } } @@ -97,32 +107,57 @@ impl<'a, T: Render + ?Sized> Render for Upper<'a, T> { /// TSCHÜSS /// ``` #[inline] -pub fn upper(expr: &T) -> Upper { +pub fn upper(expr: T) -> Upper { Upper(expr) } /// Helper struct for 'lower' filter -pub struct Lower<'a, T: ?Sized>(&'a T); +#[derive(Clone, Copy)] +pub struct Lower(T); -impl<'a, T: Render + ?Sized> Render for Lower<'a, T> { - fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { +impl RenderOnce for Lower { + fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> { + let mut tmp = Buffer::new(); + self.0.render_once(&mut tmp)?; + // Estimate assuming ASCII and non-convertible UTF-8 are the most common. + b.reserve(tmp.len()); let old_len = b.len(); - self.0.render(b)?; - - let content = b.as_str().get(old_len..).ok_or(RenderError::BufSize)?; - let s = content.to_lowercase(); - unsafe { b._set_len(old_len) }; - b.push_str(&*s); + for c in tmp.as_str().chars() { + // see comments in str::to_lowercase + if c == 'Σ' { + let lower = tmp.as_str().to_lowercase(); + unsafe { b._set_len(old_len) }; + b.push_str(&lower); + return Ok(()); + } + for c in c.to_lowercase() { + b.push(c); + } + } Ok(()) } - fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { + fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> { let old_len = b.len(); - self.0.render_escaped(b)?; - - let s = b.as_str()[old_len..].to_lowercase(); + self.0.render_once(b)?; + let mut tmp = Buffer::new(); + let s = &b.as_str()[old_len..]; + // Estimate assuming ASCII and non-convertible UTF-8 are the most common. + tmp.reserve(s.len()); + for c in s.chars() { + // see comments in str::to_lowercase + if c == 'Σ' { + let lower = s.to_lowercase(); + unsafe { b._set_len(old_len) }; + b.push_str(&lower); + return Ok(()); + } + for c in c.to_lowercase() { + tmp.push(c); + } + } unsafe { b._set_len(old_len) }; - b.push_str(&*s); + escape::escape_to_buf(tmp.as_str(), b); Ok(()) } } @@ -141,25 +176,26 @@ impl<'a, T: Render + ?Sized> Render for Lower<'a, T> { /// ὀδυσσεύς /// ``` #[inline] -pub fn lower(expr: &T) -> Lower { +pub fn lower(expr: T) -> Lower { Lower(expr) } /// Helper struct for 'trim' filter -pub struct Trim<'a, T: ?Sized>(&'a T); +#[derive(Clone, Copy)] +pub struct Trim(T); -impl<'a, T: Render + ?Sized> Render for Trim<'a, T> { +impl RenderOnce for Trim { #[inline] - fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { + fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> { let old_len = b.len(); - self.0.render(b)?; + self.0.render_once(b)?; trim_impl(b, old_len) } #[inline] - fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { + fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> { let old_len = b.len(); - self.0.render_escaped(b)?; + self.0.render_once_escaped(b)?; trim_impl(b, old_len) } } @@ -211,25 +247,26 @@ fn trim_impl(b: &mut Buffer, old_len: usize) -> Result<(), RenderError> { /// Hello world /// ``` #[inline] -pub fn trim(expr: &T) -> Trim { +pub fn trim(expr: T) -> Trim { Trim(expr) } /// Helper struct for 'truncate' filter -pub struct Truncate<'a, T: ?Sized>(&'a T, usize); +#[derive(Clone, Copy)] +pub struct Truncate(T, usize); -impl<'a, T: Render + ?Sized> Render for Truncate<'a, T> { +impl RenderOnce for Truncate { #[inline] - fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { + fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> { let old_len = b.len(); - self.0.render(b)?; + self.0.render_once(b)?; truncate_impl(b, old_len, self.1) } #[inline] - fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { + fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> { let old_len = b.len(); - self.0.render_escaped(b)?; + self.0.render_once_escaped(b)?; truncate_impl(b, old_len, self.1) } } @@ -242,7 +279,7 @@ fn truncate_impl( 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.wrapping_add(idx)) }; + unsafe { b._set_len(old_len + idx) }; b.push_str("..."); } @@ -265,15 +302,16 @@ fn truncate_impl( /// Hello... /// ``` #[inline] -pub fn truncate(expr: &T, limit: usize) -> Truncate { +pub fn truncate(expr: T, limit: usize) -> Truncate { Truncate(expr, limit) } cfg_json! { /// Helper struct for 'json' filter - pub struct Json<'a, T: ?Sized>(&'a T); + #[derive(Clone, Copy)] + pub struct Json(T); - impl<'a, T: serde::Serialize + ?Sized> Render for Json<'a, T> { + impl Render for Json { #[inline] fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { struct Writer<'a>(&'a mut Buffer); @@ -297,7 +335,7 @@ cfg_json! { } } - serde_json::to_writer(Writer(b), self.0) + serde_json::to_writer(Writer(b), &self.0) .map_err(|e| RenderError::new(&e.to_string())) } @@ -326,7 +364,7 @@ cfg_json! { } } - serde_json::to_writer(Writer(b), self.0) + serde_json::to_writer(Writer(b), &self.0) .map_err(|e| RenderError::new(&e.to_string())) } } @@ -342,7 +380,7 @@ cfg_json! { /// } /// ``` #[inline] - pub fn json(expr: &T) -> Json { + pub fn json(expr: T) -> Json { Json(expr) } } @@ -351,124 +389,124 @@ cfg_json! { mod tests { use super::*; - fn assert_render(expr: &T, expected: &str) { + fn assert_render(expr: T, expected: &str) { let mut buf = Buffer::new(); - Render::render(expr, &mut buf).unwrap(); + RenderOnce::render_once(expr, &mut buf).unwrap(); assert_eq!(buf.as_str(), expected); } - fn assert_render_escaped(expr: &T, expected: &str) { + fn assert_render_escaped(expr: T, expected: &str) { let mut buf = Buffer::new(); - Render::render_escaped(expr, &mut buf).unwrap(); + RenderOnce::render_once_escaped(expr, &mut buf).unwrap(); assert_eq!(buf.as_str(), expected); } #[test] fn test_lower() { - assert_render(&lower(""), ""); - assert_render_escaped(&lower(""), ""); + assert_render(lower(""), ""); + assert_render_escaped(lower(""), ""); - assert_render(&lower("lorem ipsum"), "lorem ipsum"); - assert_render(&lower("LOREM IPSUM"), "lorem ipsum"); + assert_render(lower("lorem ipsum"), "lorem ipsum"); + assert_render(lower("LOREM IPSUM"), "lorem ipsum"); - assert_render_escaped(&lower("hElLo, WOrLd!"), "hello, world!"); - assert_render_escaped(&lower("hElLo, WOrLd!"), "hello, world!"); + assert_render_escaped(lower("hElLo, WOrLd!"), "hello, world!"); + assert_render_escaped(lower("hElLo, WOrLd!"), "hello, world!"); - assert_render_escaped(&lower("

TITLE

"), "<h1>title</h1>"); - assert_render_escaped(&lower("<<&\"\">>"), "<<&"">>"); + assert_render_escaped(lower("

TITLE

"), "<h1>title</h1>"); + assert_render_escaped(lower("<<&\"\">>"), "<<&"">>"); // non-ascii - assert_render(&lower("aBcAbc"), "abcabc"); - assert_render(&lower("ὈΔΥΣΣΕΎΣ"), "ὀδυσσεύς"); + assert_render(lower("aBcAbc"), "abcabc"); + assert_render(lower("ὈΔΥΣΣΕΎΣ"), "ὀδυσσεύς"); } #[test] fn test_upper() { - assert_render(&upper(""), ""); - assert_render_escaped(&upper(""), ""); + assert_render(upper(""), ""); + assert_render_escaped(upper(""), ""); - assert_render(&upper("lorem ipsum"), "LOREM IPSUM"); - assert_render(&upper("LOREM IPSUM"), "LOREM IPSUM"); + assert_render(upper("lorem ipsum"), "LOREM IPSUM"); + assert_render(upper("LOREM IPSUM"), "LOREM IPSUM"); - assert_render(&upper("hElLo, WOrLd!"), "HELLO, WORLD!"); - assert_render(&upper("hElLo, WOrLd!"), "HELLO, WORLD!"); + assert_render(upper("hElLo, WOrLd!"), "HELLO, WORLD!"); + assert_render(upper("hElLo, WOrLd!"), "HELLO, WORLD!"); // non-ascii - assert_render(&upper("aBcAbc"), "ABCABC"); - assert_render(&upper("tschüß"), "TSCHÜSS"); + assert_render(upper("aBcAbc"), "ABCABC"); + assert_render(upper("tschüß"), "TSCHÜSS"); } #[test] fn test_trim() { - assert_render(&trim(""), ""); - assert_render_escaped(&trim(""), ""); + assert_render(trim(""), ""); + assert_render_escaped(trim(""), ""); - assert_render(&trim("\n \t\r\x0C"), ""); + assert_render(trim("\n \t\r\x0C"), ""); - assert_render(&trim("hello world!"), "hello world!"); - assert_render(&trim("hello world!\n"), "hello world!"); - assert_render(&trim("\thello world!"), "hello world!"); - assert_render(&trim("\thello world!\r\n"), "hello world!"); + assert_render(trim("hello world!"), "hello world!"); + assert_render(trim("hello world!\n"), "hello world!"); + assert_render(trim("\thello world!"), "hello world!"); + assert_render(trim("\thello world!\r\n"), "hello world!"); - assert_render_escaped(&trim(" "), "<html>"); - assert_render_escaped(&lower("<<&\"\">>"), "<<&"">>"); + assert_render_escaped(trim(" "), "<html>"); + assert_render_escaped(lower("<<&\"\">>"), "<<&"">>"); // non-ascii whitespace - assert_render(&trim("\u{A0}空白\u{3000}\u{205F}"), "空白"); + assert_render(trim("\u{A0}空白\u{3000}\u{205F}"), "空白"); } #[test] fn test_truncate() { - assert_render(&truncate("", 0), ""); - assert_render(&truncate("", 5), ""); + assert_render(truncate("", 0), ""); + assert_render(truncate("", 5), ""); - assert_render(&truncate("apple ", 0), "..."); - assert_render(&truncate("apple ", 1), "a..."); - assert_render(&truncate("apple ", 2), "ap..."); - assert_render(&truncate("apple ", 3), "app..."); - assert_render(&truncate("apple ", 4), "appl..."); - assert_render(&truncate("apple ", 5), "apple..."); - assert_render(&truncate("apple ", 6), "apple "); - assert_render(&truncate("apple ", 7), "apple "); + assert_render(truncate("apple ", 0), "..."); + assert_render(truncate("apple ", 1), "a..."); + assert_render(truncate("apple ", 2), "ap..."); + assert_render(truncate("apple ", 3), "app..."); + assert_render(truncate("apple ", 4), "appl..."); + assert_render(truncate("apple ", 5), "apple..."); + assert_render(truncate("apple ", 6), "apple "); + assert_render(truncate("apple ", 7), "apple "); - assert_render(&truncate(&std::f64::consts::PI, 10), "3.14159265..."); - assert_render(&truncate(&std::f64::consts::PI, 20), "3.141592653589793"); + assert_render(truncate(std::f64::consts::PI, 10), "3.14159265..."); + assert_render(truncate(std::f64::consts::PI, 20), "3.141592653589793"); - assert_render_escaped(&truncate("foo
bar", 10), "foo<br&..."); - assert_render_escaped(&truncate("foo
bar", 20), "foo<br>bar"); + assert_render_escaped(truncate("foo
bar", 10), "foo<br&..."); + assert_render_escaped(truncate("foo
bar", 20), "foo<br>bar"); // non-ascii - assert_render(&truncate("魑魅魍魎", 0), "..."); - assert_render(&truncate("魑魅魍魎", 1), "魑..."); - assert_render(&truncate("魑魅魍魎", 2), "魑魅..."); - assert_render(&truncate("魑魅魍魎", 3), "魑魅魍..."); - assert_render(&truncate("魑魅魍魎", 4), "魑魅魍魎"); - assert_render(&truncate("魑魅魍魎", 5), "魑魅魍魎"); + assert_render(truncate("魑魅魍魎", 0), "..."); + assert_render(truncate("魑魅魍魎", 1), "魑..."); + assert_render(truncate("魑魅魍魎", 2), "魑魅..."); + assert_render(truncate("魑魅魍魎", 3), "魑魅魍..."); + assert_render(truncate("魑魅魍魎", 4), "魑魅魍魎"); + assert_render(truncate("魑魅魍魎", 5), "魑魅魍魎"); } #[cfg(feature = "json")] #[test] fn test_json() { - assert_render(&json(""), "\"\""); - assert_render(&json(&serde_json::json!({})), "{}"); + assert_render(json(""), "\"\""); + assert_render(json(serde_json::json!({})), "{}"); - assert_render_escaped(&json(&123_i32), "123"); - assert_render_escaped(&json("Pokémon"), ""Pokémon""); + assert_render_escaped(json(123_i32), "123"); + assert_render_escaped(json("Pokémon"), ""Pokémon""); } #[test] fn compine() { assert_render( - &lower(&upper("Li Europan lingues es membres del sam familie.")), + lower(upper("Li Europan lingues es membres del sam familie.")), "li europan lingues es membres del sam familie.", ); - assert_render(&lower(&lower("ハートのA")), "ハートのa"); - assert_render(&upper(&upper("ハートのA")), "ハートのA"); + assert_render(lower(lower("ハートのA")), "ハートのa"); + assert_render(upper(upper("ハートのA")), "ハートのA"); - assert_render(&truncate(&trim("\t起来!\r\n"), 1), "起..."); - assert_render(&truncate(&trim("\t起来!\r\n"), 3), "起来!"); + assert_render(truncate(trim("\t起来!\r\n"), 1), "起..."); + assert_render(truncate(trim("\t起来!\r\n"), 3), "起来!"); - assert_render(&truncate(&lower("Was möchtest du?"), 10), "was möchte..."); - assert_render(&truncate(&upper("Was möchtest du?"), 10), "WAS MÖCHTE..."); + assert_render(truncate(lower("Was möchtest du?"), 10), "was möchte..."); + assert_render(truncate(upper("Was möchtest du?"), 10), "WAS MÖCHTE..."); } } diff --git a/sailfish/src/runtime/macros.rs b/sailfish/src/runtime/macros.rs deleted file mode 100644 index 19d9e58..0000000 --- a/sailfish/src/runtime/macros.rs +++ /dev/null @@ -1,29 +0,0 @@ -#[macro_export] -#[doc(hidden)] -macro_rules! render { - ($buf:ident, $value:expr) => { - $crate::runtime::Render::render(&($value), $buf)? - }; -} - -#[macro_export] -#[doc(hidden)] -macro_rules! render_escaped { - ($buf:ident, $value:expr) => { - $crate::runtime::Render::render_escaped(&($value), $buf)? - }; -} - -#[macro_export] -#[doc(hidden)] -macro_rules! render_text { - ($buf:ident, $value:expr) => { - $buf.push_str($value) - }; -} - -#[macro_export] -#[doc(hidden)] -macro_rules! render_noop { - ($buf:ident, $value:expr) => {}; -} diff --git a/sailfish/src/runtime/mod.rs b/sailfish/src/runtime/mod.rs index 0b1857a..fd539ad 100644 --- a/sailfish/src/runtime/mod.rs +++ b/sailfish/src/runtime/mod.rs @@ -3,16 +3,16 @@ #[macro_use] mod utils; +mod alias_funcs; mod buffer; pub mod escape; pub mod filter; -mod macros; mod render; mod size_hint; pub use buffer::Buffer; -pub use render::{Render, RenderError, RenderResult}; +pub use render::{Render, RenderError, RenderOnce, RenderResult}; pub use size_hint::SizeHint; #[doc(hidden)] -pub use crate::{render, render_escaped, render_noop, render_text}; +pub use alias_funcs::{render, render_escaped, render_text}; diff --git a/sailfish/src/runtime/render.rs b/sailfish/src/runtime/render.rs index e9b3f70..5a590cb 100644 --- a/sailfish/src/runtime/render.rs +++ b/sailfish/src/runtime/render.rs @@ -12,7 +12,7 @@ use std::sync::{Arc, MutexGuard, RwLockReadGuard, RwLockWriteGuard}; use super::buffer::Buffer; use super::escape; -/// types which can be rendered inside buffer block (`<%= %>`) +/// 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. @@ -52,6 +52,49 @@ pub trait Render { } } +/// types which can be rendered inside buffer block (`<%= %>`) +/// +/// See [`Render`] for more information. +pub trait RenderOnce: Sized { + /// render to `Buffer` without escaping + fn render_once(self, b: &mut Buffer) -> Result<(), RenderError>; + + /// 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(()) + } +} + +// impl<'a, T: ?Sized> Render for &'a T +// where +// T: Render, +// { +// fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { +// T::render(self, b) +// } + +// fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { +// T::render_escaped(self, b) +// } +// } + +impl RenderOnce for T +where + T: Render, +{ + fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> { + self.render(b) + } + + fn render_once_escaped(self, b: &mut Buffer) -> Result<(), RenderError> { + self.render_escaped(b) + } +} + // /// Autoref-based stable specialization // /// // /// Explanation can be found [here](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md) @@ -78,13 +121,13 @@ pub trait Render { impl Render for String { #[inline] fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { - b.push_str(&**self); + b.push_str(self); Ok(()) } #[inline] fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { - escape::escape_to_buf(&**self, b); + escape::escape_to_buf(self, b); Ok(()) } } @@ -128,13 +171,13 @@ impl Render for PathBuf { #[inline] fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { // TODO: speed up on Windows using OsStrExt - b.push_str(&*self.to_string_lossy()); + b.push_str(self.to_str().ok_or(RenderError::Fmt(fmt::Error))?); Ok(()) } #[inline] fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { - escape::escape_to_buf(&*self.to_string_lossy(), b); + escape::escape_to_buf(self.to_str().ok_or(RenderError::Fmt(fmt::Error))?, b); Ok(()) } } @@ -143,13 +186,13 @@ impl Render for Path { #[inline] fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { // TODO: speed up on Windows using OsStrExt - b.push_str(&*self.to_string_lossy()); + b.push_str(self.to_str().ok_or(RenderError::Fmt(fmt::Error))?); Ok(()) } #[inline] fn render_escaped(&self, b: &mut Buffer) -> Result<(), RenderError> { - escape::escape_to_buf(&*self.to_string_lossy(), b); + escape::escape_to_buf(self.to_str().ok_or(RenderError::Fmt(fmt::Error))?, b); Ok(()) } } @@ -288,10 +331,10 @@ impl Render for f64 { macro_rules! render_deref { ( $(#[doc = $doc:tt])* - [$($bounds:tt)+] $($desc:tt)+ + $(default[$($default:tt)+])? [$($generics:tt)+] [$($bounds:tt)*] $desc:ty $(, [$($deref:tt)+])? ) => { $(#[doc = $doc])* - impl <$($bounds)+> Render for $($desc)+ { + $($($default)+)? impl <$($generics)+> Render for $desc where $($bounds)* { #[inline] fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { (**self).render(b) @@ -305,17 +348,20 @@ macro_rules! render_deref { }; } -render_deref!(['a, T: Render + ?Sized] &'a T); -render_deref!(['a, T: Render + ?Sized] &'a mut T); -render_deref!([T: Render + ?Sized] Box); -render_deref!([T: Render + ?Sized] Rc); -render_deref!([T: Render + ?Sized] Arc); -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_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!(default[default] [T: Render + ?Sized] [] Box); +render_deref!([T: Render + ?Sized] [] Rc); +render_deref!([T: Render + ?Sized] [] Arc); +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>, [*]); macro_rules! render_nonzero { ($($type:ty,)*) => { @@ -422,33 +468,33 @@ mod tests { #[test] fn receiver_coercion() { let mut b = Buffer::new(); - Render::render(&1, &mut b).unwrap(); - Render::render(&&1, &mut b).unwrap(); - Render::render(&&&1, &mut b).unwrap(); - Render::render(&&&&1, &mut b).unwrap(); + RenderOnce::render_once(&1, &mut b).unwrap(); + RenderOnce::render_once(&&1, &mut b).unwrap(); + RenderOnce::render_once(&&&1, &mut b).unwrap(); + RenderOnce::render_once(&&&&1, &mut b).unwrap(); assert_eq!(b.as_str(), "1111"); b.clear(); - Render::render(&true, &mut b).unwrap(); - Render::render(&&false, &mut b).unwrap(); - Render::render_escaped(&&&true, &mut b).unwrap(); - Render::render_escaped(&&&&false, &mut b).unwrap(); + 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(); assert_eq!(b.as_str(), "truefalsetruefalse"); b.clear(); let s = "apple"; - Render::render_escaped(&s, &mut b).unwrap(); - Render::render_escaped(&s, &mut b).unwrap(); - Render::render_escaped(&&s, &mut b).unwrap(); - Render::render_escaped(&&&s, &mut b).unwrap(); - Render::render_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).unwrap(); assert_eq!(b.as_str(), "appleappleappleappleapple"); b.clear(); - Render::render_escaped(&'c', &mut b).unwrap(); - Render::render_escaped(&&'<', &mut b).unwrap(); - Render::render_escaped(&&&'&', &mut b).unwrap(); - Render::render_escaped(&&&&' ', &mut b).unwrap(); + 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(); assert_eq!(b.as_str(), "c<& "); b.clear(); } @@ -460,7 +506,7 @@ 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(&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(); @@ -473,17 +519,17 @@ mod tests { fn float() { let mut b = Buffer::new(); - Render::render_escaped(&0.0f64, &mut b).unwrap(); - Render::render_escaped(&std::f64::INFINITY, &mut b).unwrap(); - Render::render_escaped(&std::f64::NEG_INFINITY, &mut b).unwrap(); - Render::render_escaped(&std::f64::NAN, &mut b).unwrap(); + 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(); assert_eq!(b.as_str(), "0.0inf-infNaN"); b.clear(); - Render::render_escaped(&0.0f32, &mut b).unwrap(); - Render::render_escaped(&std::f32::INFINITY, &mut b).unwrap(); - Render::render_escaped(&std::f32::NEG_INFINITY, &mut b).unwrap(); - Render::render_escaped(&std::f32::NAN, &mut b).unwrap(); + 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(); assert_eq!(b.as_str(), "0.0inf-infNaN"); } @@ -491,22 +537,22 @@ mod tests { fn test_char() { let mut b = Buffer::new(); - let funcs: Vec Result<(), RenderError>> = - vec![Render::render, Render::render_escaped]; + let funcs: Vec Result<(), RenderError>> = + vec![RenderOnce::render_once, RenderOnce::render_once_escaped]; for func in funcs { - func(&'a', &mut b).unwrap(); - func(&'b', &mut b).unwrap(); - func(&'c', &mut b).unwrap(); - func(&'d', &mut b).unwrap(); + func('a', &mut b).unwrap(); + func('b', &mut b).unwrap(); + func('c', &mut b).unwrap(); + func('d', &mut b).unwrap(); assert_eq!(b.as_str(), "abcd"); b.clear(); - func(&'あ', &mut b).unwrap(); - func(&'い', &mut b).unwrap(); - func(&'う', &mut b).unwrap(); - func(&'え', &mut b).unwrap(); + func('あ', &mut b).unwrap(); + func('い', &mut b).unwrap(); + func('う', &mut b).unwrap(); + func('え', &mut b).unwrap(); assert_eq!(b.as_str(), "あいうえ"); b.clear(); @@ -516,8 +562,8 @@ mod tests { #[test] fn test_nonzero() { let mut b = Buffer::with_capacity(2); - Render::render(&NonZeroU8::new(10).unwrap(), &mut b).unwrap(); - Render::render_escaped(&NonZeroI16::new(-20).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(); assert_eq!(b.as_str(), "10-20"); }