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

sunng87 / handlebars-rust / 20846737858

09 Jan 2026 09:03AM UTC coverage: 83.49% (+0.04%) from 83.449%
20846737858

Pull #737

github

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

59 of 67 new or added lines in 9 files covered. (88.06%)

1 existing line in 1 file now uncovered.

1684 of 2017 relevant lines covered (83.49%)

6.95 hits per line

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

84.89
/src/render.rs
1
use std::borrow::Cow;
2
use std::collections::{BTreeMap, VecDeque};
3
use std::fmt;
4
use std::rc::Rc;
5

6
use serde_json::value::Value as Json;
7

8
use crate::block::BlockContext;
9
use crate::context::Context;
10
use crate::error::RenderError;
11
use crate::helpers::HelperDef;
12
use crate::json::path::Path;
13
use crate::json::value::{JsonRender, PathAndJson, ScopedJson};
14
use crate::output::{Output, StringOutput};
15
use crate::registry::Registry;
16
use crate::support;
17
use crate::support::str::newline_matcher;
18
use crate::template::TemplateElement::{
19
    DecoratorBlock, DecoratorExpression, Expression, HelperBlock, HtmlExpression, PartialBlock,
20
    PartialExpression, RawString,
21
};
22
use crate::template::{
23
    BlockParam, DecoratorTemplate, HelperTemplate, Parameter, Template, TemplateElement,
24
    TemplateMapping,
25
};
26
use crate::{RenderErrorReason, partial};
27

28
const HELPER_MISSING: &str = "helperMissing";
29
const BLOCK_HELPER_MISSING: &str = "blockHelperMissing";
30

31
/// The context of a render call
32
///
33
/// This context stores information of a render and a writer where generated
34
/// content is written to.
35
///
36
#[derive(Clone)]
37
pub struct RenderContext<'reg: 'rc, 'rc> {
38
    dev_mode_templates: Option<&'rc BTreeMap<String, Cow<'rc, Template>>>,
39

40
    blocks: VecDeque<BlockContext<'rc>>,
41

42
    // copy-on-write context
43
    modified_context: Option<Rc<Context>>,
44

45
    // when rendering partials, store rendered string into partial_block_stack
46
    // for `@partial-block` referencing. If it's a `{{> partial}}`, push `None`
47
    // to stack and report error when child partial try to referencing
48
    // `@partial-block`.
49
    partial_block_stack: VecDeque<Option<String>>,
50
    local_helpers: BTreeMap<String, Rc<dyn HelperDef + Send + Sync + 'rc>>,
51
    /// current template name
52
    current_template: Option<&'rc String>,
53
    /// root template name
54
    root_template: Option<&'reg String>,
55
    disable_escape: bool,
56

57
    // Indicates whether the previous text that we rendered ended on a newline.
58
    // This is necessary to make indenting decisions after the end of partials.
59
    trailing_newline: bool,
60

61
    // This should be set to true whenever any output is written.
62
    // We need this to detect empty partials/helpers for indenting decisions.
63
    content_produced: bool,
64

65
    // The next text that we render should indent itself.
66
    indent_before_write: bool,
67
    indent_string: Option<Cow<'rc, str>>,
68

69
    // Threads the recursive_lookup state from the registry down
70
    // through to the context navigation layer
71
    recursive_lookup: bool,
72
}
73

74
impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> {
75
    /// Create a render context
76
    pub fn new(root_template: Option<&'reg String>) -> RenderContext<'reg, 'rc> {
15✔
77
        let mut blocks = VecDeque::with_capacity(5);
14✔
78
        blocks.push_front(BlockContext::new());
29✔
79

80
        let modified_context = None;
15✔
81
        RenderContext {
82
            partial_block_stack: VecDeque::new(),
14✔
83
            local_helpers: BTreeMap::new(),
14✔
84
            current_template: None,
85
            root_template,
86
            disable_escape: false,
87
            trailing_newline: false,
88
            content_produced: false,
89
            indent_before_write: false,
90
            indent_string: None,
91
            blocks,
92
            modified_context,
93
            dev_mode_templates: None,
94
            recursive_lookup: false,
95
        }
96
    }
97

98
    /// Push a block context into render context stack. This is typically
99
    /// called when you entering a block scope.
100
    pub fn push_block(&mut self, block: BlockContext<'rc>) {
11✔
101
        self.blocks.push_front(block);
10✔
102
    }
103

104
    /// Pop and drop current block context.
105
    /// This is typically called when leaving a block scope.
106
    pub fn pop_block(&mut self) {
8✔
107
        self.blocks.pop_front();
10✔
108
    }
109

110
    /// Remove all blocks
111
    pub(crate) fn clear_blocks(&mut self) {
×
112
        self.blocks.clear();
×
113
    }
114

115
    /// Borrow a reference to current block context
116
    pub fn block(&self) -> Option<&BlockContext<'rc>> {
×
117
        self.blocks.front()
×
118
    }
119

120
    /// Borrow a mutable reference to current block context in order to
121
    /// modify some data.
122
    pub fn block_mut(&mut self) -> Option<&mut BlockContext<'rc>> {
7✔
123
        self.blocks.front_mut()
8✔
124
    }
125

126
    /// Replace blocks hold by render context and return the replaced blocks
127
    pub fn replace_blocks(
×
128
        &mut self,
129
        blocks: VecDeque<BlockContext<'rc>>,
130
    ) -> VecDeque<BlockContext<'rc>> {
131
        std::mem::replace(&mut self.blocks, blocks)
×
132
    }
133

134
    /// Get the modified context data if any
135
    pub fn context(&self) -> Option<Rc<Context>> {
13✔
136
        self.modified_context.clone()
12✔
137
    }
138

139
    /// Set new context data into the render process.
140
    /// This is typically called in decorators where user can modify
141
    /// the data they were rendering.
142
    pub fn set_context(&mut self, ctx: Context) {
1✔
143
        self.modified_context = Some(Rc::new(ctx));
2✔
144
    }
145

146
    /// Evaluate a Json path in current scope.
147
    ///
148
    /// Typically you don't need to evaluate it by yourself.
149
    /// The Helper and Decorator API will provide your evaluated value of
150
    /// their parameters and hash data.
151
    pub fn evaluate(
3✔
152
        &self,
153
        context: &'rc Context,
154
        relative_path: &str,
155
    ) -> Result<ScopedJson<'rc>, RenderError> {
156
        let path = Path::parse(relative_path)?;
3✔
157
        self.evaluate2(context, &path)
3✔
158
    }
159

160
    pub(crate) fn evaluate2(
12✔
161
        &self,
162
        context: &'rc Context,
163
        path: &Path,
164
    ) -> Result<ScopedJson<'rc>, RenderError> {
165
        match path {
14✔
166
            Path::Local((level, name, _)) => Ok(self
2✔
167
                .get_local_var(*level, name)
1✔
168
                .map_or_else(|| ScopedJson::Missing, |v| ScopedJson::Derived(v.clone()))),
6✔
169
            Path::Relative((segs, _)) => {
12✔
170
                context.navigate(segs, &self.blocks, self.recursive_lookup)
14✔
171
            }
172
        }
173
    }
174

175
    /// Get registered partial in this render context
176
    pub fn get_partial(&self, name: &str) -> Option<&'rc Template> {
7✔
177
        // resolve partial from block
178
        for block in &self.blocks {
13✔
179
            if let Some(partial) = block.get_local_partial(name) {
12✔
180
                return Some(partial);
6✔
181
            }
182
        }
183

184
        None
4✔
185
    }
186

187
    pub(crate) fn push_partial_block(&mut self, partial_block: Option<String>) {
7✔
188
        self.partial_block_stack.push_front(partial_block);
6✔
189
    }
190

191
    pub(crate) fn pop_partial_block(&mut self) {
6✔
192
        self.partial_block_stack.pop_front();
6✔
193
    }
194

195
    pub(crate) fn peek_partial_block(&self) -> Option<&Option<String>> {
1✔
196
        self.partial_block_stack.front()
1✔
197
    }
198

199
    pub(crate) fn set_indent_string(&mut self, indent: Option<Cow<'rc, str>>) {
7✔
200
        self.indent_string = indent;
13✔
201
    }
202

203
    #[inline]
204
    pub(crate) fn get_indent_string(&self) -> Option<&Cow<'rc, str>> {
15✔
205
        self.indent_string.as_ref()
16✔
206
    }
207

208
    pub(crate) fn get_dev_mode_template(&self, name: &str) -> Option<&'rc Template> {
4✔
209
        self.dev_mode_templates
4✔
210
            .and_then(|dmt| dmt.get(name).map(|t| &**t))
4✔
211
    }
212

213
    pub(crate) fn set_dev_mode_templates(
1✔
214
        &mut self,
215
        t: Option<&'rc BTreeMap<String, Cow<'rc, Template>>>,
216
    ) {
217
        self.dev_mode_templates = t;
1✔
218
    }
219

220
    fn get_local_var(&self, level: usize, name: &str) -> Option<&Json> {
1✔
221
        self.blocks
×
222
            .get(level)
1✔
223
            .and_then(|blk| blk.get_local_var(name))
3✔
224
    }
225

226
    /// Test if given template name is current template.
227
    pub fn is_current_template(&self, p: &str) -> bool {
6✔
228
        self.current_template.is_some_and(|s| s == p)
12✔
229
    }
230

231
    /// Register a helper in this render context.
232
    /// This is a feature provided by Decorator where you can create
233
    /// temporary helpers.
234
    pub fn register_local_helper(
1✔
235
        &mut self,
236
        name: &str,
237
        def: Box<dyn HelperDef + Send + Sync + 'rc>,
238
    ) {
239
        self.local_helpers.insert(name.to_string(), def.into());
2✔
240
    }
241

242
    /// Remove a helper from render context
243
    pub fn unregister_local_helper(&mut self, name: &str) {
1✔
244
        self.local_helpers.remove(name);
1✔
245
    }
246

247
    /// Attempt to get a helper from current render context.
248
    pub fn get_local_helper(&self, name: &str) -> Option<Rc<dyn HelperDef + Send + Sync + 'rc>> {
11✔
249
        self.local_helpers.get(name).cloned()
10✔
250
    }
251

252
    #[inline]
253
    fn has_local_helper(&self, name: &str) -> bool {
11✔
254
        self.local_helpers.contains_key(name)
10✔
255
    }
256

257
    /// Returns the current template name.
258
    /// Note that the name can be vary from root template when you are rendering
259
    /// from partials.
260
    pub fn get_current_template_name(&self) -> Option<&'rc String> {
6✔
261
        self.current_template
7✔
262
    }
263

264
    /// Set the current template name.
265
    pub fn set_current_template_name(&mut self, name: Option<&'rc String>) {
17✔
266
        self.current_template = name;
16✔
267
    }
268

269
    /// Get root template name if any.
270
    /// This is the template name that you call `render` from `Handlebars`.
271
    pub fn get_root_template_name(&self) -> Option<&'reg String> {
×
272
        self.root_template
×
273
    }
274

275
    /// Get the escape toggle
276
    pub fn is_disable_escape(&self) -> bool {
12✔
277
        self.disable_escape
10✔
278
    }
279

280
    /// Set the escape toggle.
281
    /// When toggle is on, `escape_fn` will be called when rendering.
282
    pub fn set_disable_escape(&mut self, disable: bool) {
4✔
283
        self.disable_escape = disable;
4✔
284
    }
285

286
    #[inline]
287
    pub fn set_trailing_newline(&mut self, trailing_newline: bool) {
15✔
288
        self.trailing_newline = trailing_newline;
17✔
289
    }
290

291
    #[inline]
292
    pub fn get_trailine_newline(&self) -> bool {
12✔
293
        self.trailing_newline
14✔
294
    }
295

296
    #[inline]
297
    pub fn set_content_produced(&mut self, content_produced: bool) {
16✔
298
        self.content_produced = content_produced;
15✔
299
    }
300

301
    #[inline]
302
    pub fn get_content_produced(&self) -> bool {
14✔
303
        self.content_produced
12✔
304
    }
305

306
    #[inline]
307
    pub fn set_indent_before_write(&mut self, indent_before_write: bool) {
17✔
308
        self.indent_before_write = indent_before_write;
17✔
309
    }
310

311
    #[inline]
312
    pub fn get_indent_before_write(&self) -> bool {
16✔
313
        self.indent_before_write
16✔
314
    }
315

316
    pub fn set_recursive_lookup(&mut self, enabled: bool) {
14✔
317
        self.recursive_lookup = enabled;
16✔
318
    }
319
}
320

321
impl fmt::Debug for RenderContext<'_, '_> {
322
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
×
323
        f.debug_struct("RenderContextInner")
×
324
            .field("dev_mode_templates", &self.dev_mode_templates)
×
325
            .field("blocks", &self.blocks)
×
326
            .field("modified_context", &self.modified_context)
×
327
            .field("partial_block_stack", &self.partial_block_stack)
×
328
            .field("root_template", &self.root_template)
×
329
            .field("current_template", &self.current_template)
×
330
            .field("disable_escape", &self.disable_escape)
×
331
            .finish()
332
    }
333
}
334

335
/// Render-time Helper data when using in a helper definition
336
#[derive(Debug, Clone)]
337
pub struct Helper<'rc> {
338
    name: Cow<'rc, str>,
339
    params: Vec<PathAndJson<'rc>>,
340
    hash: BTreeMap<&'rc str, PathAndJson<'rc>>,
341
    template: Option<&'rc Template>,
342
    inverse: Option<&'rc Template>,
343
    block_param: Option<&'rc BlockParam>,
344
    block: bool,
345
}
346

347
impl<'reg: 'rc, 'rc> Helper<'rc> {
348
    fn try_from_template(
11✔
349
        ht: &'rc HelperTemplate,
350
        registry: &'reg Registry<'reg>,
351
        context: &'rc Context,
352
        render_context: &mut RenderContext<'reg, 'rc>,
353
    ) -> Result<Helper<'rc>, RenderError> {
354
        let name = ht.name.expand_as_name(registry, context, render_context)?;
11✔
355
        let mut pv = Vec::with_capacity(ht.params.len());
22✔
356
        for p in &ht.params {
32✔
357
            let r = p.expand(registry, context, render_context)?;
21✔
358
            pv.push(r);
9✔
359
        }
360

361
        let mut hm = BTreeMap::new();
10✔
362
        for (k, p) in &ht.hash {
24✔
363
            let r = p.expand(registry, context, render_context)?;
6✔
364
            hm.insert(k.as_ref(), r);
6✔
365
        }
366

367
        Ok(Helper {
11✔
368
            name,
11✔
369
            params: pv,
10✔
370
            hash: hm,
10✔
371
            template: ht.template.as_ref(),
11✔
372
            inverse: ht.inverse.as_ref(),
10✔
373
            block_param: ht.block_param.as_ref(),
11✔
374
            block: ht.block,
10✔
375
        })
376
    }
377

378
    /// Returns helper name
379
    pub fn name(&self) -> &str {
11✔
380
        &self.name
12✔
381
    }
382

383
    /// Returns all helper params, resolved within the context
384
    pub fn params(&self) -> &Vec<PathAndJson<'rc>> {
3✔
385
        &self.params
×
386
    }
387

388
    /// Returns nth helper param, resolved within the context.
389
    ///
390
    /// ## Example
391
    ///
392
    /// To get the first param in `{{my_helper abc}}` or `{{my_helper 2}}`,
393
    /// use `h.param(0)` in helper definition.
394
    /// Variable `abc` is auto resolved in current context.
395
    ///
396
    /// ```
397
    /// use handlebars::*;
398
    ///
399
    /// fn my_helper(h: &Helper, rc: &mut RenderContext) -> Result<(), RenderError> {
400
    ///     let v = h.param(0).map(|v| v.value())
401
    ///         .ok_or(RenderErrorReason::ParamNotFoundForIndex("myhelper", 0));
402
    ///     // ..
403
    ///     Ok(())
404
    /// }
405
    /// ```
406
    pub fn param(&self, idx: usize) -> Option<&PathAndJson<'rc>> {
11✔
407
        self.params.get(idx)
12✔
408
    }
409

410
    /// Returns hash, resolved within the context
411
    pub fn hash(&self) -> &BTreeMap<&'rc str, PathAndJson<'rc>> {
2✔
412
        &self.hash
2✔
413
    }
414

415
    /// Return hash value of a given key, resolved within the context
416
    ///
417
    /// ## Example
418
    ///
419
    /// To get the first param in `{{my_helper v=abc}}` or `{{my_helper v=2}}`,
420
    /// use `h.hash_get("v")` in helper definition.
421
    /// Variable `abc` is auto resolved in current context.
422
    ///
423
    /// ```
424
    /// use handlebars::*;
425
    ///
426
    /// fn my_helper(h: &Helper, rc: &mut RenderContext) -> Result<(), RenderError> {
427
    ///     let v = h.hash_get("v").map(|v| v.value())
428
    ///         .ok_or(RenderErrorReason::ParamNotFoundForIndex("my_helper", 0));
429
    ///     // ..
430
    ///     Ok(())
431
    /// }
432
    /// ```
433
    pub fn hash_get(&self, key: &str) -> Option<&PathAndJson<'rc>> {
5✔
434
        self.hash.get(key)
5✔
435
    }
436

437
    /// Returns the default inner template if the helper is a block helper.
438
    ///
439
    /// Typically you will render the template via: `template.render(registry, render_context)`
440
    ///
441
    pub fn template(&self) -> Option<&'rc Template> {
10✔
442
        self.template
9✔
443
    }
444

445
    /// Returns the template of `else` branch if any
446
    pub fn inverse(&self) -> Option<&'rc Template> {
2✔
447
        self.inverse
2✔
448
    }
449

450
    /// Returns if the helper is a block one `{{#helper}}{{/helper}}` or not `{{helper 123}}`
451
    pub fn is_block(&self) -> bool {
2✔
452
        self.block
2✔
453
    }
454

455
    /// Returns if the helper has either a block param or block param pair
456
    pub fn has_block_param(&self) -> bool {
×
457
        self.block_param.is_some()
×
458
    }
459

460
    /// Returns block param if any
461
    pub fn block_param(&self) -> Option<&'rc str> {
8✔
462
        if let Some(&BlockParam::Single(Parameter::Name(ref s))) = self.block_param {
14✔
463
            Some(s)
3✔
464
        } else {
465
            None
7✔
466
        }
467
    }
468

469
    /// Return block param pair (for example |key, val|) if any
470
    pub fn block_param_pair(&self) -> Option<(&'rc str, &'rc str)> {
6✔
471
        if let Some(&BlockParam::Pair((Parameter::Name(ref s1), Parameter::Name(ref s2)))) =
20✔
472
            self.block_param
×
473
        {
474
            Some((s1, s2))
2✔
475
        } else {
476
            None
6✔
477
        }
478
    }
479
}
480

481
/// Render-time Decorator data when using in a decorator definition
482
#[derive(Debug)]
483
pub struct Decorator<'rc> {
484
    name: Cow<'rc, str>,
485
    params: Vec<PathAndJson<'rc>>,
486
    hash: BTreeMap<&'rc str, PathAndJson<'rc>>,
487
    template: Option<&'rc Template>,
488
    indent: Option<Cow<'rc, str>>,
489
}
490

491
impl<'reg: 'rc, 'rc> Decorator<'rc> {
492
    fn try_from_template(
6✔
493
        dt: &'rc DecoratorTemplate,
494
        registry: &'reg Registry<'reg>,
495
        context: &'rc Context,
496
        render_context: &mut RenderContext<'reg, 'rc>,
497
    ) -> Result<Decorator<'rc>, RenderError> {
498
        let name = dt.name.expand_as_name(registry, context, render_context)?;
6✔
499

500
        let mut pv = Vec::with_capacity(dt.params.len());
12✔
501
        for p in &dt.params {
17✔
502
            let r = p.expand(registry, context, render_context)?;
10✔
503
            pv.push(r);
5✔
504
        }
505

506
        let mut hm = BTreeMap::new();
7✔
507
        for (k, p) in &dt.hash {
15✔
508
            let r = p.expand(registry, context, render_context)?;
4✔
509
            hm.insert(k.as_ref(), r);
4✔
510
        }
511

512
        let indent = match (render_context.get_indent_string(), dt.indent.as_ref()) {
6✔
513
            (None, None) => None,
7✔
514
            (Some(s), None) => Some(s.clone()),
1✔
515
            (None, Some(s)) => Some(Cow::Borrowed(&**s)),
5✔
516
            (Some(s1), Some(s2)) => {
2✔
517
                let mut res = s1.to_string();
4✔
518
                res.push_str(s2);
4✔
519
                Some(Cow::from(res))
2✔
520
            }
521
        };
522

523
        Ok(Decorator {
6✔
524
            name,
6✔
525
            params: pv,
7✔
526
            hash: hm,
6✔
527
            template: dt.template.as_ref(),
6✔
528
            indent,
7✔
529
        })
530
    }
531

532
    /// Returns helper name
533
    pub fn name(&self) -> &str {
6✔
534
        self.name.as_ref()
7✔
535
    }
536

537
    /// Returns all helper params, resolved within the context
538
    pub fn params(&self) -> &Vec<PathAndJson<'rc>> {
×
539
        &self.params
×
540
    }
541

542
    /// Returns nth helper param, resolved within the context
543
    pub fn param(&self, idx: usize) -> Option<&PathAndJson<'rc>> {
6✔
544
        self.params.get(idx)
7✔
545
    }
546

547
    /// Returns hash, resolved within the context
548
    pub fn hash(&self) -> &BTreeMap<&'rc str, PathAndJson<'rc>> {
7✔
549
        &self.hash
6✔
550
    }
551

552
    /// Return hash value of a given key, resolved within the context
553
    pub fn hash_get(&self, key: &str) -> Option<&PathAndJson<'rc>> {
×
554
        self.hash.get(key)
×
555
    }
556

557
    /// Returns the default inner template if any
558
    pub fn template(&self) -> Option<&'rc Template> {
6✔
559
        self.template
6✔
560
    }
561

562
    pub fn indent(&self) -> Option<&Cow<'rc, str>> {
6✔
563
        self.indent.as_ref()
7✔
564
    }
565
}
566

567
/// Render trait
568
pub trait Renderable {
569
    /// render into `RenderContext`'s `writer`
570
    fn render<'reg: 'rc, 'rc>(
571
        &'rc self,
572
        registry: &'reg Registry<'reg>,
573
        context: &'rc Context,
574
        rc: &mut RenderContext<'reg, 'rc>,
575
        out: &mut dyn Output,
576
    ) -> Result<(), RenderError>;
577

578
    /// render into string
579
    fn renders<'reg: 'rc, 'rc>(
×
580
        &'rc self,
581
        registry: &'reg Registry<'reg>,
582
        ctx: &'rc Context,
583
        rc: &mut RenderContext<'reg, 'rc>,
584
    ) -> Result<String, RenderError> {
585
        let mut so = StringOutput::new();
×
586
        self.render(registry, ctx, rc, &mut so)?;
×
587
        so.into_string()
×
588
            .map_err(|e| RenderErrorReason::from(e).into())
×
589
    }
590
}
591

592
/// Evaluate decorator
593
pub trait Evaluable {
594
    fn eval<'reg: 'rc, 'rc>(
595
        &'rc self,
596
        registry: &'reg Registry<'reg>,
597
        context: &'rc Context,
598
        rc: &mut RenderContext<'reg, 'rc>,
599
    ) -> Result<(), RenderError>;
600
}
601

602
#[inline]
603
fn call_helper_for_value<'reg: 'rc, 'rc>(
3✔
604
    hd: &dyn HelperDef,
605
    ht: &Helper<'rc>,
606
    r: &'reg Registry<'reg>,
607
    ctx: &'rc Context,
608
    rc: &mut RenderContext<'reg, 'rc>,
609
) -> Result<PathAndJson<'rc>, RenderError> {
610
    match hd.call_inner(ht, r, ctx, rc) {
3✔
611
        Ok(result) => Ok(PathAndJson::new(None, result)),
3✔
612
        Err(e) => {
2✔
613
            if e.is_unimplemented() {
6✔
614
                // parse value from output
615
                let mut so = StringOutput::new();
1✔
616

617
                // here we don't want subexpression result escaped,
618
                // so we temporarily disable it
619
                let disable_escape = rc.is_disable_escape();
2✔
620
                rc.set_disable_escape(true);
1✔
621

622
                hd.call(ht, r, ctx, rc, &mut so)?;
1✔
623
                rc.set_disable_escape(disable_escape);
1✔
624

625
                let string = so.into_string().map_err(RenderError::from)?;
1✔
626
                Ok(PathAndJson::new(
2✔
627
                    None,
1✔
628
                    ScopedJson::Derived(Json::String(string)),
1✔
629
                ))
630
            } else {
631
                Err(e)
1✔
632
            }
633
        }
634
    }
635
}
636

637
impl Parameter {
638
    pub fn expand_as_name<'reg: 'rc, 'rc>(
14✔
639
        &'rc self,
640
        registry: &'reg Registry<'reg>,
641
        ctx: &'rc Context,
642
        rc: &mut RenderContext<'reg, 'rc>,
643
    ) -> Result<Cow<'rc, str>, RenderError> {
644
        match self {
15✔
645
            Parameter::Name(name) => Ok(Cow::Borrowed(name)),
12✔
646
            Parameter::Path(p) => Ok(Cow::Borrowed(p.raw())),
11✔
647
            Parameter::Subexpression(_) => self
×
648
                .expand(registry, ctx, rc)
1✔
649
                .map(|v| v.value().render())
3✔
650
                .map(Cow::Owned),
1✔
651
            Parameter::Literal(j) => Ok(Cow::Owned(j.render())),
×
652
        }
653
    }
654

655
    pub fn expand<'reg: 'rc, 'rc>(
13✔
656
        &'rc self,
657
        registry: &'reg Registry<'reg>,
658
        ctx: &'rc Context,
659
        rc: &mut RenderContext<'reg, 'rc>,
660
    ) -> Result<PathAndJson<'rc>, RenderError> {
661
        match self {
15✔
662
            Parameter::Name(name) => {
×
663
                // FIXME: raise error when expanding with name?
NEW
664
                Ok(PathAndJson::new(Some(name.clone()), ScopedJson::Missing))
×
665
            }
666
            Parameter::Path(path) => {
12✔
667
                if let Some(rc_context) = rc.context() {
25✔
668
                    let result = rc.evaluate2(&rc_context, path)?;
2✔
669
                    Ok(PathAndJson::new(
2✔
670
                        Some(path.raw().clone()),
2✔
671
                        ScopedJson::Derived(result.as_json().clone()),
2✔
672
                    ))
673
                } else {
674
                    let result = rc.evaluate2(ctx, path)?;
25✔
675
                    Ok(PathAndJson::new(Some(path.raw().clone()), result))
24✔
676
                }
677
            }
678
            Parameter::Literal(j) => Ok(PathAndJson::new(None, ScopedJson::Constant(j))),
9✔
679
            Parameter::Subexpression(t) => match *t.as_element() {
4✔
680
                Expression(ref ht) => {
4✔
681
                    let name = ht.name.expand_as_name(registry, ctx, rc)?;
7✔
682

683
                    let h = Helper::try_from_template(ht, registry, ctx, rc)?;
6✔
684
                    if let Some(ref d) = rc.get_local_helper(&name) {
9✔
685
                        call_helper_for_value(d.as_ref(), &h, registry, ctx, rc)
×
686
                    } else {
687
                        let mut helper = registry.get_or_load_helper(&name)?;
3✔
688

689
                        if helper.is_none() {
7✔
690
                            helper = registry.get_or_load_helper(if ht.block {
4✔
691
                                BLOCK_HELPER_MISSING
×
692
                            } else {
693
                                HELPER_MISSING
1✔
694
                            })?;
695
                        }
696

697
                        helper
3✔
698
                            .ok_or_else(|| {
3✔
699
                                RenderErrorReason::HelperNotFound(name.to_string()).into()
×
700
                            })
701
                            .and_then(|d| call_helper_for_value(d.as_ref(), &h, registry, ctx, rc))
9✔
702
                    }
703
                }
704
                _ => unreachable!(),
705
            },
706
        }
707
    }
708
}
709

710
impl Renderable for Template {
711
    fn render<'reg: 'rc, 'rc>(
17✔
712
        &'rc self,
713
        registry: &'reg Registry<'reg>,
714
        ctx: &'rc Context,
715
        rc: &mut RenderContext<'reg, 'rc>,
716
        out: &mut dyn Output,
717
    ) -> Result<(), RenderError> {
718
        rc.set_current_template_name(self.name.as_ref());
15✔
719
        let iter = self.elements.iter();
17✔
720

721
        for (idx, t) in iter.enumerate() {
32✔
722
            t.render(registry, ctx, rc, out).map_err(|mut e| {
22✔
723
                // add line/col number if the template has mapping data
724
                if e.line_no.is_none()
6✔
725
                    && let Some(&TemplateMapping(line, col)) = self.mapping.get(idx)
2✔
726
                {
727
                    e.line_no = Some(line);
2✔
728
                    e.column_no = Some(col);
2✔
729
                }
730

731
                if e.template_name.is_none() {
4✔
732
                    e.template_name.clone_from(&self.name);
2✔
733
                }
734

735
                e
2✔
736
            })?;
737
        }
738

739
        Ok(())
14✔
740
    }
741
}
742

743
impl Evaluable for Template {
744
    fn eval<'reg: 'rc, 'rc>(
×
745
        &'rc self,
746
        registry: &'reg Registry<'reg>,
747
        ctx: &'rc Context,
748
        rc: &mut RenderContext<'reg, 'rc>,
749
    ) -> Result<(), RenderError> {
750
        let iter = self.elements.iter();
×
751

752
        for (idx, t) in iter.enumerate() {
×
753
            t.eval(registry, ctx, rc).map_err(|mut e| {
×
NEW
754
                if e.line_no.is_none()
×
NEW
755
                    && let Some(&TemplateMapping(line, col)) = self.mapping.get(idx)
×
756
                {
NEW
757
                    e.line_no = Some(line);
×
NEW
758
                    e.column_no = Some(col);
×
759
                }
760

761
                e.template_name.clone_from(&self.name);
×
762
                e
×
763
            })?;
764
        }
765
        Ok(())
×
766
    }
767
}
768

769
fn helper_exists<'reg: 'rc, 'rc>(
11✔
770
    name: &str,
771
    reg: &Registry<'reg>,
772
    rc: &RenderContext<'reg, 'rc>,
773
) -> bool {
774
    rc.has_local_helper(name) || reg.has_helper(name)
10✔
775
}
776

777
#[inline]
778
fn render_helper<'reg: 'rc, 'rc>(
11✔
779
    ht: &'rc HelperTemplate,
780
    registry: &'reg Registry<'reg>,
781
    ctx: &'rc Context,
782
    rc: &mut RenderContext<'reg, 'rc>,
783
    out: &mut dyn Output,
784
) -> Result<(), RenderError> {
785
    let h = Helper::try_from_template(ht, registry, ctx, rc)?;
11✔
786
    debug!(
×
787
        "Rendering helper: {:?}, params: {:?}, hash: {:?}",
788
        h.name(),
×
789
        h.params(),
×
790
        h.hash()
×
791
    );
792
    let mut call_indent_aware = |helper_def: &dyn HelperDef, rc: &mut RenderContext<'reg, 'rc>| {
24✔
793
        let indent_directive_before = rc.get_indent_before_write();
14✔
794
        let content_produced_before = rc.get_content_produced();
12✔
795
        rc.set_content_produced(false);
13✔
796
        rc.set_indent_before_write(
13✔
797
            indent_directive_before || (ht.indent_before_write && rc.get_trailine_newline()),
12✔
798
        );
799

800
        helper_def.call(&h, registry, ctx, rc, out)?;
12✔
801

802
        if rc.get_content_produced() {
12✔
803
            rc.set_indent_before_write(rc.get_trailine_newline());
12✔
804
        } else {
805
            rc.set_content_produced(content_produced_before);
5✔
806
            rc.set_indent_before_write(indent_directive_before);
5✔
807
        }
808
        Ok(())
11✔
809
    };
810
    if let Some(ref d) = rc.get_local_helper(h.name()) {
36✔
811
        call_indent_aware(&**d, rc)
2✔
812
    } else {
813
        let mut helper = registry.get_or_load_helper(h.name())?;
13✔
814

815
        if helper.is_none() {
30✔
816
            helper = registry.get_or_load_helper(if ht.block {
8✔
817
                BLOCK_HELPER_MISSING
1✔
818
            } else {
819
                HELPER_MISSING
2✔
820
            })?;
821
        }
822

823
        helper
14✔
824
            .ok_or_else(|| RenderErrorReason::HelperNotFound(h.name().to_owned()).into())
17✔
825
            .and_then(|d| call_indent_aware(&*d, rc))
41✔
826
    }
827
}
828

829
pub(crate) fn do_escape(r: &Registry<'_>, rc: &RenderContext<'_, '_>, content: String) -> String {
13✔
830
    if !rc.is_disable_escape() {
26✔
831
        r.get_escape_fn()(&content)
22✔
832
    } else {
833
        content
4✔
834
    }
835
}
836

837
#[inline]
838
pub fn indent_aware_write(
16✔
839
    v: &str,
840
    rc: &mut RenderContext<'_, '_>,
841
    out: &mut dyn Output,
842
) -> Result<(), RenderError> {
843
    if v.is_empty() {
16✔
844
        return Ok(());
6✔
845
    }
846
    rc.set_content_produced(true);
15✔
847

848
    if !v.starts_with(newline_matcher)
16✔
849
        && rc.get_indent_before_write()
15✔
850
        && let Some(indent) = rc.get_indent_string()
4✔
851
    {
852
        out.write(indent)?;
2✔
853
    }
854

855
    if let Some(indent) = rc.get_indent_string() {
18✔
856
        support::str::write_indented(v, indent, out)?;
2✔
857
    } else {
858
        out.write(v.as_ref())?;
15✔
859
    }
860

861
    let trailing_newline = v.ends_with(newline_matcher);
17✔
862
    rc.set_trailing_newline(trailing_newline);
15✔
863
    rc.set_indent_before_write(trailing_newline);
16✔
864

865
    Ok(())
17✔
866
}
867

868
impl Renderable for TemplateElement {
869
    fn render<'reg: 'rc, 'rc>(
16✔
870
        &'rc self,
871
        registry: &'reg Registry<'reg>,
872
        ctx: &'rc Context,
873
        rc: &mut RenderContext<'reg, 'rc>,
874
        out: &mut dyn Output,
875
    ) -> Result<(), RenderError> {
876
        match self {
44✔
877
            RawString(v) => indent_aware_write(v.as_ref(), rc, out),
13✔
878
            Expression(ht) | HtmlExpression(ht) => {
18✔
879
                let is_html_expression = matches!(self, HtmlExpression(_));
14✔
880
                if is_html_expression {
14✔
881
                    rc.set_disable_escape(true);
4✔
882
                }
883

884
                // test if the expression is to render some value
885
                let result = if ht.is_name_only() {
28✔
886
                    let helper_name = ht.name.expand_as_name(registry, ctx, rc)?;
21✔
887
                    if helper_exists(&helper_name, registry, rc) {
21✔
888
                        render_helper(ht, registry, ctx, rc, out)
6✔
889
                    } else {
890
                        debug!("Rendering value: {:?}", ht.name);
×
891
                        let context_json = ht.name.expand(registry, ctx, rc)?;
18✔
892
                        if context_json.is_value_missing() {
22✔
893
                            if registry.strict_mode() {
3✔
894
                                Err(RenderError::strict_error(
1✔
895
                                    context_json.relative_path().map(|s| s.as_str()),
4✔
896
                                ))
897
                            } else {
898
                                // helper missing
899
                                if let Some(hook) = registry.get_or_load_helper(HELPER_MISSING)? {
3✔
900
                                    let h = Helper::try_from_template(ht, registry, ctx, rc)?;
3✔
901
                                    hook.call(&h, registry, ctx, rc, out)
2✔
902
                                } else {
903
                                    Ok(())
1✔
904
                                }
905
                            }
906
                        } else {
907
                            let rendered = context_json.value().render();
22✔
908
                            let output = do_escape(registry, rc, rendered);
8✔
909
                            indent_aware_write(output.as_ref(), rc, out)
18✔
910
                        }
911
                    }
912
                } else {
913
                    // this is a helper expression
914
                    render_helper(ht, registry, ctx, rc, out)
12✔
915
                };
916

917
                if is_html_expression {
11✔
918
                    rc.set_disable_escape(false);
8✔
919
                }
920

921
                result
12✔
922
            }
923
            HelperBlock(ht) => render_helper(ht, registry, ctx, rc, out),
16✔
924
            DecoratorExpression(_) | DecoratorBlock(_) => self.eval(registry, ctx, rc),
4✔
925
            PartialExpression(dt) | PartialBlock(dt) => {
8✔
926
                let di = Decorator::try_from_template(dt, registry, ctx, rc)?;
6✔
927

928
                let indent_directive_before = rc.get_indent_before_write();
13✔
929
                let content_produced_before = rc.get_content_produced();
6✔
930

931
                rc.set_indent_before_write(
6✔
932
                    dt.indent_before_write && (rc.get_trailine_newline() || dt.indent.is_some()),
13✔
933
                );
934
                rc.set_content_produced(false);
7✔
935

936
                partial::expand_partial(&di, registry, ctx, rc, out)?;
6✔
937

938
                if rc.get_content_produced() {
7✔
939
                    rc.set_indent_before_write(rc.get_trailine_newline());
14✔
940
                } else {
941
                    rc.set_content_produced(content_produced_before);
3✔
942
                    rc.set_indent_before_write(indent_directive_before);
3✔
943
                }
944
                Ok(())
7✔
945
            }
946
            _ => Ok(()),
2✔
947
        }
948
    }
949
}
950

951
impl Evaluable for TemplateElement {
952
    fn eval<'reg: 'rc, 'rc>(
4✔
953
        &'rc self,
954
        registry: &'reg Registry<'reg>,
955
        ctx: &'rc Context,
956
        rc: &mut RenderContext<'reg, 'rc>,
957
    ) -> Result<(), RenderError> {
958
        match *self {
6✔
959
            DecoratorExpression(ref dt) | DecoratorBlock(ref dt) => {
6✔
960
                let di = Decorator::try_from_template(dt, registry, ctx, rc)?;
4✔
961
                match registry.get_decorator(di.name()) {
9✔
962
                    Some(d) => d.call(&di, registry, ctx, rc),
10✔
963
                    None => Err(RenderErrorReason::DecoratorNotFound(di.name().to_owned()).into()),
1✔
964
                }
965
            }
966
            _ => Ok(()),
×
967
        }
968
    }
969
}
970

971
#[cfg(test)]
972
mod test {
973
    use std::collections::BTreeMap;
974

975
    use super::{Helper, RenderContext, Renderable};
976
    use crate::block::BlockContext;
977
    use crate::context::Context;
978
    use crate::error::RenderError;
979
    use crate::json::path::Path;
980
    use crate::json::value::JsonRender;
981
    use crate::output::{Output, StringOutput};
982
    use crate::registry::Registry;
983
    use crate::template::TemplateElement::*;
984
    use crate::template::{HelperTemplate, Template, TemplateElement};
985
    use smol_str::SmolStr;
986

987
    #[test]
988
    fn test_raw_string() {
989
        let r = Registry::new();
990
        let raw_string = RawString(SmolStr::new("<h1>hello world</h1>"));
991

992
        let mut out = StringOutput::new();
993
        let ctx = Context::null();
994
        {
995
            let mut rc = RenderContext::new(None);
996
            raw_string.render(&r, &ctx, &mut rc, &mut out).ok().unwrap();
997
        }
998
        assert_eq!(
999
            out.into_string().unwrap(),
1000
            "<h1>hello world</h1>".to_string()
1001
        );
1002
    }
1003

1004
    #[test]
1005
    fn test_expression() {
1006
        let r = Registry::new();
1007
        let element = Expression(Box::new(HelperTemplate::with_path(Path::with_named_paths(
1008
            &["hello"],
1009
        ))));
1010

1011
        let mut out = StringOutput::new();
1012
        let mut m: BTreeMap<String, String> = BTreeMap::new();
1013
        let value = "<p></p>".to_string();
1014
        m.insert("hello".to_string(), value);
1015
        let ctx = Context::wraps(&m).unwrap();
1016
        {
1017
            let mut rc = RenderContext::new(None);
1018
            element.render(&r, &ctx, &mut rc, &mut out).ok().unwrap();
1019
        }
1020

1021
        assert_eq!(
1022
            out.into_string().unwrap(),
1023
            "&lt;p&gt;&lt;/p&gt;".to_string()
1024
        );
1025
    }
1026

1027
    #[test]
1028
    fn test_html_expression() {
1029
        let r = Registry::new();
1030
        let element = HtmlExpression(Box::new(HelperTemplate::with_path(Path::with_named_paths(
1031
            &["hello"],
1032
        ))));
1033

1034
        let mut out = StringOutput::new();
1035
        let mut m: BTreeMap<String, String> = BTreeMap::new();
1036
        let value = "world";
1037
        m.insert("hello".to_string(), value.to_string());
1038
        let ctx = Context::wraps(&m).unwrap();
1039
        {
1040
            let mut rc = RenderContext::new(None);
1041
            element.render(&r, &ctx, &mut rc, &mut out).ok().unwrap();
1042
        }
1043

1044
        assert_eq!(out.into_string().unwrap(), value.to_string());
1045
    }
1046

1047
    #[test]
1048
    fn test_template() {
1049
        let r = Registry::new();
1050
        let mut out = StringOutput::new();
1051
        let mut m: BTreeMap<String, String> = BTreeMap::new();
1052
        let value = "world".to_string();
1053
        m.insert("hello".to_string(), value);
1054
        let ctx = Context::wraps(&m).unwrap();
1055

1056
        let elements: Vec<TemplateElement> = vec![
1057
            RawString(SmolStr::new("<h1>")),
1058
            Expression(Box::new(HelperTemplate::with_path(Path::with_named_paths(
1059
                &["hello"],
1060
            )))),
1061
            RawString(SmolStr::new("</h1>")),
1062
            Comment(SmolStr::new("")),
1063
        ];
1064

1065
        let template = Template {
1066
            elements,
1067
            name: None,
1068
            mapping: Vec::new(),
1069
        };
1070

1071
        {
1072
            let mut rc = RenderContext::new(None);
1073
            template.render(&r, &ctx, &mut rc, &mut out).ok().unwrap();
1074
        }
1075

1076
        assert_eq!(out.into_string().unwrap(), "<h1>world</h1>".to_string());
1077
    }
1078

1079
    #[test]
1080
    fn test_render_context_promotion_and_demotion() {
1081
        use crate::json::value::to_json;
1082
        let mut render_context = RenderContext::new(None);
1083
        let mut block = BlockContext::new();
1084

1085
        block.set_local_var("index", to_json(0));
1086
        render_context.push_block(block);
1087

1088
        render_context.push_block(BlockContext::new());
1089
        assert_eq!(
1090
            render_context.get_local_var(1, "index").unwrap(),
1091
            &to_json(0)
1092
        );
1093

1094
        render_context.pop_block();
1095

1096
        assert_eq!(
1097
            render_context.get_local_var(0, "index").unwrap(),
1098
            &to_json(0)
1099
        );
1100
    }
1101

1102
    #[test]
1103
    fn test_render_subexpression_issue_115() {
1104
        use crate::support::str::StringWriter;
1105

1106
        let mut r = Registry::new();
1107
        r.register_helper(
1108
            "format",
1109
            Box::new(
1110
                |h: &Helper<'_>,
1111
                 _: &Registry<'_>,
1112
                 _: &Context,
1113
                 _: &mut RenderContext<'_, '_>,
1114
                 out: &mut dyn Output|
1115
                 -> Result<(), RenderError> {
1116
                    out.write(&h.param(0).unwrap().value().render())
1117
                        .map_err(RenderError::from)
1118
                },
1119
            ),
1120
        );
1121

1122
        let mut sw = StringWriter::new();
1123
        let mut m: BTreeMap<String, String> = BTreeMap::new();
1124
        m.insert("a".to_string(), "123".to_string());
1125

1126
        {
1127
            if let Err(e) = r.render_template_to_write("{{format (format a)}}", &m, &mut sw) {
1128
                panic!("{}", e);
1129
            }
1130
        }
1131

1132
        assert_eq!(sw.into_string(), "123".to_string());
1133
    }
1134

1135
    #[test]
1136
    fn test_render_error_line_no() {
1137
        let mut r = Registry::new();
1138
        let m: BTreeMap<String, String> = BTreeMap::new();
1139

1140
        let name = "invalid_template";
1141
        assert!(
1142
            r.register_template_string(name, "<h1>\n{{#if true}}\n  {{#each}}{{/each}}\n{{/if}}")
1143
                .is_ok()
1144
        );
1145

1146
        if let Err(e) = r.render(name, &m) {
1147
            assert_eq!(e.line_no.unwrap(), 3);
1148
            assert_eq!(e.column_no.unwrap(), 3);
1149
            assert_eq!(e.template_name, Some(name.to_owned()));
1150
        } else {
1151
            panic!("Error expected");
1152
        }
1153
    }
1154

1155
    #[test]
1156
    fn test_partial_failback_render() {
1157
        let mut r = Registry::new();
1158

1159
        assert!(
1160
            r.register_template_string("parent", "<html>{{> layout}}</html>")
1161
                .is_ok()
1162
        );
1163
        assert!(
1164
            r.register_template_string(
1165
                "child",
1166
                "{{#*inline \"layout\"}}content{{/inline}}{{#> parent}}{{> seg}}{{/parent}}",
1167
            )
1168
            .is_ok()
1169
        );
1170
        assert!(r.register_template_string("seg", "1234").is_ok());
1171

1172
        let r = r.render("child", &true).expect("should work");
1173
        assert_eq!(r, "<html>content</html>");
1174
    }
1175

1176
    #[test]
1177
    fn test_key_with_slash() {
1178
        let mut r = Registry::new();
1179

1180
        assert!(
1181
            r.register_template_string("t", "{{#each this}}{{@key}}: {{this}}\n{{/each}}")
1182
                .is_ok()
1183
        );
1184

1185
        let r = r.render("t", &json!({"/foo": "bar"})).unwrap();
1186

1187
        assert_eq!(r, "/foo: bar\n");
1188
    }
1189

1190
    #[test]
1191
    fn test_comment() {
1192
        let r = Registry::new();
1193

1194
        assert_eq!(
1195
            r.render_template("Hello {{this}} {{! test me }}", &0)
1196
                .unwrap(),
1197
            "Hello 0 "
1198
        );
1199
    }
1200

1201
    #[test]
1202
    fn test_zero_args_heler() {
1203
        let mut r = Registry::new();
1204

1205
        r.register_helper(
1206
            "name",
1207
            Box::new(
1208
                |_: &Helper<'_>,
1209
                 _: &Registry<'_>,
1210
                 _: &Context,
1211
                 _: &mut RenderContext<'_, '_>,
1212
                 out: &mut dyn Output|
1213
                 -> Result<(), RenderError> {
1214
                    out.write("N/A").map_err(Into::into)
1215
                },
1216
            ),
1217
        );
1218

1219
        r.register_template_string("t0", "Output name: {{name}}")
1220
            .unwrap();
1221
        r.register_template_string("t1", "Output name: {{first_name}}")
1222
            .unwrap();
1223
        r.register_template_string("t2", "Output name: {{./name}}")
1224
            .unwrap();
1225

1226
        // when "name" is available in context, use context first
1227
        assert_eq!(
1228
            r.render("t0", &json!({"name": "Alex"})).unwrap(),
1229
            "Output name: N/A"
1230
        );
1231

1232
        // when "name" is unavailable, call helper with same name
1233
        assert_eq!(
1234
            r.render("t2", &json!({"name": "Alex"})).unwrap(),
1235
            "Output name: Alex"
1236
        );
1237

1238
        // output nothing when neither context nor helper available
1239
        assert_eq!(
1240
            r.render("t1", &json!({"name": "Alex"})).unwrap(),
1241
            "Output name: "
1242
        );
1243

1244
        // generate error in strict mode for above case
1245
        r.set_strict_mode(true);
1246
        assert!(r.render("t1", &json!({"name": "Alex"})).is_err());
1247

1248
        // output nothing when helperMissing was defined
1249
        r.set_strict_mode(false);
1250
        r.register_helper(
1251
            "helperMissing",
1252
            Box::new(
1253
                |h: &Helper<'_>,
1254
                 _: &Registry<'_>,
1255
                 _: &Context,
1256
                 _: &mut RenderContext<'_, '_>,
1257
                 out: &mut dyn Output|
1258
                 -> Result<(), RenderError> {
1259
                    let name = h.name();
1260
                    write!(out, "{name} not resolved")?;
1261
                    Ok(())
1262
                },
1263
            ),
1264
        );
1265
        assert_eq!(
1266
            r.render("t1", &json!({"name": "Alex"})).unwrap(),
1267
            "Output name: first_name not resolved"
1268
        );
1269
    }
1270

1271
    #[test]
1272
    fn test_identifiers_starting_with_numbers() {
1273
        let mut r = Registry::new();
1274

1275
        assert!(
1276
            r.register_template_string("r1", "{{#if 0a}}true{{/if}}")
1277
                .is_ok()
1278
        );
1279
        let r1 = r.render("r1", &json!({"0a": true})).unwrap();
1280
        assert_eq!(r1, "true");
1281

1282
        assert!(r.register_template_string("r2", "{{eq 1a 1}}").is_ok());
1283
        let r2 = r.render("r2", &json!({"1a": 2, "a": 1})).unwrap();
1284
        assert_eq!(r2, "false");
1285

1286
        assert!(r
1287
            .register_template_string("r3", "0: {{0}} {{#if (eq 0 true)}}resolved from context{{/if}}\n1a: {{1a}} {{#if (eq 1a true)}}resolved from context{{/if}}\n2_2: {{2_2}} {{#if (eq 2_2 true)}}resolved from context{{/if}}") // YUP it is just eq that barfs! is if handled specially? maybe this test should go nearer to specific helpers that fail?
1288
            .is_ok());
1289
        let r3 = r
1290
            .render("r3", &json!({"0": true, "1a": true, "2_2": true}))
1291
            .unwrap();
1292
        assert_eq!(
1293
            r3,
1294
            "0: true \n1a: true resolved from context\n2_2: true resolved from context"
1295
        );
1296

1297
        // these should all be errors:
1298
        assert!(r.register_template_string("r4", "{{eq 1}}").is_ok());
1299
        assert!(r.register_template_string("r5", "{{eq a1}}").is_ok());
1300
        assert!(r.register_template_string("r6", "{{eq 1a}}").is_ok());
1301
        assert!(r.render("r4", &()).is_err());
1302
        assert!(r.render("r5", &()).is_err());
1303
        assert!(r.render("r6", &()).is_err());
1304
    }
1305

1306
    #[test]
1307
    fn test_recursive_path_resolution() {
1308
        let mut r = Registry::new();
1309
        const TEMPLATE: &str = "{{#each children}}outer={{{outer}}} inner={{{inner}}}{{/each}}";
1310

1311
        let data = json!({
1312
          "children": [{"inner": "inner"}],
1313
          "outer": "outer"
1314
        });
1315

1316
        let r1 = r.render_template(TEMPLATE, &data).unwrap();
1317
        assert_eq!(r1, "outer= inner=inner");
1318

1319
        r.set_recursive_lookup(true);
1320

1321
        let r2 = r.render_template(TEMPLATE, &data).unwrap();
1322
        assert_eq!(r2, "outer=outer inner=inner");
1323
    }
1324
}
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