Merge branch 'explicit_context_setting' into render

master
Tom Alexander 4 years ago
commit 92ad15ff85
Signed by: talexander
GPG Key ID: D3A179C9A53C0EDE

@ -0,0 +1,64 @@
$idx and $len
-------------
$idx and $len do not survive through an explicit context setting, which will work perfectly with my injected-context architecture.
You can use $idx and $len as your explicit context, but as scalar values I do not think there is a way to access them anyway.
Exists and Not-Exists
=====================
Looks like you can exlicitly set a context with exists and not-exists tags too. This works out well in the parser because I am using the same code for those blocks.
Partials
--------
Explicitly setting a context in a partial also works. The explicit context takes priority over the parameters in the partial tag.
This works for both regular named partials and quoted partials.
While normally parameters are injected 1 level above the current context, if both parameters and explicit are set, they are injected below the current context. So if the context tree was `1->2->3`, with just parameters you'd have `1->2->parameters->3` but with an explicit context you have `parameters->explicit`.
Helpers
-------
Explicitly setting a context in a helper works too.
Blocks and Inline Partials
--------------------------
Explicitly setting a context on an inline partial does not work, but it does not error out either, so I need to add support for this in the parser.
Explicitly setting a context on a block does work.
The explicit context for blocks is evaluated in the context for that template file containing the block, not the inline partial (so, whatever the context is when we invoke the partial containing the block).
References
----------
Explicitly setting a context does not work on a reference, but it has revealed that invalid dust is rendered verbatim. I'm going to leave that commented out until I can address that in a later branch.
Paths
-----
Explicit contexts do support deep paths.
Else Blocks
-----------
Else blocks also use an explicit context.
Complete Failure
----------------
If the walk destination does not exist, and the explicit context does not exist, then you end up with absolutely no context.
Regular Path Failure
--------------------
If the regular path fails but the explicit path succeeds then the context is set to the explicit path.
Falsey Explicit Path
--------------------
Since dust would not walk to a falsey path, theres no difference between a falsey path and a non-existent path.

@ -0,0 +1 @@
{+pet_line}BLOCK {$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}{/pet_line}

@ -0,0 +1 @@
{+pet_line:explicit}BLOCK {$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}{/pet_line}

@ -0,0 +1,6 @@
{#block}
{+pet_line}BLOCK: {contents}{~n}{/pet_line}
{/block}
{#inline_partial}
{<pet_line:message}INLINE PARTIAL: {contents}{~n}{/pet_line}
{/inline_partial}

@ -0,0 +1,4 @@
{#block.message}
{>explicit_evaluation_time_split_default/}
{/block.message}
{<pet_line:message}INLINE PARTIAL: {contents}{~n}{/pet_line}

@ -0,0 +1,56 @@
{
"block": {
"message": {
"contents": "Explicit contexts are evaluated in the context of the block."
}
},
"contents": "Explicit contexts are evaluated in the global context.",
"deep_explicit": {
"explicit": {
"pet": "cat"
}
},
"empty_array": [],
"explicit": {
"pet": "cat"
},
"inline_partial": {
"message": {
"contents": "Explicit contexts are evaluated in the context of the inline partial."
}
},
"loop": [
{
"friend": "Bob",
"person": {
"name": "Alice"
}
},
{
"friend": "Chris",
"person": {
"name": "Bob"
}
},
{
"friend": "Alice",
"person": {
"name": "Chris"
}
}
],
"partial_context": {
"block": {
"message": {
"contents": "Explicit contexts are evaluated in the context of the block inside the partial context."
}
},
"contents": "Explicit contexts are evaluated in the global context inside the partial context.",
"inline_partial": {
"message": {
"contents": "Explicit contexts are evaluated in the context of the inline partial inside the partial context."
}
}
},
"some_global": "dog"
}

@ -0,0 +1,298 @@
{! First we do it without explicit context setting which results in being able to read some_global but not pet !}
Section Regular{~n}
==============={~n}
{#loop}
{#person}
{$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{/person}
{/loop}
{! Then we do it with explicit context setting which should result in being able ot read pet but not some_global !}
Section Explicit{~n}
================{~n}
{#loop}
{#person:explicit}
{$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{/person}
{/loop}
{! What happens if we try to use an explicit context with an exists block instead of a section !}
Exists Regular{~n}
=============={~n}
{#loop}
{?person}
{$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}
{/person}
{/loop}
Exists Explicit{~n}
==============={~n}
{#loop}
{?person:explicit}
{$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}
{/person}
{/loop}
{! Can you explicitly set the context for a partial? !}
Partial Regular{~n}
==============={~n}
{#loop}
{>other/}
{/loop}
Partial Explicit{~n}
================{~n}
{#loop}
{>other:explicit/}
{/loop}
Quoted Partial Explicit{~n}
======================={~n}
{#loop}
{>"other":explicit/}
{/loop}
Partial Regular with parameters{~n}
==============================={~n}
{#loop}
{>other pet="rabbit"/}
{/loop}
Partial Explicit with parameters{~n}
================================{~n}
{#loop}
{>other:explicit pet="rabbit"/}
{/loop}
{! Can you explicitly set the context for a helper? !}
Helper Regular{~n}
=============={~n}
{#loop}
{@eq key="foo" value="foo"}
{$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}
{/eq}
{/loop}
Helper Explicit{~n}
==============={~n}
{#loop}
{@eq:explicit key="foo" value="foo"}
{$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}
{/eq}
{/loop}
{! Can you explicitly set the context for inline partials or blocks? !}
Block Regular{~n}
============={~n}
{#loop}
{>default/}
{/loop}
Inline Partial Regular{~n}
======================{~n}
{#loop}
{>override/}
{/loop}
Block Explicit{~n}
=============={~n}
{#loop}
{>default_explicit/}
{/loop}
Inline Partial Explicit{~n}
======================={~n}
{#loop}
{>override_explicit/}
{/loop}
Inline Partial and Block Explicit{~n}
================================={~n}
{#loop}
{>override_double_explicit/}
{/loop}
{! Can you explicitly set the context for references? !}
{! Commented out until I add support for rendering invalid dust verbatim
Reference Regular{~n}
================={~n}
{.}{~n}
Reference Explicit{~n}
=================={~n}
{.:some_global}{~n}
!}
{! Can you explicitly set the context with a path? !}
Path Regular{~n}
============{~n}
{#loop}
{#person}
{$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{/person}
{/loop}
Path Explicit{~n}
============={~n}
{#loop}
{#person:deep_explicit.explicit}
{$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{/person}
{/loop}
{! Can you explicitly set the context to a variable? !}
Variable Regular{~n}
================{~n}
{#loop}
{#person}
{$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{/person}
{/loop}
Variable Explicit{~n}
================={~n}
{#loop}
{#person:$idx}
{$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{/person}
{/loop}
{! What context is set on else blocks? !}
Else Block Regular{~n}
=================={~n}
{#loop}
{#empty_array}
MAIN {$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{:else}
ELSE {$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{/empty_array}
{/loop}
Else Block Explicit{~n}
==================={~n}
{#loop}
{#empty_array:explicit}
MAIN {$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}
{:else}
ELSE {$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}
{/empty_array}
{/loop}
{! What context is set when the explicit context path does not exist? !}
Failed Explicit Regular{~n}
======================={~n}
{#loop}
{#person}
{$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{/person}
{/loop}
Failed Explicit Explicit{~n}
========================{~n}
{#loop}
{#person:foobar}
{$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{/person}
{/loop}
{! What context is set when the regular path and explicit context path does not exist? !}
Failed Everything Regular{~n}
========================={~n}
BEFORE {.|js}{~n}
{#foo}
MAIN {.|js}{~n}
{:else}
ELSE {.|js}{~n}
{/foo}
Failed Everything Explicit{~n}
=========================={~n}
{#foo:bar}
MAIN {.|js}{~n}
{:else}
ELSE {.|js}{~n}
{/foo}
{! What context is set when the regular context path does not exist? !}
Failed Regular Path Regular{~n}
==========================={~n}
{#loop}
{#foo}
MAIN {$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{:else}
ELSE {$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{/foo}
{/loop}
Failed Regular Path Explicit{~n}
============================{~n}
{#loop}
{#foo:explicit}
MAIN {$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{:else}
ELSE {$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{/foo}
{/loop}
{! What context is set when the explicit path is falsey? !}
Falsey Explicit Path Regular{~n}
============================{~n}
{#loop}
{#foo}
MAIN {$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{:else}
ELSE {$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{/foo}
{/loop}
Falsey Explicit Path Explicit{~n}
============================={~n}
{#loop}
{#foo:empty_array}
MAIN {$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{:else}
ELSE {$idx}: {name} has a pet {pet} but not a {some_global}{~n}
{.|js}{~n}
{/foo}
{/loop}
{! Since partial parameters are normally injected 1 level above the current context, and explicit contexts are below the partial parameters, is the order parameters->current_context->explicit or current_context->parameters->explicit when both are being used? !}
Partial Overloaded Regular with parameters{~n}
=========================================={~n}
{#loop}
{>other_friend friend="Dave" pet="rabbit"/}
{/loop}
Partial Overloaded Explicit with parameters{~n}
==========================================={~n}
{#loop}
{>other_friend:explicit friend="Dave" pet="rabbit"/}
{/loop}
{! Is an explicit context on an inline partial evaluated in the context of the block or the context of the inline partial? !}
Explicit Evaluation Time Global{~n}
==============================={~n}
{>explicit_evaluation_time/}
Explicit Evaluation Time Partial Context{~n}
========================================{~n}
{#partial_context}
{>explicit_evaluation_time/}
{/partial_context}
{! The previous test discovered that the inline partial's explicit context is evaluated based on the context when invoking the full-blown partial, HOWEVER, it shared the same partial context for both the block and the inline partial. To conclusively prove if the explicit context is evaluated on the inline partial and not the block, we need a different partial context for each one. !}
Explicit Evaluation Time Split Partial Context Default{~n}
======================================================{~n}
{#partial_context}
{#block.message}
{>explicit_evaluation_time_split_default/}
{/block.message}
{/partial_context}
Explicit Evaluation Time Split Partial Context OVerride{~n}
======================================================={~n}
{#partial_context}
{#inline_partial}
{>explicit_evaluation_time_split_override/}
{/inline_partial}
{/partial_context}

@ -0,0 +1 @@
{$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}

@ -0,0 +1 @@
{$idx}: {person.name}'s friend {friend} has a pet {pet} but not a {some_global}{~n}

@ -0,0 +1,2 @@
{>default/}
{<pet_line}INLINE PARTIAL {$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}{/pet_line}

@ -0,0 +1,2 @@
{>default_explicit/}
{<pet_line:explicit}INLINE PARTIAL {$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}{/pet_line}

@ -0,0 +1,2 @@
{>default/}
{<pet_line:explicit}INLINE PARTIAL {$idx}: {person.name} has a pet {pet} but not a {some_global}{~n}{/pet_line}

@ -9,6 +9,7 @@ pub use parser::Filter;
pub use parser::KVPair;
pub use parser::OwnedLiteral;
pub use parser::PartialNameElement;
pub use parser::Path;
pub use parser::RValue;
pub use parser::Special;
pub use parser::Template;

@ -95,6 +95,7 @@ pub struct Span<'a> {
#[derive(Clone, Debug, PartialEq)]
pub struct Container<'a> {
pub path: Path<'a>,
pub explicit_context: Option<Path<'a>>,
pub contents: Option<Body<'a>>,
pub else_contents: Option<Body<'a>>,
}
@ -102,12 +103,14 @@ pub struct Container<'a> {
#[derive(Clone, Debug, PartialEq)]
pub struct NamedBlock<'a> {
pub name: &'a str,
pub explicit_context: Option<Path<'a>>,
pub contents: Option<Body<'a>>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ParameterizedBlock<'a> {
pub name: &'a str,
pub explicit_context: Option<Path<'a>>,
pub params: Vec<KVPair<'a>>,
pub contents: Option<Body<'a>>,
pub else_contents: Option<Body<'a>>,
@ -116,6 +119,7 @@ pub struct ParameterizedBlock<'a> {
#[derive(Clone, Debug, PartialEq)]
pub struct Partial<'a> {
pub name: Vec<PartialNameElement>,
pub explicit_context: Option<Path<'a>>,
pub params: Vec<KVPair<'a>>,
}
@ -341,20 +345,23 @@ where
F: Fn(Container<'a>) -> DustTag<'a>,
{
move |i: &'a str| {
let (i, (opening_name, inner, maybe_else, _closing_name)) = verify(
tuple((
delimited(tag(open_matcher), path, tag("}")),
opt(body),
opt(preceded(tag("{:else}"), opt(body))),
delimited(tag("{/"), path, tag("}")),
)),
|(open, _inn, _maybe_else, close)| open == close,
)(i)?;
let (i, (opening_name, maybe_explicit_context, inner, maybe_else, _closing_name)) =
verify(
tuple((
preceded(tag(open_matcher), path),
terminated(opt(preceded(tag(":"), path)), tag("}")),
opt(body),
opt(preceded(tag("{:else}"), opt(body))),
delimited(tag("{/"), path, tag("}")),
)),
|(open, _maybe_explicit, _inn, _maybe_else, close)| open == close,
)(i)?;
Ok((
i,
constructor(Container {
path: opening_name,
explicit_context: maybe_explicit_context,
contents: inner,
else_contents: maybe_else.flatten(),
}),
@ -370,12 +377,16 @@ where
F: Fn(Container<'a>) -> DustTag<'a>,
{
move |i: &'a str| {
let (i, path) = delimited(tag(open_matcher), path, tag("/}"))(i)?;
let (i, (opening_name, maybe_explicit_context)) = tuple((
preceded(tag(open_matcher), path),
terminated(opt(preceded(tag(":"), path)), tag("/}")),
))(i)?;
Ok((
i,
constructor(Container {
path: path,
path: opening_name,
explicit_context: maybe_explicit_context,
contents: None,
else_contents: None,
}),
@ -404,19 +415,21 @@ where
F: Fn(NamedBlock<'a>) -> DustTag<'a>,
{
move |i: &'a str| {
let (i, (opening_name, inner, _closing_name)) = verify(
let (i, (opening_name, maybe_explicit_context, inner, _closing_name)) = verify(
tuple((
delimited(tag(open_matcher), key, tag("}")),
preceded(tag(open_matcher), key),
terminated(opt(preceded(tag(":"), path)), tag("}")),
opt(body),
delimited(tag("{/"), key, tag("}")),
)),
|(open, _inn, close)| open == close,
|(open, _maybe_explicit, _inn, close)| open == close,
)(i)?;
Ok((
i,
constructor(NamedBlock {
name: opening_name,
explicit_context: maybe_explicit_context,
contents: inner,
}),
))
@ -431,12 +444,16 @@ where
F: Fn(NamedBlock<'a>) -> DustTag<'a>,
{
move |i: &'a str| {
let (i, name) = delimited(tag(open_matcher), key, tag("/}"))(i)?;
let (i, (opening_name, maybe_explicit_context)) = tuple((
preceded(tag(open_matcher), key),
terminated(opt(preceded(tag(":"), path)), tag("/}")),
))(i)?;
Ok((
i,
constructor(NamedBlock {
name: name,
name: opening_name,
explicit_context: maybe_explicit_context,
contents: None,
}),
))
@ -466,25 +483,28 @@ where
F: Fn(ParameterizedBlock<'a>) -> DustTag<'a>,
{
move |i: &'a str| {
let (i, (name, params, inner, maybe_else, _closing_name)) = tuple((
preceded(tag(open_matcher), tag(tag_name)),
terminated(
opt(delimited(
space1,
separated_list1(space1, key_value_pair),
space0,
)),
tag("}"),
),
opt(body),
opt(preceded(tag("{:else}"), opt(body))),
delimited(tag("{/"), tag(tag_name), tag("}")),
))(i)?;
let (i, (name, maybe_explicit_context, params, inner, maybe_else, _closing_name)) =
tuple((
preceded(tag(open_matcher), tag(tag_name)),
opt(preceded(tag(":"), path)),
terminated(
opt(delimited(
space1,
separated_list1(space1, key_value_pair),
space0,
)),
tag("}"),
),
opt(body),
opt(preceded(tag("{:else}"), opt(body))),
delimited(tag("{/"), tag(tag_name), tag("}")),
))(i)?;
Ok((
i,
constructor(ParameterizedBlock {
name: name,
explicit_context: maybe_explicit_context,
params: params.unwrap_or(Vec::new()),
contents: inner,
else_contents: maybe_else.flatten(),
@ -502,10 +522,11 @@ where
F: Fn(ParameterizedBlock<'a>) -> DustTag<'a>,
{
move |i: &'a str| {
let (i, (name, params)) = delimited(
let (i, (name, maybe_explicit_context, params)) = delimited(
tag(open_matcher),
tuple((
tag(tag_name),
opt(preceded(tag(":"), path)),
opt(delimited(
space1,
separated_list1(space1, key_value_pair),
@ -519,6 +540,7 @@ where
i,
constructor(ParameterizedBlock {
name: name,
explicit_context: maybe_explicit_context,
params: params.unwrap_or(Vec::new()),
contents: None,
else_contents: None,
@ -531,10 +553,11 @@ fn partial_with_plain_tag<'a>(
open_matcher: &'static str,
) -> impl Fn(&'a str) -> IResult<&'a str, Partial<'a>> {
move |i: &'a str| {
let (i, (name, params)) = delimited(
let (i, (name, maybe_explicit_context, params)) = delimited(
tag(open_matcher),
tuple((
key,
opt(preceded(tag(":"), path)),
opt(delimited(
space1,
separated_list1(space1, key_value_pair),
@ -550,6 +573,7 @@ fn partial_with_plain_tag<'a>(
name: vec![PartialNameElement::PNSpan {
contents: name.to_owned(),
}],
explicit_context: maybe_explicit_context,
params: params.unwrap_or(Vec::new()),
},
))
@ -567,12 +591,13 @@ fn partial_with_quoted_tag<'a>(
open_matcher: &'static str,
) -> impl Fn(&'a str) -> IResult<&'a str, Partial<'a>> {
move |i: &'a str| {
let (i, (name, params)) = delimited(
let (i, (name, maybe_explicit_context, params)) = delimited(
tag(open_matcher),
tuple((
verify(quoted_string, |s: &String| {
partial_quoted_tag(s.as_str()).is_ok()
}),
opt(preceded(tag(":"), path)),
opt(delimited(
space1,
separated_list1(space1, key_value_pair),
@ -593,6 +618,7 @@ fn partial_with_quoted_tag<'a>(
i,
Partial {
name: partial_name_elements,
explicit_context: maybe_explicit_context,
params: params.unwrap_or(Vec::new()),
},
))
@ -672,7 +698,7 @@ fn body(i: &str) -> IResult<&str, Body> {
pub fn template(i: &str) -> IResult<&str, Template> {
// DustJS ignores all preceding whitespace (tabs, newlines, spaces) but only ignores trailing newlines
let (remaining, contents) = delimited(multispace0, body, eof_whitespace)(i)?;
let (remaining, contents) = all_consuming(delimited(multispace0, body, eof_whitespace))(i)?;
Ok((remaining, Template { contents: contents }))
}
@ -872,6 +898,7 @@ mod tests {
path: Path {
keys: vec!["foo", "bar"]
},
explicit_context: None,
contents: None,
else_contents: None,
})
@ -889,6 +916,7 @@ mod tests {
path: Path {
keys: vec!["foo", "bar"]
},
explicit_context: None,
contents: None,
else_contents: None,
})
@ -906,6 +934,7 @@ mod tests {
path: Path {
keys: vec!["foo", "bar"]
},
explicit_context: None,
contents: Some(Body {
elements: vec![
TemplateElement::TESpan(Span { contents: "hello " }),
@ -931,6 +960,7 @@ mod tests {
path: Path {
keys: vec!["greeting"]
},
explicit_context: None,
contents: Some(Body {
elements: vec![
TemplateElement::TESpan(Span { contents: "hello " }),
@ -956,6 +986,44 @@ mod tests {
);
}
#[test]
fn test_empty_section_with_explicit_context() {
assert_eq!(
super::dust_tag("{#foo.bar:baz.ipsum}{/foo.bar}"),
Ok((
"",
DustTag::DTSection(Container {
path: Path {
keys: vec!["foo", "bar"]
},
explicit_context: Some(Path {
keys: vec!["baz", "ipsum"]
}),
contents: None,
else_contents: None,
})
))
);
}
#[test]
fn test_self_closing_section_with_explicit_context() {
assert_eq!(
super::dust_tag("{#foo.bar:$idx/}"),
Ok((
"",
DustTag::DTSection(Container {
path: Path {
keys: vec!["foo", "bar"]
},
explicit_context: Some(Path { keys: vec!["$idx"] }),
contents: None,
else_contents: None,
})
))
);
}
#[test]
fn test_self_closing_block() {
assert_eq!(
@ -964,6 +1032,7 @@ mod tests {
"",
DustTag::DTBlock(NamedBlock {
name: "foo",
explicit_context: None,
contents: None
})
))
@ -978,6 +1047,49 @@ mod tests {
"",
DustTag::DTBlock(NamedBlock {
name: "foo",
explicit_context: None,
contents: Some(Body {
elements: vec![
TemplateElement::TESpan(Span { contents: "hello " }),
TemplateElement::TETag(DustTag::DTReference(Reference {
path: Path { keys: vec!["name"] },
filters: Vec::new()
}))
]
})
})
))
);
}
#[test]
fn test_self_closing_block_with_explicit_context() {
assert_eq!(
super::dust_tag("{+foo:bar.baz/}"),
Ok((
"",
DustTag::DTBlock(NamedBlock {
name: "foo",
explicit_context: Some(Path {
keys: vec!["bar", "baz"]
}),
contents: None
})
))
);
}
#[test]
fn test_block_with_explicit_context() {
assert_eq!(
super::dust_tag("{+foo:bar.baz}hello {name}{/foo}"),
Ok((
"",
DustTag::DTBlock(NamedBlock {
name: "foo",
explicit_context: Some(Path {
keys: vec!["bar", "baz"]
}),
contents: Some(Body {
elements: vec![
TemplateElement::TESpan(Span { contents: "hello " }),
@ -1000,6 +1112,7 @@ mod tests {
"",
DustTag::DTInlinePartial(NamedBlock {
name: "foo",
explicit_context: None,
contents: None
})
))
@ -1014,6 +1127,7 @@ mod tests {
"",
DustTag::DTInlinePartial(NamedBlock {
name: "foo",
explicit_context: None,
contents: Some(Body {
elements: vec![
TemplateElement::TESpan(Span { contents: "hello " }),
@ -1046,7 +1160,7 @@ mod tests {
name: vec![PartialNameElement::PNSpan {
contents: "foo".to_owned()
},],
explicit_context: None,
params: vec![
KVPair {
key: "bar",
@ -1072,6 +1186,7 @@ mod tests {
name: vec![PartialNameElement::PNSpan {
contents: r#"template name * with * special " characters"#.to_owned()
},],
explicit_context: None,
params: vec![
KVPair {
key: "bar",
@ -1106,6 +1221,65 @@ mod tests {
contents: "template".to_owned()
}
],
explicit_context: None,
params: vec![
KVPair {
key: "bar",
value: RValue::RVPath(Path { keys: vec!["baz"] })
},
KVPair {
key: "animal",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
}
]
})
))
);
}
#[test]
fn test_unquoted_partial_with_explicit_context() {
assert_eq!(
dust_tag(r#"{>foo:foo.bar bar=baz animal="cat"/}"#),
Ok((
"",
DustTag::DTPartial(Partial {
name: vec![PartialNameElement::PNSpan {
contents: "foo".to_owned()
},],
explicit_context: Some(Path {
keys: vec!["foo", "bar"]
}),
params: vec![
KVPair {
key: "bar",
value: RValue::RVPath(Path { keys: vec!["baz"] })
},
KVPair {
key: "animal",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
}
]
})
))
);
}
#[test]
fn test_quoted_partial_with_explicit_context() {
assert_eq!(
dust_tag(
r#"{>"template name * with * special \" characters":foo.bar bar=baz animal="cat"/}"#
),
Ok((
"",
DustTag::DTPartial(Partial {
name: vec![PartialNameElement::PNSpan {
contents: r#"template name * with * special " characters"#.to_owned()
},],
explicit_context: Some(Path {
keys: vec!["foo", "bar"]
}),
params: vec![
KVPair {
key: "bar",
@ -1131,6 +1305,7 @@ mod tests {
name: vec![PartialNameElement::PNSpan {
contents: "foo".to_owned()
},],
explicit_context: None,
params: vec![
KVPair {
key: "a",
@ -1154,6 +1329,7 @@ mod tests {
"",
DustTag::DTHelperEquals(ParameterizedBlock {
name: "eq",
explicit_context: None,
params: vec![
KVPair {
key: "key",
@ -1190,6 +1366,74 @@ mod tests {
"",
DustTag::DTHelperEquals(ParameterizedBlock {
name: "eq",
explicit_context: None,
params: vec![
KVPair {
key: "key",
value: RValue::RVPath(Path { keys: vec!["name"] })
},
KVPair {
key: "value",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
}
],
contents: None,
else_contents: None
})
))
);
}
#[test]
fn test_helper_with_explicit_context() {
assert_eq!(
dust_tag(r#"{@eq:foo.bar key=name value="cat"}Pet the {name}!{/eq}"#),
Ok((
"",
DustTag::DTHelperEquals(ParameterizedBlock {
name: "eq",
explicit_context: Some(Path {
keys: vec!["foo", "bar"]
}),
params: vec![
KVPair {
key: "key",
value: RValue::RVPath(Path { keys: vec!["name"] })
},
KVPair {
key: "value",
value: RValue::RVLiteral(OwnedLiteral::LString("cat".to_owned()))
}
],
contents: Some(Body {
elements: vec![
TemplateElement::TESpan(Span {
contents: "Pet the "
}),
TemplateElement::TETag(DustTag::DTReference(Reference {
path: Path { keys: vec!["name"] },
filters: Vec::new()
})),
TemplateElement::TESpan(Span { contents: "!" })
]
}),
else_contents: None
})
))
);
}
#[test]
fn test_self_closing_helper_with_explicit_context() {
assert_eq!(
dust_tag(r#"{@eq:foo.bar key=name value="cat"/}"#),
Ok((
"",
DustTag::DTHelperEquals(ParameterizedBlock {
name: "eq",
explicit_context: Some(Path {
keys: vec!["foo", "bar"]
}),
params: vec![
KVPair {
key: "key",
@ -1234,6 +1478,7 @@ mod tests {
path: Path {
keys: vec!["names"]
},
explicit_context: None,
contents: Some(Body {
elements: vec![TemplateElement::TETag(DustTag::DTReference(
Reference {
@ -1259,6 +1504,7 @@ mod tests {
path: Path {
keys: vec!["names"]
},
explicit_context: None,
contents: Some(Body {
elements: vec![
TemplateElement::TEIgnoredWhitespace(
@ -1296,12 +1542,14 @@ mod tests {
path: Path {
keys: vec!["level3", "level4"]
},
explicit_context: None,
contents: Some(Body {
elements: vec![TemplateElement::TETag(DustTag::DTPartial(
Partial {
name: vec![PartialNameElement::PNSpan {
contents: "partialtwo".to_owned()
},],
explicit_context: None,
params: vec![
KVPair {
key: "v1",

@ -3,6 +3,7 @@ use crate::parser::Body;
use crate::parser::DustTag;
use crate::parser::KVPair;
use crate::parser::PartialNameElement;
use crate::parser::Path;
use crate::parser::RValue;
use crate::parser::Special;
use crate::parser::Template;
@ -33,7 +34,6 @@ pub fn compile_template<'a>(
source: &'a str,
name: String,
) -> Result<CompiledTemplate<'a>, CompileError> {
// TODO: Make this all consuming
// TODO: This could use better error management
let (_remaining, parsed_template) = template(source).expect("Failed to compile template");
Ok(CompiledTemplate {
@ -76,14 +76,18 @@ impl<'a> DustRenderer<'a> {
};
let extracted_inline_partials = extract_inline_partials(main_template);
let new_blocks = InlinePartialTreeElement::new(blocks, extracted_inline_partials);
self.render_body(&main_template.contents, breadcrumbs, &new_blocks)
let new_block_context = BlockContext {
breadcrumbs: breadcrumbs,
blocks: &new_blocks,
};
self.render_body(&main_template.contents, breadcrumbs, &new_block_context)
}
fn render_maybe_body(
&'a self,
body: &'a Option<Body>,
breadcrumbs: &Vec<&'a dyn ContextElement>,
blocks: &'a InlinePartialTreeElement<'a>,
blocks: &'a BlockContext<'a>,
) -> Result<String, RenderError> {
match body {
None => Ok("".to_owned()),
@ -95,7 +99,7 @@ impl<'a> DustRenderer<'a> {
&'a self,
body: &'a Body,
breadcrumbs: &Vec<&'a dyn ContextElement>,
blocks: &'a InlinePartialTreeElement<'a>,
blocks: &'a BlockContext<'a>,
) -> Result<String, RenderError> {
let mut output = String::new();
for elem in &body.elements {
@ -110,11 +114,12 @@ impl<'a> DustRenderer<'a> {
Ok(output)
}
/// For rendering a dynamic partial's name
fn render_partial_name(
&'a self,
body: &'a Vec<PartialNameElement>,
breadcrumbs: &Vec<&'a dyn ContextElement>,
blocks: &'a InlinePartialTreeElement<'a>,
blocks: &'a BlockContext<'a>,
) -> Result<String, RenderError> {
let converted_to_template_elements: Vec<TemplateElement<'a>> =
body.into_iter().map(|e| e.into()).collect();
@ -131,7 +136,7 @@ impl<'a> DustRenderer<'a> {
&'a self,
tag: &'a DustTag,
breadcrumbs: &Vec<&'a dyn ContextElement>,
blocks: &'a InlinePartialTreeElement<'a>,
blocks: &'a BlockContext<'a>,
) -> Result<String, RenderError> {
match tag {
DustTag::DTComment(_comment) => (),
@ -163,9 +168,16 @@ impl<'a> DustRenderer<'a> {
let val = walk_path(breadcrumbs, &container.path.keys);
match val {
Err(WalkError::CantWalk) => {
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
None,
&container.explicit_context,
None,
);
return self.render_maybe_body(
&container.else_contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
}
@ -182,9 +194,18 @@ impl<'a> DustRenderer<'a> {
final_val.get_loop_elements();
if loop_elements.is_empty() {
// Scalar value
let mut new_breadcrumbs = breadcrumbs.clone();
new_breadcrumbs.push(final_val);
self.render_body(body, &new_breadcrumbs, blocks)
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
None,
&container.explicit_context,
Some(final_val),
);
self.render_body(
body,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
)
} else {
// Array-like value
let total_length = loop_elements.len();
@ -195,12 +216,18 @@ impl<'a> DustRenderer<'a> {
.map(|(i, array_elem)| {
let injected_context =
IterationContext::new(i, total_length);
let mut new_breadcrumbs = breadcrumbs.clone();
new_breadcrumbs.push(&injected_context);
new_breadcrumbs.push(array_elem);
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
Some(&injected_context),
&container.explicit_context,
Some(array_elem),
);
self.render_body(
&body,
&new_breadcrumbs,
new_breadcrumbs
.as_ref()
.unwrap_or(breadcrumbs),
blocks,
)
})
@ -215,9 +242,16 @@ impl<'a> DustRenderer<'a> {
// an empty array or null), Dust uses the
// original context before walking the path as
// the context for rendering the else block
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
None,
&container.explicit_context,
None,
);
return self.render_maybe_body(
&container.else_contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
};
@ -225,33 +259,81 @@ impl<'a> DustRenderer<'a> {
}
}
DustTag::DTExists(container) => {
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
None,
&container.explicit_context,
None,
);
let val = walk_path(breadcrumbs, &container.path.keys);
return if val.map(|v| v.is_truthy()).unwrap_or(false) {
self.render_maybe_body(&container.contents, breadcrumbs, blocks)
self.render_maybe_body(
&container.contents,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
)
} else {
self.render_maybe_body(&container.else_contents, breadcrumbs, blocks)
self.render_maybe_body(
&container.else_contents,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
)
};
}
DustTag::DTNotExists(container) => {
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
None,
&container.explicit_context,
None,
);
let val = walk_path(breadcrumbs, &container.path.keys);
return if !val.map(|v| v.is_truthy()).unwrap_or(false) {
self.render_maybe_body(&container.contents, breadcrumbs, blocks)
self.render_maybe_body(
&container.contents,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
)
} else {
self.render_maybe_body(&container.else_contents, breadcrumbs, blocks)
self.render_maybe_body(
&container.else_contents,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
)
};
}
DustTag::DTPartial(partial) => {
let partial_name = self.render_partial_name(&partial.name, breadcrumbs, blocks)?;
if partial.params.is_empty() {
let rendered_content =
self.render_template(&partial_name, breadcrumbs, Some(blocks))?;
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
None,
&partial.explicit_context,
None,
);
let rendered_content = self.render_template(
&partial_name,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
Some(blocks.blocks),
)?;
return Ok(rendered_content);
} else {
let injected_context = ParametersContext::new(breadcrumbs, &partial.params);
let mut new_breadcrumbs = breadcrumbs.clone();
new_breadcrumbs.insert(new_breadcrumbs.len() - 1, &injected_context);
let rendered_content =
self.render_template(&partial_name, &new_breadcrumbs, Some(blocks))?;
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
Some(&injected_context),
&partial.explicit_context,
None,
);
let rendered_content = self.render_template(
&partial_name,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
Some(blocks.blocks),
)?;
return Ok(rendered_content);
}
}
@ -260,12 +342,34 @@ impl<'a> DustRenderer<'a> {
return Ok("".to_owned());
}
DustTag::DTBlock(named_block) => {
return match blocks.get_block(named_block.name) {
None => self.render_maybe_body(&named_block.contents, breadcrumbs, blocks),
Some(interior) => self.render_maybe_body(interior, breadcrumbs, blocks),
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
blocks.breadcrumbs,
None,
&named_block.explicit_context,
None,
);
return match blocks.blocks.get_block(named_block.name) {
None => self.render_maybe_body(
&named_block.contents,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
),
Some(inline_partial) => self.render_maybe_body(
inline_partial,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
),
};
}
DustTag::DTHelperEquals(parameterized_block) => {
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
None,
&parameterized_block.explicit_context,
None,
);
let param_map: HashMap<&str, &RValue<'a>> =
Self::get_rval_map(&parameterized_block.params);
// Special case: when comparing two RVPaths, if the
@ -276,7 +380,11 @@ impl<'a> DustRenderer<'a> {
return match &parameterized_block.contents {
None => Ok("".to_owned()),
Some(body) => {
let rendered_content = self.render_body(body, breadcrumbs, blocks)?;
let rendered_content = self.render_body(
body,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
)?;
Ok(rendered_content)
}
};
@ -290,21 +398,29 @@ impl<'a> DustRenderer<'a> {
let right_side: Result<&dyn ContextElement, WalkError> =
Self::get_rval(breadcrumbs, &param_map, "value")
.unwrap_or(Err(WalkError::CantWalk));
if left_side == right_side {
return self.render_maybe_body(
&parameterized_block.contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
} else {
return self.render_maybe_body(
&parameterized_block.else_contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
}
}
DustTag::DTHelperNotEquals(parameterized_block) => {
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
None,
&parameterized_block.explicit_context,
None,
);
let param_map: HashMap<&str, &RValue<'a>> =
Self::get_rval_map(&parameterized_block.params);
// Special case: when comparing two RVPaths, if the
@ -315,7 +431,11 @@ impl<'a> DustRenderer<'a> {
return match &parameterized_block.else_contents {
None => Ok("".to_owned()),
Some(body) => {
let rendered_content = self.render_body(body, breadcrumbs, blocks)?;
let rendered_content = self.render_body(
body,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
)?;
Ok(rendered_content)
}
};
@ -332,18 +452,25 @@ impl<'a> DustRenderer<'a> {
if left_side != right_side {
return self.render_maybe_body(
&parameterized_block.contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
} else {
return self.render_maybe_body(
&parameterized_block.else_contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
}
}
DustTag::DTHelperGreaterThan(parameterized_block) => {
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
None,
&parameterized_block.explicit_context,
None,
);
let param_map: HashMap<&str, &RValue<'a>> =
Self::get_rval_map(&parameterized_block.params);
let left_side: Result<&dyn ContextElement, WalkError> =
@ -358,7 +485,7 @@ impl<'a> DustRenderer<'a> {
(Err(_), _) | (_, Err(_)) => {
return self.render_maybe_body(
&parameterized_block.else_contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
)
}
@ -366,13 +493,13 @@ impl<'a> DustRenderer<'a> {
if left_side_unwrapped > right_side_unwrapped {
return self.render_maybe_body(
&parameterized_block.contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
} else {
return self.render_maybe_body(
&parameterized_block.else_contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
}
@ -380,6 +507,13 @@ impl<'a> DustRenderer<'a> {
}
}
DustTag::DTHelperGreaterThanOrEquals(parameterized_block) => {
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
None,
&parameterized_block.explicit_context,
None,
);
let param_map: HashMap<&str, &RValue<'a>> =
Self::get_rval_map(&parameterized_block.params);
let left_side: Result<&dyn ContextElement, WalkError> =
@ -394,7 +528,7 @@ impl<'a> DustRenderer<'a> {
(Err(_), _) | (_, Err(_)) => {
return self.render_maybe_body(
&parameterized_block.else_contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
)
}
@ -402,13 +536,13 @@ impl<'a> DustRenderer<'a> {
if left_side_unwrapped >= right_side_unwrapped {
return self.render_maybe_body(
&parameterized_block.contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
} else {
return self.render_maybe_body(
&parameterized_block.else_contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
}
@ -416,6 +550,13 @@ impl<'a> DustRenderer<'a> {
}
}
DustTag::DTHelperLessThan(parameterized_block) => {
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
None,
&parameterized_block.explicit_context,
None,
);
let param_map: HashMap<&str, &RValue<'a>> =
Self::get_rval_map(&parameterized_block.params);
let left_side: Result<&dyn ContextElement, WalkError> =
@ -430,7 +571,7 @@ impl<'a> DustRenderer<'a> {
(Err(_), _) | (_, Err(_)) => {
return self.render_maybe_body(
&parameterized_block.else_contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
)
}
@ -438,13 +579,13 @@ impl<'a> DustRenderer<'a> {
if left_side_unwrapped < right_side_unwrapped {
return self.render_maybe_body(
&parameterized_block.contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
} else {
return self.render_maybe_body(
&parameterized_block.else_contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
}
@ -452,6 +593,13 @@ impl<'a> DustRenderer<'a> {
}
}
DustTag::DTHelperLessThanOrEquals(parameterized_block) => {
let new_breadcrumbs = Self::new_breadcrumbs(
breadcrumbs,
breadcrumbs,
None,
&parameterized_block.explicit_context,
None,
);
let param_map: HashMap<&str, &RValue<'a>> =
Self::get_rval_map(&parameterized_block.params);
let left_side: Result<&dyn ContextElement, WalkError> =
@ -466,7 +614,7 @@ impl<'a> DustRenderer<'a> {
(Err(_), _) | (_, Err(_)) => {
return self.render_maybe_body(
&parameterized_block.else_contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
)
}
@ -474,13 +622,13 @@ impl<'a> DustRenderer<'a> {
if left_side_unwrapped <= right_side_unwrapped {
return self.render_maybe_body(
&parameterized_block.contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
} else {
return self.render_maybe_body(
&parameterized_block.else_contents,
breadcrumbs,
new_breadcrumbs.as_ref().unwrap_or(breadcrumbs),
blocks,
);
}
@ -552,6 +700,97 @@ impl<'a> DustRenderer<'a> {
}
final_filters
}
/// Generate a new breadcrumbs object
///
/// This function generates a new breadcrumbs object based on the
/// new context information provided.
///
/// breadcrumbs are the breadcrumbs that will be used in the final
/// breadcrumbs (unless omitted due to an explicit context)
///
/// explicit_context_breadcrumbs are the breadcrumbs used to
/// evaluate an explicit context path. Most of the time the two
/// breadcrumbs parameters will be identical, but for
/// blocks/inline partials the explicit_context_breadcrumbs will
/// be the breadcrumbs from the start of the partial containing
/// the block.
///
/// explicit_context is for contexts specified with a `:path`
/// inside a dust tag.
///
/// injected_context is for any generated context. This includes
/// both parameters on a tag and also the handling of $idx and
/// $len.
///
/// New context element is the element is an element to append to
/// the end, generally for use in section tags which walk to a new
/// context.
///
/// If explicit_context is not None, then the final breadcrumb stack will be:
///
/// ```text
/// breadcrumbs
/// injected_context
/// new_context_element
/// ```
///
/// However, if explicit_context is not None, then the old
/// breadcrumbs are omitted, leading to the new breadcrumb stack
/// as:
///
/// ```text
/// injected_context
/// explicit_context
/// new_context_element
/// ```
fn new_breadcrumbs<'b>(
breadcrumbs: &'b Vec<&'b dyn ContextElement>,
explicit_context_breadcrumbs: &'b Vec<&'b dyn ContextElement>,
injected_context: Option<&'b dyn ContextElement>,
explicit_context: &Option<Path<'b>>,
new_context_element: Option<&'b dyn ContextElement>,
) -> Option<Vec<&'b dyn ContextElement>> {
// If none of the additional contexts are present, return None
// to signal that the original breadcrumbs should be used
// rather than incurring a copy here.
match (injected_context, explicit_context, new_context_element) {
(None, None, None) => return None,
_ => (),
};
let mut new_stack = match explicit_context {
Some(_) => Vec::with_capacity(3),
None => breadcrumbs.clone(),
};
// TODO: Can sections have parameters, and if so, what happens then? Currently when there is an injected context or an explicit context it gets inserted behind the current context, so 1->2->3 becomes 1->2->injected->3 or explicit->3. When there is a new context(4) with injected we're doing 1->2->3->injected->4. When there is an explicit context and a new context we're doing explicit->4. But what happens if there is a section with parameters and an explicit context, hitting all the categories? Would it be parameters->explicit->4? I would definitely have to change the parameters to this function since right now iteration variables and parameters are both sharing injected_context.
injected_context.map(|ctx| {
// Special case: when there is no explicit context or new
// context element, the injected context gets inserted 1
// spot behind the current context. Otherwise, the
// injected context gets added after the current context
// but before the explicit context and new context
// element.
match (explicit_context, new_context_element) {
(None, None) => new_stack.insert(std::cmp::max(new_stack.len() - 1, 0), ctx),
_ => new_stack.push(ctx),
}
});
explicit_context.as_ref().map(|path| {
walk_path(explicit_context_breadcrumbs, &path.keys).map(|val| {
if val.is_truthy() {
new_stack.push(val)
}
});
});
new_context_element.map(|ctx| new_stack.push(ctx));
Some(new_stack)
}
}
struct BlockContext<'a> {
/// The breadcrumbs at the time of entering the current partial
breadcrumbs: &'a Vec<&'a dyn ContextElement>,
blocks: &'a InlinePartialTreeElement<'a>,
}
#[cfg(test)]

@ -42,6 +42,12 @@ where
B: Borrow<dyn ContextElement + 'a>,
P: Borrow<str>,
{
if breadcrumbs.is_empty() {
// This happens when you use a section with an explicit
// context path, where both the path and the explicit context
// path fail, leaving you with absolutely no context.
return Err(WalkError::CantWalk);
}
if path.is_empty() {
return Ok(breadcrumbs
.last()

Loading…
Cancel
Save