Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Partial indent support #505

Merged
merged 7 commits into from
May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions src/grammar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
86 changes: 81 additions & 5 deletions src/partial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
"#
);
}
Expand Down Expand Up @@ -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()
)
}
}
45 changes: 36 additions & 9 deletions src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand All @@ -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);
Expand All @@ -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();

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -443,6 +454,7 @@ pub struct Decorator<'reg, 'rc> {
params: Vec<PathAndJson<'reg, 'rc>>,
hash: BTreeMap<&'reg str, PathAndJson<'reg, 'rc>>,
template: Option<&'reg Template>,
indent: Option<&'reg String>,
}

impl<'reg: 'rc, 'rc> Decorator<'reg, 'rc> {
Expand Down Expand Up @@ -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(),
})
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
58 changes: 58 additions & 0 deletions src/support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading