• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sunng87 / handlebars-rust / 15733898660

11 Jun 2025 06:05PM UTC coverage: 82.399% (-0.1%) from 82.509%
15733898660

push

github

web-flow
feat: Added dynamic partial blocks by adding support for empty block closing tags (#713)

* Added support for empty partial block end

* Bump version to 6.3.3

* fix: panic when there was content after empty block ends

* Update Cargo.toml

---------

Co-authored-by: Ning Sun <classicning@gmail.com>

4 of 8 new or added lines in 1 file covered. (50.0%)

3 existing lines in 2 files now uncovered.

1587 of 1926 relevant lines covered (82.4%)

7.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.75
/src/template.rs
1
use std::collections::{HashMap, VecDeque};
2
use std::iter::Peekable;
3
use std::str::FromStr;
4

5
use pest::error::LineColLocation;
6
use pest::iterators::Pair;
7
use pest::{Parser, Position, Span};
8
use serde_json::value::Value as Json;
9

10
use crate::error::{TemplateError, TemplateErrorReason};
11
use crate::grammar::{HandlebarsParser, Rule};
12
use crate::json::path::{parse_json_path_from_iter, Path};
13
use crate::support;
14

15
use derive_builder::Builder;
16

17
use self::TemplateElement::{
18
    Comment, DecoratorBlock, DecoratorExpression, Expression, HelperBlock, HtmlExpression,
19
    PartialBlock, PartialExpression, RawString,
20
};
21

22
#[non_exhaustive]
23
#[derive(PartialEq, Eq, Clone, Debug)]
24
pub struct TemplateMapping(pub usize, pub usize);
25

26
/// A handlebars template
27
#[non_exhaustive]
28
#[derive(Builder, PartialEq, Eq, Clone, Debug, Default)]
29
pub struct Template {
30
    #[builder(setter(into, strip_option), default)]
31
    pub name: Option<String>,
32
    pub elements: Vec<TemplateElement>,
33
    pub mapping: Vec<TemplateMapping>,
34
}
35

36
#[derive(Default)]
37
pub(crate) struct TemplateOptions {
38
    pub(crate) prevent_indent: bool,
39
    pub(crate) is_partial: bool,
40
    pub(crate) name: Option<String>,
41
}
42

43
impl TemplateOptions {
44
    fn name(&self) -> String {
3✔
45
        self.name.clone().unwrap_or_else(|| "Unnamed".to_owned())
9✔
46
    }
47
}
48

49
#[non_exhaustive]
50
#[derive(Builder, PartialEq, Eq, Clone, Debug)]
51
pub struct Subexpression {
52
    // we use box here avoid resursive struct definition
53
    pub element: Box<TemplateElement>,
54
}
55

56
impl Subexpression {
57
    pub fn new(
5✔
58
        name: Parameter,
59
        params: Vec<Parameter>,
60
        hash: HashMap<String, Parameter>,
61
    ) -> Subexpression {
62
        Subexpression {
63
            element: Box::new(Expression(Box::new(HelperTemplate {
4✔
64
                name,
65
                params,
66
                hash,
67
                template: None,
68
                inverse: None,
69
                block_param: None,
70
                block: false,
71
                chain: false,
72
                indent_before_write: false,
73
            }))),
74
        }
75
    }
76

77
    pub fn is_helper(&self) -> bool {
×
78
        match *self.as_element() {
×
79
            TemplateElement::Expression(ref ht) => !ht.is_name_only(),
×
80
            _ => false,
×
81
        }
82
    }
83

84
    pub fn as_element(&self) -> &TemplateElement {
5✔
85
        self.element.as_ref()
5✔
86
    }
87

88
    pub fn name(&self) -> &str {
1✔
89
        match *self.as_element() {
1✔
90
            // FIXME: avoid unwrap here
91
            Expression(ref ht) => ht.name.as_name().unwrap(),
2✔
92
            _ => unreachable!(),
93
        }
94
    }
95

96
    pub fn params(&self) -> Option<&Vec<Parameter>> {
1✔
97
        match *self.as_element() {
1✔
98
            Expression(ref ht) => Some(&ht.params),
2✔
99
            _ => None,
×
100
        }
101
    }
102

103
    pub fn hash(&self) -> Option<&HashMap<String, Parameter>> {
×
104
        match *self.as_element() {
×
105
            Expression(ref ht) => Some(&ht.hash),
×
106
            _ => None,
×
107
        }
108
    }
109
}
110

111
#[non_exhaustive]
112
#[derive(PartialEq, Eq, Clone, Debug)]
113
pub enum BlockParam {
114
    Single(Parameter),
115
    Pair((Parameter, Parameter)),
116
}
117

118
#[non_exhaustive]
119
#[derive(Builder, PartialEq, Eq, Clone, Debug)]
120
pub struct ExpressionSpec {
121
    pub name: Parameter,
122
    pub params: Vec<Parameter>,
123
    pub hash: HashMap<String, Parameter>,
124
    #[builder(setter(strip_option), default)]
125
    pub block_param: Option<BlockParam>,
126
    pub omit_pre_ws: bool,
127
    pub omit_pro_ws: bool,
128
}
129

130
#[non_exhaustive]
131
#[derive(PartialEq, Eq, Clone, Debug)]
132
pub enum Parameter {
133
    // for helper name only
134
    Name(String),
135
    // for expression, helper param and hash
136
    Path(Path),
137
    Literal(Json),
138
    Subexpression(Subexpression),
139
}
140

141
#[non_exhaustive]
142
#[derive(Builder, PartialEq, Eq, Clone, Debug)]
143
pub struct HelperTemplate {
144
    pub name: Parameter,
145
    pub params: Vec<Parameter>,
146
    pub hash: HashMap<String, Parameter>,
147
    #[builder(setter(strip_option), default)]
148
    pub block_param: Option<BlockParam>,
149
    #[builder(setter(strip_option), default)]
150
    pub template: Option<Template>,
151
    #[builder(setter(strip_option), default)]
152
    pub inverse: Option<Template>,
153
    pub block: bool,
154
    pub chain: bool,
155
    pub(crate) indent_before_write: bool,
156
}
157

158
impl HelperTemplate {
159
    pub fn new(exp: ExpressionSpec, block: bool, indent_before_write: bool) -> HelperTemplate {
12✔
160
        HelperTemplate {
161
            name: exp.name,
13✔
162
            params: exp.params,
12✔
163
            hash: exp.hash,
13✔
164
            block_param: exp.block_param,
12✔
165
            block,
166
            template: None,
167
            inverse: None,
168
            chain: false,
169
            indent_before_write,
170
        }
171
    }
172

173
    pub fn new_chain(
3✔
174
        exp: ExpressionSpec,
175
        block: bool,
176
        indent_before_write: bool,
177
    ) -> HelperTemplate {
178
        HelperTemplate {
179
            name: exp.name,
3✔
180
            params: exp.params,
3✔
181
            hash: exp.hash,
3✔
182
            block_param: exp.block_param,
2✔
183
            block,
184
            template: None,
185
            inverse: None,
186
            chain: true,
187
            indent_before_write,
188
        }
189
    }
190

191
    // test only
192
    pub(crate) fn with_path(path: Path) -> HelperTemplate {
2✔
193
        HelperTemplate {
194
            name: Parameter::Path(path),
2✔
195
            params: Vec::with_capacity(5),
2✔
196
            hash: HashMap::new(),
2✔
197
            block_param: None,
198
            template: None,
199
            inverse: None,
200
            block: false,
201
            chain: false,
202
            indent_before_write: false,
203
        }
204
    }
205

206
    pub(crate) fn is_name_only(&self) -> bool {
12✔
207
        !self.block && self.params.is_empty() && self.hash.is_empty()
13✔
208
    }
209

210
    fn insert_inverse_node(&mut self, mut node: Box<HelperTemplate>) {
2✔
211
        // Create a list in "inverse" member to hold the else-chain.
212
        // Here we create the new template to save the else-chain node.
213
        // The template render could render it successfully without any code add.
214
        let mut new_chain_template = Template::new();
2✔
215
        node.inverse = self.inverse.take();
4✔
216
        new_chain_template.elements.push(HelperBlock(node));
2✔
217
        self.inverse = Some(new_chain_template);
2✔
218
    }
219

220
    fn ref_chain_head_mut(&mut self) -> Option<&mut Box<HelperTemplate>> {
3✔
221
        if self.chain {
3✔
222
            if let Some(inverse_tmpl) = &mut self.inverse {
2✔
223
                assert_eq!(inverse_tmpl.elements.len(), 1);
2✔
224
                if let HelperBlock(helper) = &mut inverse_tmpl.elements[0] {
2✔
225
                    return Some(helper);
2✔
226
                }
227
            }
228
        }
229
        None
3✔
230
    }
231

232
    fn set_chain_template(&mut self, tmpl: Option<Template>) {
3✔
233
        if let Some(hepler) = self.ref_chain_head_mut() {
11✔
234
            hepler.template = tmpl;
2✔
235
        } else {
236
            self.template = tmpl;
3✔
237
        }
238
    }
239

240
    fn revert_chain_and_set(&mut self, inverse: Option<Template>) {
10✔
241
        if self.chain {
10✔
242
            let mut prev = None;
2✔
243

244
            if let Some(head) = self.ref_chain_head_mut() {
3✔
245
                if head.template.is_some() {
6✔
246
                    // Here the prev will hold the head inverse template.
247
                    // It will be set when reverse the chain.
248
                    prev = inverse;
2✔
249
                } else {
250
                    // If the head already has template. set the inverse template.
251
                    head.template = inverse;
1✔
252
                }
253
            }
254

255
            // Reverse the else chain, to the normal list order.
256
            while let Some(mut node) = self.inverse.take() {
4✔
257
                assert_eq!(node.elements.len(), 1);
3✔
258
                if let HelperBlock(c) = &mut node.elements[0] {
3✔
259
                    self.inverse = c.inverse.take();
3✔
260
                    c.inverse = prev;
2✔
261
                    prev = Some(node);
1✔
262
                }
263
            }
264

265
            self.inverse = prev;
2✔
266
        } else {
267
            // If the helper has no else chain.
268
            // set the template to self.
269
            if self.template.is_some() {
22✔
270
                self.inverse = inverse;
3✔
271
            } else {
272
                self.template = inverse;
8✔
273
            }
274
        }
275
    }
276

277
    fn set_chained(&mut self) {
2✔
278
        self.chain = true;
1✔
279
    }
280

281
    pub fn is_chained(&self) -> bool {
×
282
        self.chain
×
283
    }
284
}
285

286
#[non_exhaustive]
287
#[derive(Builder, PartialEq, Eq, Clone, Debug)]
288
pub struct DecoratorTemplate {
289
    pub name: Parameter,
290
    pub params: Vec<Parameter>,
291
    pub hash: HashMap<String, Parameter>,
292
    #[builder(setter(strip_option), default)]
293
    pub template: Option<Template>,
294
    // for partial indent
295
    #[builder(setter(into, strip_option), default)]
296
    pub indent: Option<String>,
297
    pub(crate) indent_before_write: bool,
298
}
299

300
impl DecoratorTemplate {
301
    pub fn new(exp: ExpressionSpec, indent_before_write: bool) -> DecoratorTemplate {
6✔
302
        DecoratorTemplate {
303
            name: exp.name,
6✔
304
            params: exp.params,
5✔
305
            hash: exp.hash,
6✔
306
            template: None,
307
            indent: None,
308
            indent_before_write,
309
        }
310
    }
311
}
312

313
impl Parameter {
314
    pub fn as_name(&self) -> Option<&str> {
8✔
315
        match self {
8✔
316
            Parameter::Name(ref n) => Some(n),
8✔
317
            Parameter::Path(ref p) => Some(p.raw()),
1✔
318
            _ => None,
1✔
319
        }
320
    }
321

322
    pub fn parse(s: &str) -> Result<Parameter, TemplateError> {
×
323
        let parser = HandlebarsParser::parse(Rule::parameter, s)
×
324
            .map_err(|_| TemplateError::of(TemplateErrorReason::InvalidParam(s.to_owned())))?;
×
325

326
        let mut it = parser.flatten().peekable();
×
327
        Template::parse_param(s, &mut it, s.len() - 1)
×
328
    }
329

330
    fn debug_name(&self) -> String {
1✔
331
        if let Some(name) = self.as_name() {
1✔
332
            name.to_owned()
1✔
333
        } else {
334
            format!("{self:?}")
1✔
335
        }
336
    }
337
}
338

339
impl Template {
340
    pub fn new() -> Template {
19✔
341
        Template::default()
16✔
342
    }
343

344
    fn push_element(&mut self, e: TemplateElement, line: usize, col: usize) {
13✔
345
        self.elements.push(e);
13✔
346
        self.mapping.push(TemplateMapping(line, col));
13✔
347
    }
348

349
    fn parse_subexpression<'a, I>(
5✔
350
        source: &'a str,
351
        it: &mut Peekable<I>,
352
        limit: usize,
353
    ) -> Result<Parameter, TemplateError>
354
    where
355
        I: Iterator<Item = Pair<'a, Rule>>,
356
    {
357
        let espec = Template::parse_expression(source, it.by_ref(), limit)?;
8✔
358
        Ok(Parameter::Subexpression(Subexpression::new(
9✔
359
            espec.name,
4✔
360
            espec.params,
3✔
361
            espec.hash,
5✔
362
        )))
363
    }
364

365
    fn parse_name<'a, I>(
15✔
366
        source: &'a str,
367
        it: &mut Peekable<I>,
368
        _: usize,
369
    ) -> Result<Parameter, TemplateError>
370
    where
371
        I: Iterator<Item = Pair<'a, Rule>>,
372
    {
373
        let name_node = it.next().unwrap();
17✔
374
        let rule = name_node.as_rule();
33✔
375
        let name_span = name_node.as_span();
15✔
376
        match rule {
17✔
377
            Rule::identifier
27✔
NEW
378
            | Rule::partial_identifier
×
NEW
379
            | Rule::opt_identifier
×
NEW
380
            | Rule::opt_partial_identifier
×
NEW
381
            | Rule::invert_tag_item => Ok(Parameter::Name(name_span.as_str().to_owned())),
×
UNCOV
382
            Rule::reference => {
×
383
                let paths = parse_json_path_from_iter(it, name_span.end());
24✔
384
                Ok(Parameter::Path(Path::new(name_span.as_str(), paths)))
24✔
385
            }
386
            Rule::subexpression => {
×
387
                Template::parse_subexpression(source, it.by_ref(), name_span.end())
2✔
388
            }
389
            other => unreachable!("{other:?}"),
390
        }
391
    }
392

393
    fn parse_param<'a, I>(
13✔
394
        source: &'a str,
395
        it: &mut Peekable<I>,
396
        _: usize,
397
    ) -> Result<Parameter, TemplateError>
398
    where
399
        I: Iterator<Item = Pair<'a, Rule>>,
400
    {
401
        let mut param = it.next().unwrap();
14✔
402
        if param.as_rule() == Rule::helper_parameter {
30✔
403
            param = it.next().unwrap();
4✔
404
        }
405
        let param_rule = param.as_rule();
27✔
406
        let param_span = param.as_span();
14✔
407
        let result = match param_rule {
15✔
408
            Rule::reference => {
×
409
                let path_segs = parse_json_path_from_iter(it, param_span.end());
22✔
410
                Parameter::Path(Path::new(param_span.as_str(), path_segs))
22✔
411
            }
412
            Rule::literal => {
×
413
                // Parse the parameter as a JSON literal
414
                let param_literal = it.next().unwrap();
15✔
415
                let json_result = match param_literal.as_rule() {
18✔
416
                    Rule::string_literal
×
417
                        if it.peek().unwrap().as_rule() == Rule::string_inner_single_quote =>
18✔
418
                    {
419
                        // ...unless the parameter is a single-quoted string.
420
                        // In that case, transform it to a double-quoted string
421
                        // and then parse it as a JSON literal.
422
                        let string_inner_single_quote = it.next().unwrap();
3✔
423
                        let double_quoted = format!(
3✔
424
                            "\"{}\"",
425
                            string_inner_single_quote
9✔
426
                                .as_str()
×
427
                                .replace("\\'", "'")
×
428
                                .replace('"', "\\\"")
×
429
                        );
430
                        Json::from_str(&double_quoted)
6✔
431
                    }
432
                    _ => Json::from_str(param_span.as_str()),
17✔
433
                };
434
                if let Ok(json) = json_result {
15✔
435
                    Parameter::Literal(json)
7✔
436
                } else {
437
                    return Err(TemplateError::of(TemplateErrorReason::InvalidParam(
×
438
                        param_span.as_str().to_owned(),
×
439
                    )));
440
                }
441
            }
442
            Rule::subexpression => {
×
443
                Template::parse_subexpression(source, it.by_ref(), param_span.end())?
7✔
444
            }
445
            _ => unreachable!(),
446
        };
447

448
        while let Some(n) = it.peek() {
21✔
449
            let n_span = n.as_span();
11✔
450
            if n_span.end() > param_span.end() {
10✔
451
                break;
×
452
            }
453
            it.next();
8✔
454
        }
455

456
        Ok(result)
10✔
457
    }
458

459
    fn parse_hash<'a, I>(
3✔
460
        source: &'a str,
461
        it: &mut Peekable<I>,
462
        limit: usize,
463
    ) -> Result<(String, Parameter), TemplateError>
464
    where
465
        I: Iterator<Item = Pair<'a, Rule>>,
466
    {
467
        let name = it.next().unwrap();
3✔
468
        let name_node = name.as_span();
3✔
469
        // identifier
470
        let key = name_node.as_str().to_owned();
3✔
471

472
        let value = Template::parse_param(source, it.by_ref(), limit)?;
6✔
473
        Ok((key, value))
4✔
474
    }
475

476
    fn parse_block_param<'a, I>(_: &'a str, it: &mut Peekable<I>, limit: usize) -> BlockParam
2✔
477
    where
478
        I: Iterator<Item = Pair<'a, Rule>>,
479
    {
480
        let p1_name = it.next().unwrap();
3✔
481
        let p1_name_span = p1_name.as_span();
2✔
482
        // identifier
483
        let p1 = p1_name_span.as_str().to_owned();
3✔
484

485
        let p2 = it.peek().and_then(|p2_name| {
7✔
486
            let p2_name_span = p2_name.as_span();
3✔
487
            if p2_name_span.end() <= limit {
4✔
488
                Some(p2_name_span.as_str().to_owned())
2✔
489
            } else {
490
                None
3✔
491
            }
492
        });
493

494
        if let Some(p2) = p2 {
6✔
495
            it.next();
5✔
496
            BlockParam::Pair((Parameter::Name(p1), Parameter::Name(p2)))
3✔
497
        } else {
498
            BlockParam::Single(Parameter::Name(p1))
2✔
499
        }
500
    }
501

502
    fn parse_expression<'a, I>(
14✔
503
        source: &'a str,
504
        it: &mut Peekable<I>,
505
        limit: usize,
506
    ) -> Result<ExpressionSpec, TemplateError>
507
    where
508
        I: Iterator<Item = Pair<'a, Rule>>,
509
    {
510
        let mut params: Vec<Parameter> = Vec::new();
14✔
511
        let mut hashes: HashMap<String, Parameter> = HashMap::new();
14✔
512
        let mut omit_pre_ws = false;
17✔
513
        let mut omit_pro_ws = false;
15✔
514
        let mut block_param = None;
15✔
515

516
        if it.peek().unwrap().as_rule() == Rule::leading_tilde_to_omit_whitespace {
30✔
517
            omit_pre_ws = true;
3✔
518
            it.next();
3✔
519
        }
520

521
        let name = Template::parse_name(source, it.by_ref(), limit)?;
30✔
522

523
        loop {
×
524
            let rule;
×
525
            let end;
×
526
            if let Some(pair) = it.peek() {
33✔
527
                let pair_span = pair.as_span();
17✔
528
                if pair_span.end() < limit {
15✔
529
                    rule = pair.as_rule();
13✔
530
                    end = pair_span.end();
14✔
531
                } else {
532
                    break;
×
533
                }
534
            } else {
535
                break;
×
536
            }
537

538
            it.next();
13✔
539

540
            match rule {
14✔
541
                Rule::helper_parameter => {
×
542
                    params.push(Template::parse_param(source, it.by_ref(), end)?);
27✔
543
                }
544
                Rule::hash => {
×
545
                    let (key, value) = Template::parse_hash(source, it.by_ref(), end)?;
6✔
546
                    hashes.insert(key, value);
7✔
547
                }
548
                Rule::block_param => {
2✔
549
                    block_param = Some(Template::parse_block_param(source, it.by_ref(), end));
5✔
550
                }
551
                Rule::trailing_tilde_to_omit_whitespace => {
3✔
552
                    omit_pro_ws = true;
3✔
553
                }
554
                _ => {}
×
555
            }
556
        }
557
        Ok(ExpressionSpec {
16✔
558
            name,
15✔
559
            params,
12✔
560
            hash: hashes,
16✔
561
            block_param,
12✔
562
            omit_pre_ws,
16✔
563
            omit_pro_ws,
12✔
564
        })
565
    }
566

567
    fn remove_previous_whitespace(template_stack: &mut VecDeque<Template>) {
3✔
568
        let t = template_stack.front_mut().unwrap();
3✔
569
        if let Some(RawString(ref mut text)) = t.elements.last_mut() {
6✔
570
            text.trim_end().to_owned().clone_into(text);
3✔
571
        }
572
    }
573

574
    // in handlebars, the whitespaces around statement are
575
    // automatically trimed.
576
    // this function checks if current span has both leading and
577
    // trailing whitespaces, which we treat as a standalone statement.
578
    //
579
    //
580
    fn process_standalone_statement(
12✔
581
        template_stack: &mut VecDeque<Template>,
582
        source: &str,
583
        current_span: &Span<'_>,
584
        prevent_indent: bool,
585
        is_partial: bool,
586
    ) -> bool {
587
        let continuation = &source[current_span.end()..];
12✔
588

589
        let mut with_trailing_newline = support::str::starts_with_empty_line(continuation);
12✔
590

591
        // For full templates, we behave as if there was a trailing newline if we encounter
592
        // the end of input. See #611.
593
        with_trailing_newline |= !is_partial && continuation.is_empty();
12✔
594

595
        if with_trailing_newline {
20✔
596
            let with_leading_newline =
10✔
597
                support::str::ends_with_empty_line(&source[..current_span.start()]);
598

599
            // prevent_indent: a special toggle for partial expression
600
            // (>) that leading whitespaces are kept
601
            if prevent_indent && with_leading_newline {
22✔
602
                let t = template_stack.front_mut().unwrap();
7✔
603
                // check the last element before current
604
                if let Some(RawString(ref mut text)) = t.elements.last_mut() {
6✔
605
                    // trim leading space for standalone statement
606
                    text.trim_end_matches(support::str::whitespace_matcher)
5✔
607
                        .to_owned()
608
                        .clone_into(text);
5✔
609
                }
610
            }
611

612
            // return true when the item is the first element in root template
613
            current_span.start() == 0 || with_leading_newline
15✔
614
        } else {
615
            false
8✔
616
        }
617
    }
618

619
    fn raw_string<'a>(
13✔
620
        source: &'a str,
621
        pair: Option<Pair<'a, Rule>>,
622
        trim_start: bool,
623
        trim_start_line: bool,
624
    ) -> TemplateElement {
625
        let mut s = String::from(source);
24✔
626

627
        if let Some(pair) = pair {
24✔
628
            // the source may contains leading space because of pest's limitation
629
            // we calculate none space start here in order to correct the offset
630
            let pair_span = pair.as_span();
12✔
631

632
            let current_start = pair_span.start();
11✔
633
            let span_length = pair_span.end() - current_start;
12✔
634
            let leading_space_offset = s.len() - span_length;
26✔
635

636
            // we would like to iterate pair reversely in order to remove certain
637
            // index from our string buffer so here we convert the inner pairs to
638
            // a vector.
639
            for sub_pair in pair.into_inner().rev() {
40✔
640
                // remove escaped backslash
641
                if sub_pair.as_rule() == Rule::escape {
4✔
642
                    let escape_span = sub_pair.as_span();
2✔
643

644
                    let backslash_pos = escape_span.start();
2✔
645
                    let backslash_rel_pos = leading_space_offset + backslash_pos - current_start;
2✔
646
                    s.remove(backslash_rel_pos);
4✔
647
                }
648
            }
649
        }
650

651
        if trim_start {
12✔
652
            RawString(s.trim_start().to_owned())
2✔
653
        } else if trim_start_line {
29✔
654
            let s = s.trim_start_matches(support::str::whitespace_matcher);
10✔
655
            RawString(support::str::strip_first_newline(s).to_owned())
5✔
656
        } else {
657
            RawString(s)
10✔
658
        }
659
    }
660

661
    pub(crate) fn compile2(
14✔
662
        source: &str,
663
        options: TemplateOptions,
664
    ) -> Result<Template, TemplateError> {
665
        let mut helper_stack: VecDeque<HelperTemplate> = VecDeque::new();
15✔
666
        let mut decorator_stack: VecDeque<DecoratorTemplate> = VecDeque::new();
15✔
667
        let mut template_stack: VecDeque<Template> = VecDeque::new();
15✔
668

669
        let mut omit_pro_ws = false;
15✔
670
        // flag for newline removal of standalone statements
671
        // this option is marked as true when standalone statement is detected
672
        // then the leading whitespaces and newline of next rawstring will be trimed
673
        let mut trim_line_required = false;
15✔
674

675
        let parser_queue = HandlebarsParser::parse(Rule::handlebars, source).map_err(|e| {
36✔
676
            let (line_no, col_no) = match e.line_col {
2✔
677
                LineColLocation::Pos(line_col) => line_col,
1✔
678
                LineColLocation::Span(line_col, _) => line_col,
×
679
            };
680
            TemplateError::of(TemplateErrorReason::InvalidSyntax(
4✔
681
                e.variant.message().to_string(),
2✔
682
            ))
683
            .at(source, line_no, col_no)
4✔
684
            .in_template(options.name())
4✔
685
        })?;
686

687
        // dbg!(parser_queue.clone().flatten());
688

689
        // remove escape from our pair queue
690
        let mut it = parser_queue
29✔
691
            .flatten()
692
            .filter(|p| {
15✔
693
                // remove rules that should be silent but not for now due to pest limitation
694
                !matches!(p.as_rule(), Rule::escape)
19✔
695
            })
696
            .peekable();
697
        let mut end_pos: Option<Position<'_>> = None;
14✔
698
        loop {
15✔
699
            if let Some(pair) = it.next() {
31✔
700
                let prev_end = end_pos.as_ref().map_or(0, pest::Position::pos);
33✔
701
                let rule = pair.as_rule();
20✔
702
                let span = pair.as_span();
15✔
703

704
                let is_trailing_string = rule != Rule::template
19✔
705
                    && span.start() != prev_end
20✔
706
                    && !omit_pro_ws
7✔
707
                    && rule != Rule::raw_text
6✔
708
                    && rule != Rule::raw_block_text;
7✔
709

710
                if is_trailing_string {
23✔
711
                    // trailing string check
712
                    let (line_no, col_no) = span.start_pos().line_col();
6✔
713
                    if rule == Rule::raw_block_end {
7✔
714
                        let mut t = Template::new();
×
715
                        t.push_element(
×
716
                            Template::raw_string(
×
717
                                &source[prev_end..span.start()],
×
718
                                None,
×
719
                                false,
720
                                trim_line_required,
×
721
                            ),
722
                            line_no,
723
                            col_no,
724
                        );
725
                        template_stack.push_front(t);
×
726
                    } else {
727
                        let t = template_stack.front_mut().unwrap();
13✔
728
                        t.push_element(
7✔
729
                            Template::raw_string(
9✔
730
                                &source[prev_end..span.start()],
6✔
731
                                None,
7✔
732
                                false,
733
                                trim_line_required,
6✔
734
                            ),
735
                            line_no,
736
                            col_no,
737
                        );
738
                    }
739

740
                    // reset standalone statement marker
741
                    trim_line_required = false;
8✔
742
                }
743

744
                let (line_no, col_no) = span.start_pos().line_col();
35✔
745
                match rule {
19✔
746
                    Rule::template => {
747
                        template_stack.push_front(Template::new());
35✔
748
                    }
749
                    Rule::raw_text => {
750
                        // leading space fix
751
                        let start = if span.start() != prev_end {
28✔
752
                            prev_end
4✔
753
                        } else {
754
                            span.start()
24✔
755
                        };
756

757
                        let t = template_stack.front_mut().unwrap();
12✔
758

759
                        t.push_element(
10✔
760
                            Template::raw_string(
11✔
761
                                &source[start..span.end()],
12✔
762
                                Some(pair.clone()),
12✔
763
                                omit_pro_ws,
11✔
764
                                trim_line_required,
13✔
765
                            ),
766
                            line_no,
767
                            col_no,
768
                        );
769

770
                        // reset standalone statement marker
771
                        trim_line_required = false;
12✔
772
                    }
773
                    Rule::helper_block_start
774
                    | Rule::raw_block_start
775
                    | Rule::decorator_block_start
776
                    | Rule::partial_block_start => {
777
                        let exp = Template::parse_expression(source, it.by_ref(), span.end())?;
19✔
778

779
                        if exp.omit_pre_ws {
9✔
780
                            Template::remove_previous_whitespace(&mut template_stack);
4✔
781
                        }
782
                        omit_pro_ws = exp.omit_pro_ws;
10✔
783

784
                        // standalone statement check, it also removes leading whitespaces of
785
                        // previous rawstring when standalone statement detected
786
                        trim_line_required = Template::process_standalone_statement(
20✔
787
                            &mut template_stack,
788
                            source,
789
                            &span,
790
                            true,
791
                            options.is_partial,
10✔
792
                        );
793

794
                        let indent_before_write = trim_line_required && !exp.omit_pre_ws;
10✔
795

796
                        match rule {
9✔
797
                            Rule::helper_block_start | Rule::raw_block_start => {
798
                                let helper_template =
18✔
799
                                    HelperTemplate::new(exp.clone(), true, indent_before_write);
800
                                helper_stack.push_front(helper_template);
9✔
801
                            }
802
                            Rule::decorator_block_start | Rule::partial_block_start => {
803
                                let decorator =
3✔
804
                                    DecoratorTemplate::new(exp.clone(), indent_before_write);
805
                                decorator_stack.push_front(decorator);
3✔
806
                            }
807
                            _ => unreachable!(),
808
                        }
809

810
                        let t = template_stack.front_mut().unwrap();
19✔
811
                        t.mapping.push(TemplateMapping(line_no, col_no));
10✔
812
                    }
813
                    Rule::invert_tag | Rule::invert_chain_tag => {
814
                        // hack: invert_tag structure is similar to ExpressionSpec, so I
815
                        // use it here to represent the data
816

817
                        if rule == Rule::invert_chain_tag {
4✔
818
                            let _ = Template::parse_name(source, &mut it, span.end())?;
1✔
819
                        }
820
                        let exp = Template::parse_expression(source, it.by_ref(), span.end())?;
4✔
821

822
                        if exp.omit_pre_ws {
2✔
823
                            Template::remove_previous_whitespace(&mut template_stack);
2✔
824
                        }
825
                        omit_pro_ws = exp.omit_pro_ws;
2✔
826

827
                        // standalone statement check, it also removes leading whitespaces of
828
                        // previous rawstring when standalone statement detected
829
                        trim_line_required = Template::process_standalone_statement(
6✔
830
                            &mut template_stack,
831
                            source,
832
                            &span,
833
                            true,
834
                            options.is_partial,
3✔
835
                        );
836

837
                        let indent_before_write = trim_line_required && !exp.omit_pre_ws;
3✔
838

839
                        let t = template_stack.pop_front().unwrap();
3✔
840
                        let h = helper_stack.front_mut().unwrap();
6✔
841

842
                        if rule == Rule::invert_chain_tag {
3✔
843
                            h.set_chained();
1✔
844
                        }
845

846
                        h.set_chain_template(Some(t));
3✔
847
                        if rule == Rule::invert_chain_tag {
3✔
848
                            h.insert_inverse_node(Box::new(HelperTemplate::new_chain(
3✔
849
                                exp,
1✔
850
                                true,
851
                                indent_before_write,
1✔
852
                            )));
853
                        }
854
                    }
855

856
                    Rule::raw_block_text => {
857
                        let mut t = Template::new();
1✔
858
                        t.push_element(
2✔
859
                            Template::raw_string(
1✔
860
                                span.as_str(),
1✔
861
                                Some(pair.clone()),
1✔
862
                                omit_pro_ws,
1✔
863
                                trim_line_required,
1✔
864
                            ),
865
                            line_no,
866
                            col_no,
867
                        );
868
                        template_stack.push_front(t);
1✔
869
                    }
870
                    Rule::expression
871
                    | Rule::html_expression
872
                    | Rule::decorator_expression
873
                    | Rule::partial_expression
874
                    | Rule::helper_block_end
875
                    | Rule::raw_block_end
876
                    | Rule::decorator_block_end
877
                    | Rule::partial_block_end => {
878
                        let exp = Template::parse_expression(source, it.by_ref(), span.end())?;
29✔
879

880
                        if exp.omit_pre_ws {
12✔
881
                            Template::remove_previous_whitespace(&mut template_stack);
6✔
882
                        }
883
                        omit_pro_ws = exp.omit_pro_ws;
14✔
884

885
                        match rule {
12✔
886
                            Rule::expression | Rule::html_expression => {
887
                                let helper_template =
24✔
888
                                    HelperTemplate::new(exp.clone(), false, false);
889
                                let el = if rule == Rule::expression {
37✔
890
                                    Expression(Box::new(helper_template))
24✔
891
                                } else {
892
                                    HtmlExpression(Box::new(helper_template))
8✔
893
                                };
894
                                let t = template_stack.front_mut().unwrap();
27✔
895
                                t.push_element(el, line_no, col_no);
12✔
896
                            }
897
                            Rule::decorator_expression | Rule::partial_expression => {
898
                                // do not auto trim ident spaces for
899
                                // partial_expression(>)
900
                                let prevent_indent =
10✔
901
                                    !(rule == Rule::partial_expression && options.prevent_indent);
902
                                trim_line_required = Template::process_standalone_statement(
5✔
903
                                    &mut template_stack,
904
                                    source,
905
                                    &span,
906
                                    prevent_indent,
907
                                    options.is_partial,
5✔
908
                                );
909

910
                                // indent for partial expression >
911
                                let mut indent = None;
5✔
912
                                if rule == Rule::partial_expression
10✔
913
                                    && !options.prevent_indent
6✔
914
                                    && !exp.omit_pre_ws
5✔
915
                                {
916
                                    indent = support::str::find_trailing_whitespace_chars(
5✔
917
                                        &source[..span.start()],
5✔
918
                                    );
919
                                }
920

921
                                let mut decorator = DecoratorTemplate::new(
922
                                    exp.clone(),
5✔
923
                                    trim_line_required && !exp.omit_pre_ws,
5✔
924
                                );
925
                                decorator.indent = indent.map(std::borrow::ToOwned::to_owned);
10✔
926

927
                                let el = if rule == Rule::decorator_expression {
6✔
928
                                    DecoratorExpression(Box::new(decorator))
2✔
929
                                } else {
930
                                    PartialExpression(Box::new(decorator))
12✔
931
                                };
932
                                let t = template_stack.front_mut().unwrap();
11✔
933
                                t.push_element(el, line_no, col_no);
5✔
934
                            }
935
                            Rule::helper_block_end | Rule::raw_block_end => {
936
                                // standalone statement check, it also removes leading whitespaces of
937
                                // previous rawstring when standalone statement detected
938
                                trim_line_required = Template::process_standalone_statement(
18✔
939
                                    &mut template_stack,
940
                                    source,
941
                                    &span,
942
                                    true,
943
                                    options.is_partial,
9✔
944
                                );
945

946
                                let mut h = helper_stack.pop_front().unwrap();
9✔
947
                                let close_tag_name = exp.name.as_name();
17✔
948
                                if h.name.as_name() == close_tag_name {
9✔
949
                                    let prev_t = template_stack.pop_front().unwrap();
19✔
950
                                    h.revert_chain_and_set(Some(prev_t));
9✔
951

952
                                    let t = template_stack.front_mut().unwrap();
9✔
953
                                    t.elements.push(HelperBlock(Box::new(h)));
9✔
954
                                } else {
955
                                    return Err(TemplateError::of(
×
956
                                        TemplateErrorReason::MismatchingClosedHelper(
×
957
                                            h.name.debug_name(),
×
958
                                            exp.name.debug_name(),
×
959
                                        ),
960
                                    )
961
                                    .at(source, line_no, col_no)
×
962
                                    .in_template(options.name()));
×
963
                                }
964
                            }
965
                            Rule::decorator_block_end | Rule::partial_block_end => {
966
                                // standalone statement check, it also removes leading whitespaces of
967
                                // previous rawstring when standalone statement detected
968
                                trim_line_required = Template::process_standalone_statement(
6✔
969
                                    &mut template_stack,
970
                                    source,
971
                                    &span,
972
                                    true,
973
                                    options.is_partial,
3✔
974
                                );
975

976
                                let mut d = decorator_stack.pop_front().unwrap();
3✔
977
                                let close_tag_name = exp.name.as_name();
6✔
978
                                let empty_close_tag = close_tag_name
3✔
979
                                    .map(|name| name.is_empty())
6✔
980
                                    .unwrap_or_default();
981
                                if empty_close_tag || d.name.as_name() == close_tag_name {
6✔
982
                                    let prev_t = template_stack.pop_front().unwrap();
7✔
983
                                    d.template = Some(prev_t);
3✔
984
                                    let t = template_stack.front_mut().unwrap();
3✔
985
                                    if rule == Rule::decorator_block_end {
3✔
986
                                        t.elements.push(DecoratorBlock(Box::new(d)));
6✔
987
                                    } else {
988
                                        t.elements.push(PartialBlock(Box::new(d)));
2✔
989
                                    }
990
                                } else {
991
                                    return Err(TemplateError::of(
3✔
992
                                        TemplateErrorReason::MismatchingClosedDecorator(
1✔
993
                                            d.name.debug_name(),
1✔
994
                                            exp.name.debug_name(),
1✔
995
                                        ),
996
                                    )
997
                                    .at(source, line_no, col_no)
1✔
998
                                    .in_template(options.name()));
2✔
999
                                }
1000
                            }
1001
                            _ => unreachable!(),
1002
                        }
1003
                    }
1004
                    Rule::hbs_comment_compact => {
1005
                        trim_line_required = Template::process_standalone_statement(
2✔
1006
                            &mut template_stack,
1007
                            source,
1008
                            &span,
1009
                            true,
1010
                            options.is_partial,
1✔
1011
                        );
1012

1013
                        let text = span
1✔
1014
                            .as_str()
1015
                            .trim_start_matches("{{!")
1016
                            .trim_end_matches("}}");
1017
                        let t = template_stack.front_mut().unwrap();
1✔
1018
                        t.push_element(Comment(text.to_owned()), line_no, col_no);
1✔
1019
                    }
1020
                    Rule::hbs_comment => {
1021
                        trim_line_required = Template::process_standalone_statement(
4✔
1022
                            &mut template_stack,
1023
                            source,
1024
                            &span,
1025
                            true,
1026
                            options.is_partial,
2✔
1027
                        );
1028

1029
                        let text = span
2✔
1030
                            .as_str()
1031
                            .trim_start_matches("{{!--")
1032
                            .trim_end_matches("--}}");
1033
                        let t = template_stack.front_mut().unwrap();
2✔
1034
                        t.push_element(Comment(text.to_owned()), line_no, col_no);
2✔
1035
                    }
1036
                    _ => {}
1037
                }
1038

1039
                if rule != Rule::template {
48✔
1040
                    end_pos = Some(span.end_pos());
13✔
1041
                }
1042
            } else {
1043
                let prev_end = end_pos.as_ref().map_or(0, pest::Position::pos);
27✔
1044
                if prev_end < source.len() {
15✔
1045
                    let text = &source[prev_end..source.len()];
×
1046
                    // is some called in if check
1047
                    let (line_no, col_no) = end_pos.unwrap().line_col();
×
1048
                    let t = template_stack.front_mut().unwrap();
×
1049
                    t.push_element(RawString(text.to_owned()), line_no, col_no);
×
1050
                }
1051
                let mut root_template = template_stack.pop_front().unwrap();
29✔
1052
                root_template.name = options.name;
30✔
1053
                return Ok(root_template);
15✔
1054
            }
1055
        }
1056
    }
1057

1058
    // These two compile functions are kept for compatibility with 4.x
1059
    // Template APIs in case that some developers are using them
1060
    // without registry.
1061

1062
    pub fn compile(source: &str) -> Result<Template, TemplateError> {
1✔
1063
        Self::compile2(source, TemplateOptions::default())
1✔
1064
    }
1065

1066
    pub fn compile_with_name<S: AsRef<str>>(
1067
        source: S,
1068
        name: String,
1069
    ) -> Result<Template, TemplateError> {
1070
        Self::compile2(
1071
            source.as_ref(),
×
1072
            TemplateOptions {
×
1073
                name: Some(name),
×
1074
                ..Default::default()
×
1075
            },
1076
        )
1077
    }
1078
}
1079

1080
#[non_exhaustive]
1081
#[derive(PartialEq, Eq, Clone, Debug)]
1082
pub enum TemplateElement {
1083
    RawString(String),
1084
    HtmlExpression(Box<HelperTemplate>),
1085
    Expression(Box<HelperTemplate>),
1086
    HelperBlock(Box<HelperTemplate>),
1087
    DecoratorExpression(Box<DecoratorTemplate>),
1088
    DecoratorBlock(Box<DecoratorTemplate>),
1089
    PartialExpression(Box<DecoratorTemplate>),
1090
    PartialBlock(Box<DecoratorTemplate>),
1091
    Comment(String),
1092
}
1093

1094
#[cfg(test)]
1095
mod test {
1096
    use super::*;
1097
    use crate::error::TemplateErrorReason;
1098

1099
    #[test]
1100
    fn test_parse_escaped_tag_raw_string() {
1101
        let source = r"foo \{{bar}}";
1102
        let t = Template::compile(source).ok().unwrap();
1103
        assert_eq!(t.elements.len(), 1);
1104
        assert_eq!(
1105
            *t.elements.first().unwrap(),
1106
            RawString("foo {{bar}}".to_string())
1107
        );
1108
    }
1109

1110
    #[test]
1111
    fn test_pure_backslash_raw_string() {
1112
        let source = r"\\\\";
1113
        let t = Template::compile(source).ok().unwrap();
1114
        assert_eq!(t.elements.len(), 1);
1115
        assert_eq!(*t.elements.first().unwrap(), RawString(source.to_string()));
1116
    }
1117

1118
    #[test]
1119
    fn test_parse_escaped_block_raw_string() {
1120
        let source = r"\{{{{foo}}}} bar";
1121
        let t = Template::compile(source).ok().unwrap();
1122
        assert_eq!(t.elements.len(), 1);
1123
        assert_eq!(
1124
            *t.elements.first().unwrap(),
1125
            RawString("{{{{foo}}}} bar".to_string())
1126
        );
1127
    }
1128

1129
    #[test]
1130
    fn test_parse_template() {
1131
        let source = "<h1>{{title}} 你好</h1> {{{content}}}
1132
{{#if date}}<p>good</p>{{else}}<p>bad</p>{{/if}}<img>{{foo bar}}中文你好
1133
{{#unless true}}kitkat{{^}}lollipop{{/unless}}";
1134
        let t = Template::compile(source).ok().unwrap();
1135

1136
        assert_eq!(t.elements.len(), 10);
1137

1138
        assert_eq!(*t.elements.first().unwrap(), RawString("<h1>".to_string()));
1139
        assert_eq!(
1140
            *t.elements.get(1).unwrap(),
1141
            Expression(Box::new(HelperTemplate::with_path(Path::with_named_paths(
1142
                &["title"]
1143
            ))))
1144
        );
1145

1146
        assert_eq!(
1147
            *t.elements.get(3).unwrap(),
1148
            HtmlExpression(Box::new(HelperTemplate::with_path(Path::with_named_paths(
1149
                &["content"],
1150
            ))))
1151
        );
1152

1153
        match *t.elements.get(5).unwrap() {
1154
            HelperBlock(ref h) => {
1155
                assert_eq!(h.name.as_name().unwrap(), "if".to_string());
1156
                assert_eq!(h.params.len(), 1);
1157
                assert_eq!(h.template.as_ref().unwrap().elements.len(), 1);
1158
            }
1159
            _ => {
1160
                panic!("Helper expected here.");
1161
            }
1162
        };
1163

1164
        match *t.elements.get(7).unwrap() {
1165
            Expression(ref h) => {
1166
                assert_eq!(h.name.as_name().unwrap(), "foo".to_string());
1167
                assert_eq!(h.params.len(), 1);
1168
                assert_eq!(
1169
                    *(h.params.first().unwrap()),
1170
                    Parameter::Path(Path::with_named_paths(&["bar"]))
1171
                );
1172
            }
1173
            _ => {
1174
                panic!("Helper expression here");
1175
            }
1176
        };
1177

1178
        match *t.elements.get(9).unwrap() {
1179
            HelperBlock(ref h) => {
1180
                assert_eq!(h.name.as_name().unwrap(), "unless".to_string());
1181
                assert_eq!(h.params.len(), 1);
1182
                assert_eq!(h.inverse.as_ref().unwrap().elements.len(), 1);
1183
            }
1184
            _ => {
1185
                panic!("Helper expression here");
1186
            }
1187
        };
1188
    }
1189

1190
    #[test]
1191
    fn test_parse_block_partial_path_identifier() {
1192
        let source = "{{#> foo/bar}}{{/foo/bar}}";
1193
        assert!(Template::compile(source).is_ok());
1194
    }
1195

1196
    #[test]
1197
    fn test_parse_block_empty_end() {
1198
        let source = "{{#> foo/bar}}{{/}}";
1199
        Template::compile(source).unwrap();
1200
    }
1201

1202
    #[test]
1203
    fn test_parse_dynamic_block() {
1204
        let source = "{{#> (foo)}}{{/}}";
1205
        Template::compile(source).unwrap();
1206
    }
1207

1208
    #[test]
1209
    fn test_parse_error() {
1210
        let source = "{{#ifequals name compare=\"hello\"}}\nhello\n\t{{else}}\ngood";
1211

1212
        let terr = Template::compile(source).unwrap_err();
1213

1214
        assert!(matches!(
1215
            terr.reason(),
1216
            TemplateErrorReason::InvalidSyntax(_)
1217
        ));
1218
        assert_eq!(terr.pos(), Some((4, 5)));
1219
    }
1220

1221
    #[test]
1222
    fn test_subexpression() {
1223
        let source =
1224
            "{{foo (bar)}}{{foo (bar baz)}} hello {{#if (baz bar) then=(bar)}}world{{/if}}";
1225
        let t = Template::compile(source).ok().unwrap();
1226

1227
        assert_eq!(t.elements.len(), 4);
1228
        match *t.elements.first().unwrap() {
1229
            Expression(ref h) => {
1230
                assert_eq!(h.name.as_name().unwrap(), "foo".to_owned());
1231
                assert_eq!(h.params.len(), 1);
1232
                if let Parameter::Subexpression(t) = h.params.first().unwrap() {
1233
                    assert_eq!(t.name(), "bar".to_owned());
1234
                } else {
1235
                    panic!("Subexpression expected");
1236
                }
1237
            }
1238
            _ => {
1239
                panic!("Helper expression expected");
1240
            }
1241
        };
1242

1243
        match *t.elements.get(1).unwrap() {
1244
            Expression(ref h) => {
1245
                assert_eq!(h.name.as_name().unwrap(), "foo".to_string());
1246
                assert_eq!(h.params.len(), 1);
1247
                if let Parameter::Subexpression(t) = h.params.first().unwrap() {
1248
                    assert_eq!(t.name(), "bar".to_owned());
1249
                    if let Some(Parameter::Path(p)) = t.params().unwrap().first() {
1250
                        assert_eq!(p, &Path::with_named_paths(&["baz"]));
1251
                    } else {
1252
                        panic!("non-empty param expected ");
1253
                    }
1254
                } else {
1255
                    panic!("Subexpression expected");
1256
                }
1257
            }
1258
            _ => {
1259
                panic!("Helper expression expected");
1260
            }
1261
        };
1262

1263
        match *t.elements.get(3).unwrap() {
1264
            HelperBlock(ref h) => {
1265
                assert_eq!(h.name.as_name().unwrap(), "if".to_string());
1266
                assert_eq!(h.params.len(), 1);
1267
                assert_eq!(h.hash.len(), 1);
1268

1269
                if let Parameter::Subexpression(t) = h.params.first().unwrap() {
1270
                    assert_eq!(t.name(), "baz".to_owned());
1271
                    if let Some(Parameter::Path(p)) = t.params().unwrap().first() {
1272
                        assert_eq!(p, &Path::with_named_paths(&["bar"]));
1273
                    } else {
1274
                        panic!("non-empty param expected ");
1275
                    }
1276
                } else {
1277
                    panic!("Subexpression expected (baz bar)");
1278
                }
1279

1280
                if let Parameter::Subexpression(t) = h.hash.get("then").unwrap() {
1281
                    assert_eq!(t.name(), "bar".to_owned());
1282
                } else {
1283
                    panic!("Subexpression expected (bar)");
1284
                }
1285
            }
1286
            _ => {
1287
                panic!("HelperBlock expected");
1288
            }
1289
        }
1290
    }
1291

1292
    #[test]
1293
    fn test_white_space_omitter() {
1294
        let source = "hello~     {{~world~}} \n  !{{~#if true}}else{{/if~}}";
1295
        let t = Template::compile(source).ok().unwrap();
1296

1297
        assert_eq!(t.elements.len(), 4);
1298

1299
        assert_eq!(t.elements[0], RawString("hello~".to_string()));
1300
        assert_eq!(
1301
            t.elements[1],
1302
            Expression(Box::new(HelperTemplate::with_path(Path::with_named_paths(
1303
                &["world"]
1304
            ))))
1305
        );
1306
        assert_eq!(t.elements[2], RawString("!".to_string()));
1307

1308
        let t2 = Template::compile("{{#if true}}1  {{~ else ~}} 2 {{~/if}}")
1309
            .ok()
1310
            .unwrap();
1311
        assert_eq!(t2.elements.len(), 1);
1312
        match t2.elements[0] {
1313
            HelperBlock(ref h) => {
1314
                assert_eq!(
1315
                    h.template.as_ref().unwrap().elements[0],
1316
                    RawString("1".to_string())
1317
                );
1318
                assert_eq!(
1319
                    h.inverse.as_ref().unwrap().elements[0],
1320
                    RawString("2".to_string())
1321
                );
1322
            }
1323
            _ => unreachable!(),
1324
        }
1325
    }
1326

1327
    #[test]
1328
    fn test_unclosed_expression() {
1329
        let sources = ["{{invalid", "{{{invalid", "{{invalid}", "{{!hello"];
1330
        for s in &sources {
1331
            let result = Template::compile(s.to_owned());
1332
            let err = result.expect_err("expected a syntax error");
1333
            let syntax_error_msg = match err.reason() {
1334
                TemplateErrorReason::InvalidSyntax(s) => s,
1335
                _ => panic!("InvalidSyntax expected"),
1336
            };
1337
            assert!(
1338
                syntax_error_msg.contains("expected identifier"),
1339
                "{}",
1340
                syntax_error_msg
1341
            );
1342
        }
1343
    }
1344

1345
    #[test]
1346
    fn test_raw_helper() {
1347
        let source = "hello{{{{raw}}}}good{{night}}{{{{/raw}}}}world";
1348
        match Template::compile(source) {
1349
            Ok(t) => {
1350
                assert_eq!(t.elements.len(), 3);
1351
                assert_eq!(t.elements[0], RawString("hello".to_owned()));
1352
                assert_eq!(t.elements[2], RawString("world".to_owned()));
1353
                match t.elements[1] {
1354
                    HelperBlock(ref h) => {
1355
                        assert_eq!(h.name.as_name().unwrap(), "raw".to_owned());
1356
                        if let Some(ref ht) = h.template {
1357
                            assert_eq!(ht.elements.len(), 1);
1358
                            assert_eq!(
1359
                                *ht.elements.first().unwrap(),
1360
                                RawString("good{{night}}".to_owned())
1361
                            );
1362
                        } else {
1363
                            panic!("helper template not found");
1364
                        }
1365
                    }
1366
                    _ => {
1367
                        panic!("Unexpected element type");
1368
                    }
1369
                }
1370
            }
1371
            Err(e) => {
1372
                panic!("{}", e);
1373
            }
1374
        }
1375
    }
1376

1377
    #[test]
1378
    fn test_literal_parameter_parser() {
1379
        match Template::compile("{{hello 1 name=\"value\" valid=false ref=someref}}") {
1380
            Ok(t) => {
1381
                if let Expression(ref ht) = t.elements[0] {
1382
                    assert_eq!(ht.params[0], Parameter::Literal(json!(1)));
1383
                    assert_eq!(
1384
                        ht.hash["name"],
1385
                        Parameter::Literal(Json::String("value".to_owned()))
1386
                    );
1387
                    assert_eq!(ht.hash["valid"], Parameter::Literal(Json::Bool(false)));
1388
                    assert_eq!(
1389
                        ht.hash["ref"],
1390
                        Parameter::Path(Path::with_named_paths(&["someref"]))
1391
                    );
1392
                }
1393
            }
1394
            Err(e) => panic!("{}", e),
1395
        }
1396
    }
1397

1398
    #[test]
1399
    fn test_template_mapping() {
1400
        match Template::compile("hello\n  {{~world}}\n{{#if nice}}\n\thello\n{{/if}}") {
1401
            Ok(t) => {
1402
                assert_eq!(t.mapping.len(), t.elements.len());
1403
                assert_eq!(t.mapping[0], TemplateMapping(1, 1));
1404
                assert_eq!(t.mapping[1], TemplateMapping(2, 3));
1405
                assert_eq!(t.mapping[3], TemplateMapping(3, 1));
1406
            }
1407
            Err(e) => panic!("{}", e),
1408
        }
1409
    }
1410

1411
    #[test]
1412
    fn test_whitespace_elements() {
1413
        let c = Template::compile(
1414
            "  {{elem}}\n\t{{#if true}} \
1415
         {{/if}}\n{{{{raw}}}} {{{{/raw}}}}\n{{{{raw}}}}{{{{/raw}}}}\n",
1416
        );
1417
        let r = c.unwrap();
1418
        // the \n after last raw block is dropped by pest
1419
        assert_eq!(r.elements.len(), 9);
1420
    }
1421

1422
    #[test]
1423
    fn test_block_param() {
1424
        match Template::compile("{{#each people as |person|}}{{person}}{{/each}}") {
1425
            Ok(t) => {
1426
                if let HelperBlock(ref ht) = t.elements[0] {
1427
                    if let Some(BlockParam::Single(Parameter::Name(ref n))) = ht.block_param {
1428
                        assert_eq!(n, "person");
1429
                    } else {
1430
                        panic!("block param expected.")
1431
                    }
1432
                } else {
1433
                    panic!("Helper block expected");
1434
                }
1435
            }
1436
            Err(e) => panic!("{}", e),
1437
        }
1438

1439
        match Template::compile("{{#each people as |val key|}}{{person}}{{/each}}") {
1440
            Ok(t) => {
1441
                if let HelperBlock(ref ht) = t.elements[0] {
1442
                    if let Some(BlockParam::Pair((
1443
                        Parameter::Name(ref n1),
1444
                        Parameter::Name(ref n2),
1445
                    ))) = ht.block_param
1446
                    {
1447
                        assert_eq!(n1, "val");
1448
                        assert_eq!(n2, "key");
1449
                    } else {
1450
                        panic!("helper block param expected.");
1451
                    }
1452
                } else {
1453
                    panic!("Helper block expected");
1454
                }
1455
            }
1456
            Err(e) => panic!("{}", e),
1457
        }
1458
    }
1459

1460
    #[test]
1461
    fn test_decorator() {
1462
        match Template::compile("hello {{* ssh}} world") {
1463
            Err(e) => panic!("{}", e),
1464
            Ok(t) => {
1465
                if let DecoratorExpression(ref de) = t.elements[1] {
1466
                    assert_eq!(de.name.as_name(), Some("ssh"));
1467
                    assert_eq!(de.template, None);
1468
                }
1469
            }
1470
        }
1471

1472
        match Template::compile("hello {{> ssh}} world") {
1473
            Err(e) => panic!("{}", e),
1474
            Ok(t) => {
1475
                if let PartialExpression(ref de) = t.elements[1] {
1476
                    assert_eq!(de.name.as_name(), Some("ssh"));
1477
                    assert_eq!(de.template, None);
1478
                }
1479
            }
1480
        }
1481

1482
        match Template::compile("{{#*inline \"hello\"}}expand to hello{{/inline}}{{> hello}}") {
1483
            Err(e) => panic!("{}", e),
1484
            Ok(t) => {
1485
                if let DecoratorBlock(ref db) = t.elements[0] {
1486
                    assert_eq!(db.name, Parameter::Name("inline".to_owned()));
1487
                    assert_eq!(
1488
                        db.params[0],
1489
                        Parameter::Literal(Json::String("hello".to_owned()))
1490
                    );
1491
                    assert_eq!(
1492
                        db.template.as_ref().unwrap().elements[0],
1493
                        TemplateElement::RawString("expand to hello".to_owned())
1494
                    );
1495
                }
1496
            }
1497
        }
1498

1499
        match Template::compile("{{#> layout \"hello\"}}expand to hello{{/layout}}{{> hello}}") {
1500
            Err(e) => panic!("{}", e),
1501
            Ok(t) => {
1502
                if let PartialBlock(ref db) = t.elements[0] {
1503
                    assert_eq!(db.name, Parameter::Name("layout".to_owned()));
1504
                    assert_eq!(
1505
                        db.params[0],
1506
                        Parameter::Literal(Json::String("hello".to_owned()))
1507
                    );
1508
                    assert_eq!(
1509
                        db.template.as_ref().unwrap().elements[0],
1510
                        TemplateElement::RawString("expand to hello".to_owned())
1511
                    );
1512
                }
1513
            }
1514
        }
1515
    }
1516

1517
    #[test]
1518
    fn test_panic_with_tag_name() {
1519
        let s = "{{#>(X)}}{{/X}}";
1520
        let result = Template::compile(s);
1521
        assert!(result.is_err());
1522
        assert_eq!("decorator \"Subexpression(Subexpression { element: Expression(HelperTemplate { name: Path(Relative(([Named(\\\"X\\\")], \\\"X\\\"))), params: [], hash: {}, block_param: None, template: None, inverse: None, block: false, chain: false, indent_before_write: false }) })\" was opened, but \"X\" is closing", format!("{}", result.unwrap_err().reason()));
1523
    }
1524
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc