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

sunng87 / handlebars-rust / 20740331273

06 Jan 2026 06:35AM UTC coverage: 83.35% (-0.1%) from 83.449%
20740331273

Pull #737

github

web-flow
Merge 775bc6f56 into b1b39cd03
Pull Request #737: refactor: use smol_str for template strings

45 of 52 new or added lines in 9 files covered. (86.54%)

1 existing line in 1 file now uncovered.

1682 of 2018 relevant lines covered (83.35%)

7.05 hits per line

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

86.37
/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
use smol_str::SmolStr;
10

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

16
use derive_builder::Builder;
17

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

207
    pub(crate) fn is_name_only(&self) -> bool {
15✔
208
        !self.block && self.params.is_empty() && self.hash.is_empty()
14✔
209
    }
210

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

340
impl Template {
341
    pub fn new() -> Template {
17✔
342
        Template::default()
18✔
343
    }
344

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

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

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

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

449
        while let Some(n) = it.peek() {
23✔
450
            let n_span = n.as_span();
12✔
451
            if n_span.end() > param_span.end() {
11✔
452
                break;
×
453
            }
454
            it.next();
9✔
455
        }
456

457
        Ok(result)
11✔
458
    }
459

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

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

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

486
        let p2 = it.peek().and_then(|p2_name| {
8✔
487
            let p2_name_span = p2_name.as_span();
2✔
488
            if p2_name_span.end() <= limit {
6✔
489
                Some(SmolStr::new(p2_name_span.as_str()))
2✔
490
            } else {
491
                None
2✔
492
            }
493
        });
494

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

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

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

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

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

539
            it.next();
14✔
540

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

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

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

590
        let mut with_trailing_newline = support::str::starts_with_empty_line(continuation);
10✔
591

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

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

600
            // prevent_indent: a special toggle for partial expression
601
            // (>) that leading whitespaces are kept
602
            if prevent_indent && with_leading_newline {
21✔
603
                let t = template_stack.front_mut().unwrap();
8✔
604
                // check the last element before current
605
                if let Some(RawString(text)) = &mut t.elements.last_mut() {
14✔
606
                    // trim leading space for standalone statement
607
                    *text = SmolStr::new(text.trim_end_matches(support::str::whitespace_matcher));
12✔
608
                }
609
            }
610

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

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

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

631
            let current_start = pair_span.start();
16✔
632
            let span_length = pair_span.end() - current_start;
14✔
633
            let leading_space_offset = s.len() - span_length;
28✔
634

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

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

650
        if trim_start {
16✔
651
            RawString(SmolStr::new(s.trim_start()))
2✔
652
        } else if trim_start_line {
20✔
653
            let s = s.trim_start_matches(support::str::whitespace_matcher);
12✔
654
            RawString(SmolStr::new(support::str::strip_first_newline(s)))
6✔
655
        } else {
656
            RawString(SmolStr::new(s))
27✔
657
        }
658
    }
659

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

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

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

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

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

703
                let is_trailing_string = rule != Rule::template
17✔
704
                    && span.start() != prev_end
17✔
705
                    && !omit_pro_ws
9✔
706
                    && rule != Rule::raw_text
9✔
707
                    && rule != Rule::raw_block_text;
9✔
708

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

739
                    // reset standalone statement marker
740
                    trim_line_required = false;
9✔
741
                }
742

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

756
                        let t = template_stack.front_mut().unwrap();
16✔
757

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

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

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

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

793
                        let indent_before_write = trim_line_required && !exp.omit_pre_ws;
9✔
794

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

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

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

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

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

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

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

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

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

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

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

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

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

920
                                let mut decorator = DecoratorTemplate::new(
921
                                    exp.clone(),
6✔
922
                                    trim_line_required && !exp.omit_pre_ws,
6✔
923
                                );
924
                                decorator.indent = indent.map(SmolStr::new);
12✔
925

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1138
        assert_eq!(t.elements.len(), 10);
1139

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

1151
        assert_eq!(
1152
            *t.elements.get(3).unwrap(),
1153
            HtmlExpression(Box::new(HelperTemplate::with_path(Path::with_named_paths(
1154
                &["content"],
1155
            ))))
1156
        );
1157

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

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

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

1195
    #[test]
1196
    fn test_parse_block_partial_path_identifier() {
1197
        let source = "{{#> foo/bar}}{{/foo/bar}}";
1198
        assert!(Template::compile(source).is_ok());
1199
    }
1200

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

1207
    #[test]
1208
    fn test_parse_dynamic_block() {
1209
        let source = "{{#> (foo)}}{{/}}";
1210
        Template::compile(source).unwrap();
1211
    }
1212

1213
    #[test]
1214
    fn test_parse_error() {
1215
        let source = "{{#ifequals name compare=\"hello\"}}\nhello\n\t{{else}}\ngood";
1216

1217
        let terr = Template::compile(source).unwrap_err();
1218

1219
        assert!(matches!(
1220
            terr.reason(),
1221
            TemplateErrorReason::InvalidSyntax(_)
1222
        ));
1223
        assert_eq!(terr.pos(), Some((4, 5)));
1224
    }
1225

1226
    #[test]
1227
    fn test_subexpression() {
1228
        let source =
1229
            "{{foo (bar)}}{{foo (bar baz)}} hello {{#if (baz bar) then=(bar)}}world{{/if}}";
1230
        let t = Template::compile(source).ok().unwrap();
1231

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

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

1268
        match *t.elements.get(3).unwrap() {
1269
            HelperBlock(ref h) => {
1270
                assert_eq!(h.name.as_name().unwrap(), "if".to_string());
1271
                assert_eq!(h.params.len(), 1);
1272
                assert_eq!(h.hash.len(), 1);
1273

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

1285
                if let Parameter::Subexpression(t) = h.hash.get("then").unwrap() {
1286
                    assert_eq!(t.name(), "bar".to_owned());
1287
                } else {
1288
                    panic!("Subexpression expected (bar)");
1289
                }
1290
            }
1291
            _ => {
1292
                panic!("HelperBlock expected");
1293
            }
1294
        }
1295
    }
1296

1297
    #[test]
1298
    fn test_white_space_omitter() {
1299
        let source = "hello~     {{~world~}} \n  !{{~#if true}}else{{/if~}}";
1300
        let t = Template::compile(source).ok().unwrap();
1301

1302
        assert_eq!(t.elements.len(), 4);
1303

1304
        assert_eq!(t.elements[0], RawString(SmolStr::new("hello~")));
1305
        assert_eq!(
1306
            t.elements[1],
1307
            Expression(Box::new(HelperTemplate::with_path(Path::with_named_paths(
1308
                &["world"]
1309
            ))))
1310
        );
1311
        assert_eq!(t.elements[2], RawString(SmolStr::new("!")));
1312

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

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

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

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

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

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

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

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

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

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

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

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

1522
    #[test]
1523
    fn test_panic_with_tag_name() {
1524
        let s = "{{#>(X)}}{{/X}}";
1525
        let result = Template::compile(s);
1526
        assert!(result.is_err());
1527
        assert_eq!(
1528
            "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",
1529
            format!("{}", result.unwrap_err().reason())
1530
        );
1531
    }
1532
}
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