#![doc(html_logo_url = "https://gitlab.com/encre-org/pochoir/raw/main/.assets/logo.png")]
#![forbid(unsafe_code)]
#![warn(
missing_docs,
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
unstable_features,
unused_import_braces,
unused_qualifications,
rustdoc::private_doc_tests,
rustdoc::broken_intra_doc_links,
rustdoc::private_intra_doc_links,
clippy::unnecessary_wraps,
clippy::too_many_lines,
clippy::string_to_string,
clippy::explicit_iter_loop,
clippy::unnecessary_cast,
clippy::missing_errors_doc,
clippy::pedantic,
clippy::clone_on_ref_ptr,
clippy::non_ascii_literal,
clippy::dbg_macro,
clippy::map_err_ignore,
clippy::use_debug,
clippy::map_err_ignore,
clippy::use_self,
clippy::useless_let_if_seq,
clippy::verbose_file_reads,
clippy::panic,
clippy::unimplemented,
clippy::todo
)]
#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
pub mod compiler;
mod component_file;
pub mod error;
pub mod providers;
pub mod transformers;
#[doc(inline)]
pub use compiler::{compile, is_custom_element, transform_and_compile};
#[doc(inline)]
pub use component_file::ComponentFile;
#[doc(inline)]
pub use error::{component_not_found, Error, Result};
#[doc(inline)]
pub use providers::{FilesystemProvider, StaticMapProvider};
#[doc(inline)]
pub use transformers::{
Transformer, TransformerElementContext, TransformerResult, TransformerTreeContext, Transformers,
};
pub use common::Spanned;
pub use lang::{object, Context, FromValue, Function, IntoValue, Value};
pub use pochoir_common as common;
pub use pochoir_lang as lang;
pub use pochoir_parser as parser;
pub use pochoir_template_engine as template_engine;
#[cfg(test)]
mod tests {
use std::path::Path;
use crate::{error::SpannedWithComponent, transformers::Transformer};
use super::*;
use lang::{object, IntoValue};
use pretty_assertions::assert_eq;
#[test]
fn inherited_variable() {
let component_html = r#"<button class="rounded-full border-3 border-blue-500 font-black">{{ count }}</button>"#;
let default_html = "<my-button></my-button>";
let mut context = Context::new();
context.insert_inherited("count", 10);
assert_eq!(
compile("default", &mut context, |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-button" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r#"<button class="rounded-full border-3 border-blue-500 font-black">10</button>"#
);
}
#[test]
fn basic_component() {
let component_html = r#"<button class="rounded-full border-3 border-blue-500 font-black">Click me!</button>"#;
let default_html = r#"<div class="bg-red-500"><p></p>
<my-button></my-button>
<img src="/path/to/file.png" />
{{ hello }}
<!-- This is a comment -->
<a href="/hello/world"></a>
</div>
<my-button></my-button>
{% if checked %}
<input type="checkbox" checked />
{% else %}
<input type="checkbox" />
{% endif %}"#;
let mut context = Context::new();
context.insert("hello", "Hello world!");
context.insert("checked", true);
assert_eq!(
compile("default", &mut context, |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-button" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r#"<div class="bg-red-500"><p></p>
<button class="rounded-full border-3 border-blue-500 font-black">Click me!</button>
<img src="/path/to/file.png">
Hello world!
<a href="/hello/world"></a>
</div>
<button class="rounded-full border-3 border-blue-500 font-black">Click me!</button>
<input type="checkbox" checked>
"#
);
}
#[test]
fn component_args() {
let component_html = r#"{% if basic != null %}<button class="btn-{{ variant }}">Click me!</button>{% else %}<button class="not-basic btn-{{ variant }}">Click me!</button>{% endif %}"#;
let default_html = r#"<div class="bg-red-500"><p></p>
<my-button variant="primary" basic></my-button>
<img src="/path/to/file.png" />
{{ hello }}
<!-- This is a comment -->
<a href="/hello/world"></a>
</div>
<my-button
variant="{{ custom_variant }}"
></my-button>
{% if checked %}
<input type="checkbox" checked />
{% else %}
<input type="checkbox" />
{% endif %}"#;
let mut context = Context::new();
context.insert("hello", "Hello world!");
context.insert("custom_variant", "secondary");
context.insert("checked", true);
assert_eq!(
compile("default", &mut context, |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-button" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r#"<div class="bg-red-500"><p></p>
<button class="btn-primary">Click me!</button>
<img src="/path/to/file.png">
Hello world!
<a href="/hello/world"></a>
</div>
<button class="not-basic btn-secondary">Click me!</button>
<input type="checkbox" checked>
"#
);
}
#[test]
fn component_slot_with_default_content() {
let component_html = "<div><slot>The number is {{ base_num + num + 1 }}</slot></div>";
let default_html = r#"<my-button num="{{ 2 }}"></my-button><my-button>Replaced slot content with {{ base_num }}</my-button>"#;
let mut context = Context::new();
context.insert("base_num", 41);
context.make_inherited("base_num");
assert_eq!(
compile("default", &mut context, |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-button" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"<div>The number is 44</div><div>Replaced slot content with 41</div>".to_string(),
);
}
#[test]
fn component_named_slots() {
let component_html = r#"<div>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer">Default footer content</slot>
</footer>
</div>"#;
let default_html = r#"<my-card>
<h1 slot="header">My header</h1>
<div><p>Hello main world</p></div>
<p slot="footer">Footer content</p>
In main
</my-card>"#;
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-card" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"<div>
<header>
<h1>My header</h1>
</header>
<main>
<div><p>Hello main world</p></div>
In main
</main>
<footer>
<p>Footer content</p>
</footer>
</div>"
.to_string(),
);
}
#[test]
fn component_scoped_slots() {
let component_html = r#"<div>
<header>
<slot name="header" text="{{ text }}"></slot>
</header>
<main>
<slot number="42"></slot>
</main>
<footer>
<slot name="footer">Default footer content</slot>
</footer>
</div>"#;
let default_html = r#"<my-card text="Some text header">
<h1 slot="header">{{ text }}</h1>
<div><p>The number is {{ number }}</p></div>
</my-card>"#;
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-card" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"<div>
<header>
<h1>Some text header</h1>
</header>
<main>
<div><p>The number is 42</p></div>
</main>
<footer>
Default footer content
</footer>
</div>"
.to_string(),
);
}
#[test]
fn component_scoped_slot_2() {
let component_html = r#"{% if basic != null %}<button class="btn-{{ variant }}"><slot></slot></button>{% else %}<button class="not-basic btn-{{ variant }}"><slot custom_variant="{{ variant }}"></slot></button>{% endif %}"#;
let default_html = r#"<div class="bg-red-500"><p></p>
<my-button variant="primary" basic>Hello world!</my-button>
<img src="/path/to/file.png" />
{{ hello }}
<!-- This is a comment -->
<a href="/hello/world"></a>
</div>
<my-button
variant="{{ custom_variant }}"
><h3>Some HTML <b>with bold text</b> and {{ custom_variant }}</h3></my-button>
{% if checked %}
<input type="checkbox" checked />
{% else %}
<input type="checkbox" />
{% endif %}"#;
let mut context = Context::new();
context.insert("hello", "Hello world!");
context.insert("custom_variant", "secondary");
context.insert("checked", true);
assert_eq!(
compile("default", &mut context, |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-button" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r#"<div class="bg-red-500"><p></p>
<button class="btn-primary">Hello world!</button>
<img src="/path/to/file.png">
Hello world!
<a href="/hello/world"></a>
</div>
<button class="not-basic btn-secondary"><h3>Some HTML <b>with bold text</b> and secondary</h3></button>
<input type="checkbox" checked>
"#
.to_string(),
);
}
#[test]
fn component_nested_slots() {
let component2_html =
r#"<article><h1><slot></slot></h1><slot name="paragraph"></slot></article>"#;
let component1_html = r#"<div><my-component2>Hello!<p slot="paragraph">A paragraph with a <slot></slot></p></my-component2></div>"#;
let default_html = "<my-component1><b>message</b>!</my-component1>";
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-component1" {
Ok(ComponentFile::new_inline(component1_html))
}else if name == "my-component2" {
Ok(ComponentFile::new_inline(component2_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"<div><article><h1>Hello!</h1><p>A paragraph with a <b>message</b>!</p></article></div>"
.to_string(),
);
}
#[test]
fn component_slot_instances_with_same_name() {
let component_html = r#"<div><main><slot name="main"></slot></main><footer><slot name="footer"></slot></footer></div>"#;
let default_html = r#"<my-card><h1 slot="main">A header</h1><p slot="main">A paragraph</p><p slot="footer">Some footer</p><p slot="main">Another paragraph</p></my-card>"#;
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-card" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"<div><main><h1>A header</h1><p>A paragraph</p><p>Another paragraph</p></main><footer><p>Some footer</p></footer></div>"
.to_string(),
);
}
#[test]
fn component_as_slot() {
let component2_html = "<h1>Component 2: {{ msg }}</h1>";
let component1_html = r#"<div><slot name="content"></slot><slot></slot></div>"#;
let default_html = r#"<my-component1><my-component2 slot="content" msg="content slot"></my-component2><my-component2 msg="default slot"></my-component2></my-component1>"#;
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-component1" {
Ok(ComponentFile::new_inline(component1_html))
} else if name == "my-component2" {
Ok(ComponentFile::new_inline(component2_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"<div><h1>Component 2: content slot</h1><h1>Component 2: default slot</h1></div>"
.to_string(),
);
}
#[test]
fn statement_in_component_prop() {
let component_html = r#"<div>{{ my_prop + " world!" }}</div>"#;
let default_html = r#"<my-component my_prop="{% if my_bool %}hello{% else %}world{% endif %}"></my-component>"#;
assert_eq!(
compile(
"default",
&mut Context::from_iter([("my_bool".to_string(), Value::Bool(true))]),
|name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-component" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
}
)
.unwrap(),
"<div>hello world!</div>".to_string(),
);
}
#[test]
fn component_slots_with_for() {
let list_html = r#"
<ul>
{% for el in elements %} <li><slot el="{{ el }}"></slot></li>
{% endfor %}</ul>"#;
let default_html = r#"<my-list elements='{{ [{ active: true, name: "Item 1" }, { name: "Item 2", active: false }, { name: "Item 3", active: false }] }}'><p class="{% if el.active %}bg-blue-500{% endif %}">{{ el.name }}</p></my-list>"#;
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-list" {
Ok(ComponentFile::new_inline(list_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r#"
<ul>
<li><p class="bg-blue-500">Item 1</p></li>
<li><p class>Item 2</p></li>
<li><p class>Item 3</p></li>
</ul>"#
.to_string(),
);
}
#[test]
#[allow(clippy::needless_raw_string_hashes)]
fn nested_if() {
let default_html = r##"{% if is_link %}<a href="#">{% if is_bold %}<b>{% endif %}Link content{% if is_bold %}</b>{% endif %}</a>{% endif %}"##;
assert_eq!(
compile(
"default",
&mut Context::from_iter([
("is_bold".to_string(), Value::Bool(false)),
("is_link".to_string(), Value::Bool(true)),
]),
|name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
}
)
.unwrap(),
r##"<a href="#"></a>"##.to_string(),
);
let default_html = r##"{% if 2 > 1 %}a{% if 2 > 3 %}b{% endif %}{% endif %}"##;
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"a",
);
}
#[test]
fn for_with_destructuring() {
let default_html = r#"<ul>
{% for index, item in enumerate(items) %}
<li style="{% if index == 1 || index == len(items) %}color: red;{% else %}color: blue;{% endif %}">{{ item }}</li>
{% endfor %}
</ul>"#;
assert_eq!(
compile(
"default",
&mut Context::from_iter([(
"items".to_string(),
vec!["Living room", "Kitchen", "Bathroom",].into_value()
)]),
|name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
}
)
.unwrap(),
r#"<ul>
<li style="color: red;">Living room</li>
<li style="color: blue;">Kitchen</li>
<li style="color: red;">Bathroom</li>
</ul>"#
.to_string(),
);
let default_html = "<ul>
{% for city, street in locations %}
<li>{{ street }} (in {{ city }})</li>
{% endfor %}
</ul>";
assert_eq!(
compile(
"default",
&mut Context::from_iter([(
"locations".to_string(),
vec![
object! { "city" => "London", "street" => "10 High Street" },
object! { "city" => "New York", "street" => "898 Brandywine Rd." },
]
.into_value(),
)]),
|name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
}
)
.unwrap(),
"<ul>
<li>10 High Street (in London)</li>
<li>898 Brandywine Rd. (in New York)</li>
</ul>"
.to_string(),
);
}
#[test]
fn for_with_range() {
let default_html = "<ul>{% for item in 1..=4 %}<li>{{ list[item] }}</li>{% endfor %}</ul>";
let mut context = Context::new();
context.insert("list", vec!["a", "b", "c", "d", "e", "f", "g"]);
assert_eq!(
compile("default", &mut context, |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"<ul><li>b</li><li>c</li><li>d</li><li>e</li></ul>".to_string(),
);
}
#[test]
fn for_with_unbounded_range() {
let default_html = "<ul>{% for item in 1.. %}<li>{{ list[item] }}</li>{% endfor %}</ul>";
let mut context = Context::new();
context.insert("list", vec!["a", "b", "c", "d", "e", "f", "g"]);
assert_eq!(
compile("default", &mut context, |name| {
if name == "default" {
Ok(ComponentFile::new(Path::new("index.html"), default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap_err(),
SpannedWithComponent::new(Error::UnboundedRangeInForLoop)
.with_span(19..22)
.with_file_path("index.html")
.with_component_name("default"),
);
}
#[test]
#[allow(clippy::needless_raw_string_hashes)]
fn let_in_templates() {
let default_html = r#"<a href="{% let foo = 'foo' %}#">{{ foo }}</a>
{% let bar = to_string(32 + 10) %}
{{ "life is " + bar }}"#;
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r##"<a href="#">foo</a>
life is 42"##
.to_string(),
);
}
#[test]
fn prepend_html_and_compile() {
struct TreeTransformer;
impl Transformer for TreeTransformer {
fn on_tree_parsed(&mut self, ctx: &mut TransformerTreeContext) -> TransformerResult {
let mut div = ctx.tree.get_mut(ctx.tree.select("div").unwrap().unwrap());
div.prepend_html("prepended-html", "<h1>Title</h1><h3>Subtitle</h3>")
.unwrap();
Ok(())
}
}
let default_html = r#"<div><ul><li class="list-item">{{ one_message }}</li><li class="list-item">Two</li><li class="not-list-item">Three</li></ul></div>"#;
let mut context = Context::new();
context.insert("one_message", "One");
context.insert("num", "#1");
let mut transformers = Transformers::new().with_transformer(TreeTransformer);
assert_eq!(
transform_and_compile(
"default",
&mut context,
|name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
},
&mut transformers
)
.unwrap(),
r#"<div><h1>Title</h1><h3>Subtitle</h3><ul><li class="list-item">One</li><li class="list-item">Two</li><li class="not-list-item">Three</li></ul></div>"#
);
}
#[test]
fn replace_html_and_compile() {
struct TreeTransformer;
impl Transformer for TreeTransformer {
fn on_tree_parsed(&mut self, ctx: &mut TransformerTreeContext) -> TransformerResult {
let second_node = ctx
.tree
.get_mut(ctx.tree.select("li:nth-child(2)").unwrap().unwrap());
second_node
.replace_html("replaced-html", r#"<li id="second-item" class="list-item">Changed content!</li><li id="third-item"></li>"#)
.unwrap();
Ok(())
}
}
let default_html = r#"<ul><li class="list-item">{{ one_message }}</li><li class="list-item">Two</li><li class="not-list-item">Three</li></ul>"#;
let mut context = Context::new();
context.insert("one_message", "One");
let mut transformers = Transformers::new().with_transformer(TreeTransformer);
assert_eq!(
transform_and_compile(
"default",
&mut context,
|name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
},
&mut transformers
)
.unwrap(),
r#"<ul><li class="list-item">One</li><li id="second-item" class="list-item">Changed content!</li><li id="third-item"></li><li class="not-list-item">Three</li></ul>"#
);
}
#[test]
fn several_replace_html() {
struct TreeTransformer;
impl Transformer for TreeTransformer {
fn on_tree_parsed(&mut self, ctx: &mut TransformerTreeContext) -> TransformerResult {
for img_id in ctx.tree.select_all("img") {
ctx.tree
.get_mut(img_id)
.replace_html("replaced-html", r"<picture><source></picture>")
.unwrap();
}
Ok(())
}
}
let default_html = r"<!DOCTYPE html><html><img><img></html>";
let mut transformers = Transformers::new().with_transformer(TreeTransformer);
assert_eq!(
transform_and_compile(
"default",
&mut Context::new(),
|name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
},
&mut transformers
)
.unwrap(),
"<!DOCTYPE html><html><picture><source></picture><picture><source></picture></html>"
);
}
#[test]
fn append_html_and_compile() {
struct TreeTransformer;
impl Transformer for TreeTransformer {
fn on_tree_parsed(&mut self, ctx: &mut TransformerTreeContext) -> TransformerResult {
let mut div = ctx.tree.get_mut(ctx.tree.select("div").unwrap().unwrap());
div
.append_html("appended-html", "<main><h3>Some main</h3>With a lot of text</main><footer>Footer content with <b>bold text</b>.</footer>")
.unwrap();
Ok(())
}
}
let default_html = r#"<div><ul><li class="list-item">{{ one_message }}</li><li class="list-item">Two</li><li class="not-list-item">Three</li></ul></div>"#;
let mut context = Context::new();
context.insert("one_message", "One");
context.insert("word", "content");
let mut transformers = Transformers::new().with_transformer(TreeTransformer);
assert_eq!(
transform_and_compile(
"default",
&mut context,
|name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
},
&mut transformers
)
.unwrap(),
r#"<div><ul><li class="list-item">One</li><li class="list-item">Two</li><li class="not-list-item">Three</li></ul><main><h3>Some main</h3>With a lot of text</main><footer>Footer content with <b>bold text</b>.</footer></div>"#
);
}
#[test]
fn cyclic_template_error() {
let default_html = "Hello <my-component></my-component> world!";
let error_msg = compile("my-component", &mut Context::new(), |name| {
if name == "my-component" {
Ok(ComponentFile::new(
Path::new("my-component.html"),
default_html,
))
} else {
Err(component_not_found(name))
}
})
.unwrap_err();
assert_eq!(
error_msg,
SpannedWithComponent::new(Error::CyclicComponent {
name: "my-component".to_string()
})
.with_span(6..35)
.with_file_path("my-component.html")
.with_component_name("my-component"),
);
}
#[test]
fn unknown_statement_error() {
let default_html = "<div>{% switch sth %}{% endswitch %}</div>";
let error_msg = compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new(Path::new("index.html"), default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap_err();
assert_eq!(
error_msg,
SpannedWithComponent::new(Error::UnknownStatement {
stmt: "switch".to_string()
})
.with_span(8..18)
.with_file_path("index.html")
.with_component_name("default"),
);
let default_html = r#"<div class="{% switch sth %}{% endswitch %}"></div>"#;
let error_msg = compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new(Path::new("index.html"), default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap_err();
assert_eq!(
error_msg,
SpannedWithComponent::new(Error::TemplateError(
template_engine::Error::UnknownStatement("switch".to_string())
))
.with_span(15..25)
.with_file_path("index.html")
.with_component_name("default"),
);
}
#[test]
#[allow(clippy::too_many_lines)]
fn eval_text_without_elements() {
let default_html = "{% if cond %}1{% endif %}
{% if cond %}1{% endif %}2
{% if cond %}{{ expr }}{% endif %}
{% if cond %}1{% endif %}{{ expr }}
{% if cond %}{{ expr }}{% endif %}{{ expr }}
{% if cond %}1{{ expr }}{% endif %}
{% if cond %}1{{ expr }}2{% endif %}
{% if cond %}{{ expr }}{% endif %}3{{ expr }}
{% if cond %}{{ expr }}{% endif %}3{{ expr }}4
1{% if cond %}2{{ expr }}3{% endif %}4{{ expr }}5
1{% if cond %}2{% else %}3{% endif %}
{% if cond %}1{{ expr }}2{% else %}3{{ expr }}4{% endif %}5{{ expr }}6";
let mut context = Context::new();
context.insert("cond", true);
context.insert("expr", "hello");
assert_eq!(
compile("default", &mut context, |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"1
12
hello
1hello
hellohello
1hello
1hello2
hello3hello
hello3hello4
12hello34hello5
12
1hello25hello6",
);
let mut context = Context::new();
context.insert("cond", false);
context.insert("expr", "hello");
assert_eq!(
compile("default", &mut context, |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"
2
hello
hello
3hello
3hello4
14hello5
13
3hello45hello6",
);
}
#[test]
fn define_component_using_template_element() {
let default_html = r#"
<template name="hello-world">
<h1>Hello world!</h1>
<p class="small">Just an hello world component.</p>
</template>
<hello-world />"#;
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r#"
<h1>Hello world!</h1>
<p class="small">Just an hello world component.</p>
"#,
);
}
#[test]
fn template_element_is_scoped_to_parent_element() {
let default_html = r#"
<div>
<template name="my-button">
{# This is a button okay? #}
<button class="button" type="button"></button>
</template>
<my-button />
<div>
<my-button />
</div>
</div>
<my-button />"#;
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new(Path::new("index.html"), default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap_err(),
SpannedWithComponent::new(Error::ComponentNotFound {
name: "my-button".to_string()
})
.with_span(347..360)
.with_file_path("index.html")
.with_component_name("default"),
);
}
#[test]
fn template_element_override_component() {
let component_html = "<div>My component</div>";
let default_html = r#"
<my-component />
<template name="my-component">
<div>Overridden</div>
</template>
<my-component />"#;
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-component" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"
<div>My component</div>
<div>Overridden</div>
",
);
}
#[test]
fn component_not_found_error() {
let default_html = "<my-component />";
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new(
Path::new("my-component.html"),
default_html,
))
} else {
Err(component_not_found(name))
}
})
.unwrap_err(),
SpannedWithComponent::new(Error::ComponentNotFound {
name: "my-component".to_string()
})
.with_span(0..16)
.with_file_path("my-component.html")
.with_component_name("default"),
);
}
#[test]
fn attribute_case() {
let component_html = "<h1>ID: {{ movie_id }}</h1>";
let default_html = r#"<my-component movie-id="44bg12" />"#;
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-component" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"<h1>ID: 44bg12</h1>",
);
}
#[test]
fn attribute_expression_with_different_types() {
let default_html = r#"<input type="date" value="{{ date.year }}-{{ date.month }}-{{ date.day }}" />"#;
let mut ctx = Context::from_object(object! {
"date" => object! {
"year" => 1905,
"month" => "06",
"day" => 30,
},
});
assert_eq!(
compile("default", &mut ctx, |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r#"<input type="date" value="1905-06-30">"#,
);
}
#[test]
fn attribute_select_html() {
let default_html = r#"<select>
<option value="1" selected="{{ active == 1 }}"></option>
<option value="2" selected="{{ active == 2 }}"></option>
<option value="3" selected="{{ active == 3 }}"></option>
</select>"#;
let mut ctx = Context::new();
ctx.insert("active", 2);
assert_eq!(
compile("default", &mut ctx, |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r#"<select>
<option value="1"></option>
<option value="2" selected></option>
<option value="3"></option>
</select>"#,
);
}
#[test]
fn declarative_shadow_dom() {
let component_html = r#"
<template shadowrootmode="open">
<style>
button {
color: seagreen;
}
</style>
<button>
<slot></slot>
</button>
</template>"#;
let default_html = "<my-component>My Button</my-component>";
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-component" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r#"<my-component>
<template shadowrootmode="open">
<style>
button {
color: seagreen;
}
</style>
<button>
<slot></slot>
</button>
</template>My Button</my-component>"#,
);
}
#[test]
fn declarative_shadow_dom_with_expressions() {
let mut context = Context::new();
context.insert("label", "world!");
context.insert("foo", "my-id");
let component_html = r#"
{{ id }}
<template shadowrootmode="open">
<style>
button {
color: seagreen;
}
</style>
<button id="{{ id }}-bar">
<slot></slot>
</button>
</template>"#;
let default_html =
r#"<my-component id="{{ foo }}">My Button<p>Hello {{ label }}</p></my-component>"#;
assert_eq!(
compile("default", &mut context, |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-component" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r#"<my-component id="my-id">
my-id
<template shadowrootmode="open">
<style>
button {
color: seagreen;
}
</style>
<button id="my-id-bar">
<slot></slot>
</button>
</template>My Button<p>Hello world!</p></my-component>"#,
);
}
#[test]
fn declarative_shadow_dom_with_slots() {
let component_html = r#"
<template shadowrootmode="open">
<style>
button {
color: seagreen;
}
</style>
<button>
<slot name="prefix" />
<slot />
<slot name="suffix" />
</button>
</template>"#;
let default_html = r#"<my-component>
<span slot="prefix">Prefix</span>
My Button
<span slot="suffix">Suffix</span>
</my-component>"#;
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else if name == "my-component" {
Ok(ComponentFile::new_inline(component_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r#"<my-component>
<template shadowrootmode="open">
<style>
button {
color: seagreen;
}
</style>
<button>
<slot name="prefix"></slot>
<slot></slot>
<slot name="suffix"></slot>
</button>
</template>
<span slot="prefix">Prefix</span>
My Button
<span slot="suffix">Suffix</span>
</my-component>"#,
);
}
#[test]
fn spaceless_statement() {
let default_html = "{% spaceless %}
<ul>
<li>Hello</li>
<li>World </li>
</ul>
{% endspaceless %}";
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"<ul><li>Hello</li><li>World </li></ul>",
);
let default_html = "{% spaceless %}
Hello {{ world }}!
{% endspaceless %}
Text after spaceless
Another {{ 'expression' }}";
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"Hello null!
Text after spaceless
Another expression",
);
}
#[test]
fn verbatim_statement() {
let default_html = "{% verbatim %}
{% spaceless %}
<ul>
<li>Hello</li>
<li>World </li>
</ul>
{% endspaceless %}
{{ hello }}
{% verbatim %}
{% endverbatim %}";
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"
{%spaceless%}
<ul>
<li>Hello</li>
<li>World </li>
</ul>
{%endspaceless%}
{{hello}}
{%verbatim%}
",
);
let default_html = "{% verbatim %}
Hello {{ world }}!
{% endverbatim %}
Text after verbatim";
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(default_html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
"
Hello {{world}}!
Text after verbatim",
);
}
#[test]
fn spaceless_remove_whitespace_around_statement() {
let html_1 = r#"{% for num in [1, 2, 3] %}{% spaceless %}
<span>{{ num }}</span>
{% endspaceless %}{% endfor %}
"#;
let html_2 = r#"{% for num in [1, 2, 3] %}
{% spaceless %}
<span>{{ num }}</span>
{% endspaceless %}
{% endfor %}
"#;
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(html_1))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(html_2))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
);
}
#[test]
fn pochoir_once_attribute() {
let html_script_component =
r#"<h1>Copied!</h1><script pochoir-once>alert("hello");</script>"#;
let html = "<script-component></script-component><script-component></script-component>";
assert_eq!(
compile("default", &mut Context::new(), |name| {
if name == "default" {
Ok(ComponentFile::new_inline(html))
} else if name == "script-component" {
Ok(ComponentFile::new_inline(html_script_component))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r#"<h1>Copied!</h1><script>alert("hello");</script><h1>Copied!</h1>"#,
);
}
#[test]
fn pochoir_doc_example() {
let mut context = Context::new();
context.insert("fruits", vec!["Banana", "Orange", "Apple"]);
context.insert("selected_fruit", "Orange");
let html = r#"<select>
{% for fruit in fruits %}
<option selected="{{ fruit == selected_fruit }}">{{ fruit }}</option>
{% endfor %}
</select>"#;
assert_eq!(
compile("default", &mut context, |name| {
if name == "default" {
Ok(ComponentFile::new_inline(html))
} else {
Err(component_not_found(name))
}
})
.unwrap(),
r#"<select>
<option>Banana</option>
<option selected>Orange</option>
<option>Apple</option>
</select>"#,
);
}
}