diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 7598546ef..eade1bf6f 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,5 +1,5 @@ github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: Sunng +patreon: open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel @@ -7,4 +7,3 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl liberapay: Sunng issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username -custom: ['https://www.buymeacoffee.com/Sunng'] diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..1332f8eb5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: +- package-ecosystem: cargo + directory: "/" + schedule: + interval: daily + time: "13:00" + open-pull-requests-limit: 10 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000..4563e2148 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,74 @@ +name: CI +on: + schedule: [{cron: "0 13 * * *"}] + push: + branches: + - master + pull_request: + +jobs: + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + build: [stable, beta, nightly, macos, windows] + include: + - build: stable + os: ubuntu-latest + rust: stable + - build: beta + os: ubuntu-latest + rust: beta + - build: nightly + os: ubuntu-latest + rust: nightly + - build: macos + os: macos-latest + rust: stable + - build: windows + os: windows-latest + rust: stable + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + components: clippy + override: true + - name: Lint + run: cargo clippy --all-features + - name: Build and run tests + run: cargo test --all-features + + rustfmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + components: rustfmt + override: true + - run: cargo fmt -- --check + + msrv: + name: MSRV + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: "1.56" + override: true + - run: cargo build --all-features + + security_audit: + name: rust_audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.travis.yml b/.travis.yml index 5e1054ae4..fdddff315 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,16 +11,24 @@ matrix: before_script: - rustup component add clippy script: - - cargo clippy -- -D warnings + - cargo clippy --all-features -- -D warnings + - rust: stable + name: fmt-check + before_script: + - rustup component add rustfmt + script: + - cargo fmt -- --check - rust: beta - rust: stable - - rust: 1.32.0 + - rust: 1.45.0 script: - cargo build - rust: nightly name: minimal-versions script: - cargo -Z minimal-versions build --verbose + - rust: nightly + os: windows script: - cargo $TASK --verbose diff --git a/CHANGELOG.md b/CHANGELOG.md index 237a1195d..d98290362 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,116 @@ # Change Log +## [4.2.2](https://github.com/sunng87/handlebars-rust/compare/4.2.1...4.2.2) - 2022-03-09 + +* [Fixed] Block param scope leaked into partials [#496] +* [Changed] Use Rust 2021 edition and update MSRV to 1.56 + +## [4.2.1](https://github.com/sunng87/handlebars-rust/compare/4.2.0...4.2.1) - 2022-01-17 + +* [Fixed] Nested partial `@partial-block` referencing issue [#488] +* [Fixed] Docs generation on docs.rs for `rust-embed` feature + +## [4.2.0](https://github.com/sunng87/handlebars-rust/compare/4.1.6...4.2.0) - 2022-01-05 + +* [Added] RustEmbed support for loading templates from [#484] +* [Fixed] Parser support for variables begins with digit [#479] +* [Changed] Keep indent whitespaces for partial expression `{{> + partial}}` as default in handlebarsjs. A new option `prevent_indent` + is provided on `Handlebars` to turn off this behaviour. [#486] +* [Changed] Update MSRV to 1.51 due to dependency changes + +## [4.1.6](https://github.com/sunng87/handlebars-rust/compare/4.1.5...4.1.6) - 2021-12-03 + +* [Added] Create `Context` from owned `serde_json::Value` [#477] + +## [4.1.5](https://github.com/sunng87/handlebars-rust/compare/4.1.4...4.1.5) - 2021-11-17 + +* [Fixed] Single-quote string literal is supported, again [#475] + +## [4.1.4](https://github.com/sunng87/handlebars-rust/compare/4.1.3...4.1.4) - 2021-11-06 + +* [Fixed] Corrected empty line stripping strategy [#473] + +## [4.1.3](https://github.com/sunng87/handlebars-rust/compare/4.1.2...4.1.3) - 2021-09-10 + +* [Added] `@last` variable for `each` block with object [#466] +* [Fixed] Missing whitespaces behind expression [#468] + +## [4.1.2](https://github.com/sunng87/handlebars-rust/compare/4.1.1...4.1.2) - 2021-08-11 + +* [Added] Support for generic types in `handlebars_helper!`. +* [Added] Getter and setter for rhai `Engine` from registry. +* [Fixed] Improve doc for `dev_mode` that it has to be enabled before + adding templates. + +## [4.1.1](https://github.com/sunng87/handlebars-rust/compare/4.1.0...4.1.1) - 2021-07-31 + +* [Changed] Update rhai to 1.0 [#455] +* [Fixed] Empty line stripping for partial include statement, and other corner cases [#458] + +## [4.1.0](https://github.com/sunng87/handlebars-rust/compare/4.0.1...4.1.0) - 2021-07-05 + +* [Added] export `StringOutput` as requested in #442 +* [Changed] strict mode now applies to our helper macro `handlebars_helper!` and + built-in helpers based on it. +* [Fixed] Line stripping feature for standalone statment introduced in #404 is now + aligned with handlebarsjs. #448 + +## [4.0.1](https://github.com/sunng87/handlebars-rust/compare/4.0.0...4.0.1) - 2021-06-15 + +* [Fixed] Each block render error with empty array or object [#445] + +## [4.0.0](https://github.com/sunng87/handlebars-rust/compare/3.4.0...4.0.0) - 2021-05-25 + +* [Added] `dev_mode` for registry: templates and scripts loaded from file are always + reloaded when dev mode enabled [#395] +* [Added] Registry is now `Clone` [#395] +* [Added] New built-in helper `len` [#421] +* [Changed] Updated `rhai` to 0.19 and then 0.20 [#391] +* [Changed] `#each` helper now renders else block for non-iterable data [#380] +* [Changed] `TemplateError` and `ScriptError` is now a cause of `RenderError` [#395] +* [Changed] Empty lines around block helpers are now stripped [#404] +* [Changed] **Breaking** `RenderContext::get_partial` now returns `Option<&Template>` +* [Changed] **Breaking** Capitalize names like `HtmlExpression` and `IoError` based on clippy recommendations [#424] +* [Changed] **Breaking** Improved return type of `call_inner` from `HelperDef` to avoid misleading [#437] +* [Fixed] reference starts with `null`, `true` and `false` were parsed incorrectly [#382] +* [Fixed] dir source path separator bug on windows [#389] [#405] +* [Fixed] stack overflow with nested `@partial-block` [#401] +* [Fixed] value access issue when upper block has a base value [#419] +* [Fixed] escape rules for Json string literal [#423] +* [Fixed] **Breaking** zero-arity subexpressions support [#433] + Zero-arity subexpression no longer resolved as variable. The behaviour is now aligned with handlebarsjs. + For instance, `{{(parent)}}` can no longer access `parent` field of the context object, use + `{{lookup this "parent"}}` instead. This change applies to partial inclusion, too. +* [Removed] **Breaking** option to disable source map is removed [#395] +* [Removed] **Breaking** `TemplateFileError` and `TemplateRenderError` are removed and merged into + `TemplateError` and `RenderError` [#395] + +## [3.5.5](https://github.com/sunng87/handlebars-rust/compare/3.5.4...3.5.5) - 2021-05-03 + +* [Fixed] Panic on reporting invalid tag name [#427] + +## [3.5.4](https://github.com/sunng87/handlebars-rust/compare/3.5.3...3.5.4) - 2021-03-27 + +* [Fixed] Json string literal with escape char [#422] + +## [3.5.3](https://github.com/sunng87/handlebars-rust/compare/3.5.2...3.5.3) - 2021-02-20 + +* [Fixed] value access issue when upper block has a base value [#419] + +## [3.5.2](https://github.com/sunng87/handlebars-rust/compare/3.5.1...3.5.2) - 2020-12-29 + +* [Fixed] allow `/` as trailing separator on Windows, backported from master [#405] + +## [3.5.1](https://github.com/sunng87/handlebars-rust/compare/3.5.0...3.5.1) - 2020-10-25 + +* [Fixed] dir source path separator bug on windows [#389] + +## [3.5.0](https://github.com/sunng87/handlebars-rust/compare/3.4.0...3.5.0) - 2020-09-23 + +* [Changed] `#each` helper now renders else block for non-iterable data [#380] +* [Fixed] reference starts with `null`, `true` and `false` were parsed incorrectly [#382] + ## [3.4.0](https://github.com/sunng87/handlebars-rust/compare/3.3.0...3.4.0) - 2020-08-14 * [Added] Debug log that can be turned on by using envlog or other implementation, to trace data resolution during rendering [#369] diff --git a/Cargo.toml b/Cargo.toml index 74f618f6c..65f514b75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,16 @@ [package] name = "handlebars" -version = "3.4.1-alpha.0" +version = "4.2.2" authors = ["Ning Sun "] description = "Handlebars templating implemented in Rust." license = "MIT" keywords = ["handlebars", "templating", "web"] -categories = ["template-engine"] +categories = ["template-engine", "web-programming"] homepage = "https://github.com/sunng87/handlebars-rust" repository = "https://github.com/sunng87/handlebars-rust" documentation = "https://docs.rs/crate/handlebars/" readme = "README.md" -edition = "2018" +edition = "2021" [lib] name = "handlebars" @@ -28,15 +28,20 @@ pest_derive = "2.1.0" serde = "1.0.0" serde_json = "1.0.39" walkdir = { version = "2.2.3", optional = true } -rhai = { version = "0.18.1", optional = true, features = ["sync", "serde"] } +rhai = { version = "1", optional = true, features = ["sync", "serde"] } +rust-embed = { version = "6.3.0", optional = true } [dev-dependencies] -env_logger = "0.7.1" +env_logger = "0.9" maplit = "1.0.0" serde_derive = "1.0.75" tempfile = "3.0.0" criterion = "0.3" -pprof = { version = "0.3.13", features = ["flamegraph", "protobuf"] } +tiny_http = "0.11" +time = { version = "0.3.7", features = ["serde", "formatting", "parsing"]} + +[target.'cfg(unix)'.dev-dependencies] +pprof = { version = "0.7", features = ["flamegraph", "protobuf"] } [features] dir_source = ["walkdir"] @@ -52,4 +57,5 @@ name = "bench" harness = false [package.metadata.docs.rs] -all-features = true +features = ["dir_source", "script_helper", "rust-embed"] +rustdoc-args = ["--cfg", "docsrs"] diff --git a/README.md b/README.md index a65c7a1e0..eee72e870 100644 --- a/README.md +++ b/README.md @@ -8,24 +8,21 @@ Handlebars-rust is the template engine that renders the official Rust website [rust-lang.org](https://www.rust-lang.org), [its book](https://doc.rust-lang.org/book/). -[![Build Status](https://travis-ci.org/sunng87/handlebars-rust.svg?branch=master)](https://travis-ci.org/sunng87/handlebars-rust) -[![](https://meritbadge.herokuapp.com/handlebars)](https://crates.io/crates/handlebars) +[![CI](https://github.com/sunng87/handlebars-rust/actions/workflows/main.yml/badge.svg)](https://github.com/sunng87/handlebars-rust/actions/workflows/main.yml) +[![](https://img.shields.io/crates/v/handlebars)](https://crates.io/crates/handlebars) [![](https://img.shields.io/crates/d/handlebars.svg)](https://crates.io/crates/handlebars) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) [![Docs](https://docs.rs/handlebars/badge.svg)](https://docs.rs/crate/handlebars/) +![rustc](https://img.shields.io/badge/rustc-1.56+-lightgray.svg) [![Donate](https://img.shields.io/badge/donate-liberapay-yellow.svg)](https://liberapay.com/Sunng/donate) -[![Donate](https://img.shields.io/badge/donate-buymeacoffee-yellow.svg)](https://www.buymeacoffee.com/Sunng) ## Getting Started ### Quick Start ```rust -extern crate handlebars; -#[macro_use] -extern crate serde_json; - use handlebars::Handlebars; +use serde_json::json; fn main() -> Result<(), Box> { let mut reg = Handlebars::new(); @@ -38,6 +35,7 @@ fn main() -> Result<(), Box> { // register template using given name reg.register_template_string("tpl_1", "Good afternoon, {{name}}")?; println!("{}", reg.render("tpl_1", &json!({"name": "foo"}))?); + Ok(()) } ``` @@ -48,37 +46,34 @@ If you are not familiar with [handlebars language syntax](https://handlebarsjs.com), it is recommended to walk through their introduction first. -Check the `render` example in the source tree. The example shows you how -to: - -* Create a `Handlebars` registry and register the template from files; -* Create a custom Helper with closure or struct implementing - `HelperDef`, and register it; -* Define and prepare some data; -* Render it; - -Run `cargo run --example render` to see results -(or `RUST_LOG=handlebars=info cargo run --example render` for logging -output). - -Checkout `examples/` for more concrete demos of the current API. - +Examples are provided in source tree to demo usage of various api. + +* [quick](https://github.com/sunng87/handlebars-rust/blob/master/examples/quick.rs) + the very basic example of registry and render apis +* [render](https://github.com/sunng87/handlebars-rust/blob/master/examples/render.rs) + how to define custom helpers with function, trait impl or macro, and also how + to use custom helpers. +* [render_file](https://github.com/sunng87/handlebars-rust/blob/master/examples/render_file.rs) + similar to render, but render to file instead of string +* [partials](https://github.com/sunng87/handlebars-rust/blob/master/examples/partials.rs) + template inheritance with handlebars +* [decorator](https://github.com/sunng87/handlebars-rust/blob/master/examples/decorator.rs) + how to use decorator to change data or define custom helper +* [script](https://github.com/sunng87/handlebars-rust/blob/master/examples/script.rs) + how to define custom helper with rhai scripting language, + just like using javascript for handlebarsjs +* [error](https://github.com/sunng87/handlebars-rust/blob/master/examples/error.rs) + simple case for error +* [dev_mode](https://github.com/sunng87/handlebars-rust/blob/master/examples/dev_mode.rs) + a web server hosts handlebars in `dev_mode`, you can edit the template and see the change + without restarting your server. ## Minimum Rust Version Policy Handlebars will track Rust nightly and stable channel. When dropping -support for previous stable versions, I will bump **minor** version +support for previous stable versions, I will bump **patch** version and clarify in CHANGELOG. -### Rust compatibility table - -| Handlebars version range | Minimum Rust version | -| --- | --- | -| ~3.0.0 | 1.32 | -| ~2.0.0 | 1.32 | -| ~1.1.0 | 1.30 | -| ~1.0.0 | 1.23 | - ## Document [Rust doc](https://docs.rs/crate/handlebars/). @@ -99,14 +94,6 @@ I'm always looking for maintainers to work together on this library, let me know (via email or anywhere in the issue tracker) if you want to join. -## Donations - -I'm now accepting donations on [liberapay](https://liberapay.com/Sunng/donate) -and [buymeacoffee](https://www.buymeacoffee.com/Sunng) if you find my -work helpful and want to keep it going. - -[![buymeacoffee](https://www.buymeacoffee.com/assets/img/guidelines/download-assets-3.svg)](https://www.buymeacoffee.com/Sunng) - ## Why (this) Handlebars? Handlebars is a real-world templating system that you can use to build @@ -146,6 +133,10 @@ And using it in your template: {{hex 16}} ``` +By default, handlebars-rust ships [additional helpers](https://github.com/sunng87/handlebars-rust/blob/master/src/helpers/helper_extras.rs#L6) +(compared with original js version) +that is useful when working with `if`. + With `script_helper` feature flag enabled, you can also create helpers using [rhai](https://github.com/jonathandturner/rhai) script, just like JavaScript for handlebars-js. This feature was in early stage. Its API was limited at the @@ -155,7 +146,7 @@ moment, and can change in future. Every time I look into a templating system, I will investigate its support for [template -inheritance](https://docs.djangoproject.com/en/1.9/ref/templates/language/#template-inheritance). +inheritance](https://docs.djangoproject.com/en/3.2/ref/templates/language/#template-inheritance). Template include is not sufficient for template reuse. In most cases you will need a skeleton of page as parent (header, footer, etc.), and @@ -164,6 +155,11 @@ embed your page into this parent. You can find a real example of template inheritance in `examples/partials.rs` and templates used by this file. +#### Auto-reload in dev mode + +By turning on `dev_mode`, handlebars auto reloads any template and scripts that +loaded from files or directory. This can be handy for template development. + #### WebAssembly compatible Handlebars 3.0 can be used in WebAssembly projects. @@ -178,7 +174,7 @@ Handlebars 3.0 can be used in WebAssembly projects. example](https://github.com/seanmonstar/warp/blob/master/examples/handlebars_template.rs) * Tower-web: [Built-in](https://github.com/carllerche/tower-web) * Actix: [handlebars - example](https://github.com/actix/examples/blob/master/template_handlebars/src/main.rs) + example](https://github.com/actix/examples/blob/master/template_engines/handlebars/src/main.rs) * Tide: [tide-handlebars](https://github.com/No9/tide-handlebars) ### Adopters diff --git a/benches/bench.rs b/benches/bench.rs index b24e8c696..f5665c754 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -3,23 +3,32 @@ extern crate criterion; #[macro_use] extern crate serde_derive; +use criterion::Criterion; +use handlebars::{to_json, Context, Handlebars, Template}; +use serde_json::value::Value as Json; use std::collections::BTreeMap; -use std::fs::{create_dir_all, File}; -use std::io::Write; -use std::path::Path; +#[cfg(unix)] use criterion::profiler::Profiler; -use criterion::Criterion; -use handlebars::{to_json, Context, Handlebars, Template}; +#[cfg(unix)] use pprof::protos::Message; +#[cfg(unix)] use pprof::ProfilerGuard; -use serde_json::value::Value as Json; +#[cfg(unix)] +use std::fs::{create_dir_all, File}; +#[cfg(unix)] +use std::io::Write; +#[cfg(unix)] +use std::path::Path; + +#[cfg(unix)] #[derive(Default)] struct CpuProfiler<'a> { guard: Option>, } +#[cfg(unix)] impl<'a> Profiler for CpuProfiler<'a> { fn start_profiling(&mut self, _benchmark_id: &str, benchmark_dir: &Path) { create_dir_all(&benchmark_dir).unwrap(); @@ -47,6 +56,7 @@ impl<'a> Profiler for CpuProfiler<'a> { } } +#[cfg(unix)] fn profiled() -> Criterion { Criterion::default().with_profiler(CpuProfiler::default()) } @@ -205,10 +215,22 @@ fn large_nested_loop(c: &mut Criterion) { }); } +#[cfg(unix)] criterion_group!( name = benches; config = profiled(); targets = parse_template, render_template, large_loop_helper, large_loop_helper_with_context_creation, - large_nested_loop + large_nested_loop ); + +#[cfg(not(unix))] +criterion_group!( + benches, + parse_template, + render_template, + large_loop_helper, + large_loop_helper_with_context_creation, + large_nested_loop +); + criterion_main!(benches); diff --git a/examples/decorator/template.hbs b/examples/decorator/template.hbs index c21a40395..5b965182b 100644 --- a/examples/decorator/template.hbs +++ b/examples/decorator/template.hbs @@ -7,12 +7,12 @@

CSL {{year}}

    - {{#each teams as |t| ~}} + {{#each teams as |t|}}
  • {{~log @index~}} {{t.name}}: {{format t.pts ~}}
  • - {{/each~}} + {{/each}}
{{*set version="v3.0"}} diff --git a/examples/dev_mode.rs b/examples/dev_mode.rs new file mode 100644 index 000000000..3256eed37 --- /dev/null +++ b/examples/dev_mode.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use handlebars::Handlebars; +use serde_json::json; +use tiny_http::{Response, Server}; + +fn handlebars() -> Handlebars<'static> { + let mut reg = Handlebars::new(); + // enable dev mode for template reloading + reg.set_dev_mode(true); + // register a template from the file + // modified the file after the server starts to see things changing + reg.register_template_file("tpl", "./examples/dev_mode/template.hbs") + .unwrap(); + + reg +} + +fn main() { + let hbs = Arc::new(handlebars()); + + let server = Server::http("127.0.0.1:3030").expect("Failed to start demo server."); + println!("Edit ./examples/dev_mode/template.hbs and request http://localhost:3030 to see the change on the fly."); + + for req in server.incoming_requests() { + let result = hbs + .render("tpl", &json!({"model": "t14s", "brand": "Thinkpad"})) + .unwrap_or_else(|e| e.to_string()); + req.respond(Response::from_string(result)).unwrap(); + } +} diff --git a/examples/dev_mode/template.hbs b/examples/dev_mode/template.hbs new file mode 100644 index 000000000..19bb80657 --- /dev/null +++ b/examples/dev_mode/template.hbs @@ -0,0 +1,8 @@ + + + My Laptop + + +

My current laptop is {{brand}}: {{model}}

+ + diff --git a/examples/error.rs b/examples/error.rs index dcfcd0bc0..3fb874e2b 100644 --- a/examples/error.rs +++ b/examples/error.rs @@ -1,5 +1,6 @@ extern crate env_logger; extern crate handlebars; +#[macro_use] extern crate serde_json; use std::error::Error; @@ -11,14 +12,29 @@ fn main() -> Result<(), Box> { let mut handlebars = Handlebars::new(); // template not found - handlebars - .register_template_file("notfound", "./examples/error/notfound.hbs") - .unwrap_or_else(|e| println!("{}", e)); + println!( + "{}", + handlebars + .register_template_file("notfound", "./examples/error/notfound.hbs") + .unwrap_err() + ); - // an invalid template - handlebars - .register_template_file("error", "./examples/error/error.hbs") - .unwrap_or_else(|e| println!("{}", e)); + // an invalid templat + println!( + "{}", + handlebars + .register_template_file("error", "./examples/error/error.hbs") + .unwrap_err() + ); + + // render error + let e1 = handlebars + .render_template("{{#if}}", &json!({})) + .unwrap_err(); + let be1 = Box::new(e1); + println!("{}", be1); + println!("{}", be1.source().unwrap()); + println!("{:?}", be1.source().unwrap().source()); Ok(()) } diff --git a/examples/error/template.hbs b/examples/error/template.hbs index eefc5388a..9ed5b7bde 100644 --- a/examples/error/template.hbs +++ b/examples/error/template.hbs @@ -5,13 +5,13 @@

CSL {{year}}

    - {{#each teams as |t| ~}} + {{#each teams as |t|}} {{! ranking_label will produce a render error when first parameter is not a number }}
  • {{~log @index~}} {{t.name}}: {{format t.pts ~}}
  • - {{/each~}} + {{/each}}

Rendered by Handlebars from {{engine}} data.

diff --git a/examples/partials/base0.hbs b/examples/partials/base0.hbs index 63360e2af..34b38e763 100644 --- a/examples/partials/base0.hbs +++ b/examples/partials/base0.hbs @@ -2,6 +2,6 @@ {{title}}

Derived from base0.hbs

- {{~> page}} + {{> page}} diff --git a/examples/partials/base1.hbs b/examples/partials/base1.hbs index 3eddcd763..8f771dfad 100644 --- a/examples/partials/base1.hbs +++ b/examples/partials/base1.hbs @@ -2,6 +2,6 @@ {{title}}

Derived from base1.hbs

- {{~> page}} + {{> page}} diff --git a/examples/partials/template2.hbs b/examples/partials/template2.hbs index 091f242c8..934416eb7 100644 --- a/examples/partials/template2.hbs +++ b/examples/partials/template2.hbs @@ -1,5 +1,4 @@ {{#*inline "page"}}

Rendered in partial, parent is {{parent}}

{{/inline}} -{{! remove whitespaces with ~ }} -{{~> (parent)~}} +{{> (lookup this "parent")}} diff --git a/examples/render/template.hbs b/examples/render/template.hbs index b6817adb1..2a62d014f 100644 --- a/examples/render/template.hbs +++ b/examples/render/template.hbs @@ -5,12 +5,12 @@

CSL {{year}}

    - {{#each teams as |t| ~}} + {{#each teams as |t|}}
  • {{~log @index~}} {{t.name}}: {{format t.pts ~}}
  • - {{/each~}} + {{/each}}

Rendered by Handlebars from {{engine}} data.

diff --git a/examples/render_file.rs b/examples/render_file.rs index c0a39c17d..7b7599672 100644 --- a/examples/render_file.rs +++ b/examples/render_file.rs @@ -129,9 +129,12 @@ fn main() -> Result<(), Box> { let data = make_data(); - let mut source_template = File::open(&"./examples/render_file/template.hbs")?; + handlebars + .register_template_file("template", "./examples/render_file/template.hbs") + .unwrap(); + let mut output_file = File::create("target/table.html")?; - handlebars.render_template_source_to_write(&mut source_template, &data, &mut output_file)?; + handlebars.render_to_write("template", &data, &mut output_file)?; println!("target/table.html generated"); Ok(()) } diff --git a/examples/render_file/template.hbs b/examples/render_file/template.hbs index b6817adb1..84a0d6bc0 100644 --- a/examples/render_file/template.hbs +++ b/examples/render_file/template.hbs @@ -5,12 +5,13 @@

CSL {{year}}

    - {{#each teams as |t| ~}} + {{#each teams as |t| }}
  • {{~log @index~}} + {{!-- I'm comment --}} {{t.name}}: {{format t.pts ~}}
  • - {{/each~}} + {{/each}}

Rendered by Handlebars from {{engine}} data.

diff --git a/examples/script/goals.rhai b/examples/script/goals.rhai index e53373b08..65828202e 100644 --- a/examples/script/goals.rhai +++ b/examples/script/goals.rhai @@ -1,3 +1,3 @@ let goals = params[0]; -len(goals) +goals.len() diff --git a/examples/script/template.hbs b/examples/script/template.hbs index cfcb0d2cc..c04253c72 100644 --- a/examples/script/template.hbs +++ b/examples/script/template.hbs @@ -1,10 +1,10 @@ Bundesliga Match Day -{{#each this as |match| ~}} +{{#each this as |match|}} {{#each match as |team|}} {{team.name}} - {{score team.goals}} {{#each team.goals as |scorer|}} > {{scorer}} {{/each}} - {{~ /each}} + {{/each}} --- {{/each}} diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 000000000..572e03bdf --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ + +target +corpus +artifacts diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 000000000..3d41d053e --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,26 @@ + +[package] +name = "handlebars-fuzz" +version = "0.0.0" +authors = ["Automatically generated"] +publish = false +edition = "2018" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.handlebars] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[[bin]] +name = "render_template_no_data" +path = "fuzz_targets/render_template_no_data.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/render_template_no_data.rs b/fuzz/fuzz_targets/render_template_no_data.rs new file mode 100644 index 000000000..5342e42a2 --- /dev/null +++ b/fuzz/fuzz_targets/render_template_no_data.rs @@ -0,0 +1,8 @@ +#![no_main] +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &str| { + let tpl = handlebars::Handlebars::new(); + + let _ = tpl.render_template(&data, &Vec::::new()); +}); diff --git a/release.toml b/release.toml index e0f365aa1..6ee7a8383 100644 --- a/release.toml +++ b/release.toml @@ -1,4 +1,6 @@ sign-commit = true +sign-tag = true +dev-version = true pre-release-replacements = [ {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", prerelease=false}, {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", prerelease=false}, diff --git a/src/block.rs b/src/block.rs index 6bbc2b083..1c520bf5b 100644 --- a/src/block.rs +++ b/src/block.rs @@ -3,6 +3,7 @@ use std::collections::BTreeMap; use serde_json::value::Value as Json; use crate::error::RenderError; +use crate::local_vars::LocalVars; #[derive(Clone, Debug)] pub enum BlockParamHolder { @@ -60,12 +61,11 @@ pub struct BlockContext<'reg> { base_path: Vec, /// the base_value of current block scope, when the block is using a /// constant or derived value as block base - /// FIXME: we use owned json temporarily to avoid lifetime issue base_value: Option, /// current block context variables block_params: BlockParams<'reg>, /// local variables in current context - local_variables: BTreeMap, + local_variables: LocalVars, } impl<'reg> BlockContext<'reg> { @@ -75,13 +75,13 @@ impl<'reg> BlockContext<'reg> { } /// set a local variable into current scope - pub fn set_local_var(&mut self, name: String, value: Json) { - self.local_variables.insert(name, value); + pub fn set_local_var(&mut self, name: &str, value: Json) { + self.local_variables.put(name, value); } /// get a local variable from current scope pub fn get_local_var(&self, name: &str) -> Option<&Json> { - self.local_variables.get(&format!("@{}", name)) + self.local_variables.get(name) } /// borrow a reference to current scope's base path diff --git a/src/cli.rs b/src/cli.rs index af83b7f32..571900da2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -13,12 +13,12 @@ fn usage() -> ! { } fn parse_json(text: &str) -> Json { - let text = if text.starts_with('@') { - fs::read_to_string(&text[1..]).unwrap() + let result = if let Some(text) = text.strip_prefix('@') { + fs::read_to_string(text).unwrap() } else { text.to_owned() }; - match Json::from_str(&text) { + match Json::from_str(&result) { Ok(json) => json, Err(_) => usage(), } diff --git a/src/context.rs b/src/context.rs index b1c6bccdd..b8f6af1b4 100644 --- a/src/context.rs +++ b/src/context.rs @@ -36,7 +36,7 @@ fn parse_json_visitor<'a, 'reg>( relative_path: &[PathSeg], block_contexts: &'a VecDeque>, always_for_absolute_path: bool, -) -> Result, RenderError> { +) -> ResolvedPath<'a> { let mut path_context_depth: i64 = 0; let mut with_block_param = None; let mut from_root = false; @@ -45,7 +45,7 @@ fn parse_json_visitor<'a, 'reg>( for path_seg in relative_path { match path_seg { PathSeg::Named(the_path) => { - if let Some((holder, base_path)) = get_in_block_params(&block_contexts, the_path) { + if let Some((holder, base_path)) = get_in_block_params(block_contexts, the_path) { with_block_param = Some((holder, base_path)); } break; @@ -65,7 +65,7 @@ fn parse_json_visitor<'a, 'reg>( match with_block_param { Some((BlockParamHolder::Value(ref value), _)) => { merge_json_path(&mut path_stack, &relative_path[1..]); - Ok(ResolvedPath::BlockParamValue(path_stack, value)) + ResolvedPath::BlockParamValue(path_stack, value) } Some((BlockParamHolder::Path(ref paths), base_path)) => { extend(&mut path_stack, base_path); @@ -74,40 +74,41 @@ fn parse_json_visitor<'a, 'reg>( } merge_json_path(&mut path_stack, &relative_path[1..]); - Ok(ResolvedPath::AbsolutePath(path_stack)) + ResolvedPath::AbsolutePath(path_stack) } None => { if path_context_depth > 0 { - if let Some(ref context_base_path) = block_contexts + let blk = block_contexts .get(path_context_depth as usize) - .map(|blk| blk.base_path()) - { - extend(&mut path_stack, context_base_path); + .or_else(|| block_contexts.front()); + + if let Some(base_value) = blk.and_then(|blk| blk.base_value()) { + merge_json_path(&mut path_stack, relative_path); + ResolvedPath::LocalValue(path_stack, base_value) } else { - // TODO: is this correct behaviour? - if let Some(ref base_path) = block_contexts.front().map(|blk| blk.base_path()) { + if let Some(base_path) = blk.map(|blk| blk.base_path()) { extend(&mut path_stack, base_path); } + merge_json_path(&mut path_stack, relative_path); + ResolvedPath::AbsolutePath(path_stack) } - merge_json_path(&mut path_stack, relative_path); - Ok(ResolvedPath::AbsolutePath(path_stack)) } else if from_root { merge_json_path(&mut path_stack, relative_path); - Ok(ResolvedPath::AbsolutePath(path_stack)) + ResolvedPath::AbsolutePath(path_stack) } else if always_for_absolute_path { if let Some(base_value) = block_contexts.front().and_then(|blk| blk.base_value()) { merge_json_path(&mut path_stack, relative_path); - Ok(ResolvedPath::LocalValue(path_stack, base_value)) + ResolvedPath::LocalValue(path_stack, base_value) } else { if let Some(base_path) = block_contexts.front().map(|blk| blk.base_path()) { extend(&mut path_stack, base_path); } merge_json_path(&mut path_stack, relative_path); - Ok(ResolvedPath::AbsolutePath(path_stack)) + ResolvedPath::AbsolutePath(path_stack) } } else { merge_json_path(&mut path_stack, relative_path); - Ok(ResolvedPath::RelativePath(path_stack)) + ResolvedPath::RelativePath(path_stack) } } } @@ -137,14 +138,14 @@ fn get_in_block_params<'a, 'reg>( None } -pub(crate) fn merge_json(base: &Json, addition: &HashMap<&&str, &Json>) -> Json { +pub(crate) fn merge_json(base: &Json, addition: &HashMap<&str, &Json>) -> Json { let mut base_map = match base { Json::Object(ref m) => m.clone(), _ => Map::new(), }; for (k, v) in addition.iter() { - base_map.insert((*(*k)).to_string(), (*v).clone()); + base_map.insert(k.to_string(), (*v).clone()); } Json::Object(base_map) @@ -170,10 +171,7 @@ impl Context { block_contexts: &VecDeque>, ) -> Result, RenderError> { // always use absolute at the moment until we get base_value lifetime issue fixed - let resolved_visitor = parse_json_visitor(&relative_path, block_contexts, true)?; - - // debug logging - debug!("Accessing context value: {:?}", resolved_visitor); + let resolved_visitor = parse_json_visitor(relative_path, block_contexts, true); match resolved_visitor { ResolvedPath::AbsolutePath(paths) => { @@ -222,6 +220,12 @@ impl Context { } } +impl From for Context { + fn from(data: Json) -> Context { + Context { data } + } +} + #[cfg(test)] mod test { use crate::block::{BlockContext, BlockParams}; @@ -333,7 +337,7 @@ mod test { let s = "hello".to_owned(); let mut hash = HashMap::new(); let v = value::to_json("h1"); - hash.insert(&"tag", &v); + hash.insert("tag", &v); let ctx_a1 = Context::wraps(&context::merge_json(&map, &hash)).unwrap(); assert_eq!( diff --git a/src/decorators/inline.rs b/src/decorators/inline.rs index cbad5d820..373abbc7b 100644 --- a/src/decorators/inline.rs +++ b/src/decorators/inline.rs @@ -7,12 +7,13 @@ use crate::render::{Decorator, RenderContext}; #[derive(Clone, Copy)] pub struct InlineDecorator; -fn get_name<'reg: 'rc, 'rc>(d: &'rc Decorator<'reg, 'rc>) -> Result<&'rc str, RenderError> { +fn get_name<'reg: 'rc, 'rc>(d: &Decorator<'reg, 'rc>) -> Result { d.param(0) .ok_or_else(|| RenderError::new("Param required for decorator \"inline\"")) .and_then(|v| { v.value() .as_str() + .map(|v| v.to_owned()) .ok_or_else(|| RenderError::new("inline name must be string")) }) } @@ -31,7 +32,7 @@ impl DecoratorDef for InlineDecorator { .template() .ok_or_else(|| RenderError::new("inline should have a block"))?; - rc.set_partial(name.to_owned(), template); + rc.set_partial(name, template); Ok(()) } } @@ -47,11 +48,10 @@ mod test { #[test] fn test_inline() { - let t0 = Template::compile( - "{{#*inline \"hello\"}}the hello world inline partial.{{/inline}}".to_string(), - ) - .ok() - .unwrap(); + let t0 = + Template::compile("{{#*inline \"hello\"}}the hello world inline partial.{{/inline}}") + .ok() + .unwrap(); let hbs = Registry::new(); diff --git a/src/error.rs b/src/error.rs index 6b905b3ac..f4721623f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,13 +12,14 @@ use walkdir::Error as WalkdirError; use rhai::{EvalAltResult, ParseError}; /// Error when rendering data on template. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct RenderError { pub desc: String, pub template_name: Option, pub line_no: Option, pub column_no: Option, cause: Option>, + unimplemented: bool, } impl fmt::Display for RenderError { @@ -27,9 +28,7 @@ impl fmt::Display for RenderError { (Some(line), Some(col)) => write!( f, "Error rendering \"{}\" line {}, col {}: {}", - self.template_name - .as_ref() - .unwrap_or(&"Unnamed template".to_owned(),), + self.template_name.as_deref().unwrap_or("Unnamed template"), line, col, self.desc @@ -41,38 +40,53 @@ impl fmt::Display for RenderError { impl Error for RenderError { fn source(&self) -> Option<&(dyn Error + 'static)> { - self.cause.as_ref().map(|e| &**e as &(dyn Error + 'static)) + self.cause + .as_ref() + .map(|e| e.as_ref() as &(dyn Error + 'static)) } } impl From for RenderError { fn from(e: IOError) -> RenderError { - RenderError::from_error("Error on output generation.", e) + RenderError::from_error("Cannot generate output.", e) } } impl From for RenderError { fn from(e: SerdeError) -> RenderError { - RenderError::from_error("Error when accessing JSON data.", e) + RenderError::from_error("Failed to access JSON data.", e) } } impl From for RenderError { fn from(e: FromUtf8Error) -> RenderError { - RenderError::from_error("Error on bytes generation.", e) + RenderError::from_error("Failed to generate bytes.", e) } } impl From for RenderError { fn from(e: ParseIntError) -> RenderError { - RenderError::from_error("Error on accessing array/vector with string index.", e) + RenderError::from_error("Cannot access array/vector with string index.", e) + } +} + +impl From for RenderError { + fn from(e: TemplateError) -> RenderError { + RenderError::from_error("Failed to parse template.", e) } } #[cfg(feature = "script_helper")] impl From> for RenderError { fn from(e: Box) -> RenderError { - RenderError::from_error("Error on converting data to Rhai dynamic.", e) + RenderError::from_error("Cannot convert data to Rhai dynamic", e) + } +} + +#[cfg(feature = "script_helper")] +impl From for RenderError { + fn from(e: ScriptError) -> RenderError { + RenderError::from_error("Failed to load rhai script", e) } } @@ -80,10 +94,14 @@ impl RenderError { pub fn new>(desc: T) -> RenderError { RenderError { desc: desc.as_ref().to_owned(), - template_name: None, - line_no: None, - column_no: None, - cause: None, + ..Default::default() + } + } + + pub(crate) fn unimplemented() -> RenderError { + RenderError { + unimplemented: true, + ..Default::default() } } @@ -95,31 +113,25 @@ impl RenderError { RenderError::new(&msg) } - #[deprecated] - pub fn with(cause: E) -> RenderError + pub fn from_error(error_info: &str, cause: E) -> RenderError where E: Error + Send + Sync + 'static, { - let mut e = RenderError::new(cause.to_string()); + let mut e = RenderError::new(error_info); e.cause = Some(Box::new(cause)); e } - pub fn from_error(error_kind: &str, cause: E) -> RenderError - where - E: Error + Send + Sync + 'static, - { - let mut e = RenderError::new(format!("{}: {}", error_kind, cause.to_string())); - e.cause = Some(Box::new(cause)); - - e + #[inline] + pub(crate) fn is_unimplemented(&self) -> bool { + self.unimplemented } } quick_error! { /// Template parsing error - #[derive(PartialEq, Debug, Clone)] + #[derive(Debug)] pub enum TemplateErrorReason { MismatchingClosedHelper(open: String, closed: String) { display("helper {:?} was opened, but {:?} is closing", @@ -138,11 +150,18 @@ quick_error! { NestedSubexpression { display("nested subexpression is not supported") } + IoError(err: IOError, name: String) { + display("Template \"{}\": {}", name, err) + } + #[cfg(feature = "dir_source")] + WalkdirError(err: WalkdirError) { + display("Walk dir error: {}", err) + } } } /// Error on parsing template. -#[derive(Debug, PartialEq)] +#[derive(Debug)] pub struct TemplateError { pub reason: TemplateErrorReason, pub template_name: Option, @@ -177,6 +196,20 @@ impl TemplateError { impl Error for TemplateError {} +impl From<(IOError, String)> for TemplateError { + fn from(err_info: (IOError, String)) -> TemplateError { + let (e, name) = err_info; + TemplateError::of(TemplateErrorReason::IoError(e, name)) + } +} + +#[cfg(feature = "dir_source")] +impl From for TemplateError { + fn from(e: WalkdirError) -> TemplateError { + TemplateError::of(TemplateErrorReason::WalkdirError(e)) + } +} + fn template_segment(template_str: &str, line: usize, col: usize) -> String { let range = 3; let line_start = if line >= range { line - range } else { 0 }; @@ -190,12 +223,12 @@ fn template_segment(template_str: &str, line: usize, col: usize) -> String { buf.push_str(" |"); for c in 0..line_content.len() { if c != col { - buf.push_str("-"); + buf.push('-'); } else { - buf.push_str("^"); + buf.push('^'); } } - buf.push_str("\n"); + buf.push('\n'); } } } @@ -223,69 +256,11 @@ impl fmt::Display for TemplateError { } } -quick_error! { - /// A combined error type for `TemplateError` and `IOError` - #[derive(Debug)] - pub enum TemplateFileError { - TemplateError(err: TemplateError) { - from() - source(err) - display("{}", err) - } - IOError(err: IOError, name: String) { - source(err) - display("Template \"{}\": {}", name, err) - } - } -} - -#[cfg(feature = "dir_source")] -impl From for TemplateFileError { - fn from(error: WalkdirError) -> TemplateFileError { - let path_string: String = error - .path() - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_default(); - TemplateFileError::IOError(IOError::from(error), path_string) - } -} - -quick_error! { - /// A combined error type for `TemplateError`, `IOError` and `RenderError` - #[derive(Debug)] - pub enum TemplateRenderError { - TemplateError(err: TemplateError) { - from() - source(err) - display("{}", err) - } - RenderError(err: RenderError) { - from() - source(err) - display("{}", err) - } - IOError(err: IOError, name: String) { - source(err) - display("Template \"{}\": {}", name, err) - } - } -} - -impl TemplateRenderError { - pub fn as_render_error(&self) -> Option<&RenderError> { - if let TemplateRenderError::RenderError(ref e) = *self { - Some(&e) - } else { - None - } - } -} - #[cfg(feature = "script_helper")] quick_error! { #[derive(Debug)] pub enum ScriptError { - IOError(err: IOError) { + IoError(err: IOError) { from() source(err) } diff --git a/src/grammar.pest b/src/grammar.pest index cc3f52177..6abf620a5 100644 --- a/src/grammar.pest +++ b/src/grammar.pest @@ -1,5 +1,5 @@ WHITESPACE = _{ " "|"\t"|"\n"|"\r" } -keywords = @{ "as" | "else" } +keywords = { "as" | "else" } escape = @{ ("\\" ~ "{{" ~ "{{"?) | ("\\" ~ "\\"+ ~ &"{{") } raw_text = ${ ( escape | (!"{{" ~ ANY) )+ } @@ -12,17 +12,28 @@ literal = { string_literal | null_literal | boolean_literal } -null_literal = { "null" } -boolean_literal = { "true"|"false" } -number_literal = @{ "-"? ~ ASCII_DIGIT+ ~ "."? ~ ASCII_DIGIT* -~ ("E" ~ "-"? ~ ASCII_DIGIT+)? } -string_literal = @{ ("\"" ~ (!"\"" ~ ("\\\"" | ANY))* ~ "\"") | ("'" ~ (!"'" ~ ("\\'" | ANY))* ~ "'") } +null_literal = @{ "null" ~ !symbol_char } +boolean_literal = @{ ("true"|"false") ~ !symbol_char } +number_literal = @{ "-"? ~ ASCII_DIGIT+ ~ "."? ~ ASCII_DIGIT* ~ ("E" ~ "-"? ~ ASCII_DIGIT+)? ~ !symbol_char } +json_char_double_quote = { + !("\"" | "\\") ~ ANY + | "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t") + | "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4}) +} +json_char_single_quote = { + !("'" | "\\") ~ ANY + | "\\" ~ ("'" | "\\" | "/" | "b" | "f" | "n" | "r" | "t") + | "\\" ~ ("u" ~ ASCII_HEX_DIGIT{4}) +} +string_inner_double_quote = @{ json_char_double_quote* } +string_inner_single_quote = @{ json_char_single_quote* } +string_literal = ${ ("\"" ~ string_inner_double_quote ~ "\"") | ("'" ~ string_inner_single_quote ~ "'") } array_literal = { "[" ~ literal? ~ ("," ~ literal)* ~ "]" } object_literal = { "{" ~ (string_literal ~ ":" ~ literal)? ~ ("," ~ string_literal ~ ":" ~ literal)* ~ "}" } -symbol_char = _{'a'..'z'|'A'..'Z'|ASCII_DIGIT|"-"|"_"|"$"|'\u{80}'..'\u{7ff}'|'\u{800}'..'\u{ffff}'|'\u{10000}'..'\u{10ffff}'} -partial_symbol_char = _{'a'..'z'|'A'..'Z'|ASCII_DIGIT|"-"|"_"|'\u{80}'..'\u{7ff}'|'\u{800}'..'\u{ffff}'|'\u{10000}'..'\u{10ffff}'|"/"|"."} +symbol_char = _{ASCII_ALPHANUMERIC|"-"|"_"|"$"|'\u{80}'..'\u{7ff}'|'\u{800}'..'\u{ffff}'|'\u{10000}'..'\u{10ffff}'} +partial_symbol_char = _{ASCII_ALPHANUMERIC|"-"|"_"|'\u{80}'..'\u{7ff}'|'\u{800}'..'\u{ffff}'|'\u{10000}'..'\u{10ffff}'|"/"|"."} path_char = _{ "/" } identifier = @{ symbol_char+ } @@ -45,7 +56,8 @@ pro_whitespace_omitter = { "~" } expression = { !invert_tag ~ "{{" ~ pre_whitespace_omitter? ~ ((identifier ~ (hash|param)+) | name ) ~ pro_whitespace_omitter? ~ "}}" } -html_expression_triple_bracket = _{ "{{{" ~ pre_whitespace_omitter? ~ name ~ +html_expression_triple_bracket = _{ "{{{" ~ pre_whitespace_omitter? ~ + ((identifier ~ (hash|param)+) | name ) ~ pro_whitespace_omitter? ~ "}}}" } amp_expression = _{ "{{" ~ pre_whitespace_omitter? ~ "&" ~ name ~ pro_whitespace_omitter? ~ "}}" } @@ -55,9 +67,10 @@ decorator_expression = { "{{" ~ pre_whitespace_omitter? ~ "*" ~ exp_line ~ pro_whitespace_omitter? ~ "}}" } partial_expression = { "{{" ~ pre_whitespace_omitter? ~ ">" ~ partial_exp_line ~ pro_whitespace_omitter? ~ "}}" } + invert_tag_item = { "else"|"^" } invert_tag = { !escape ~ "{{" ~ pre_whitespace_omitter? ~ invert_tag_item - ~ pro_whitespace_omitter? ~ "}}"} + ~ pro_whitespace_omitter? ~ "}}" } helper_block_start = { "{{" ~ pre_whitespace_omitter? ~ "#" ~ exp_line ~ pro_whitespace_omitter? ~ "}}" } helper_block_end = { "{{" ~ pre_whitespace_omitter? ~ "/" ~ identifier ~ @@ -84,7 +97,7 @@ raw_block_end = { "{{{{" ~ pre_whitespace_omitter? ~ "/" ~ identifier ~ pro_whitespace_omitter? ~ "}}}}" } raw_block = _{ raw_block_start ~ raw_block_text ~ raw_block_end } -hbs_comment = { "{{!--" ~ (!"--}}" ~ ANY)* ~ "--}}" } +hbs_comment = { "{{!" ~ "--" ~ (!"--}}" ~ ANY)* ~ "--" ~ "}}" } hbs_comment_compact = { "{{!" ~ (!"}}" ~ ANY)* ~ "}}" } template = { ( diff --git a/src/grammar.rs b/src/grammar.rs index d493f06a0..d2f4150fd 100644 --- a/src/grammar.rs +++ b/src/grammar.rs @@ -4,6 +4,38 @@ #[grammar = "grammar.pest"] pub struct HandlebarsParser; +#[inline] +pub(crate) fn whitespace_matcher(c: char) -> bool { + c == ' ' || c == '\t' +} + +#[inline] +pub(crate) fn newline_matcher(c: char) -> bool { + c == '\n' || c == '\r' +} + +#[inline] +pub(crate) fn strip_first_newline(s: &str) -> &str { + if let Some(s) = s.strip_prefix("\r\n") { + s + } else if let Some(s) = s.strip_prefix('\n') { + s + } else { + s + } +} + +pub(crate) fn ends_with_empty_line(text: &str) -> bool { + let s = text.trim_end_matches(whitespace_matcher); + // also matches when text is just whitespaces + s.ends_with(newline_matcher) || s.is_empty() +} + +pub(crate) fn starts_with_empty_line(text: &str) -> bool { + text.trim_start_matches(whitespace_matcher) + .starts_with(newline_matcher) +} + #[cfg(test)] mod test { use super::{HandlebarsParser, Rule}; @@ -84,6 +116,7 @@ mod test { "this.[$id]", "[$id]", "$id", + "this.[null]", ]; for i in s.iter() { assert_rule!(Rule::reference, i); @@ -100,7 +133,7 @@ mod test { #[test] fn test_param() { - let s = vec!["hello", "\"json literal\""]; + let s = vec!["hello", "\"json literal\"", "nullable", "truestory"]; for i in s.iter() { assert_rule!(Rule::param, i); } @@ -124,14 +157,13 @@ mod test { let s = vec![ "\"json string\"", "\"quot: \\\"\"", - "'json string'", - "'quot: \\''", "[]", "[\"hello\"]", "[1,2,3,4,true]", "{\"hello\": \"world\"}", "{}", "{\"a\":1, \"b\":2 }", + "\"nullable\"", ]; for i in s.iter() { assert_rule!(Rule::literal, i); @@ -143,7 +175,8 @@ mod test { let s = vec!["{{!-- {{this.title}} - --}}"]; + --}}", + "{{! -- good --}}"]; for i in s.iter() { assert_rule!(Rule::hbs_comment, i); } @@ -170,6 +203,8 @@ mod test { "{{exp 1}}", "{{exp \"literal\"}}", "{{exp \"literal with space\"}}", + "{{exp 'literal with space'}}", + r#"{{exp "literal with escape \\\\"}}"#, "{{exp ref}}", "{{exp (sub)}}", "{{exp (sub 123)}}", @@ -177,6 +212,8 @@ mod test { "{{exp {}}}", "{{exp key=1}}", "{{exp key=ref}}", + "{{exp key='literal with space'}}", + "{{exp key=\"literal with space\"}}", "{{exp key=(sub)}}", "{{exp key=(sub 0)}}", "{{exp key=(sub 0 key=1)}}", @@ -196,7 +233,14 @@ mod test { #[test] fn test_html_expression() { - let s = vec!["{{{html}}}", "{{{(html)}}}", "{{{(html)}}}", "{{&html}}"]; + let s = vec![ + "{{{html}}}", + "{{{(html)}}}", + "{{{(html)}}}", + "{{&html}}", + "{{{html 1}}}", + "{{{html p=true}}}", + ]; for i in s.iter() { assert_rule!(Rule::html_expression, i); } @@ -289,6 +333,7 @@ mod test { "./[/foo]", "[foo]", "@root/a/b", + "nullable", ]; for i in s.iter() { assert_rule_match!(Rule::path, i); diff --git a/src/helpers/block_util.rs b/src/helpers/block_util.rs index b1ac70d37..6971fdd8a 100644 --- a/src/helpers/block_util.rs +++ b/src/helpers/block_util.rs @@ -1,10 +1,9 @@ use crate::block::BlockContext; -use crate::error::RenderError; use crate::json::value::PathAndJson; pub(crate) fn create_block<'reg: 'rc, 'rc>( param: &'rc PathAndJson<'reg, 'rc>, -) -> Result, RenderError> { +) -> BlockContext<'reg> { let mut block = BlockContext::new(); if let Some(new_path) = param.context_path() { @@ -14,5 +13,5 @@ pub(crate) fn create_block<'reg: 'rc, 'rc>( block.set_base_value(param.value().clone()); } - Ok(block) + block } diff --git a/src/helpers/helper_each.rs b/src/helpers/helper_each.rs index 9958bbd93..e4276a74c 100644 --- a/src/helpers/helper_each.rs +++ b/src/helpers/helper_each.rs @@ -5,7 +5,7 @@ use crate::block::{BlockContext, BlockParams}; use crate::context::Context; use crate::error::RenderError; use crate::helpers::{HelperDef, HelperResult}; -use crate::json::value::{to_json, JsonTruthy}; +use crate::json::value::to_json; use crate::output::Output; use crate::registry::Registry; use crate::render::{Helper, RenderContext, Renderable}; @@ -18,7 +18,7 @@ fn update_block_context<'reg>( is_first: bool, value: &Json, ) { - if let Some(ref p) = base_path { + if let Some(p) = base_path { if is_first { *block.base_path_mut() = copy_on_push_vec(p, relative_path); } else if let Some(ptr) = block.base_path_mut().last_mut() { @@ -79,9 +79,11 @@ impl HelperDef for EachHelper { let template = h.template(); match template { - Some(t) => match (value.value().is_truthy(false), value.value()) { - (true, &Json::Array(ref list)) => { - let block_context = create_block(&value)?; + Some(t) => match *value.value() { + Json::Array(ref list) + if !list.is_empty() || (list.is_empty() && h.inverse().is_none()) => + { + let block_context = create_block(value); rc.push_block(block_context); let len = list.len(); @@ -94,12 +96,12 @@ impl HelperDef for EachHelper { let is_last = i == len - 1; let index = to_json(i); - block.set_local_var("@first".to_string(), to_json(is_first)); - block.set_local_var("@last".to_string(), to_json(is_last)); - block.set_local_var("@index".to_string(), index.clone()); + block.set_local_var("first", to_json(is_first)); + block.set_local_var("last", to_json(is_last)); + block.set_local_var("index", index.clone()); - update_block_context(block, array_path, i.to_string(), is_first, &v); - set_block_param(block, h, array_path, &index, &v)?; + update_block_context(block, array_path, i.to_string(), is_first, v); + set_block_param(block, h, array_path, &index, v)?; } t.render(r, ctx, rc, out)?; @@ -108,44 +110,45 @@ impl HelperDef for EachHelper { rc.pop_block(); Ok(()) } - (true, &Json::Object(ref obj)) => { - let block_context = create_block(&value)?; + Json::Object(ref obj) + if !obj.is_empty() || (obj.is_empty() && h.inverse().is_none()) => + { + let block_context = create_block(value); rc.push_block(block_context); - let mut is_first = true; + let len = obj.len(); + let obj_path = value.context_path(); - for (k, v) in obj.iter() { + for (i, (k, v)) in obj.iter().enumerate() { if let Some(ref mut block) = rc.block_mut() { - let key = to_json(k); + let is_first = i == 0usize; + let is_last = i == len - 1; - block.set_local_var("@first".to_string(), to_json(is_first)); - block.set_local_var("@key".to_string(), key.clone()); + let key = to_json(k); + block.set_local_var("first", to_json(is_first)); + block.set_local_var("last", to_json(is_last)); + block.set_local_var("key", key.clone()); - update_block_context(block, obj_path, k.to_string(), is_first, &v); - set_block_param(block, h, obj_path, &key, &v)?; + update_block_context(block, obj_path, k.to_string(), is_first, v); + set_block_param(block, h, obj_path, &key, v)?; } t.render(r, ctx, rc, out)?; - - if is_first { - is_first = false; - } } rc.pop_block(); Ok(()) } - (false, _) => { + _ => { if let Some(else_template) = h.inverse() { - else_template.render(r, ctx, rc, out)?; + else_template.render(r, ctx, rc, out) + } else if r.strict_mode() { + Err(RenderError::strict_error(value.relative_path())) + } else { + Ok(()) } - Ok(()) } - _ => Err(RenderError::new(format!( - "Param type is not iterable: {:?}", - value.value() - ))), }, None => Ok(()), } @@ -162,6 +165,19 @@ mod test { use std::collections::BTreeMap; use std::str::FromStr; + #[test] + fn test_empty_each() { + let mut hbs = Registry::new(); + hbs.set_strict_mode(true); + + let data = json!({ + "a": [ ], + }); + + let template = "{{#each a}}each{{/each}}"; + assert_eq!(hbs.render_template(template, &data).unwrap(), ""); + } + #[test] fn test_each() { let mut handlebars = Registry::new(); @@ -172,7 +188,10 @@ mod test { ) .is_ok()); assert!(handlebars - .register_template_string("t1", "{{#each this}}{{@first}}|{{@key}}:{{this}}|{{/each}}") + .register_template_string( + "t1", + "{{#each this}}{{@first}}|{{@last}}|{{@key}}:{{this}}|{{/each}}" + ) .is_ok()); let r0 = handlebars.render("t0", &vec![1u16, 2u16, 3u16]); @@ -183,9 +202,13 @@ mod test { let mut m: BTreeMap = BTreeMap::new(); m.insert("ftp".to_string(), 21); + m.insert("gopher".to_string(), 70); m.insert("http".to_string(), 80); let r1 = handlebars.render("t1", &m); - assert_eq!(r1.ok().unwrap(), "true|ftp:21|false|http:80|".to_string()); + assert_eq!( + r1.ok().unwrap(), + "true|false|ftp:21|false|false|gopher:70|false|true|http:80|".to_string() + ); } #[test] @@ -479,4 +502,99 @@ mod test { let rendered = reg.render_template(template, &input).unwrap(); assert_eq!("01", rendered); } + + #[test] + fn test_non_iterable() { + let reg = Registry::new(); + let template = "{{#each this}}each block{{else}}else block{{/each}}"; + let input = json!("strings aren't iterable"); + let rendered = reg.render_template(template, &input).unwrap(); + assert_eq!("else block", rendered); + } + + #[test] + fn test_recursion() { + let mut reg = Registry::new(); + assert!(reg + .register_template_string( + "walk", + "(\ + {{#each this}}\ + {{#if @key}}{{@key}}{{else}}{{@index}}{{/if}}: \ + {{this}} \ + {{> walk this}}, \ + {{/each}}\ + )", + ) + .is_ok()); + + let input = json!({ + "array": [42, {"wow": "cool"}, [[]]], + "object": { "a": { "b": "c", "d": ["e"] } }, + "string": "hi" + }); + let expected_output = "(\ + array: [42, [object], [[], ], ] (\ + 0: 42 (), \ + 1: [object] (wow: cool (), ), \ + 2: [[], ] (0: [] (), ), \ + ), \ + object: [object] (\ + a: [object] (\ + b: c (), \ + d: [e, ] (0: e (), ), \ + ), \ + ), \ + string: hi (), \ + )"; + + let rendered = reg.render("walk", &input).unwrap(); + assert_eq!(expected_output, rendered); + } + + #[test] + fn test_strict_each() { + let mut reg = Registry::new(); + + assert!(reg + .render_template("{{#each data}}{{/each}}", &json!({})) + .is_ok()); + assert!(reg + .render_template("{{#each data}}{{/each}}", &json!({"data": 24})) + .is_ok()); + + reg.set_strict_mode(true); + + assert!(reg + .render_template("{{#each data}}{{/each}}", &json!({})) + .is_err()); + assert!(reg + .render_template("{{#each data}}{{/each}}", &json!({"data": 24})) + .is_err()); + assert!(reg + .render_template("{{#each data}}{{else}}food{{/each}}", &json!({})) + .is_ok()); + assert!(reg + .render_template("{{#each data}}{{else}}food{{/each}}", &json!({"data": 24})) + .is_ok()); + } + + #[test] + fn newline_stripping_for_each() { + let reg = Registry::new(); + + let tpl = r#"
    + {{#each a}} + {{!-- comment --}} +
  • {{this}}
  • + {{/each}} +
"#; + assert_eq!( + r#"
    +
  • 0
  • +
  • 1
  • +
"#, + reg.render_template(tpl, &json!({"a": [0, 1]})).unwrap() + ); + } } diff --git a/src/helpers/helper_boolean.rs b/src/helpers/helper_extras.rs similarity index 72% rename from src/helpers/helper_boolean.rs rename to src/helpers/helper_extras.rs index c58f9fb4e..fe8f7a7d3 100644 --- a/src/helpers/helper_boolean.rs +++ b/src/helpers/helper_extras.rs @@ -1,4 +1,5 @@ //! Helpers for boolean operations +use serde_json::Value as Json; use crate::json::value::JsonTruthy; @@ -11,6 +12,14 @@ handlebars_helper!(lte: |x: i64, y: i64| x <= y); handlebars_helper!(and: |x: Json, y: Json| x.is_truthy(false) && y.is_truthy(false)); handlebars_helper!(or: |x: Json, y: Json| x.is_truthy(false) || y.is_truthy(false)); handlebars_helper!(not: |x: Json| !x.is_truthy(false)); +handlebars_helper!(len: |x: Json| { + match x { + Json::Array(a) => a.len(), + Json::Object(m) => m.len(), + Json::String(s) => s.len(), + _ => 0 + } +}); #[cfg(test)] mod test_conditions { @@ -75,4 +84,29 @@ mod test_conditions { .unwrap(); assert_eq!(&result, "ipsum"); } + + #[test] + fn test_len() { + let handlebars = crate::Handlebars::new(); + + let result = handlebars + .render_template("{{len value}}", &json!({"value": [1,2,3]})) + .unwrap(); + assert_eq!(&result, "3"); + + let result = handlebars + .render_template("{{len value}}", &json!({"value": {"a" :1, "b": 2}})) + .unwrap(); + assert_eq!(&result, "2"); + + let result = handlebars + .render_template("{{len value}}", &json!({"value": "tomcat"})) + .unwrap(); + assert_eq!(&result, "6"); + + let result = handlebars + .render_template("{{len value}}", &json!({"value": 3})) + .unwrap(); + assert_eq!(&result, "0"); + } } diff --git a/src/helpers/helper_if.rs b/src/helpers/helper_if.rs index 3f8f040bd..342c74567 100644 --- a/src/helpers/helper_if.rs +++ b/src/helpers/helper_if.rs @@ -36,7 +36,7 @@ impl HelperDef for IfHelper { let tmpl = if value { h.template() } else { h.inverse() }; match tmpl { - Some(ref t) => t.render(r, ctx, rc, out), + Some(t) => t.render(r, ctx, rc, out), None => Ok(()), } } @@ -126,4 +126,64 @@ mod test { .unwrap() ); } + + #[test] + fn test_invisible_line_stripping() { + let hbs = Registry::new(); + assert_eq!( + "yes\n", + hbs.render_template("{{#if a}}\nyes\n{{/if}}\n", &json!({"a": true})) + .unwrap() + ); + + assert_eq!( + "yes\r\n", + hbs.render_template("{{#if a}}\r\nyes\r\n{{/if}}\r\n", &json!({"a": true})) + .unwrap() + ); + + assert_eq!( + "x\ny", + hbs.render_template("{{#if a}}x{{/if}}\ny", &json!({"a": true})) + .unwrap() + ); + + assert_eq!( + "y\nz", + hbs.render_template("{{#if a}}\nx\n{{^}}\ny\n{{/if}}\nz", &json!({"a": false})) + .unwrap() + ); + + assert_eq!( + r#"yes + foo + bar + baz"#, + hbs.render_template( + r#"yes + {{#if true}} + foo + bar + {{/if}} + baz"#, + &json!({}) + ) + .unwrap() + ); + + assert_eq!( + r#" foo + bar + baz"#, + hbs.render_template( + r#" {{#if true}} + foo + bar + {{/if}} + baz"#, + &json!({}) + ) + .unwrap() + ); + } } diff --git a/src/helpers/helper_lookup.rs b/src/helpers/helper_lookup.rs index 87f9c5a8a..bf887debe 100644 --- a/src/helpers/helper_lookup.rs +++ b/src/helpers/helper_lookup.rs @@ -14,10 +14,10 @@ impl HelperDef for LookupHelper { fn call_inner<'reg: 'rc, 'rc>( &self, h: &Helper<'reg, 'rc>, - _: &'reg Registry<'reg>, + r: &'reg Registry<'reg>, _: &'rc Context, _: &mut RenderContext<'reg, 'rc>, - ) -> Result>, RenderError> { + ) -> Result, RenderError> { let collection_value = h .param(0) .ok_or_else(|| RenderError::new("Param not found for helper \"lookup\""))?; @@ -30,15 +30,19 @@ impl HelperDef for LookupHelper { .value() .as_u64() .and_then(|u| v.get(u as usize)) - .map(|i| ScopedJson::Derived(i.clone())), + .unwrap_or(&Json::Null), Json::Object(ref m) => index .value() .as_str() .and_then(|k| m.get(k)) - .map(|i| ScopedJson::Derived(i.clone())), - _ => None, + .unwrap_or(&Json::Null), + _ => &Json::Null, }; - Ok(value) + if r.strict_mode() && value.is_null() { + Err(RenderError::strict_error(None)) + } else { + Ok(value.clone().into()) + } } } @@ -80,4 +84,21 @@ mod test { let r2 = handlebars.render("t2", &m2); assert_eq!(r2.ok().unwrap(), "world".to_string()); } + + #[test] + fn test_strict_lookup() { + let mut hbs = Registry::new(); + + assert_eq!( + hbs.render_template("{{lookup kk 1}}", &json!({"kk": []})) + .unwrap(), + "" + ); + + hbs.set_strict_mode(true); + + assert!(hbs + .render_template("{{lookup kk 1}}", &json!({"kk": []})) + .is_err()); + } } diff --git a/src/helpers/helper_with.rs b/src/helpers/helper_with.rs index 4b68b783e..dee387c0c 100644 --- a/src/helpers/helper_with.rs +++ b/src/helpers/helper_with.rs @@ -24,11 +24,8 @@ impl HelperDef for WithHelper { .param(0) .ok_or_else(|| RenderError::new("Param not found for helper \"with\""))?; - let not_empty = param.value().is_truthy(false); - let template = if not_empty { h.template() } else { h.inverse() }; - - if not_empty { - let mut block = create_block(¶m)?; + if param.value().is_truthy(false) { + let mut block = create_block(param); if let Some(block_param) = h.block_param() { let mut params = BlockParams::new(); @@ -42,17 +39,20 @@ impl HelperDef for WithHelper { } rc.push_block(block); - } - let result = match template { - Some(t) => t.render(r, ctx, rc, out), - None => Ok(()), - }; + if let Some(t) = h.template() { + t.render(r, ctx, rc, out)?; + }; - if not_empty { rc.pop_block(); + Ok(()) + } else if let Some(t) = h.inverse() { + t.render(r, ctx, rc, out) + } else if r.strict_mode() { + Err(RenderError::strict_error(param.relative_path())) + } else { + Ok(()) } - result } } @@ -246,4 +246,31 @@ mod test { let template = "{{#with (lookup a \"b\")}}{{#with this}}{{c}}{{/with}}{{/with}}"; assert_eq!("d", hb.render_template(template, &data).unwrap()); } + + #[test] + fn test_strict_with() { + let mut hb = Registry::new(); + + assert_eq!( + hb.render_template("{{#with name}}yes{{/with}}", &json!({})) + .unwrap(), + "" + ); + assert_eq!( + hb.render_template("{{#with name}}yes{{else}}no{{/with}}", &json!({})) + .unwrap(), + "no" + ); + + hb.set_strict_mode(true); + + assert!(hb + .render_template("{{#with name}}yes{{/with}}", &json!({})) + .is_err()); + assert_eq!( + hb.render_template("{{#with name}}yes{{else}}no{{/with}}", &json!({})) + .unwrap(), + "no" + ); + } } diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs index 8d1c2d7d6..ff5fa2495 100644 --- a/src/helpers/mod.rs +++ b/src/helpers/mod.rs @@ -32,7 +32,8 @@ pub type HelperResult = Result<(), RenderError>; /// ``` /// use handlebars::*; /// -/// fn upper(h: &Helper<'_, '_>, _: &Handlebars<'_>, _: &Context, rc: &mut RenderContext<'_, '_>, out: &mut Output) +/// fn upper(h: &Helper<'_, '_>, _: &Handlebars<'_>, _: &Context, rc: +/// &mut RenderContext<'_, '_>, out: &mut dyn Output) /// -> HelperResult { /// // get parameter from helper or throw an error /// let param = h.param(0).and_then(|v| v.value().as_str()).unwrap_or(""); @@ -74,18 +75,42 @@ pub type HelperResult = Result<(), RenderError>; /// hbs.register_helper("plus", Box::new(plus)); /// ``` /// - pub trait HelperDef { + /// A simplified api to define helper + /// + /// To implement your own `call_inner`, you will return a new `ScopedJson` + /// which has a JSON value computed from current context. + /// + /// ### Calling from subexpression + /// + /// When calling the helper as a subexpression, the value and its type can + /// be received by upper level helpers. + /// + /// Note that the value can be `json!(null)` which is treated as `false` in + /// helpers like `if` and rendered as empty string. fn call_inner<'reg: 'rc, 'rc>( &self, _: &Helper<'reg, 'rc>, _: &'reg Registry<'reg>, _: &'rc Context, _: &mut RenderContext<'reg, 'rc>, - ) -> Result>, RenderError> { - Ok(None) + ) -> Result, RenderError> { + Err(RenderError::unimplemented()) } + /// A complex version of helper interface. + /// + /// This function offers `Output`, which you can write custom string into + /// and render child template. Helpers like `if` and `each` are implemented + /// with this. Because the data written into `Output` are typically without + /// type information. So helpers defined by this function are not composable. + /// + /// ### Calling from subexpression + /// + /// Although helpers defined by this are not composable, when called from + /// subexpression, handlebars tries to parse the string output as JSON to + /// re-build its type. This can be buggy with numrical and other literal values. + /// So it is not recommended to use these helpers in subexpression. fn call<'reg: 'rc, 'rc>( &self, h: &Helper<'reg, 'rc>, @@ -94,17 +119,26 @@ pub trait HelperDef { rc: &mut RenderContext<'reg, 'rc>, out: &mut dyn Output, ) -> HelperResult { - if let Some(result) = self.call_inner(h, r, ctx, rc)? { - if r.strict_mode() && result.is_missing() { - return Err(RenderError::strict_error(None)); - } else { - // auto escape according to settings - let output = do_escape(r, rc, result.render()); - out.write(output.as_ref())?; + match self.call_inner(h, r, ctx, rc) { + Ok(result) => { + if r.strict_mode() && result.is_missing() { + Err(RenderError::strict_error(None)) + } else { + // auto escape according to settings + let output = do_escape(r, rc, result.render()); + out.write(output.as_ref())?; + Ok(()) + } + } + Err(e) => { + if e.is_unimplemented() { + // default implementation, do nothing + Ok(()) + } else { + Err(e) + } } } - - Ok(()) } } @@ -132,8 +166,8 @@ impl< } mod block_util; -pub(crate) mod helper_boolean; mod helper_each; +pub(crate) mod helper_extras; mod helper_if; mod helper_log; mod helper_lookup; diff --git a/src/helpers/scripting.rs b/src/helpers/scripting.rs index d111ab4f3..cec3b9763 100644 --- a/src/helpers/scripting.rs +++ b/src/helpers/scripting.rs @@ -7,23 +7,22 @@ use crate::json::value::{PathAndJson, ScopedJson}; use crate::registry::Registry; use crate::render::{Helper, RenderContext}; -use rhai::de::from_dynamic; -use rhai::ser::to_dynamic; +use rhai::serde::{from_dynamic, to_dynamic}; use rhai::{Dynamic, Engine, Scope, AST}; use serde_json::value::Value as Json; -pub struct ScriptHelper { +pub(crate) struct ScriptHelper { pub(crate) script: AST, } #[inline] fn call_script_helper<'reg: 'rc, 'rc>( - params: &Vec>, + params: &[PathAndJson<'reg, 'rc>], hash: &BTreeMap<&'reg str, PathAndJson<'reg, 'rc>>, engine: &Engine, script: &AST, -) -> Result>, RenderError> { +) -> Result, RenderError> { let params: Dynamic = to_dynamic(params.iter().map(|p| p.value()).collect::>())?; let hash: Dynamic = to_dynamic( @@ -42,7 +41,7 @@ fn call_script_helper<'reg: 'rc, 'rc>( let result_json: Json = from_dynamic(&result)?; - Ok(Some(ScopedJson::Derived(result_json))) + Ok(ScopedJson::Derived(result_json)) } impl HelperDef for ScriptHelper { @@ -52,7 +51,7 @@ impl HelperDef for ScriptHelper { reg: &'reg Registry<'reg>, _ctx: &'rc Context, _rc: &mut RenderContext<'reg, 'rc>, - ) -> Result>, RenderError> { + ) -> Result, RenderError> { call_script_helper(h.params(), h.hash(), ®.engine, &self.script) } } @@ -105,7 +104,6 @@ mod test { }; let result = call_script_helper(¶ms, &hash, &engine, &ast) - .unwrap() .unwrap() .render(); assert_eq!("1,true,2,no", &result); diff --git a/src/json/path.rs b/src/json/path.rs index a6ba472d8..8270371b2 100644 --- a/src/json/path.rs +++ b/src/json/path.rs @@ -74,7 +74,7 @@ fn get_local_path_and_level(paths: &[PathSeg]) -> Option<(usize, String)> { paths.get(0).and_then(|seg| { if seg == &PathSeg::Ruled(Rule::path_local) { let mut level = 0; - while paths[level + 1] == PathSeg::Ruled(Rule::path_up) { + while paths.get(level + 1)? == &PathSeg::Ruled(Rule::path_up) { level += 1; } if let Some(PathSeg::Named(name)) = paths.get(level + 1) { @@ -124,7 +124,7 @@ where path_stack } -pub(crate) fn merge_json_path<'a>(path_stack: &mut Vec, relative_path: &'a [PathSeg]) { +pub(crate) fn merge_json_path(path_stack: &mut Vec, relative_path: &[PathSeg]) { for seg in relative_path { match seg { PathSeg::Named(ref s) => { diff --git a/src/json/value.rs b/src/json/value.rs index c5a4384d1..863eab0b8 100644 --- a/src/json/value.rs +++ b/src/json/value.rs @@ -34,10 +34,7 @@ impl<'reg: 'rc, 'rc> ScopedJson<'reg, 'rc> { } pub fn is_missing(&self) -> bool { - match self { - ScopedJson::Missing => true, - _ => false, - } + matches!(self, ScopedJson::Missing) } pub fn into_derived(self) -> ScopedJson<'reg, 'rc> { diff --git a/src/lib.rs b/src/lib.rs index 84c842053..2453e4b8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ -#![doc(html_root_url = "https://docs.rs/handlebars/3.4.0")] +#![doc(html_root_url = "https://docs.rs/handlebars/4.2.2")] +#![cfg_attr(docsrs, feature(doc_cfg))] //! # Handlebars //! //! [Handlebars](http://handlebarsjs.com/) is a modern and extensible templating solution originally created in the JavaScript world. It's used by many popular frameworks like [Ember.js](http://emberjs.com) and Chaplin. It's also ported to some other platforms such as [Java](https://github.com/jknack/handlebars.java). @@ -53,19 +54,26 @@ //! //! ### Extensible helper system //! -//! You can write your own helper with Rust! It can be a block helper or -//! inline helper. Put your logic into the helper and don't repeat -//! yourself. +//! Helper is the control system of handlebars language. In the original JavaScript +//! version, you can implement your own helper with JavaScript. +//! +//! Handlebars-rust offers similar mechanism that custom helper can be defined with +//! rust function, or [rhai](https://github.com/jonathandturner/rhai) script. //! //! The built-in helpers like `if` and `each` were written with these //! helper APIs and the APIs are fully available to developers. //! +//! ### Auto-reload in dev mode +//! +//! By turning on `dev_mode`, handlebars auto reloads any template and scripts that +//! loaded from files or directory. This can be handy for template development. +//! //! ### Template inheritance //! //! Every time I look into a templating system, I will investigate its //! support for [template inheritance][t]. //! -//! [t]: https://docs.djangoproject.com/en/1.9/ref/templates/language/#template-inheritance +//! [t]: https://docs.djangoproject.com/en/3.2/ref/templates/language/#template-inheritance //! //! Template include is not sufficient for template reuse. In most cases //! you will need a skeleton of page as parent (header, footer, etc.), and @@ -147,7 +155,7 @@ //! use handlebars::Handlebars; //! use std::collections::BTreeMap; //! -//! # fn main() -> Result<(), Box> { +//! # fn main() -> Result<(), Box> { //! let mut handlebars = Handlebars::new(); //! let source = "hello {{world}}"; //! @@ -158,6 +166,14 @@ //! # } //! ``` //! +//! #### Additional features for loading template from +//! +//! * Feature `dir_source` enables template loading +//! `register_templates_directory` from given directory. +//! * Feature `rust-embed` enables template loading +//! `register_embed_templates` from embedded resources in rust struct +//! generated with `RustEmbed`. +//! //! ### Rendering Something //! //! Since handlebars is originally based on JavaScript type system. It supports dynamic features like duck-typing, truthy/falsey values. But for a static language like Rust, this is a little difficult. As a solution, we are using the `serde_json::value::Value` internally for data rendering. @@ -180,7 +196,7 @@ //! age: i16, //! } //! -//! # fn main() -> Result<(), Box> { +//! # fn main() -> Result<(), Box> { //! let source = "Hello, {{name}}"; //! //! let mut handlebars = Handlebars::new(); @@ -229,7 +245,7 @@ //! //! #### Escaping //! -//! As per the handlebars spec, output using `{{expression}}` is escaped by default (to be precise, the characters `&"<>` are replaced by their respective html / xml entities). However, since the use cases of a rust template engine are probably a bit more diverse than those of a JavaScript one, this implementation allows the user to supply a custom escape function to be used instead. For more information see the `EscapeFn` type and `Handlebars::register_escape_fn()` method. +//! As per the handlebars spec, output using `{{expression}}` is escaped by default (to be precise, the characters `&"<>` are replaced by their respective html / xml entities). However, since the use cases of a rust template engine are probably a bit more diverse than those of a JavaScript one, this implementation allows the user to supply a custom escape function to be used instead. For more information see the `EscapeFn` type and `Handlebars::register_escape_fn()` method. In particular, `no_escape()` can be used as the escape function if no escaping at all should be performed. //! //! ### Custom Helper //! @@ -321,11 +337,16 @@ //! //! * `{{{{raw}}}} ... {{{{/raw}}}}` escape handlebars expression within the block //! * `{{#if ...}} ... {{else}} ... {{/if}}` if-else block +//! (See [the handlebarjs documentation](https://handlebarsjs.com/guide/builtin-helpers.html#if) on how to use this helper.) //! * `{{#unless ...}} ... {{else}} .. {{/unless}}` if-not-else block -//! * `{{#each ...}} ... {{/each}}` iterates over an array or object. Handlebars-rust doesn't support mustache iteration syntax so use this instead. +//! (See [the handlebarjs documentation](https://handlebarsjs.com/guide/builtin-helpers.html#unless) on how to use this helper.) +//! * `{{#each ...}} ... {{/each}}` iterates over an array or object. Handlebars-rust doesn't support mustache iteration syntax so use `each` instead. +//! (See [the handlebarjs documentation](https://handlebarsjs.com/guide/builtin-helpers.html#each) on how to use this helper.) //! * `{{#with ...}} ... {{/with}}` change current context. Similar to `{{#each}}`, used for replace corresponding mustache syntax. +//! (See [the handlebarjs documentation](https://handlebarsjs.com/guide/builtin-helpers.html#with) on how to use this helper.) //! * `{{lookup ... ...}}` get value from array by `@index` or `@key` -//! * `{{> ...}}` include template with name +//! (See [the handlebarjs documentation](https://handlebarsjs.com/guide/builtin-helpers.html#lookup) on how to use this helper.) +//! * `{{> ...}}` include template by its name //! * `{{log ...}}` log value with rust logger, default level: INFO. Currently you cannot change the level. //! * Boolean helpers that can be used in `if` as subexpression, for example `{{#if (gt 2 1)}} ...`: //! * `eq` @@ -337,6 +358,7 @@ //! * `and` //! * `or` //! * `not` +//! * `{{len ...}}` returns length of array/object/string //! //! ### Template inheritance //! @@ -345,7 +367,7 @@ //! //! -#![allow(dead_code)] +#![allow(dead_code, clippy::upper_case_acronyms)] #![warn(rust_2018_idioms)] #![recursion_limit = "200"] @@ -371,11 +393,11 @@ extern crate serde_json; pub use self::block::{BlockContext, BlockParams}; pub use self::context::Context; pub use self::decorators::DecoratorDef; -pub use self::error::{RenderError, TemplateError, TemplateFileError, TemplateRenderError}; +pub use self::error::{RenderError, TemplateError}; pub use self::helpers::{HelperDef, HelperResult}; pub use self::json::path::Path; pub use self::json::value::{to_json, JsonRender, PathAndJson, ScopedJson}; -pub use self::output::Output; +pub use self::output::{Output, StringOutput}; pub use self::registry::{html_escape, no_escape, EscapeFn, Registry as Handlebars}; pub use self::render::{Decorator, Evaluable, Helper, RenderContext, Renderable}; pub use self::template::Template; @@ -392,10 +414,12 @@ mod error; mod grammar; mod helpers; mod json; +mod local_vars; mod output; mod partial; mod registry; mod render; +mod sources; mod support; pub mod template; mod util; diff --git a/src/local_vars.rs b/src/local_vars.rs new file mode 100644 index 000000000..2a34b1f16 --- /dev/null +++ b/src/local_vars.rs @@ -0,0 +1,37 @@ +use std::collections::BTreeMap; + +use serde_json::value::Value as Json; + +#[derive(Default, Debug, Clone)] +pub(crate) struct LocalVars { + first: Option, + last: Option, + index: Option, + key: Option, + + extra: BTreeMap, +} + +impl LocalVars { + pub fn put(&mut self, key: &str, value: Json) { + match key { + "first" => self.first = Some(value), + "last" => self.last = Some(value), + "index" => self.index = Some(value), + "key" => self.key = Some(value), + _ => { + self.extra.insert(key.to_owned(), value); + } + } + } + + pub fn get(&self, key: &str) -> Option<&Json> { + match key { + "first" => self.first.as_ref(), + "last" => self.last.as_ref(), + "index" => self.index.as_ref(), + "key" => self.key.as_ref(), + _ => self.extra.get(key), + } + } +} diff --git a/src/macros.rs b/src/macros.rs index 29babb49d..bd1eab0bb 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -38,7 +38,7 @@ #[macro_export] macro_rules! handlebars_helper { - ($struct_name:ident: |$($name:ident: $tpe:tt),* + ($struct_name:ident: |$($name:ident: $tpe:tt$(<$($gen:ty),+>)?),* $($(,)?{$($hash_name:ident: $hash_tpe:tt=$dft_val:literal),*})? $($(,)?*$args:ident)? $($(,)?**$kwargs:ident)?| @@ -51,25 +51,31 @@ macro_rules! handlebars_helper { fn call_inner<'reg: 'rc, 'rc>( &self, h: &$crate::Helper<'reg, 'rc>, - _: &'reg $crate::Handlebars<'reg>, + r: &'reg $crate::Handlebars<'reg>, _: &'rc $crate::Context, _: &mut $crate::RenderContext<'reg, 'rc>, - ) -> Result>, $crate::RenderError> { + ) -> Result<$crate::ScopedJson<'reg, 'rc>, $crate::RenderError> { let mut param_idx = 0; $( let $name = h.param(param_idx) - .map(|x| x.value()) + .and_then(|x| { + if r.strict_mode() && x.is_value_missing() { + None + } else { + Some(x.value()) + } + }) .ok_or_else(|| $crate::RenderError::new(&format!( "`{}` helper: Couldn't read parameter {}", stringify!($struct_name), stringify!($name), ))) .and_then(|x| - handlebars_helper!(@as_json_value x, $tpe) + handlebars_helper!(@as_json_value x, $tpe$(<$($gen),+>)?) .ok_or_else(|| $crate::RenderError::new(&format!( "`{}` helper: Couldn't convert parameter {} to type `{}`. \ It's {:?} as JSON. Got these params: {:?}", - stringify!($struct_name), stringify!($name), stringify!($tpe), + stringify!($struct_name), stringify!($name), stringify!($tpe$(<$($gen),+>)?), x, h.params(), ))) )?; @@ -97,7 +103,7 @@ macro_rules! handlebars_helper { $(let $kwargs = h.hash().iter().map(|(k, v)| (k.to_owned(), v.value())).collect::>();)? let result = $body; - Ok(Some($crate::ScopedJson::Derived($crate::JsonValue::from(result)))) + Ok($crate::ScopedJson::Derived($crate::JsonValue::from(result))) } } }; @@ -111,64 +117,70 @@ macro_rules! handlebars_helper { (@as_json_value $x:ident, bool) => { $x.as_bool() }; (@as_json_value $x:ident, null) => { $x.as_null() }; (@as_json_value $x:ident, Json) => { Some($x) }; + (@as_json_value $x:ident, $tpe:tt$(<$($gen:ty),+>)?) => { serde_json::from_value::<$tpe$(<$($gen),+>)?>($x.clone()).ok() }; } -/// This macro is defined if the `logging` feature is set. -/// -/// It ignores all logging calls inside the library. #[cfg(feature = "no_logging")] -#[macro_export] -macro_rules! debug { - (target: $target:expr, $($arg:tt)*) => {}; - ($($arg:tt)*) => {}; -} +#[macro_use] +#[doc(hidden)] +pub mod logging { + /// This macro is defined if the `logging` feature is set. + /// + /// It ignores all logging calls inside the library. + #[doc(hidden)] + #[macro_export] + macro_rules! debug { + (target: $target:expr, $($arg:tt)*) => {}; + ($($arg:tt)*) => {}; + } -/// This macro is defined if the `logging` feature is not set. -/// -/// It ignores all logging calls inside the library. -#[cfg(feature = "no_logging")] -#[macro_export] -macro_rules! error { - (target: $target:expr, $($arg:tt)*) => {}; - ($($arg:tt)*) => {}; -} + /// This macro is defined if the `logging` feature is not set. + /// + /// It ignores all logging calls inside the library. + #[doc(hidden)] + #[macro_export] + macro_rules! error { + (target: $target:expr, $($arg:tt)*) => {}; + ($($arg:tt)*) => {}; + } -/// This macro is defined if the `logging` feature is not set. -/// -/// It ignores all logging calls inside the library. -#[cfg(feature = "no_logging")] -#[macro_export] -macro_rules! info { - (target: $target:expr, $($arg:tt)*) => {}; - ($($arg:tt)*) => {}; -} + /// This macro is defined if the `logging` feature is not set. + /// + /// It ignores all logging calls inside the library. + #[doc(hidden)] + #[macro_export] + macro_rules! info { + (target: $target:expr, $($arg:tt)*) => {}; + ($($arg:tt)*) => {}; + } -/// This macro is defined if the `logging` feature is not set. -/// -/// It ignores all logging calls inside the library. -#[cfg(feature = "no_logging")] -#[macro_export] -macro_rules! log { - (target: $target:expr, $($arg:tt)*) => {}; - ($($arg:tt)*) => {}; -} + /// This macro is defined if the `logging` feature is not set. + /// + /// It ignores all logging calls inside the library. + #[doc(hidden)] + #[macro_export] + macro_rules! log { + (target: $target:expr, $($arg:tt)*) => {}; + ($($arg:tt)*) => {}; + } -/// This macro is defined if the `logging` feature is not set. -/// -/// It ignores all logging calls inside the library. -#[cfg(feature = "no_logging")] -#[macro_export] -macro_rules! trace { - (target: $target:expr, $($arg:tt)*) => {}; - ($($arg:tt)*) => {}; -} + /// This macro is defined if the `logging` feature is not set. + /// + /// It ignores all logging calls inside the library. + #[doc(hidden)] + #[macro_export] + macro_rules! trace { + (target: $target:expr, $($arg:tt)*) => {}; + ($($arg:tt)*) => {}; + } -/// This macro is defined if the `logging` feature is not set. -/// -/// It ignores all logging calls inside the library. -#[cfg(feature = "no_logging")] -#[macro_export] -macro_rules! warn { - (target: $target:expr, $($arg:tt)*) => {}; - ($($arg:tt)*) => {}; + /// This macro is defined if the `logging` feature is not set. + /// + /// It ignores all logging calls inside the library. + #[doc(hidden)] + #[macro_export] + macro_rules! warn { + (target: $target:expr, $($arg:tt)*) => {}; + ($($arg:tt)*) => {}; + } } diff --git a/src/output.rs b/src/output.rs index f1c5865a5..67e62b849 100644 --- a/src/output.rs +++ b/src/output.rs @@ -46,3 +46,9 @@ impl StringOutput { String::from_utf8(self.buf) } } + +impl Default for StringOutput { + fn default() -> Self { + StringOutput::new() + } +} diff --git a/src/partial.rs b/src/partial.rs index ae80adec1..401449546 100644 --- a/src/partial.rs +++ b/src/partial.rs @@ -1,7 +1,9 @@ +use std::borrow::Cow; use std::collections::HashMap; use serde_json::value::Value as Json; +use crate::block::BlockContext; use crate::context::{merge_json, Context}; use crate::error::RenderError; use crate::json::path::Path; @@ -10,45 +12,27 @@ use crate::registry::Registry; use crate::render::{Decorator, Evaluable, RenderContext, Renderable}; use crate::template::Template; -fn render_partial<'reg: 'rc, 'rc>( - t: &'reg Template, - d: &Decorator<'reg, 'rc>, +pub(crate) const PARTIAL_BLOCK: &str = "@partial-block"; + +fn find_partial<'reg: 'rc, 'rc: 'a, 'a>( + rc: &'a RenderContext<'reg, 'rc>, r: &'reg Registry<'reg>, - ctx: &'rc Context, - local_rc: &mut RenderContext<'reg, 'rc>, - out: &mut dyn Output, -) -> Result<(), RenderError> { - // partial context path - if let Some(ref param_ctx) = d.param(0) { - if let (Some(p), Some(block)) = (param_ctx.context_path(), local_rc.block_mut()) { - *block.base_path_mut() = p.clone(); - } + d: &Decorator<'reg, 'rc>, + name: &str, +) -> Result>, RenderError> { + if let Some(partial) = rc.get_partial(name) { + return Ok(Some(Cow::Borrowed(partial))); } - // @partial-block - if let Some(t) = d.template() { - local_rc.set_partial("@partial-block".to_owned(), t); + if let Some(tpl) = r.get_or_load_template_optional(name) { + return tpl.map(Option::Some); } - let result = if d.hash().is_empty() { - t.render(r, ctx, local_rc, out) - } else { - let hash_ctx = d - .hash() - .iter() - .map(|(k, v)| (k, v.value())) - .collect::>(); - let current_path = Path::current(); - let partial_context = - merge_json(local_rc.evaluate2(ctx, ¤t_path)?.as_json(), &hash_ctx); - let ctx = Context::wraps(&partial_context)?; - let mut partial_rc = local_rc.new_for_block(); - t.render(r, &ctx, &mut partial_rc, out) - }; - - local_rc.remove_partial("@partial-block"); - - result + if let Some(tpl) = d.template() { + return Ok(Some(Cow::Borrowed(tpl))); + } + + Ok(None) } pub fn expand_partial<'reg: 'rc, 'rc>( @@ -68,22 +52,82 @@ pub fn expand_partial<'reg: 'rc, 'rc>( return Err(RenderError::new("Cannot include self in >")); } - let partial = rc.get_partial(tname); + let partial = find_partial(rc, r, d, tname)?; + + if let Some(t) = partial { + // clone to avoid lifetime issue + // FIXME refactor this to avoid + let mut local_rc = rc.clone(); + + // if tname == PARTIAL_BLOCK + let is_partial_block = tname == PARTIAL_BLOCK; - match partial { - Some(t) => { - let mut local_rc = rc.clone(); - render_partial(&t, d, r, ctx, &mut local_rc, out)?; + // add partial block depth there are consecutive partial + // blocks in the stack. + if is_partial_block { + local_rc.inc_partial_block_depth(); + } else { + // depth cannot be lower than 0, which is guaranted in the + // `dec_partial_block_depth` method + local_rc.dec_partial_block_depth(); } - None => { - if let Some(t) = r.get_template(tname).or_else(|| d.template()) { - let mut local_rc = rc.clone(); - render_partial(t, d, r, ctx, &mut local_rc, out)?; - } + + let mut block_created = false; + + if let Some(base_path) = d.param(0).and_then(|p| p.context_path()) { + // path given, update base_path + let mut block = BlockContext::new(); + *block.base_path_mut() = base_path.to_vec(); + block_created = true; + + // clear blocks to prevent block params from parent + // template to be leaked into partials + local_rc.clear_blocks(); + local_rc.push_block(block); + } else if !d.hash().is_empty() { + let mut block = BlockContext::new(); + // hash given, update base_value + let hash_ctx = d + .hash() + .iter() + .map(|(k, v)| (*k, v.value())) + .collect::>(); + + let merged_context = merge_json( + local_rc.evaluate2(ctx, &Path::current())?.as_json(), + &hash_ctx, + ); + + block.set_base_value(merged_context); + block_created = true; + + // clear blocks to prevent block params from parent + // template to be leaked into partials + // see `test_partial_context_issue_495` for the case. + local_rc.clear_blocks(); + local_rc.push_block(block); + } + + // @partial-block + if let Some(pb) = d.template() { + local_rc.push_partial_block(pb); + } + + let result = t.render(r, ctx, &mut local_rc, out); + + // cleanup + if block_created { + local_rc.pop_block(); + } + + if d.template().is_some() { + local_rc.pop_partial_block(); } - } - Ok(()) + result + } else { + Ok(()) + } } #[cfg(test)] @@ -264,4 +308,238 @@ mod test { let r0 = handlebars.render("t", &data); assert_eq!(r0.ok().unwrap(), "2 true2 false"); } + + #[test] + fn test_nested_partial_block() { + let mut handlebars = Registry::new(); + let template1 = "{{> @partial-block }}"; + let template2 = "{{#> t1 }}{{> @partial-block }}{{/ t1 }}"; + let template3 = "{{#> t2 }}Hello{{/ t2 }}"; + + handlebars + .register_template_string("t1", &template1) + .unwrap(); + handlebars + .register_template_string("t2", &template2) + .unwrap(); + + let page = handlebars.render_template(&template3, &json!({})).unwrap(); + assert_eq!("Hello", page); + } + + #[test] + fn test_up_to_partial_level() { + let outer = r#"{{>inner name="fruit:" vegetables=fruits}}"#; + let inner = "{{#each vegetables}}{{../name}} {{this}},{{/each}}"; + + let data = json!({ "fruits": ["carrot", "tomato"] }); + + let mut handlebars = Registry::new(); + handlebars.register_template_string("outer", outer).unwrap(); + handlebars.register_template_string("inner", inner).unwrap(); + + assert_eq!( + handlebars.render("outer", &data).unwrap(), + "fruit: carrot,fruit: tomato," + ); + } + + #[test] + fn line_stripping_with_inline_and_partial() { + let tpl0 = r#"{{#*inline "foo"}}foo +{{/inline}} +{{> foo}} +{{> foo}} +{{> foo}}"#; + let tpl1 = r#"{{#*inline "foo"}}foo{{/inline}} +{{> foo}} +{{> foo}} +{{> foo}}"#; + + let hbs = Registry::new(); + assert_eq!( + r#"foo +foo +foo +"#, + hbs.render_template(tpl0, &json!({})).unwrap() + ); + assert_eq!( + r#" +foofoofoo"#, + hbs.render_template(tpl1, &json!({})).unwrap() + ); + } + + #[test] + fn test_partial_indent() { + let outer = r#" {{> inner inner_solo}} + +{{#each inners}} + {{> inner}} +{{/each}} + + {{#each inners}} + {{> inner}} + {{/each}} +"#; + let inner = r#"name: {{name}} +"#; + + let mut hbs = Registry::new(); + + hbs.register_template_string("inner", inner).unwrap(); + hbs.register_template_string("outer", outer).unwrap(); + + let result = hbs + .render( + "outer", + &json!({ + "inner_solo": {"name": "inner_solo"}, + "inners": [ + {"name": "hello"}, + {"name": "there"} + ] + }), + ) + .unwrap(); + + assert_eq!( + result, + r#" name: inner_solo + + name: hello + name: there + + name: hello + name: there +"# + ); + } + // Rule::partial_expression should not trim leading indent by default + + #[test] + fn test_partial_prevent_indent() { + let outer = r#" {{> inner inner_solo}} + +{{#each inners}} + {{> inner}} +{{/each}} + + {{#each inners}} + {{> inner}} + {{/each}} +"#; + let inner = r#"name: {{name}} +"#; + + let mut hbs = Registry::new(); + hbs.set_prevent_indent(true); + + hbs.register_template_string("inner", inner).unwrap(); + hbs.register_template_string("outer", outer).unwrap(); + + let result = hbs + .render( + "outer", + &json!({ + "inner_solo": {"name": "inner_solo"}, + "inners": [ + {"name": "hello"}, + {"name": "there"} + ] + }), + ) + .unwrap(); + + assert_eq!( + result, + r#"name: inner_solo + +name: hello +name: there + +name: hello +name: there +"# + ); + } + + #[test] + fn test_nested_partials() { + let mut hb = Registry::new(); + hb.register_template_string("partial", "{{> @partial-block}}") + .unwrap(); + hb.register_template_string( + "index", + r#"{{#>partial}} + Yo + {{#>partial}} + Yo 2 + {{/partial}} +{{/partial}}"#, + ) + .unwrap(); + assert_eq!( + r#" Yo + Yo 2 +"#, + hb.render("index", &()).unwrap() + ); + + hb.register_template_string("partial2", "{{> @partial-block}}") + .unwrap(); + let r2 = hb + .render_template( + r#"{{#> partial}} +{{#> partial2}} +:( +{{/partial2}} +{{/partial}}"#, + &(), + ) + .unwrap(); + assert_eq!(":(\n", r2); + } + + #[test] + fn test_partial_context_issue_495() { + let mut hb = Registry::new(); + hb.register_template_string( + "t1", + r#"{{~#*inline "displayName"~}} +Template:{{name}} +{{/inline}} +{{#each data as |name|}} +Name:{{name}} +{{>displayName name="aaaa"}} +{{/each}}"#, + ) + .unwrap(); + + hb.register_template_string( + "t1", + r#"{{~#*inline "displayName"~}} +Template:{{this}} +{{/inline}} +{{#each data as |name|}} +Name:{{name}} +{{>displayName}} +{{/each}}"#, + ) + .unwrap(); + + let data = json!({ + "data": ["hudel", "test"] + }); + + assert_eq!( + r#"Name:hudel +Template:hudel +Name:test +Template:test +"#, + hb.render("t1", &data).unwrap() + ); + } } diff --git a/src/registry.rs b/src/registry.rs index 946b0ec7a..fbb1707c4 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -1,9 +1,9 @@ +use std::borrow::Cow; use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; -use std::fs::File; -use std::io::prelude::*; -use std::io::BufReader; +use std::io::{Error as IoError, Write}; use std::path::Path; +use std::sync::Arc; use serde::Serialize; @@ -11,13 +11,16 @@ use crate::context::Context; use crate::decorators::{self, DecoratorDef}; #[cfg(feature = "script_helper")] use crate::error::ScriptError; -use crate::error::{RenderError, TemplateError, TemplateFileError, TemplateRenderError}; +use crate::error::{RenderError, TemplateError}; use crate::helpers::{self, HelperDef}; use crate::output::{Output, StringOutput, WriteOutput}; use crate::render::{RenderContext, Renderable}; +use crate::sources::{FileSource, Source}; use crate::support::str::{self, StringWriter}; -use crate::template::Template; +use crate::template::{Template, TemplateOptions}; +#[cfg(feature = "dir_source")] +use std::path; #[cfg(feature = "dir_source")] use walkdir::{DirEntry, WalkDir}; @@ -27,12 +30,15 @@ use rhai::Engine; #[cfg(feature = "script_helper")] use crate::helpers::scripting::ScriptHelper; +#[cfg(feature = "rust-embed")] +use rust_embed::RustEmbed; + /// This type represents an *escape fn*, that is a function whose purpose it is /// to escape potentially problematic characters in a string. /// /// An *escape fn* is represented as a `Box` to avoid unnecessary type /// parameters (and because traits cannot be aliased using `type`). -pub type EscapeFn = Box String + Send + Sync>; +pub type EscapeFn = Arc String + Send + Sync>; /// The default *escape fn* replaces the characters `&"<>` /// with the equivalent html / xml entities. @@ -49,15 +55,25 @@ pub fn no_escape(data: &str) -> String { /// The single entry point of your Handlebars templates /// /// It maintains compiled templates and registered helpers. +#[derive(Clone)] pub struct Registry<'reg> { templates: HashMap, - helpers: HashMap>, - decorators: HashMap>, + + helpers: HashMap>, + decorators: HashMap>, + escape_fn: EscapeFn, - source_map: bool, strict_mode: bool, + dev_mode: bool, + prevent_indent: bool, #[cfg(feature = "script_helper")] - pub(crate) engine: Engine, + pub(crate) engine: Arc, + + template_sources: + HashMap + Send + Sync + 'reg>>, + #[cfg(feature = "script_helper")] + script_sources: + HashMap + Send + Sync + 'reg>>, } impl<'reg> Debug for Registry<'reg> { @@ -66,7 +82,8 @@ impl<'reg> Debug for Registry<'reg> { .field("templates", &self.templates) .field("helpers", &self.helpers.keys()) .field("decorators", &self.decorators.keys()) - .field("source_map", &self.source_map) + .field("strict_mode", &self.strict_mode) + .field("dev_mode", &self.dev_mode) .finish() } } @@ -101,13 +118,17 @@ impl<'reg> Registry<'reg> { pub fn new() -> Registry<'reg> { let r = Registry { templates: HashMap::new(), + template_sources: HashMap::new(), helpers: HashMap::new(), decorators: HashMap::new(), - escape_fn: Box::new(html_escape), - source_map: true, + escape_fn: Arc::new(html_escape), strict_mode: false, + dev_mode: false, + prevent_indent: false, + #[cfg(feature = "script_helper")] + engine: Arc::new(rhai_engine()), #[cfg(feature = "script_helper")] - engine: rhai_engine(), + script_sources: HashMap::new(), }; r.setup_builtins() @@ -122,39 +143,30 @@ impl<'reg> Registry<'reg> { self.register_helper("raw", Box::new(helpers::RAW_HELPER)); self.register_helper("log", Box::new(helpers::LOG_HELPER)); - self.register_helper("eq", Box::new(helpers::helper_boolean::eq)); - self.register_helper("ne", Box::new(helpers::helper_boolean::ne)); - self.register_helper("gt", Box::new(helpers::helper_boolean::gt)); - self.register_helper("gte", Box::new(helpers::helper_boolean::gte)); - self.register_helper("lt", Box::new(helpers::helper_boolean::lt)); - self.register_helper("lte", Box::new(helpers::helper_boolean::lte)); - self.register_helper("and", Box::new(helpers::helper_boolean::and)); - self.register_helper("or", Box::new(helpers::helper_boolean::or)); - self.register_helper("not", Box::new(helpers::helper_boolean::not)); + self.register_helper("eq", Box::new(helpers::helper_extras::eq)); + self.register_helper("ne", Box::new(helpers::helper_extras::ne)); + self.register_helper("gt", Box::new(helpers::helper_extras::gt)); + self.register_helper("gte", Box::new(helpers::helper_extras::gte)); + self.register_helper("lt", Box::new(helpers::helper_extras::lt)); + self.register_helper("lte", Box::new(helpers::helper_extras::lte)); + self.register_helper("and", Box::new(helpers::helper_extras::and)); + self.register_helper("or", Box::new(helpers::helper_extras::or)); + self.register_helper("not", Box::new(helpers::helper_extras::not)); + self.register_helper("len", Box::new(helpers::helper_extras::len)); self.register_decorator("inline", Box::new(decorators::INLINE_DECORATOR)); self } - /// Enable handlebars template source map - /// - /// Source map provides line/col reporting on error. It uses slightly - /// more memory to maintain the data. - /// - /// Default is true. - pub fn source_map_enabled(&mut self, enable: bool) { - self.source_map = enable; - } - - /// Enable handlebars strict mode + /// Enable or disable handlebars strict mode /// /// By default, handlebars renders empty string for value that /// undefined or never exists. Since rust is a static type /// language, we offer strict mode in handlebars-rust. In strict /// mode, if you were to render a value that doesn't exist, a /// `RenderError` will be raised. - pub fn set_strict_mode(&mut self, enable: bool) { - self.strict_mode = enable; + pub fn set_strict_mode(&mut self, enabled: bool) { + self.strict_mode = enabled; } /// Return strict mode state, default is false. @@ -168,11 +180,51 @@ impl<'reg> Registry<'reg> { self.strict_mode } + /// Return dev mode state, default is false + /// + /// With dev mode turned on, handlebars enables a set of development + /// friendly features, that may affect its performance. + pub fn dev_mode(&self) -> bool { + self.dev_mode + } + + /// Enable or disable dev mode + /// + /// With dev mode turned on, handlebars enables a set of development + /// friendly features, that may affect its performance. + /// + /// **Note that you have to enable dev mode before adding templates to + /// the registry**. Otherwise it won't take effect at all. + pub fn set_dev_mode(&mut self, enabled: bool) { + self.dev_mode = enabled; + + // clear template source when disabling dev mode + if !enabled { + self.template_sources.clear(); + } + } + + /// Enable or disable indent for partial include tag `{{>}}` + /// + /// By default handlebars keeps indent whitespaces for partial + /// include tag, to change this behaviour, set this toggle to `true`. + pub fn set_prevent_indent(&mut self, enable: bool) { + self.prevent_indent = enable; + } + + /// Return state for `prevent_indent` option, default to `false`. + pub fn prevent_indent(&self) -> bool { + self.prevent_indent + } + /// Register a `Template` /// /// This is infallible since the template has already been parsed and /// insert cannot fail. If there is an existing template with this name it /// will be overwritten. + /// + /// Dev mode doesn't apply for pre-compiled template because it's lifecycle + /// is not managed by the registry. pub fn register_template(&mut self, name: &str, tpl: Template) { self.templates.insert(name.to_string(), tpl); } @@ -188,7 +240,13 @@ impl<'reg> Registry<'reg> { where S: AsRef, { - let template = Template::compile_with_name(tpl_str, name.to_owned(), self.source_map)?; + let template = Template::compile2( + tpl_str.as_ref(), + TemplateOptions { + name: Some(name.to_owned()), + prevent_indent: self.prevent_indent, + }, + )?; self.register_template(name, template); Ok(()) } @@ -204,19 +262,30 @@ impl<'reg> Registry<'reg> { self.register_template_string(name, partial_str) } - /// Register a template from a path + /// Register a template from a path on file system + /// + /// If dev mode is enabled, the registry will keep reading the template file + /// from file system everytime it's visited. pub fn register_template_file

( &mut self, name: &str, tpl_path: P, - ) -> Result<(), TemplateFileError> + ) -> Result<(), TemplateError> where P: AsRef, { - let mut reader = BufReader::new( - File::open(tpl_path).map_err(|e| TemplateFileError::IOError(e, name.to_owned()))?, - ); - self.register_template_source(name, &mut reader) + let source = FileSource::new(tpl_path.as_ref().into()); + let template_string = source + .load() + .map_err(|err| TemplateError::from((err, name.to_owned())))?; + + self.register_template_string(name, template_string)?; + if self.dev_mode { + self.template_sources + .insert(name.to_owned(), Arc::new(source)); + } + + Ok(()) } /// Register templates from a directory @@ -231,18 +300,26 @@ impl<'reg> Registry<'reg> { /// /// This method is not available by default. /// You will need to enable the `dir_source` feature to use it. + /// + /// When dev_mode enabled, like `register_template_file`, templates is reloaded + /// from file system everytime it's visied. #[cfg(feature = "dir_source")] + #[cfg_attr(docsrs, doc(cfg(feature = "dir_source")))] pub fn register_templates_directory

( &mut self, tpl_extension: &'static str, dir_path: P, - ) -> Result<(), TemplateFileError> + ) -> Result<(), TemplateError> where P: AsRef, { let dir_path = dir_path.as_ref(); - let prefix_len = if dir_path.to_string_lossy().ends_with('/') { + let prefix_len = if dir_path + .to_string_lossy() + .ends_with(|c| c == '\\' || c == '/') + // `/` will work on windows too so we still need to check + { dir_path.to_string_lossy().len() } else { dir_path.to_string_lossy().len() + 1 @@ -261,42 +338,55 @@ impl<'reg> Registry<'reg> { let tpl_file_path = entry.path().to_string_lossy(); let tpl_name = &tpl_file_path[prefix_len..tpl_file_path.len() - tpl_extension.len()]; - let tpl_canonical_name = tpl_name.replace("\\", "/"); + // replace platform path separator with our internal one + let tpl_canonical_name = tpl_name.replace(path::MAIN_SEPARATOR, "/"); self.register_template_file(&tpl_canonical_name, &tpl_path)?; } Ok(()) } - /// Register a template from `std::io::Read` source - pub fn register_template_source( - &mut self, - name: &str, - tpl_source: &mut R, - ) -> Result<(), TemplateFileError> + /// Register templates using a + /// [RustEmbed](https://github.com/pyros2097/rust-embed) type + /// + /// File names from embed struct are used as template name. + /// + /// ```skip + /// #[derive(RustEmbed)] + /// #[folder = "templates"] + /// struct Assets; + /// + /// let mut hbs = Handlebars::new(); + /// hbs.register_embed_templates::(); + /// ``` + /// + #[cfg(feature = "rust-embed")] + #[cfg_attr(docsrs, doc(cfg(feature = "rust-embed")))] + pub fn register_embed_templates(&mut self) -> Result<(), TemplateError> where - R: Read, + E: RustEmbed, { - let mut buf = String::new(); - tpl_source - .read_to_string(&mut buf) - .map_err(|e| TemplateFileError::IOError(e, name.to_owned()))?; - self.register_template_string(name, buf)?; + for item in E::iter() { + let file_name = item.as_ref(); + if let Some(file) = E::get(file_name) { + let data = file.data; + + let tpl_content = String::from_utf8_lossy(data.as_ref()); + self.register_template_string(file_name, tpl_content)?; + } + } Ok(()) } /// Remove a template from the registry pub fn unregister_template(&mut self, name: &str) { self.templates.remove(name); + self.template_sources.remove(name); } /// Register a helper - pub fn register_helper( - &mut self, - name: &str, - def: Box, - ) -> Option> { - self.helpers.insert(name.to_string(), def) + pub fn register_helper(&mut self, name: &str, def: Box) { + self.helpers.insert(name.to_string(), def.into()); } /// Register a [rhai](https://docs.rs/rhai/) script as handlebars helper @@ -319,37 +409,52 @@ impl<'reg> Registry<'reg> { /// (value * 100).to_string() + label /// ``` /// - /// #[cfg(feature = "script_helper")] - pub fn register_script_helper( - &mut self, - name: &str, - script: String, - ) -> Result>, ScriptError> { - let compiled = self.engine.compile(&script)?; + #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))] + pub fn register_script_helper(&mut self, name: &str, script: &str) -> Result<(), ScriptError> { + let compiled = self.engine.compile(script)?; let script_helper = ScriptHelper { script: compiled }; - Ok(self - .helpers - .insert(name.to_string(), Box::new(script_helper))) + self.helpers + .insert(name.to_string(), Arc::new(script_helper)); + Ok(()) } /// Register a [rhai](https://docs.rs/rhai/) script from file + /// + /// When dev mode is enable, script file is reloaded from original file + /// everytime it is called. #[cfg(feature = "script_helper")] + #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))] pub fn register_script_helper_file

( &mut self, name: &str, script_path: P, - ) -> Result>, ScriptError> + ) -> Result<(), ScriptError> where P: AsRef, { - let mut script = String::new(); - { - let mut file = File::open(script_path)?; - file.read_to_string(&mut script)?; - } + let source = FileSource::new(script_path.as_ref().into()); + let script = source.load()?; + + self.script_sources + .insert(name.to_owned(), Arc::new(source)); + self.register_script_helper(name, &script) + } - self.register_script_helper(name, script) + /// Borrow a read-only reference to current rhai engine + #[cfg(feature = "script_helper")] + #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))] + pub fn engine(&self) -> &Engine { + self.engine.as_ref() + } + + /// Set a custom rhai engine for the registry. + /// + /// *Note that* you need to set custom engine before adding scripts. + #[cfg(feature = "script_helper")] + #[cfg_attr(docsrs, doc(cfg(feature = "script_helper")))] + pub fn set_engine(&mut self, engine: Engine) { + self.engine = Arc::new(engine); } /// Register a decorator @@ -357,8 +462,8 @@ impl<'reg> Registry<'reg> { &mut self, name: &str, def: Box, - ) -> Option> { - self.decorators.insert(name.to_string(), def) + ) { + self.decorators.insert(name.to_string(), def.into()); } /// Register a new *escape fn* to be used from now on by this registry. @@ -366,12 +471,12 @@ impl<'reg> Registry<'reg> { &mut self, escape_fn: F, ) { - self.escape_fn = Box::new(escape_fn); + self.escape_fn = Arc::new(escape_fn); } /// Restore the default *escape fn*. pub fn unregister_escape_fn(&mut self) { - self.escape_fn = Box::new(html_escape); + self.escape_fn = Arc::new(html_escape); } /// Get a reference to the current *escape fn*. @@ -389,9 +494,65 @@ impl<'reg> Registry<'reg> { self.templates.get(name) } + #[inline] + pub(crate) fn get_or_load_template_optional( + &'reg self, + name: &str, + ) -> Option, RenderError>> { + if let (true, Some(source)) = (self.dev_mode, self.template_sources.get(name)) { + let r = source + .load() + .map_err(|e| TemplateError::from((e, name.to_owned()))) + .and_then(|tpl_str| { + Template::compile2( + tpl_str.as_ref(), + TemplateOptions { + name: Some(name.to_owned()), + prevent_indent: self.prevent_indent, + }, + ) + }) + .map(Cow::Owned) + .map_err(RenderError::from); + Some(r) + } else { + self.templates.get(name).map(|t| Ok(Cow::Borrowed(t))) + } + } + + #[inline] + pub(crate) fn get_or_load_template( + &'reg self, + name: &str, + ) -> Result, RenderError> { + if let Some(result) = self.get_or_load_template_optional(name) { + result + } else { + Err(RenderError::new(format!("Template not found: {}", name))) + } + } + /// Return a registered helper - pub fn get_helper(&self, name: &str) -> Option<&(dyn HelperDef + Send + Sync + 'reg)> { - self.helpers.get(name).map(|v| v.as_ref()) + #[inline] + pub(crate) fn get_or_load_helper( + &'reg self, + name: &str, + ) -> Result>, RenderError> { + #[cfg(feature = "script_helper")] + if let (true, Some(source)) = (self.dev_mode, self.script_sources.get(name)) { + return source + .load() + .map_err(ScriptError::from) + .and_then(|s| { + let helper = Box::new(ScriptHelper { + script: self.engine.compile(&s)?, + }) as Box; + Ok(Some(helper.into())) + }) + .map_err(RenderError::from); + } + + Ok(self.helpers.get(name).cloned()) } #[inline] @@ -400,11 +561,19 @@ impl<'reg> Registry<'reg> { } /// Return a registered decorator - pub fn get_decorator(&self, name: &str) -> Option<&(dyn DecoratorDef + Send + Sync + 'reg)> { + #[inline] + pub(crate) fn get_decorator( + &self, + name: &str, + ) -> Option<&(dyn DecoratorDef + Send + Sync + 'reg)> { self.decorators.get(name).map(|v| v.as_ref()) } /// Return all templates registered + /// + /// **Note that** in dev mode, the template returned from this method may + /// not reflect its latest state. This method doesn't try to reload templates + /// from its source. pub fn get_templates(&self) -> &HashMap { &self.templates } @@ -412,8 +581,10 @@ impl<'reg> Registry<'reg> { /// Unregister all templates pub fn clear_templates(&mut self) { self.templates.clear(); + self.template_sources.clear(); } + #[inline] fn render_to_output( &self, name: &str, @@ -423,12 +594,10 @@ impl<'reg> Registry<'reg> { where O: Output, { - self.get_template(name) - .ok_or_else(|| RenderError::new(format!("Template not found: {}", name))) - .and_then(|t| { - let mut render_context = RenderContext::new(t.name.as_ref()); - t.render(self, &ctx, &mut render_context, output) - }) + self.get_or_load_template(name).and_then(|t| { + let mut render_context = RenderContext::new(t.name.as_ref()); + t.render(self, ctx, &mut render_context, output) + }) } /// Render a registered template with some data into a string @@ -466,11 +635,7 @@ impl<'reg> Registry<'reg> { } /// Render a template string using current registry without registering it - pub fn render_template( - &self, - template_string: &str, - data: &T, - ) -> Result + pub fn render_template(&self, template_string: &str, data: &T) -> Result where T: Serialize, { @@ -484,17 +649,22 @@ impl<'reg> Registry<'reg> { &self, template_string: &str, ctx: &Context, - ) -> Result { - let tpl = Template::compile2(template_string, self.source_map)?; + ) -> Result { + let tpl = Template::compile2( + template_string, + TemplateOptions { + prevent_indent: self.prevent_indent, + ..Default::default() + }, + )?; let mut out = StringOutput::new(); { let mut render_context = RenderContext::new(None); - tpl.render(self, &ctx, &mut render_context, &mut out)?; + tpl.render(self, ctx, &mut render_context, &mut out)?; } - out.into_string() - .map_err(|e| TemplateRenderError::from(RenderError::from(e))) + out.into_string().map_err(RenderError::from) } /// Render a template string using current registry without registering it @@ -503,36 +673,22 @@ impl<'reg> Registry<'reg> { template_string: &str, data: &T, writer: W, - ) -> Result<(), TemplateRenderError> + ) -> Result<(), RenderError> where T: Serialize, W: Write, { - let tpl = Template::compile2(template_string, self.source_map)?; + let tpl = Template::compile2( + template_string, + TemplateOptions { + prevent_indent: self.prevent_indent, + ..Default::default() + }, + )?; let ctx = Context::wraps(data)?; let mut render_context = RenderContext::new(None); let mut out = WriteOutput::new(writer); tpl.render(self, &ctx, &mut render_context, &mut out) - .map_err(TemplateRenderError::from) - } - - /// Render a template source using current registry without registering it - pub fn render_template_source_to_write( - &self, - template_source: &mut R, - data: &T, - writer: W, - ) -> Result<(), TemplateRenderError> - where - T: Serialize, - W: Write, - R: Read, - { - let mut tpl_str = String::new(); - template_source - .read_to_string(&mut tpl_str) - .map_err(|e| TemplateRenderError::IOError(e, "Unnamed template source".to_owned()))?; - self.render_template_to_write(&tpl_str, data, writer) } } @@ -546,11 +702,8 @@ mod test { use crate::render::{Helper, RenderContext, Renderable}; use crate::support::str::StringWriter; use crate::template::Template; - #[cfg(feature = "dir_source")] - use std::fs::{DirBuilder, File}; - #[cfg(feature = "dir_source")] + use std::fs::File; use std::io::Write; - #[cfg(feature = "dir_source")] use tempfile::tempdir; #[derive(Clone, Copy)] @@ -592,7 +745,7 @@ mod test { // built-in helpers plus 1 let num_helpers = 7; - let num_boolean_helpers = 9; // stuff like gt and lte + let num_boolean_helpers = 10; // stuff like gt and lte let num_custom_helpers = 1; // dummy from above assert_eq!( r.helpers.len(), @@ -603,6 +756,8 @@ mod test { #[test] #[cfg(feature = "dir_source")] fn test_register_templates_directory() { + use std::fs::DirBuilder; + let mut r = Registry::new(); { let dir = tempdir().unwrap(); @@ -703,6 +858,29 @@ mod test { dir.close().unwrap(); } + + { + let dir = tempdir().unwrap(); + + let file1_path = dir.path().join("t10.hbs"); + let mut file1: File = File::create(&file1_path).unwrap(); + writeln!(file1, "

Bonjour {{world}}!

").unwrap(); + + let mut dir_path = dir + .path() + .to_string_lossy() + .replace(std::path::MAIN_SEPARATOR, "/"); + if !dir_path.ends_with("/") { + dir_path.push('/'); + } + r.register_templates_directory(".hbs", dir_path).unwrap(); + + assert_eq!(r.templates.len(), 8); + assert_eq!(r.templates.contains_key("t10"), true); + + drop(file1); + dir.close().unwrap(); + } } #[test] @@ -779,10 +957,7 @@ mod test { let render_error = r .render_template("accessing non-exists key {{the_key_never_exists}}", &data) .unwrap_err(); - assert_eq!( - render_error.as_render_error().unwrap().column_no.unwrap(), - 26 - ); + assert_eq!(render_error.column_no.unwrap(), 26); let data2 = json!([1, 2, 3]); assert!(r @@ -794,10 +969,7 @@ mod test { let render_error2 = r .render_template("accessing invalid array index {{this.[3]}}", &data2) .unwrap_err(); - assert_eq!( - render_error2.as_render_error().unwrap().column_no.unwrap(), - 31 - ); + assert_eq!(render_error2.column_no.unwrap(), 31); } use crate::json::value::ScopedJson; @@ -809,8 +981,8 @@ mod test { _: &'reg Registry<'reg>, _: &'rc Context, _: &mut RenderContext<'reg, 'rc>, - ) -> Result>, RenderError> { - Ok(Some(ScopedJson::Missing)) + ) -> Result, RenderError> { + Ok(ScopedJson::Missing) } } @@ -891,8 +1063,161 @@ mod test { .unwrap(); assert_eq!( "0123", - reg.render_with_context("t0", &Context::wraps(&data).unwrap()) - .unwrap() + reg.render_with_context("t0", &Context::from(data)).unwrap() + ); + } + + #[test] + fn test_keys_starts_with_null() { + env_logger::init(); + let reg = Registry::new(); + let data = json!({ + "optional": true, + "is_null": true, + "nullable": true, + "null": true, + "falsevalue": true, + }); + assert_eq!( + "optional: true --> true", + reg.render_template( + "optional: {{optional}} --> {{#if optional }}true{{else}}false{{/if}}", + &data + ) + .unwrap() + ); + assert_eq!( + "is_null: true --> true", + reg.render_template( + "is_null: {{is_null}} --> {{#if is_null }}true{{else}}false{{/if}}", + &data + ) + .unwrap() ); + assert_eq!( + "nullable: true --> true", + reg.render_template( + "nullable: {{nullable}} --> {{#if nullable }}true{{else}}false{{/if}}", + &data + ) + .unwrap() + ); + assert_eq!( + "falsevalue: true --> true", + reg.render_template( + "falsevalue: {{falsevalue}} --> {{#if falsevalue }}true{{else}}false{{/if}}", + &data + ) + .unwrap() + ); + assert_eq!( + "null: true --> false", + reg.render_template( + "null: {{null}} --> {{#if null }}true{{else}}false{{/if}}", + &data + ) + .unwrap() + ); + assert_eq!( + "null: true --> true", + reg.render_template( + "null: {{null}} --> {{#if this.[null]}}true{{else}}false{{/if}}", + &data + ) + .unwrap() + ); + } + + #[test] + fn test_dev_mode_template_reload() { + let mut reg = Registry::new(); + reg.set_dev_mode(true); + assert!(reg.dev_mode()); + + let dir = tempdir().unwrap(); + let file1_path = dir.path().join("t1.hbs"); + { + let mut file1: File = File::create(&file1_path).unwrap(); + write!(file1, "

Hello {{{{name}}}}!

").unwrap(); + } + + reg.register_template_file("t1", &file1_path).unwrap(); + + assert_eq!( + reg.render("t1", &json!({"name": "Alex"})).unwrap(), + "

Hello Alex!

" + ); + + { + let mut file1: File = File::create(&file1_path).unwrap(); + write!(file1, "

Privet {{{{name}}}}!

").unwrap(); + } + + assert_eq!( + reg.render("t1", &json!({"name": "Alex"})).unwrap(), + "

Privet Alex!

" + ); + + dir.close().unwrap(); + } + + #[test] + #[cfg(feature = "script_helper")] + fn test_script_helper() { + let mut reg = Registry::new(); + + reg.register_script_helper("acc", "params.reduce(|sum, x| x + sum, 0)") + .unwrap(); + + assert_eq!( + reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(), + "10" + ); + } + + #[test] + #[cfg(feature = "script_helper")] + fn test_script_helper_dev_mode() { + let mut reg = Registry::new(); + reg.set_dev_mode(true); + + let dir = tempdir().unwrap(); + let file1_path = dir.path().join("acc.rhai"); + { + let mut file1: File = File::create(&file1_path).unwrap(); + write!(file1, "params.reduce(|sum, x| x + sum, 0)").unwrap(); + } + + reg.register_script_helper_file("acc", &file1_path).unwrap(); + + assert_eq!( + reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(), + "10" + ); + + { + let mut file1: File = File::create(&file1_path).unwrap(); + write!(file1, "params.reduce(|sum, x| x * sum, 1)").unwrap(); + } + + assert_eq!( + reg.render_template("{{acc 1 2 3 4}}", &json!({})).unwrap(), + "24" + ); + + dir.close().unwrap(); + } + + #[test] + #[cfg(feature = "script_helper")] + fn test_engine_access() { + use rhai::Engine; + + let mut registry = Registry::new(); + let mut eng = Engine::new(); + eng.set_max_string_size(1000); + registry.set_engine(eng); + + assert_eq!(1000, registry.engine().max_string_size()); } } diff --git a/src/render.rs b/src/render.rs index b49310fbf..19303c950 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,7 +1,6 @@ use std::borrow::{Borrow, Cow}; use std::collections::{BTreeMap, VecDeque}; use std::fmt; -use std::ops::Deref; use std::rc::Rc; use serde_json::value::Value as Json; @@ -40,7 +39,9 @@ pub struct RenderContext<'reg, 'rc> { #[derive(Clone)] pub struct RenderContextInner<'reg: 'rc, 'rc> { partials: BTreeMap, - local_helpers: BTreeMap>, + partial_block_stack: VecDeque<&'reg Template>, + partial_block_depth: isize, + local_helpers: BTreeMap>, /// current template name current_template: Option<&'reg String>, /// root template name @@ -53,6 +54,8 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { pub fn new(root_template: Option<&'reg String>) -> RenderContext<'reg, 'rc> { let inner = Rc::new(RenderContextInner { partials: BTreeMap::new(), + partial_block_stack: VecDeque::new(), + partial_block_depth: 0, local_helpers: BTreeMap::new(), current_template: None, root_template, @@ -98,6 +101,10 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { self.blocks.pop_front(); } + pub(crate) fn clear_blocks(&mut self) { + self.blocks.clear(); + } + /// Borrow a reference to current block context pub fn block(&self) -> Option<&BlockContext<'reg>> { self.blocks.front() @@ -158,8 +165,15 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { } /// Get registered partial in this render context - pub fn get_partial(&self, name: &str) -> Option<&&Template> { - self.inner().partials.get(name) + pub fn get_partial(&self, name: &str) -> Option<&Template> { + if name == partial::PARTIAL_BLOCK { + return self + .inner() + .partial_block_stack + .get(self.inner().partial_block_depth as usize) + .copied(); + } + self.inner().partials.get(name).copied() } /// Register a partial for this context @@ -167,6 +181,25 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { self.inner_mut().partials.insert(name, partial); } + pub(crate) fn push_partial_block(&mut self, partial: &'reg Template) { + self.inner_mut().partial_block_stack.push_front(partial); + } + + pub(crate) fn pop_partial_block(&mut self) { + self.inner_mut().partial_block_stack.pop_front(); + } + + pub(crate) fn inc_partial_block_depth(&mut self) { + self.inner_mut().partial_block_depth += 1; + } + + pub(crate) fn dec_partial_block_depth(&mut self) { + let depth = &mut self.inner_mut().partial_block_depth; + if *depth > 0 { + *depth -= 1; + } + } + /// Remove a registered partial pub fn remove_partial(&mut self, name: &str) { self.inner_mut().partials.remove(name); @@ -175,7 +208,7 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { fn get_local_var(&self, level: usize, name: &str) -> Option<&Json> { self.blocks .get(level) - .and_then(|blk| blk.get_local_var(&name)) + .and_then(|blk| blk.get_local_var(name)) } /// Test if given template name is current template. @@ -192,11 +225,11 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { pub fn register_local_helper( &mut self, name: &str, - def: Box, - ) -> Option> { + def: Box, + ) { self.inner_mut() .local_helpers - .insert(name.to_string(), def.into()) + .insert(name.to_string(), def.into()); } /// Remove a helper from render context @@ -205,7 +238,7 @@ impl<'reg: 'rc, 'rc> RenderContext<'reg, 'rc> { } /// Attempt to get a helper from current render context. - pub fn get_local_helper(&self, name: &str) -> Option> { + pub fn get_local_helper(&self, name: &str) -> Option> { self.inner().local_helpers.get(name).cloned() } @@ -248,9 +281,11 @@ impl<'reg, 'rc> fmt::Debug for RenderContextInner<'reg, 'rc> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { f.debug_struct("RenderContextInner") .field("partials", &self.partials) + .field("partial_block_stack", &self.partial_block_stack) + .field("partial_block_depth", &self.partial_block_depth) .field("root_template", &self.root_template) .field("current_template", &self.current_template) - .field("disable_eacape", &self.disable_escape) + .field("disable_escape", &self.disable_escape) .finish() } } @@ -504,6 +539,7 @@ pub trait Evaluable { ) -> Result<(), RenderError>; } +#[inline] fn call_helper_for_value<'reg: 'rc, 'rc>( hd: &dyn HelperDef, ht: &Helper<'reg, 'rc>, @@ -511,25 +547,30 @@ fn call_helper_for_value<'reg: 'rc, 'rc>( ctx: &'rc Context, rc: &mut RenderContext<'reg, 'rc>, ) -> Result, RenderError> { - if let Some(result) = hd.call_inner(ht, r, ctx, rc)? { - Ok(PathAndJson::new(None, result)) - } else { - // parse value from output - let mut so = StringOutput::new(); - - // here we don't want subexpression result escaped, - // so we temporarily disable it - let disable_escape = rc.is_disable_escape(); - rc.set_disable_escape(true); - - hd.call(ht, r, ctx, rc, &mut so)?; - rc.set_disable_escape(disable_escape); - - let string = so.into_string().map_err(RenderError::from)?; - Ok(PathAndJson::new( - None, - ScopedJson::Derived(Json::String(string)), - )) + match hd.call_inner(ht, r, ctx, rc) { + Ok(result) => Ok(PathAndJson::new(None, result)), + Err(e) => { + if e.is_unimplemented() { + // parse value from output + let mut so = StringOutput::new(); + + // here we don't want subexpression result escaped, + // so we temporarily disable it + let disable_escape = rc.is_disable_escape(); + rc.set_disable_escape(true); + + hd.call(ht, r, ctx, rc, &mut so)?; + rc.set_disable_escape(disable_escape); + + let string = so.into_string().map_err(RenderError::from)?; + Ok(PathAndJson::new( + None, + ScopedJson::Derived(Json::String(string)), + )) + } else { + Err(e) + } + } } } @@ -577,30 +618,27 @@ impl Parameter { Parameter::Literal(ref j) => Ok(PathAndJson::new(None, ScopedJson::Constant(j))), Parameter::Subexpression(ref t) => match *t.as_element() { Expression(ref ht) => { - if ht.is_name_only() { - ht.name.expand(registry, ctx, rc) + let name = ht.name.expand_as_name(registry, ctx, rc)?; + + let h = Helper::try_from_template(ht, registry, ctx, rc)?; + if let Some(ref d) = rc.get_local_helper(&name) { + call_helper_for_value(d.as_ref(), &h, registry, ctx, rc) } else { - let name = ht.name.expand_as_name(registry, ctx, rc)?; + let mut helper = registry.get_or_load_helper(&name)?; - let h = Helper::try_from_template(ht, registry, ctx, rc)?; - if let Some(ref d) = rc.get_local_helper(&name) { - let helper_def = d.deref(); - call_helper_for_value(helper_def, &h, registry, ctx, rc) - } else { - registry - .get_helper(&name) - .or_else(|| { - registry.get_helper(if ht.block { - BLOCK_HELPER_MISSING - } else { - HELPER_MISSING - }) - }) - .ok_or_else(|| { - RenderError::new(format!("Helper not defined: {:?}", ht.name)) - }) - .and_then(move |d| call_helper_for_value(d, &h, registry, ctx, rc)) + if helper.is_none() { + helper = registry.get_or_load_helper(if ht.block { + BLOCK_HELPER_MISSING + } else { + HELPER_MISSING + })?; } + + helper + .ok_or_else(|| { + RenderError::new(format!("Helper not defined: {:?}", ht.name)) + }) + .and_then(|d| call_helper_for_value(d.as_ref(), &h, registry, ctx, rc)) } } _ => unreachable!(), @@ -624,11 +662,9 @@ impl Renderable for Template { t.render(registry, ctx, rc, out).map_err(|mut e| { // add line/col number if the template has mapping data if e.line_no.is_none() { - if let Some(ref mapping) = self.mapping { - if let Some(&TemplateMapping(line, col)) = mapping.get(idx) { - e.line_no = Some(line); - e.column_no = Some(col); - } + if let Some(&TemplateMapping(line, col)) = self.mapping.get(idx) { + e.line_no = Some(line); + e.column_no = Some(col); } } @@ -655,11 +691,9 @@ impl Evaluable for Template { for (idx, t) in iter.enumerate() { t.eval(registry, ctx, rc).map_err(|mut e| { if e.line_no.is_none() { - if let Some(ref mapping) = self.mapping { - if let Some(&TemplateMapping(line, col)) = mapping.get(idx) { - e.line_no = Some(line); - e.column_no = Some(col); - } + if let Some(&TemplateMapping(line, col)) = self.mapping.get(idx) { + e.line_no = Some(line); + e.column_no = Some(col); } } @@ -679,6 +713,7 @@ fn helper_exists<'reg: 'rc, 'rc>( rc.has_local_helper(name) || reg.has_helper(name) } +#[inline] fn render_helper<'reg: 'rc, 'rc>( ht: &'reg HelperTemplate, registry: &'reg Registry<'reg>, @@ -696,17 +731,19 @@ fn render_helper<'reg: 'rc, 'rc>( if let Some(ref d) = rc.get_local_helper(h.name()) { d.call(&h, registry, ctx, rc, out) } else { - registry - .get_helper(h.name()) - .or_else(|| { - registry.get_helper(if ht.block { - BLOCK_HELPER_MISSING - } else { - HELPER_MISSING - }) - }) - .ok_or_else(|| RenderError::new(format!("Helper not defined: {:?}", ht.name))) - .and_then(move |d| d.call(&h, registry, ctx, rc, out)) + let mut helper = registry.get_or_load_helper(h.name())?; + + if helper.is_none() { + helper = registry.get_or_load_helper(if ht.block { + BLOCK_HELPER_MISSING + } else { + HELPER_MISSING + })?; + } + + helper + .ok_or_else(|| RenderError::new(format!("Helper not defined: {:?}", h.name()))) + .and_then(|d| d.call(&h, registry, ctx, rc, out)) } } @@ -731,9 +768,14 @@ impl Renderable for TemplateElement { out.write(v.as_ref())?; Ok(()) } - Expression(ref ht) => { + Expression(ref ht) | HtmlExpression(ref ht) => { + let is_html_expression = matches!(self, HtmlExpression(_)); + if is_html_expression { + rc.set_disable_escape(true); + } + // test if the expression is to render some value - if ht.is_name_only() { + let result = if ht.is_name_only() { let helper_name = ht.name.expand_as_name(registry, ctx, rc)?; if helper_exists(&helper_name, registry, rc) { render_helper(ht, registry, ctx, rc, out) @@ -745,7 +787,7 @@ impl Renderable for TemplateElement { Err(RenderError::strict_error(context_json.relative_path())) } else { // helper missing - if let Some(hook) = registry.get_helper(HELPER_MISSING) { + if let Some(hook) = registry.get_or_load_helper(HELPER_MISSING)? { let h = Helper::try_from_template(ht, registry, ctx, rc)?; hook.call(&h, registry, ctx, rc, out) } else { @@ -762,20 +804,13 @@ impl Renderable for TemplateElement { } else { // this is a helper expression render_helper(ht, registry, ctx, rc, out) - } - } - HTMLExpression(ref v) => { - debug!("Rendering value: {:?}", v); - let context_json = v.expand(registry, ctx, rc)?; + }; - // strict mode check - if registry.strict_mode() && context_json.is_value_missing() { - return Err(RenderError::strict_error(context_json.relative_path())); + if is_html_expression { + rc.set_disable_escape(false); } - let rendered = context_json.value().render(); - out.write(rendered.as_ref())?; - Ok(()) + result } HelperBlock(ref ht) => render_helper(ht, registry, ctx, rc, out), DecoratorExpression(_) | DecoratorBlock(_) => self.eval(registry, ctx, rc), @@ -855,7 +890,9 @@ fn test_expression() { #[test] fn test_html_expression() { let r = Registry::new(); - let element = HTMLExpression(Parameter::Path(Path::with_named_paths(&["hello"]))); + let element = HtmlExpression(Box::new(HelperTemplate::with_path(Path::with_named_paths( + &["hello"], + )))); let mut out = StringOutput::new(); let mut m: BTreeMap = BTreeMap::new(); @@ -891,7 +928,7 @@ fn test_template() { let template = Template { elements, name: None, - mapping: None, + mapping: Vec::new(), }; { @@ -908,7 +945,7 @@ fn test_render_context_promotion_and_demotion() { let mut render_context = RenderContext::new(None); let mut block = BlockContext::new(); - block.set_local_var("@index".to_string(), to_json(0)); + block.set_local_var("index", to_json(0)); render_context.push_block(block); render_context.push_block(BlockContext::new()); @@ -925,29 +962,6 @@ fn test_render_context_promotion_and_demotion() { ); } -#[test] -fn test_render_subexpression() { - use crate::support::str::StringWriter; - - let r = Registry::new(); - let mut sw = StringWriter::new(); - - let mut m: BTreeMap = BTreeMap::new(); - m.insert("hello".to_string(), "world".to_string()); - m.insert("world".to_string(), "nice".to_string()); - m.insert("const".to_string(), "truthy".to_string()); - - { - if let Err(e) = - r.render_template_to_write("

{{#if (const)}}{{(hello)}}{{/if}}

", &m, &mut sw) - { - panic!("{}", e); - } - } - - assert_eq!(sw.into_string(), "

world

".to_string()); -} - #[test] fn test_render_subexpression_issue_115() { use crate::support::str::StringWriter; @@ -1094,16 +1108,54 @@ fn test_zero_args_heler() { r.register_helper( "helperMissing", Box::new( - |_: &Helper<'_, '_>, + |h: &Helper<'_, '_>, _: &Registry<'_>, _: &Context, _: &mut RenderContext<'_, '_>, out: &mut dyn Output| - -> Result<(), RenderError> { out.write("Default").map_err(Into::into) }, + -> Result<(), RenderError> { + let name = h.name(); + out.write(&format!("{} not resolved", name))?; + Ok(()) + }, ), ); assert_eq!( r.render("t1", &json!({"name": "Alex"})).unwrap(), - "Output name: Default" + "Output name: first_name not resolved" + ); +} + +#[test] +fn test_identifiers_starting_with_numbers() { + let mut r = Registry::new(); + + assert!(r + .register_template_string("r1", "{{#if 0a}}true{{/if}}") + .is_ok()); + let r1 = r.render("r1", &json!({"0a": true})).unwrap(); + assert_eq!(r1, "true"); + + assert!(r.register_template_string("r2", "{{eq 1a 1}}").is_ok()); + let r2 = r.render("r2", &json!({"1a": 2, "a": 1})).unwrap(); + assert_eq!(r2, "false"); + + assert!(r + .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? + .is_ok()); + let r3 = r + .render("r3", &json!({"0": true, "1a": true, "2_2": true})) + .unwrap(); + assert_eq!( + r3, + "0: true \n1a: true resolved from context\n2_2: true resolved from context" ); + + // these should all be errors: + assert!(r.register_template_string("r4", "{{eq 1}}").is_ok()); + assert!(r.register_template_string("r5", "{{eq a1}}").is_ok()); + assert!(r.register_template_string("r6", "{{eq 1a}}").is_ok()); + assert!(r.render("r4", &()).is_err()); + assert!(r.render("r5", &()).is_err()); + assert!(r.render("r6", &()).is_err()); } diff --git a/src/sources.rs b/src/sources.rs new file mode 100644 index 000000000..8c8b2ba57 --- /dev/null +++ b/src/sources.rs @@ -0,0 +1,34 @@ +use std::fs::File; +use std::io::{BufReader, Error as IOError, Read}; +use std::path::PathBuf; + +pub(crate) trait Source { + type Item; + type Error; + + fn load(&self) -> Result; +} + +pub(crate) struct FileSource { + path: PathBuf, +} + +impl FileSource { + pub(crate) fn new(path: PathBuf) -> FileSource { + FileSource { path } + } +} + +impl Source for FileSource { + type Item = String; + type Error = IOError; + + fn load(&self) -> Result { + let mut reader = BufReader::new(File::open(&self.path)?); + + let mut buf = String::new(); + reader.read_to_string(&mut buf)?; + + Ok(buf) + } +} diff --git a/src/template.rs b/src/template.rs index 79db4315c..39d24590e 100644 --- a/src/template.rs +++ b/src/template.rs @@ -5,11 +5,11 @@ use std::str::FromStr; use pest::error::LineColLocation; use pest::iterators::Pair; -use pest::{Parser, Position}; +use pest::{Parser, Position, Span}; use serde_json::value::Value as Json; use crate::error::{TemplateError, TemplateErrorReason}; -use crate::grammar::{HandlebarsParser, Rule}; +use crate::grammar::{self, HandlebarsParser, Rule}; use crate::json::path::{parse_json_path_from_iter, Path}; use self::TemplateElement::*; @@ -18,11 +18,26 @@ use self::TemplateElement::*; pub struct TemplateMapping(pub usize, pub usize); /// A handlebars template -#[derive(PartialEq, Clone, Debug)] +#[derive(PartialEq, Clone, Debug, Default)] pub struct Template { pub name: Option, pub elements: Vec, - pub mapping: Option>, + pub mapping: Vec, +} + +#[derive(Default)] +pub(crate) struct TemplateOptions { + pub(crate) prevent_indent: bool, + pub(crate) name: Option, +} + +impl TemplateOptions { + fn name(&self) -> String { + self.name + .as_ref() + .cloned() + .unwrap_or_else(|| "Unnamed".to_owned()) + } } #[derive(PartialEq, Clone, Debug)] @@ -164,26 +179,24 @@ impl Parameter { let mut it = parser.flatten().peekable(); Template::parse_param(s, &mut it, s.len() - 1) } + + fn debug_name(&self) -> String { + if let Some(name) = self.as_name() { + name.to_owned() + } else { + format!("{:?}", self) + } + } } impl Template { - pub fn new(mapping: bool) -> Template { - Template { - elements: Vec::new(), - name: None, - mapping: if mapping { Some(Vec::new()) } else { None }, - } + pub fn new() -> Template { + Template::default() } fn push_element(&mut self, e: TemplateElement, line: usize, col: usize) { self.elements.push(e); - if let Some(ref mut maps) = self.mapping { - maps.push(TemplateMapping(line, col)); - } - } - - pub fn compile>(source: S) -> Result { - Template::compile2(source, false) + self.mapping.push(TemplateMapping(line, col)); } fn parse_subexpression<'a, I>( @@ -289,11 +302,7 @@ impl Template { Ok((key, value)) } - fn parse_block_param<'a, I>( - _: &'a str, - it: &mut Peekable, - limit: usize, - ) -> Result + fn parse_block_param<'a, I>(_: &'a str, it: &mut Peekable, limit: usize) -> BlockParam where I: Iterator>, { @@ -313,9 +322,9 @@ impl Template { if let Some(p2) = p2 { it.next(); - Ok(BlockParam::Pair((Parameter::Name(p1), Parameter::Name(p2)))) + BlockParam::Pair((Parameter::Name(p1), Parameter::Name(p2))) } else { - Ok(BlockParam::Single(Parameter::Name(p1))) + BlockParam::Single(Parameter::Name(p1)) } } @@ -366,7 +375,7 @@ impl Template { hashes.insert(key, value); } Rule::block_param => { - block_param = Some(Template::parse_block_param(source, it.by_ref(), end)?); + block_param = Some(Template::parse_block_param(source, it.by_ref(), end)); } Rule::pro_whitespace_omitter => { omit_pro_ws = true; @@ -386,12 +395,40 @@ impl Template { fn remove_previous_whitespace(template_stack: &mut VecDeque