Coder Social home page Coder Social logo

maciejhirsz / ramhorns Goto Github PK

View Code? Open in Web Editor NEW
284.0 6.0 29.0 305 KB

Fast Mustache template engine implementation in pure Rust.

Home Page: https://crates.io/crates/ramhorns

License: Mozilla Public License 2.0

Rust 99.57% HTML 0.41% CSS 0.02%
rust templating-engine templates mustache mustache-templates handlebars

ramhorns's Introduction

Ramhorns logo

Ramhorns

Tests badge Crates.io version badge Docs Crates.io license badge

Fast Mustache template engine implementation in pure Rust.

Ramhorns loads and processes templates at runtime. It comes with a derive macro which allows for templates to be rendered from native Rust data structures without doing temporary allocations, intermediate HashMaps or what have you.

With a touch of magic ๐ŸŽฉ, the power of friendship ๐Ÿฅ‚, and a sparkle of FNV hashing โœจ, render times easily compete with static template engines like Askama.

Cargo.toml

[dependencies]
ramhorns = "0.5"

Example

use ramhorns::{Template, Content};

#[derive(Content)]
struct Post<'a> {
    title: &'a str,
    teaser: &'a str,
}

#[derive(Content)]
struct Blog<'a> {
    title: String,        // Strings are cool
    posts: Vec<Post<'a>>, // &'a [Post<'a>] would work too
}

// Standard Mustache action here
let source = "<h1>{{title}}</h1>\
              {{#posts}}<article><h2>{{title}}</h2><p>{{teaser}}</p></article>{{/posts}}\
              {{^posts}}<p>No posts yet :(</p>{{/posts}}";

let tpl = Template::new(source).unwrap();

let rendered = tpl.render(&Blog {
    title: "My Awesome Blog!".to_string(),
    posts: vec![
        Post {
            title: "How I tried Ramhorns and found love ๐Ÿ’–",
            teaser: "This can happen to you too",
        },
        Post {
            title: "Rust is kinda awesome",
            teaser: "Yes, even the borrow checker! ๐Ÿฆ€",
        },
    ]
});

assert_eq!(rendered, "<h1>My Awesome Blog!</h1>\
                      <article>\
                          <h2>How I tried Ramhorns and found love ๐Ÿ’–</h2>\
                          <p>This can happen to you too</p>\
                      </article>\
                      <article>\
                          <h2>Rust is kinda awesome</h2>\
                          <p>Yes, even the borrow checker! ๐Ÿฆ€</p>\
                      </article>");

Features

  • Rendering common types, such as &str, String, bools, and numbers into {{variables}}.
  • Unescaped printing with {{{tripple-brace}}} or {{&ampersant}}.
  • Rendering sections {{#foo}} ... {{/foo}}.
  • Rendering inverse sections {{^foo}} ... {{/foo}}.
  • Rendering partials {{>file.html}}.
  • Zero-copy CommonMark rendering from fields marked with #[md].

Benches

Rendering a tiny template:

test a_simple_ramhorns            ... bench:          82 ns/iter (+/- 4) = 1182 MB/s
test b_simple_askama              ... bench:         178 ns/iter (+/- 8) = 544 MB/s
test c_simple_tera                ... bench:         416 ns/iter (+/- 98) = 233 MB/s
test c_simple_tera_from_serialize ... bench:         616 ns/iter (+/- 33) = 157 MB/s
test d_simple_mustache            ... bench:         613 ns/iter (+/- 34) = 158 MB/s
test e_simple_handlebars          ... bench:         847 ns/iter (+/- 40) = 114 MB/s

Rendering a tiny template with partials:

test pa_partials_ramhorns         ... bench:          85 ns/iter (+/- 7) = 1141 MB/s
test pb_partials_askama           ... bench:         210 ns/iter (+/- 9) = 461 MB/s
test pc_partials_mustache         ... bench:         827 ns/iter (+/- 39) = 117 MB/s
test pd_partials_handlebars       ... bench:         846 ns/iter (+/- 29) = 114 MB/s

Compiling a template from a string:

test xa_parse_ramhorns            ... bench:         190 ns/iter (+/- 10) = 821 MB/s
test xb_parse_mustache            ... bench:       3,229 ns/iter (+/- 159) = 48 MB/s
test xe_parse_handlebars          ... bench:       6,883 ns/iter (+/- 383) = 22 MB/s

Worth noting here is that Askama is processing templates at compile time and generates static rust code for rendering. This is great for performance, but it also means you can't swap out templates without recompiling your Rust binaries. In some cases, like for a static site generator, this is unfortunately a deal breaker.

Parsing the templates on runtime is never going to be free, however Ramhorns has a really fast parser built on top of Logos, that makes even that part of the process snappy.

The Mustache crate is the closest thing to Ramhorns in design and feature set.

License

Ramhorns is free software, and is released under the terms of the Mozilla Public License version 2.0. See LICENSE.

ramhorns's People

Contributors

creativcoder avatar grego avatar halvko avatar maciejhirsz avatar mrjohz avatar tiagolobocastro avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

ramhorns's Issues

Native Markdown support

I think more often than not, when rendering text into the templates these days, markdown is actually what we want. That would mean processing the markdown separately, outside of the template engine, and then pasting the resulting HTML in.

Given this is an experimental crate, I can use it to make some experiments and adding markdown support by default could be a cool feature.

The result of this:

#[derive(Content)]
struct Post<'a> {
    title: &'a str,
    body: &'a str,
}

let tpl = Template::new("<h1>{{title}}</h1><div>{{body}}</div>").unwrap();
let result = tpl.render(&Post {
    title: "Hello, _Ramhorns_!",
    body: "Now with native **Markdown** support!",
});

Would be:

<h1>Hello, <i>Ramhorns</i>!</h1><div>Now with native <b>Markdown</b> support!</div>

This can be done behind a (enabled by default) feature flag. It can also eliminate having to do extra allocations and copying buffers around, as the markdown can be parsed and then written to the output buffer by Ramhorns directly.


Other options of doing that would be either using an optional attribute on the struct:

#[derive(Content)]
struct Post<'a>
    #[md] title: &'a str,
    #[md] body: &'a str,
}

A newtype Markdown(pub T) wrapper around T: Content, such as:

#[derive(Content)]
struct Post<'a>
    title: Markdown(&'a str),
    body: Markdown(&'a str),
}

Or introducing a new syntax into the template (this is my least favorite option), such as {{* render_as_markdown }}

Recursive partials cause stack overflow

Example mustache:

test1.mustache

This is a test: {{> test2.mustache}}

test2.mustache

{{#value}}{{> test2.mustache}}{{/value}}{{^value}}Base case{{/value}}

This causes:

thread 'main' has overflowed its stack

License issue

Just wondering if the License is intended, as GPL is very restrictive for commercial use, requiring one to release the code. Can you change to MIT or similar?

(macro-hygiene) ramhorn derive codegen clashes with syn::Result

Hi @maciejhirsz

Thank you for this library!

While using ramhorns in my project: I have a use case where I need to use both syn and ramhorn together.

Following is the minimal reproducible example:

use syn::Result;

use ramhorns::{Content};

#[derive(Content)]
struct Post<'a> {
    title: &'a str
}

But, On running cargo build, I get the following error:

error[E0107]: wrong number of type arguments: expected 1, found 2
  --> src/main.rs:53:10
   |
53 | #[derive(Content)]
   |          ^^^^^^^ unexpected type argument
   |
   = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)

error: aborting due to previous error; 7 warnings emitted

For more information about this error, try `rustc --explain E0107`.

From what i understand, The code generated by ramhorn-derive generates Result<...> on Content's traits methods and that clashes with syn::Result. And as you can see the error message is also misleading.

What's the best way to solve this? I could alias my import of syn, but consider a larger project which imports ramhorn and derive and also has a syn::Result imported already, they might not know where to look for this error. As my example was small, I was able remove code incrementally and figured this out.

Lambdas sections?

Hi,

Mustache supports lambdas sections โ€“ is my understanding correct that this isn't supported in ramhorns?

If so, are there any plans for this?

Quick Question: Setting Pulldown-Cmark Options

If I have something like this:

#[derive(Content)]
struct Thing {
    #[md]
    content: String,
}

And I want to render the following markdown (note, has a table):

# This is Markdown
Nice table:

| this | that |
| ---- | ---- |
|    2 |    1 |

Rendering tables with pulldown-cmark requires one to set the ENABLE_TABLES option.

Does Ramhorns support tables in Markdown? More generally, does it support setting Options for #[md]? Or do we need to pull in pulldown-cmark and do this manually?

Thanks!

derive Content on enums

Hi,

currently Enums are not supported by the derive macro.
I would therefore like to propose a way to interpret enums in ramhorns.
If this finds a consensus I am willing to try to implement this.

General Enum Handling

I think to access enum items Mustache sections are fitting as they include the notion of false values or emptiness:

enum Status {
    Available,
    OutOfStock,
}

Would get accessed like this:

The Item is currently: 
{{#Available}}
available
{{/Available}}
{{#OutOfStock}}
out of stock
{{/OutOfStock}}

This leaves us with how to handle different types of enums:

Enum Types:

Following enum types are currently present in rust:

  1. Fieldless Enumerations. I.e. No item has data attached, this is much like a c enum and can be interpreted as integer type
  2. Enums with attached data.
    2.1 An Item can have a Struct like form
    2.2 An Item can have a Tuple like form
    2.3 An Item can have no data attached

Fieldless Enumerations

I think these are pretty easy to handle as shown in the example above. The question is how to handle the numerical value of these. We could use the same syntax as for tuple like enums as discussed below.

Enums with attached data

If any item of an enum has attached data the whole enum can't be interpreted as integer anymore according to the Rust reference. We have to therefore define how the different Item cases are handled. I assume that all attached data types implement the Content trait.

Struct like

The easiest is probably the struct like form, this I believe can just be handled like normal structs. We open a section and access the fields by name in the section.

Tuple like

Handling the tuple like form is more complicated as we don't have an identifier for the fields. We could work around this with numbers as identifiers where the number n identifies the nth Item like so:

enum Quantity {
    Numerical(i32),
    WithUnit(i32, String),
}

We could access these like this:

We currently have 
{{#Numerical}}
    {{1}}
{{/Numerical}}
{{#WithUnit}}
    {{1}} {{2}}
{{/WithUnit}}
in stock.

No data attached

Since there is no data attached the section would just check whether it is truthy or not. No data can be accessed as there is none present.

Summary

I believe this would give much more flexibility to ramhorns while keeping mostly true to the mustache templates ideas. And prevent the need to unwrap all enum variants into their own Option just to be able to use them with ramhorns.

I am looking forward to your thoughts and feedback!

How to use ramhorns with serde_json::Value?

I'd like to be able to use ramhorns with arbitrary data supplied via JSON. From what I can tell, serde_json::Value fits the requirement for doing this but ramhorns uses a derive trait which won't work with serde_json::Value because it's an enum.

Is there a workaround or an easier way to accomplish using arbitrary data with ramhorns that doesn't require using the ramhorns::Content trait?

LGPL license?

Context: There is a PR for the paperclip crate (paperclip-rs/paperclip#506) that introduces openAPI v3 spec + moustache templates and adds ramhorns as a dependency for processing said templates. However, since ramhorns is under GPL v3 and the paperclip crate is licensed under MIT then merging this would require changing the entire crate's license to GPL v3, which is something that the author (understandably) doesn't want to do. In that case relicensing ramhorns under LGPL v3 would allow it to be included there (while retaining most of the benefits of GPL v3 license)

Is it something that's feasible?

Support for custom file extensions

I think it would be nice if it was possible to specify a file extension when loading a folder with ramhorns.

Would this be a worthwhile addition?

Status of ramhorns

I read that you did this as an experimental template engine, but i like what you did so far. Especially that this template engine is pretty fast and allows hot-reloading of templates at runtime.

So i just wanted to know if you have any plans of continuing this project.

Generic type bounds are not handled

I have a use-case where I would like to wrap the content for individual pages with a generic "page + metadata" content struct (e.g. to provide i18n, script information, etc).

#[derive(ramhorns::Content)]
struct SamplePage {
    username: String,
    age: u64,
}

#[derive(ramhorns::Content)]
struct PageWithMetadata<T: ramhorns::Content> {
    #[ramhorns(flatten)]
    page: T,
    metadata: (),
}

However, this fails with a series of fairly-indecipherable errors:

error[E0658]: associated type bounds are unstable
 --> src/main.rs:8:25
  |
8 | struct PageWithMetadata<T: ramhorns::Content> {
  |                         ^^^^^^^^^^^^^^^^^^^^
  |
  = note: see issue #52662 <https://github.com/rust-lang/rust/issues/52662> for more information

error[E0107]: wrong number of type arguments: expected 1, found 0
 --> src/main.rs:8:8
  |
8 | struct PageWithMetadata<T: ramhorns::Content> {
  |        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected 1 type argument

error[E0229]: associated type bindings are not allowed here
 --> src/main.rs:8:25
  |
8 | struct PageWithMetadata<T: ramhorns::Content> {
  |                         ^^^^^^^^^^^^^^^^^^^^ associated type not allowed here

Reading briefly through the source, it seems like the generic part is not being parsed or broken down further, and simply being quoted in when necessary, which works for simple type parameters (e.g. <T>) but not when there are bounds on those types.

I also tried this by using a where T: ramhorns::Content block:

error[E0277]: the trait bound `T: ramhorns::Content` is not satisfied
 --> src/main.rs:7:10
  |
7 | #[derive(ramhorns::Content)]
  |          ^^^^^^^^^^^^^^^^^ the trait `ramhorns::Content` is not implemented for `T`
8 | struct PageWithMetadata<T> where T: ramhorns::Content {
  |                                     ----------------- required by this bound in `PageWithMetadata`
  |
  = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider restricting type parameter `T`
  |
8 | struct PageWithMetadata<T: ramhorns::Content> where T: ramhorns::Content {
  |                          ^^^^^^^^^^^^^^^^^^^

Is this supported at all/is there an obvious alternative way to achieve this? My other way of solving this would be by putting the metadata inside the SamplePage struct, but this involves more repetition and messy APIs, so I was hoping to avoid this.

Thanks!

1.0 Roadmap

I feel like we are in a pretty good place right now with the feature set and the API surface. Checklist for 1.0 release:

  • Remove(!) #[md] and markdown support. Figure out more generic way to support writing to encoders without intermediate buffers from iterators like pulldown_cmark's one.
  • Template interrogation as per #1 (seriously, 1).
  • ..?

Feature request: interrogate templates

One of the amazing and often overlooked benefits of a logic-less template is that the templates can be interrogated for the context variables they require, and the structure of the required context can be inferred from that.

As an example use case (in Ruby), see https://github.com/nyarly/diecut

It'd be great if Ramhorns could expose this functionality, ideally in a nicer way even than the Ruby Mustache does. As a glorious aspiration, being able to feed the inference from a template into Serde would be magic.

Support for partials?

G'day,

Was just giving your library a go and was wondering whether you were considering support for partials? Apologies if this is already possible, have been searching around for something to fill the use case where I need to compose several separate templates.

Cheers

BTreeMap vs HashMap

I have tested an implementation where I replaced the hashmap with btreemap. (https://github.com/godofdream/ramhorns/tree/btreemap)
It seems that in most cases the btreemap is faster on my machine (AMD Ryzen 5 3600 6-Core Processor).
Theoretically also the ramusage should be smaller.

test a_simple_ramhorns            ... bench:          85 ns/iter (+/- 6) = 1141 MB/s
test a_simple_ramhorns_btree            ... bench:          81 ns/iter (+/- 5) = 1197 MB/s
test b_simple_askama              ... bench:         209 ns/iter (+/- 14) = 464 MB/s
test c_simple_tera                ... bench:         485 ns/iter (+/- 9) = 200 MB/s
test c_simple_tera_from_serialize ... bench:         735 ns/iter (+/- 13) = 131 MB/s
test d_simple_mustache            ... bench:         570 ns/iter (+/- 10) = 170 MB/s
test e_simple_handlebars          ... bench:       1,097 ns/iter (+/- 24) = 88 MB/s
test pa_partials_ramhorns         ... bench:          83 ns/iter (+/- 9) = 1168 MB/s
test pa_partials_ramhorns_btree         ... bench:          77 ns/iter (+/- 8) = 1259 MB/s
test pb_partials_askama           ... bench:         218 ns/iter (+/- 14) = 444 MB/s
test pc_partials_mustache         ... bench:         758 ns/iter (+/- 13) = 127 MB/s
test pd_partials_handlebars       ... bench:         991 ns/iter (+/- 16) = 97 MB/s
test xa_parse_ramhorns             ... bench:         196 ns/iter (+/- 7) = 795 MB/s
test xa_parse_ramhorns_btree            ... bench:         143 ns/iter (+/- 2) = 1090 MB/s
test xb_parse_mustache           ... bench:       3,925 ns/iter (+/- 75) = 39 MB/s
test xe_parse_handlebars          ... bench:       8,862 ns/iter (+/- 126) = 17 MB/s

Would it make sense to make the btreemap/hashmap switchable by feature?

Is it possible to name elements of a Vec<String>?

So I'm making a personal blog site (surprise), and I wanted to add post tags. No problem, i'll add a field to my struct real quick.

#[derive(Content)]
pub struct Post {
    //...
    pub tags: Vec<String>
}

Now to make a row of links to each tag. Let's see.

{{#tags}}<a href="/post/tag/{{ ? }}">{{ ? }}</a> {{/tags}}

...I'm not sure how to name each item in the Vec. The section repeats 3 times if there are 3 elements in the Vec, but I don't know how to actually refer to the element, to make the sections different.

I was able to solve it using a tuple struct, just because I can name the field 0, like this.

#[derive(Content)]
pub struct Post {
    //...
    pub tags: Vec<Tag>
}

#[derive(Content, Clone, PartialEq, Eq, Hash, Default, Debug)] //idk im not used to tuple structs
pub struct Tag(String);
{{#tags}}<a href="/post/tag/{{0}}">{{0}}</a> {{/tags}}

I'm a little bummed that I needed a useless tuple struct for this though. Is there a nicer way to do this?

Confusing result when missing partial files

Sorry if this isn't 100% correct, I am really new to Rust.

When loading a directory of templates I got an odd error message. I eventually found out it is because my partial templates were not found on disk, but I had no idea which ones! I was able to eventually find it but it was a little cumbersome.

NOTE: My main issue was that I wasn't including the file extension in the partial names.

Example:
template.mustache

This is my template: {{> partial_that_isnt_on_disk.mustache}}

Error message:

thread 'main' panicked at 'Failed to load mustache templates: Io(Os { code: 2, kind: NotFound, message: "The system cannot find the file specified." })'

Compatibility with serde

Is it possible to pass a serde serializable struct to the render method?

For example:

#[derive(Serialize, Deserialize)]
struct MyStruct {
   ...
}
let tpl = Template::new(source).unwrap();
let my_struct = MyStruct{}
let rendered = tpl.render(my_struct)

We need to replace rust_mustace with something actively maintained and this library appears to be a good candidate; anyway, the ability to render whatever serializable instance is fundamental for us.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.