//! Build-in filters use std::fmt; use std::ptr; use crate::{Buffer, Escape, Render, RenderError, RenderOnce}; /// Helper struct for 'display' filter #[derive(Clone, Copy)] pub struct Display(T); impl Render for Display { fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { use fmt::Write; write!(b, "{}", self.0).map_err(RenderError::from) } } /// render using `std::fmt::Display` trait /// /// # Examples /// /// ```text /// filename: <%\html filename.display() | disp %> /// ``` #[inline] pub fn disp(expr: T) -> Display { Display(expr) } /// Helper struct for 'dbg' filter #[derive(Clone, Copy)] pub struct Debug(T); impl Render for Debug { fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { use fmt::Write; write!(b, "{:?}", self.0).map_err(RenderError::from) } } /// render using `std::fmt::Debug` trait /// /// # Examples /// /// The following examples produce exactly same results, but former is faster /// /// ```text /// table content: <%\html table | dbg %> /// ``` /// /// ```text /// table content: <%\html format!("{:?}", table) %> /// ``` #[inline] pub fn dbg(expr: T) -> Debug { Debug(expr) } /// Helper struct for 'upper' filter #[derive(Clone, Copy)] pub struct Upper(T); impl RenderOnce for Upper { #[inline] 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(()) } #[inline] fn render_once_escaped( self, b: &mut Buffer, e: &E, ) -> Result<(), RenderError> { let old_len = b.len(); 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) }; e.escape_to_buf(b, tmp.as_str()); Ok(()) } } /// convert the rendered contents to uppercase /// /// # Examples /// /// ```text /// <%\html "tschüß" | upper %> /// ``` /// /// result: /// /// ```text /// TSCHÜSS /// ``` #[inline] pub fn upper(expr: T) -> Upper { Upper(expr) } /// Helper struct for 'lower' filter #[derive(Clone, Copy)] pub struct Lower(T); impl RenderOnce for Lower { #[inline] 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(); for c in tmp.as_str().chars() { // See comments in str::to_lowercase if c == 'Σ' { // This is very inefficient, but it seems unlikely to be worth the effort // duplicating what already exists in the standard library for unicode support. let lower = tmp.as_str().to_lowercase(); unsafe { b._set_len(old_len) }; b.push_str(&lower); return Ok(()); } for c in c.to_lowercase() { b.push(c); } } Ok(()) } #[inline] fn render_once_escaped( self, b: &mut Buffer, e: &E, ) -> Result<(), RenderError> { let old_len = b.len(); 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) }; e.escape_to_buf(b, &lower); return Ok(()); } for c in c.to_lowercase() { tmp.push(c); } } unsafe { b._set_len(old_len) }; e.escape_to_buf(b, tmp.as_str()); Ok(()) } } /// convert the rendered contents to lowercase /// /// # Examples /// /// ```text /// <%\html "ὈΔΥΣΣΕΎΣ" | lower %> /// ``` /// /// result: /// /// ```text /// ὀδυσσεύς /// ``` #[inline] pub fn lower(expr: T) -> Lower { Lower(expr) } /// Helper struct for 'trim' filter #[derive(Clone, Copy)] pub struct Trim(T); impl RenderOnce for Trim { #[inline] fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> { let old_len = b.len(); self.0.render_once(b)?; trim_impl(b, old_len) } #[inline] fn render_once_escaped( self, b: &mut Buffer, e: &E, ) -> Result<(), RenderError> { let old_len = b.len(); self.0.render_once_escaped(b, e)?; trim_impl(b, old_len) } } fn trim_impl(b: &mut Buffer, old_len: usize) -> Result<(), RenderError> { let new_contents = b.as_str().get(old_len..).ok_or(RenderError::BufSize)?; let trimmed = new_contents.trim(); let trimmed_len = trimmed.len(); if new_contents.len() != trimmed_len { // performs inplace trimming if new_contents.as_ptr() != trimmed.as_ptr() { debug_assert!(new_contents.as_ptr() < trimmed.as_ptr()); let offset = trimmed.as_ptr() as usize - new_contents.as_ptr() as usize; unsafe { ptr::copy( b.as_mut_ptr().add(old_len + offset), b.as_mut_ptr().add(old_len), trimmed_len, ); } } debug_assert!(b.capacity() >= old_len + trimmed_len); // SAFETY: `new_contents.len() = b.len() - old_len` and // `trimmed_len < new_contents.len()`, so `old_len + trimmed_len < b.len()`. unsafe { b._set_len(old_len + trimmed_len); } } Ok(()) } /// Remove leading and trailing writespaces from rendered results /// /// # Examples /// /// ```text /// <%\html " Hello world\n" | trim %> /// ``` /// /// result: /// /// ```text /// Hello world /// ``` #[inline] pub fn trim(expr: T) -> Trim { Trim(expr) } /// Helper struct for 'truncate' filter #[derive(Clone, Copy)] pub struct Truncate(T, usize); impl RenderOnce for Truncate { #[inline] fn render_once(self, b: &mut Buffer) -> Result<(), RenderError> { let old_len = b.len(); self.0.render_once(b)?; truncate_impl(b, old_len, self.1) } #[inline] fn render_once_escaped( self, b: &mut Buffer, e: &E, ) -> Result<(), RenderError> { let old_len = b.len(); 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 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(()) } /// Limit length of rendered contents, appends '...' if truncated /// /// # Examples /// /// The following example renders the first 20 characters of `message` /// /// ```test /// <%\html "Hello, world!" | truncate(5) %> /// ``` /// /// result: /// /// ```text /// Hello... /// ``` #[inline] pub fn truncate(expr: T, limit: usize) -> Truncate { Truncate(expr, limit) } cfg_json! { /// Helper struct for 'json' filter #[derive(Clone, Copy)] pub struct Json(T); impl Render for Json { #[inline] fn render(&self, b: &mut Buffer) -> Result<(), RenderError> { struct Writer<'a>(&'a mut Buffer); impl<'a> std::io::Write for Writer<'a> { #[inline] fn write(&mut self, buf: &[u8]) -> std::io::Result { let buf = unsafe { std::str::from_utf8_unchecked(buf) }; self.0.push_str(buf); Ok(buf.len()) } #[inline] fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { self.write(buf).map(|_| {}) } #[inline] fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } serde_json::to_writer(Writer(b), &self.0) .map_err(|e| RenderError::new(&e.to_string())) } #[inline] fn render_escaped( &self, b: &mut Buffer, e: &E, ) -> Result<(), RenderError> { struct Writer<'a, E: Escape>(&'a mut Buffer, &'a E); impl<'a, E: Escape> std::io::Write for Writer<'a, E> { #[inline] fn write(&mut self, buf: &[u8]) -> std::io::Result { let buf = unsafe { std::str::from_utf8_unchecked(buf) }; self.1.escape_to_buf(self.0, buf); Ok(buf.len()) } #[inline] fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> { self.write(buf).map(|_| {}) } #[inline] fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } serde_json::to_writer(Writer(b, e), &self.0) .map_err(|e| RenderError::new(&e.to_string())) } } /// Serialize the given data structure as JSON into the buffer /// /// # Examples /// /// ```text /// { /// "name": "JSON example", /// "data": <%- data | json %> /// } /// ``` #[inline] pub fn json(expr: T) -> Json { Json(expr) } } #[cfg(test)] mod tests { use crate::EscapeHtml; use super::*; fn assert_render(expr: T, expected: &str) { let mut buf = Buffer::new(); RenderOnce::render_once(expr, &mut buf).unwrap(); assert_eq!(buf.as_str(), expected); } fn assert_render_escaped(expr: T, expected: &str) { let mut buf = Buffer::new(); RenderOnce::render_once_escaped(expr, &mut buf, &EscapeHtml).unwrap(); assert_eq!(buf.as_str(), expected); } #[test] fn test_lower() { assert_render(lower(""), ""); assert_render_escaped(lower(""), ""); 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("

TITLE

"), "<h1>title</h1>"); assert_render_escaped(lower("<<&\"\">>"), "<<&"">>"); // non-ascii assert_render(lower("aBcAbc"), "abcabc"); assert_render(lower("ὈΔΥΣΣΕΎΣ"), "ὀδυσσεύς"); } #[test] fn test_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("hElLo, WOrLd!"), "HELLO, WORLD!"); assert_render(upper("hElLo, WOrLd!"), "HELLO, WORLD!"); // non-ascii 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("\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_escaped(trim(" "), "<html>"); assert_render_escaped(lower("<<&\"\">>"), "<<&"">>"); // non-ascii whitespace assert_render(trim("\u{A0}空白\u{3000}\u{205F}"), "空白"); } #[test] fn test_truncate() { 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(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"); // 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), "魑魅魍魎"); } #[cfg(feature = "json")] #[test] fn test_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""); } #[test] fn compine() { assert_render( 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(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..."); } }