diff --git a/src/grammar.rs b/src/grammar.rs index d2f4150fd..bed8c3b62 100644 --- a/src/grammar.rs +++ b/src/grammar.rs @@ -4,38 +4,6 @@ #[grammar = "grammar.pest"] pub struct HandlebarsParser; -#[inline] -pub(crate) fn whitespace_matcher(c: char) -> bool { - c == ' ' || c == '\t' -} - -#[inline] -pub(crate) fn newline_matcher(c: char) -> bool { - c == '\n' || c == '\r' -} - -#[inline] -pub(crate) fn strip_first_newline(s: &str) -> &str { - if let Some(s) = s.strip_prefix("\r\n") { - s - } else if let Some(s) = s.strip_prefix('\n') { - s - } else { - s - } -} - -pub(crate) fn ends_with_empty_line(text: &str) -> bool { - let s = text.trim_end_matches(whitespace_matcher); - // also matches when text is just whitespaces - s.ends_with(newline_matcher) || s.is_empty() -} - -pub(crate) fn starts_with_empty_line(text: &str) -> bool { - text.trim_start_matches(whitespace_matcher) - .starts_with(newline_matcher) -} - #[cfg(test)] mod test { use super::{HandlebarsParser, Rule}; diff --git a/src/partial.rs b/src/partial.rs index 401449546..72bce5379 100644 --- a/src/partial.rs +++ b/src/partial.rs @@ -113,6 +113,9 @@ pub fn expand_partial<'reg: 'rc, 'rc>( local_rc.push_partial_block(pb); } + // indent + local_rc.set_indent_string(d.indent()); + let result = t.render(r, ctx, &mut local_rc, out); // cleanup @@ -454,13 +457,13 @@ foofoofoo"#, assert_eq!( result, - r#"name: inner_solo + r#" name: inner_solo -name: hello -name: there + name: hello + name: there -name: hello -name: there + name: hello + name: there "# ); } @@ -542,4 +545,77 @@ Template:test hb.render("t1", &data).unwrap() ); } + + #[test] + fn test_multiline_partial_indent() { + let mut hb = Registry::new(); + + hb.register_template_string( + "t1", + r#"{{#*inline "thepartial"}} + inner first line + inner second line +{{/inline}} + {{> thepartial}} +outer third line"#, + ) + .unwrap(); + assert_eq!( + r#" inner first line + inner second line +outer third line"#, + hb.render("t1", &()).unwrap() + ); + + hb.register_template_string( + "t2", + r#"{{#*inline "thepartial"}}inner first line +inner second line +{{/inline}} + {{> thepartial}} +outer third line"#, + ) + .unwrap(); + assert_eq!( + r#" inner first line + inner second line +outer third line"#, + hb.render("t2", &()).unwrap() + ); + + hb.register_template_string( + "t3", + r#"{{#*inline "thepartial"}}{{a}}{{/inline}} + {{> thepartial}} +outer third line"#, + ) + .unwrap(); + assert_eq!( + r#" + inner first line + inner second lineouter third line"#, + hb.render("t3", &json!({"a": "inner first line\ninner second line"})) + .unwrap() + ); + + let mut hb2 = Registry::new(); + hb2.set_prevent_indent(true); + + hb2.register_template_string( + "t1", + r#"{{#*inline "thepartial"}} + inner first line + inner second line +{{/inline}} + {{> thepartial}} +outer third line"#, + ) + .unwrap(); + assert_eq!( + r#" inner first line + inner second line +outer third line"#, + hb2.render("t1", &()).unwrap() + ) + } } diff --git a/src/render.rs b/src/render.rs index 19303c950..90533601a 100644 --- a/src/render.rs +++ b/src/render.rs @@ -14,6 +14,7 @@ use crate::json::value::{JsonRender, PathAndJson, ScopedJson}; use crate::output::{Output, StringOutput}; use crate::partial; use crate::registry::Registry; +use crate::support; use crate::template::TemplateElement::*; use crate::template::{ BlockParam, DecoratorTemplate, HelperTemplate, Parameter, Template, TemplateElement, @@ -47,10 +48,11 @@ pub struct RenderContextInner<'reg: 'rc, 'rc> { /// root template name root_template: Option<&'reg String>, disable_escape: bool, + indent_string: Option<&'reg String>, } impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { - /// Create a render context from a `Write` + /// Create a render context pub fn new(root_template: Option<&'reg String>) -> RenderContext<'reg, 'rc> { let inner = Rc::new(RenderContextInner { partials: BTreeMap::new(), @@ -60,6 +62,7 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { current_template: None, root_template, disable_escape: false, + indent_string: None, }); let mut blocks = VecDeque::with_capacity(5); @@ -73,7 +76,6 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { } } - // TODO: better name pub(crate) fn new_for_block(&self) -> RenderContext<'reg, 'rc> { let inner = self.inner.clone(); @@ -200,6 +202,15 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { } } + pub(crate) fn set_indent_string(&mut self, indent: Option<&'reg String>) { + self.inner_mut().indent_string = indent; + } + + #[inline] + pub(crate) fn get_indent_string(&self) -> Option<&'reg String> { + self.inner.indent_string + } + /// Remove a registered partial pub fn remove_partial(&mut self, name: &str) { self.inner_mut().partials.remove(name); @@ -443,6 +454,7 @@ pub struct Decorator<'reg, 'rc> { params: Vec>, hash: BTreeMap<&'reg str, PathAndJson<'reg, 'rc>>, template: Option<&'reg Template>, + indent: Option<&'reg String>, } impl<'reg: 'rc, 'rc> Decorator<'reg, 'rc> { @@ -471,6 +483,7 @@ impl<'reg: 'rc, 'rc> Decorator<'reg, 'rc> { params: pv, hash: hm, template: dt.template.as_ref(), + indent: dt.indent.as_ref(), }) } @@ -503,6 +516,10 @@ impl<'reg: 'rc, 'rc> Decorator<'reg, 'rc> { pub fn template(&self) -> Option<&'reg Template> { self.template } + + pub fn indent(&self) -> Option<&'reg String> { + self.indent + } } /// Render trait @@ -755,6 +772,20 @@ pub(crate) fn do_escape(r: &Registry<'_>, rc: &RenderContext<'_, '_>, content: S } } +#[inline] +fn indent_aware_write( + v: &str, + rc: &mut RenderContext<'_, '_>, + out: &mut dyn Output, +) -> Result<(), RenderError> { + if let Some(indent) = rc.get_indent_string() { + out.write(support::str::with_indent(v.as_ref(), indent).as_ref())?; + } else { + out.write(v.as_ref())?; + } + Ok(()) +} + impl Renderable for TemplateElement { fn render<'reg: 'rc, 'rc>( &'reg self, @@ -763,11 +794,8 @@ impl Renderable for TemplateElement { rc: &mut RenderContext<'reg, 'rc>, out: &mut dyn Output, ) -> Result<(), RenderError> { - match *self { - RawString(ref v) => { - out.write(v.as_ref())?; - Ok(()) - } + match self { + RawString(ref v) => indent_aware_write(v.as_ref(), rc, out), Expression(ref ht) | HtmlExpression(ref ht) => { let is_html_expression = matches!(self, HtmlExpression(_)); if is_html_expression { @@ -797,8 +825,7 @@ impl Renderable for TemplateElement { } else { let rendered = context_json.value().render(); let output = do_escape(registry, rc, rendered); - out.write(output.as_ref())?; - Ok(()) + indent_aware_write(output.as_ref(), rc, out) } } } else { diff --git a/src/support.rs b/src/support.rs index bd5564d32..aced51739 100644 --- a/src/support.rs +++ b/src/support.rs @@ -57,6 +57,64 @@ pub mod str { output } + /// add indent for lines but last + pub fn with_indent<'a>(s: &'a str, indent: &String) -> String { + let mut output = String::new(); + + let mut it = s.chars().peekable(); + while let Some(c) = it.next() { + output.push(c); + // check if c is not the last character, we don't append + // indent for last line break + if c == '\n' && it.peek().is_some() { + output.push_str(indent); + } + } + + output + } + + #[inline] + pub(crate) fn whitespace_matcher(c: char) -> bool { + c == ' ' || c == '\t' + } + + #[inline] + pub(crate) fn newline_matcher(c: char) -> bool { + c == '\n' || c == '\r' + } + + #[inline] + pub(crate) fn strip_first_newline(s: &str) -> &str { + if let Some(s) = s.strip_prefix("\r\n") { + s + } else if let Some(s) = s.strip_prefix('\n') { + s + } else { + s + } + } + + pub(crate) fn find_trailing_whitespace_chars(s: &str) -> Option<&str> { + let trimmed = s.trim_end_matches(whitespace_matcher); + if trimmed.len() == s.len() { + None + } else { + Some(&s[trimmed.len()..]) + } + } + + pub(crate) fn ends_with_empty_line(text: &str) -> bool { + let s = text.trim_end_matches(whitespace_matcher); + // also matches when text is just whitespaces + s.ends_with(newline_matcher) || s.is_empty() + } + + pub(crate) fn starts_with_empty_line(text: &str) -> bool { + text.trim_start_matches(whitespace_matcher) + .starts_with(newline_matcher) + } + #[cfg(test)] mod test { use crate::support::str::StringWriter; diff --git a/src/template.rs b/src/template.rs index 39d24590e..2f6068499 100644 --- a/src/template.rs +++ b/src/template.rs @@ -9,8 +9,9 @@ use pest::{Parser, Position, Span}; use serde_json::value::Value as Json; use crate::error::{TemplateError, TemplateErrorReason}; -use crate::grammar::{self, HandlebarsParser, Rule}; +use crate::grammar::{HandlebarsParser, Rule}; use crate::json::path::{parse_json_path_from_iter, Path}; +use crate::support; use self::TemplateElement::*; @@ -161,6 +162,20 @@ pub struct DecoratorTemplate { pub params: Vec, pub hash: HashMap, pub template: Option