The powerful template system that Go needs

Plush Build Status GoDoc

Plush is the templating system that Go both needs and deserves. Powerful, flexible, and extendable, Plush is there to make writing your templates that much easier.

Introduction Video

Installation

$ go get -u github.com/gobuffalo/plush

Usage

Plush allows for the embedding of dynamic code inside of your templates. Take the following example:

<!-- input -->
<p><%= "plush is great" %></p>

<!-- output -->
<p>plush is great</p>

Controlling Output

By using the <%= %> tags we tell Plush to dynamically render the inner content, in this case the string plush is great, into the template between the <p></p> tags.

If we were to change the example to use <% %> tags instead the inner content will be evaluated and executed, but not injected into the template:

<!-- input -->
<p><% "plush is great" %></p>

<!-- output -->
<p></p>

By using the <% %> tags we can create variables (and functions!) inside of templates to use later:

<!-- does not print output -->
<%
let h = {name: "mark"}
let greet = fn(n) {
  return "hi " + n
}
%>
<!-- prints output -->
<h1><%= greet(h["name"]) %></h1>

Full Example:

html := `<html>
<%= if (names && len(names) > 0) { %>
	<ul>
		<%= for (n) in names { %>
			<li><%= capitalize(n) %></li>
		<% } %>
	</ul>
<% } else { %>
	<h1>Sorry, no names. :(</h1>
<% } %>
</html>`

ctx := plush.NewContext()
ctx.Set("names", []string{"john", "paul", "george", "ringo"})

s, err := plush.Render(html, ctx)
if err != nil {
  log.Fatal(err)
}

fmt.Print(s)
// output: <html>
// <ul>
// 		<li>John</li>
// 		<li>Paul</li>
// 		<li>George</li>
// 		<li>Ringo</li>
// 		</ul>
// </html>

Comments

You can add comments like this:

<%# This is a comment %>

If/Else Statements

The basic syntax of if/else if/else statements is as follows:

<%
if (true) {
  # do something
} else if (false) {
  # do something
} else {
  # do something else
}
%>

When using if/else statements to control output, remember to use the <%= %> tag to output the result of the statement:

<%= if (true) { %>
  <!-- some html here -->
<% } else { %>
  <!-- some other html here -->
<% } %>

Operators

Complex if statements can be built in Plush using "common" operators:

  • == - checks equality of two expressions
  • != - checks that the two expressions are not equal
  • ~= - checks a string against a regular expression (foo ~= "^fo")
  • < - checks the left expression is less than the right expression
  • <= - checks the left expression is less than or equal to the right expression
  • > - checks the left expression is greater than the right expression
  • >= - checks the left expression is greater than or equal to the right expression
  • && - requires both the left and right expression to be true
  • || - requires either the left or right expression to be true

Grouped Expressions

<%= if ((1 < 2) && (someFunc() == "hi")) { %>
  <!-- some html here -->
<% } else { %>
  <!-- some other html here -->
<% } %>

Maps

Maps in Plush will get translated to the Go type map[string]interface{} when used. Creating, and using maps in Plush is not too different than in JSON:

<% let h = {key: "value", "a number": 1, bool: true} %>

Would become the following in Go:

map[string]interface{}{
  "key": "value",
  "a number": 1,
  "bool": true,
}

Accessing maps is just like access a JSON object:

<%= h["key"] %>

Using maps as options to functions in Plush is incredibly powerful. See the sections on Functions and Helpers to see more examples.

Arrays

Arrays in Plush will get translated to the Go type []interface{} when used.

<% let a = [1, 2, "three", "four", h] %>
[]interface{}{ 1, 2, "three", "four", h }

For Loops

There are three different types that can be looped over: maps, arrays/slices, and iterators. The format for them all looks the same:

<%= for (key, value) in expression { %>
  <%= key %> <%= value %>
<% } %>

The values inside the () part of the statement are the names you wish to give to the key (or index) and the value of the expression. The expression can be an array, map, or iterator type.

Arrays

Using Index and Value

<%= for (i, x) in someArray { %>
  <%= i %> <%= x %>
<% } %>

Using Just the Value

<%= for (val) in someArray { %>
  <%= val %>
<% } %>

Maps

Using Index and Value

<%= for (k, v) in someMap { %>
  <%= k %> <%= v %>
<% } %>

Using Just the Value

<%= for (v) in someMap { %>
  <%= v %>
<% } %>

Iterators

type ranger struct {
	pos int
	end int
}

func (r *ranger) Next() interface{} {
	if r.pos < r.end {
		r.pos++
		return r.pos
	}
	return nil
}

func betweenHelper(a, b int) Iterator {
	return &ranger{pos: a, end: b - 1}
}
html := `<%= for (v) in between(3,6) { return v } %>`

ctx := plush.NewContext()
ctx.Set("between", betweenHelper)

s, err := plush.Render(html, ctx)
if err != nil {
  log.Fatal(err)
}
fmt.Print(s)
// output: 45

Helpers

For a full list, and documentation of, all the Helpers included in Plush, see github.com/gobuffalo/helpers.

Custom Helpers

html := `<p><%= one() %></p>
<p><%= greet("mark")%></p>
<%= can("update") { %>
<p>i can update</p>
<% } %>
<%= can("destroy") { %>
<p>i can destroy</p>
<% } %>
`

ctx := NewContext()

// one() #=> 1
ctx.Set("one", func() int {
  return 1
})

// greet("mark") #=> "Hi mark"
ctx.Set("greet", func(s string) string {
  return fmt.Sprintf("Hi %s", s)
})

// can("update") #=> returns the block associated with it
// can("adsf") #=> ""
ctx.Set("can", func(s string, help HelperContext) (template.HTML, error) {
  if s == "update" {
    h, err := help.Block()
    return template.HTML(h), err
  }
  return "", nil
})

s, err := Render(html, ctx)
if err != nil {
  log.Fatal(err)
}
fmt.Print(s)
// output: <p>1</p>
// <p>Hi mark</p>
// <p>i can update</p>

Special Thanks

This package absolutely 100% could not have been written without the help of Thorsten Ball's incredible book, Writing an Interpeter in Go.

Not only did the book make understanding the process of writing lexers, parsers, and asts, but it also provided the basis for the syntax of Plush itself.

If you have yet to read Thorsten's book, I can't recommend it enough. Please go and buy it!

Owner
Buffalo - The Go Web Eco-System
Buffalo - The Go Web Eco-System
Comments
  • Cannot get the **_User_** value.

    Cannot get the **_User_** value.

    type Order struct {
    	ID          uuid.UUID `json:"id" db:"id"`
    	CreatedAt   time.Time `json:"created_at" db:"created_at"`
    	UpdatedAt   time.Time `json:"updated_at" db:"updated_at"`
    	UserID      uuid.UUID `json:"user_id" db:"user_id"`
    	Num         string    `json:"num" db:"num"`
    	Status      string    `json:"status" db:"status"`
    	TotalMoney  float64   `json:"total_money" db:"total_money"`
    	Money       float64   `json:"money" db:"money"`
    	Description string    `json:"description" db:"description"`
    	User        User      `json:"user" db:"-"`
    	Goods       []Good    `json:"goods" db:"-"`
    }
    
    type User struct {
    	ID                   uuid.UUID `json:"id" db:"id"`
    	CreatedAt            time.Time `json:"created_at" db:"created_at"`
    	UpdatedAt            time.Time `json:"updated_at" db:"updated_at"`
    	Mobile               string    `json:"mobile" db:"mobile"`
    	Role                 string    `json:"role" db:"role"`
    	Addr                 string    `json:"addr" db:"addr"`
    	PasswordHash         string    `json:"-" db:"password_hash"`
    	Password             string    `json:"-" db:"-"`
    	PasswordConfirmation string    `json:"-" db:"-"`
    }
    

    index.html:

     <%= for (order) in orders { %>
       <%= order.User.Mobile %>
      <% } %>
    

    Cannot get the User value.

  • Add markdown file support in partials

    Add markdown file support in partials

    This is a fix for https://github.com/gobuffalo/gobuffalo/issues/404, but not necessarily a good one:

    What is bad?

    In contrast to the js support in partial_helper.go#47, the markdown support activates if the content type includes markdown or if the file ends in .md (JS support requires both to be true). This is inconsistent and might lead to confusion.

    Why did I still do it this way?

    Concluding from my tests, buffalo doesn't set a markdown content type but uses text/html. I could not yet find the place to change this in buffalo. I'm not sure if buffalo can change the content type for markdown files as r.HTML seems to define it on initialization where the file type might not yet be clear: https://gobuffalo.io/en/docs/rendering#automatic-extensions

    Solution

    To be honest, I don't think this really is a big deal. If someone can give me some pointers on how to fix it in buffalo though, I'd be happy to give it a try and make this fix a bit better.

  • string concatenation in for loop

    string concatenation in for loop

    Writing menu renderer with recursive calls. The problem is that I cannot append a string to menuString inside a for loop. Or I don't know some catch :\

    package main
    
    import (
    	"fmt"
    	"github.com/gobuffalo/plush"
    	"log"
    )
    
    func main()  {
    	html := `<html>
    <%
    let renderMenu = fn(menuItems, inner) {
      let menuString = "<ul class='nav nav-pills nav-sidebar flex-column' data-widget='treeview' role='menu' data-accordion='false'>"
      if (inner) {
        menuString = "<ul class='nav nav-treeview'>"
      }
    
      for (item) in menuItems {
        if (len(item["items"]) > 0) {
          menuString = menuString + "<li class='nav-item has-treeview menu-open'><a href='#' class='nav-link'><p>" + item["name"] + "<i class='right fas fa-angle-left right'></i></p></a>" + renderMenu(item["items"], true) + "</li>"
        } else {
          menuString = menuString + "<li class='nav-item'><a href='" + item["link"] + "' class='nav-link'><p>" + item["name"] + "</p></a></li>"
        }
      }
    
      return raw(menuString + "</ul>")
    }
    
    let renderMenuOuter = fn(menuItems) {
      return renderMenu(menuItems, false)
    }
    %>
    
    <%= renderMenuOuter(menuItems) %>
    </html>`
    
    	ctx := plush.NewContext()
    
    	menuItems := []interface{} {
    		map[string]interface{} {
    			"name": "o1",
    			"link": "#",
    			"items": []map[string]interface{} {
    				{
    					"name": "i1",
    					"link": "/i1",
    				},
    				{
    					"name": "i2",
    					"link": "/i2",
    				},
    			},
    		},
    		map[string]interface{} {
    			"name": "o2",
    			"link": "/o2",
    		},
    		map[string]interface{} {
    			"name": "o3",
    			"link": "/o3",
    		},
    	}
    	ctx.Set("menuItems", menuItems)
    
    	s, err := plush.Render(html, ctx)
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	fmt.Print(s)
    }
    
    

    Current render result is:

    <html>
    
    <ul class='nav nav-pills nav-sidebar flex-column' data-widget='treeview' role='menu' data-accordion='false'></ul>
    </html>a
    

    Expected: to have some <li> elements

  • Fix invalid if condition parameter

    Fix invalid if condition parameter

    Fixes issue 115

    The new pull requests confirm the validity of the If condition parameter during parsing and compiling. It checks for syntax error and if the evaluated If condition is of a type Bool or nil

    During Parsing:

    We check if the expression. Condition implements ast.Comparable. If not, then a syntax error is returned. If the type of expression. Condition is of type ast.InflixExpression then we also check the Left and Right expressions do implement ast.Comparable recursively.

    During Compilation:

    We confirm the evaluated type return of the if condition to be either bool or nil.

  • Memory leak issue potentially caused by rendering authenticity_token in meta tag

    Memory leak issue potentially caused by rendering authenticity_token in meta tag

    Description

    Memory usage in heap is increasing proportionately to the volume of access caused by the following memory allocations. pprof report shows as follows.

    With 10000 access 
    $ go tool pprof -top  http://127.0.0.1:6060/debug/pprof/heap
          flat  flat%   sum%        cum   cum%
    30285.50kB 92.21% 92.21% 30285.50kB 92.21%  bytes.(*Buffer).String (inline)
    
    With 20000 access 
    $ go tool pprof -top  http://127.0.0.1:6060/debug/pprof/heap
          flat  flat%   sum%        cum   cum%
       57.15MB 85.65% 85.65%    57.15MB 85.65%  bytes.(*Buffer).String (inline)
    

    After many trials, I found that the issue doesn't happen by removing following meta tag in application.plush.html

    <meta name="csrf-token" content="<%= authenticity_token %>" />
    

    Steps to Reproduce the Problem

    1. Create new buffalo project
    $ buffalo new buffalonew
    
    1. Prepare postgres database
    2. Add following code for pprof in main.go
    import (
    	"fmt"
    	"net/http"
    	_ "net/http/pprof"
    
    	"github.com/gobuffalo/envy"
    )
    
    func init() {
    	go func() {
    		if envy.Get("GO_ENV", "development") == "development" {
    			fmt.Println("start pprof on :6060")
    			log.Println(http.ListenAndServe("127.0.0.1:6060", nil))
    		}
    	}()
    }
    
    1. Launch the project
    $ cd buffalonew
    $ buffalo dev
    
    1. Confirm it's running image

    2. Run pprof report

    $ go tool pprof -top  http://127.0.0.1:6060/debug/pprof/heap   
    
    1. Generate dummy access with ab command
    $ ab -n 10000 -c 100 http://127.0.0.1:3000/
    
    1. Run pprof report again and compare with step gobuffalo/buffalo#6
    $ go tool pprof -top  http://127.0.0.1:6060/debug/pprof/heap   
    
    1. After a few hours later, try pprof again. The memory allocation is still there...

    Expected Behavior

    The memory allocation with bytes.(*Buffer).String will be cleaned up in heap after decent durations

    Actual Behavior

    The memory allocation with bytes.(*Buffer).String is not cleaned up until killing the buffalo process

    Info

    • This leak seems to be caused by line 62 in plush/compiler It can be traced by pprof graphical report. https://github.com/gobuffalo/plush/blob/master/compiler.go

    Environment

    • macOS Big Sur Version 11.0.1
    • go version go1.15.5 darwin/amd64
    • Buffalo version is: v0.16.17

    Question

    Our real application seems working properly with removing authenticity_token in header. Forms are working with the token generated in input tag. Do you think if there are any issues with removing the token in header meta?

    Please feel free to contact me for additional info. Thanks!

  • Support `else if`

    Support `else if`

    To learn more about Go I thought it would be cool to implement else if support.

    What do you think about this approach?

    Codeclimate is failing, but I can fix that later if I know I'm on the right track.

  • Can't evaluate if conditions properly with an undefined variable

    Can't evaluate if conditions properly with an undefined variable

    Plush evaluates undefined variables in if conditions without throwing an error; however, it becomes an issue with this example.

    	r := require.New(t)
    	type page struct {
    		PageTitle string
    	}
    	g := &page{"cafe"}
    	ctx := NewContext()
    	ctx.Set("pages", g)
    	ctx.Set("path", "home")
    
    	input := `<%= if ( path != "pagePath" || (page && page.PageTitle != "cafe") ) { %>hi<%} %>`
    
    	s, err := Render(input, ctx)
    	r.NoError(err)
    	r.Equal("hi", s)
    
    

    Currently, this should return "hi" as one of the conditions path != "pagePath" is true; however, it returns an empty string because the if condition values on the right of the or return an error, so it bubbles back up and results in evaluating the condition as false.

    This behaviour is inconsistent because this condition evaluates as true <%= if ( path != "pagePath" || !page) { %>hi<%} %>

  • catch out of bounds slice/array index access

    catch out of bounds slice/array index access

    An error will return to the user if they try to access an index that doesn't exist.

    Example: array index out of bounds, got index 5, while array size is 5

  • Rendering with backslash in template before <%=var%> fails

    Rendering with backslash in template before <%=var%> fails

    Following unit test

    func Test_Issue_With_Backslash(t *testing.T) {
    	r := require.New(t)
    	input := `c:\\temp\\<%=subfolder%>`
    	s, err := Render(input, NewContextWith(map[string]interface{}{
    		"subfolder": "test",
    	}))
    	r.NoError(err)
    	r.Equal(`c:\\temp\\test`, s)
    }
    

    fails with:

    Not equal: 
    	            	expected: "c:\\\\temp\\\\test"
    	            	received: "c:\\\\temp\\<%!=(MISSING)subfolder%!>(MISSING)"
    

    (While the following unit test runs fine:

    func Test_With_Slash(t *testing.T) {
    	r := require.New(t)
    	input := `c:/temp/<%=subfolder%>`
    	s, err := Render(input, NewContextWith(map[string]interface{}{
    		"subfolder": "test",
    	}))
    	r.NoError(err)
    	r.Equal(`c:/temp/test`, s)
    

    )

  • return doesn't exit the block

    return doesn't exit the block

    If we use return somewhere (like in a conditional) execution does not exit the block as expected. Instead, it continues and future return statements seem to overwrite the previous returns.

    For a real failing test example, see https://github.com/gobuffalo/plush/pull/52

  • failure to get struct field when having map[string]struct in context

    failure to get struct field when having map[string]struct in context

    This unit test works fine:

    type address struct {
    	Street string
    }
    
    func Test_Render_Struct_Field(t *testing.T) {
    	r := require.New(t)
    
    	input := `<p><%= a.Street %></p>`
    	s, err := Render(input, NewContextWith(map[string]interface{}{
    		"a": address{Street: "avenue des nations-unies"},
    	}))
    	r.NoError(err)
    	r.Equal(`<p>avenue des nations-unies</p>`, s)
    }
    

    But when extending it to a map[string]address :

    func Test_Render_Map_Struct_Field(t *testing.T) {
    	r := require.New(t)
    
    	input := `<p><%= a["myaddress"].Street %></p>`
    	s, err := Render(input, NewContextWith(map[string]interface{}{
    		"a": map[string]address{
    			"myaddress": address{Street: "avenue des nations-unies"},
    		},
    	}))
    	r.NoError(err)
    	r.Equal(`<p>avenue des nations-unies</p>`, s)
    }
    

    It shows following error:

    Received unexpected error:
    	
    line 1: no prefix parse function for DOT found
    github.com/gobuffalo/plush.(*Template).Parse
        /Users/pdv/dev/src/github.com/gobuffalo/plush/template.go:41
    
  • Chaining function result ends in error

    Chaining function result ends in error

    Similar to the arrays issue when trying to call a member of the result of a function call plush errors.

    Code:

    ...
    <p class="mr-2">
        Employees will be reminded on 
        <date class="font-semibold">
            <%= period.EmployeeReviewReminderDate().Format("Jan 2, 2006") %>
        </date>.
    </p>
    ...
    

    Error:

    no prefix parse function for DOT found
    

    It would be good to support this.

    cc @Mido-sys

  • Proposal: do not use parenthesis when using single non-compound statements

    Proposal: do not use parenthesis when using single non-compound statements

    The parenthesis will be confusing if there are extra parenthesis surrounding it. Parenthesis could simply be used for grouping compound statements like if (true==true) || true { and the character count could also be reduced.

    Before:

    <%= if (true) { %>
      <!-- some html here -->
    <% } %>
    

    After:

    <%= if true { %>
      <!-- some html here -->
    <% } %>
    
  • Security: document that escaping is not contextual

    Security: document that escaping is not contextual

    Escaping is not contextual and HTML escaping is used in every context. This might lead newcomers to think that it is safe to interpolate user controlled data in a page, leading to XSS.

    I think it would be better to point out in README or documentation that this package does not aim to protect users from XSS but just implements a rudimentary escaping mechanism.

    Since some gophers might be used to html/template (which performs contextual autoescaping) this seems worth pointing out.

    (I found a previous similar issue in #79 that might be a signal that this is an issue some other people might have encountered)

  • Syntax to trim trailing whitespace?

    Syntax to trim trailing whitespace?

    It would be nice if there was a syntax that trimmed the whitespace from template when using an evaluated value (and did so automatically for <% %>). This is implemented in ERB (ruby) as the "<%- %>" syntax (https://stackoverflow.com/questions/4632879/erb-template-removing-the-trailing-line).

    For example, it would be great if the following template returned <pre>Hello</pre>

    <pre>
    <%- "Hello" %>
    </pre>
    
  • Render Plush templates from Command Line

    Render Plush templates from Command Line

    I have started using Plush in one of my project. Firstly, Good work :+1: As a part of the code pipeline, some of the Plush templates need to be validated for consistency and the output they will render.

    Inspired from the erb command line tool, I have added support to plush binary to render a given template based on CLI vars or input JSON file.

    Example Usage:

    meson10@xps:~/workspace/plush$ cat hello.plush 
    Hello, <%= name %>
    
    meson10@xps:~/workspace/plush$ plush render hello.plush 
    Hello, 
    
    meson10@xps:~/workspace/plush$ cat test.json 
    {"name": "world"}
    
    meson10@xps:~/workspace/plush$ plush render hello.plush -c test.json 
    Hello, world
    
    meson10@xps:~/workspace/plush$ plush render hello.plush -v name=Mark
    Hello, Mark
    
    

    If this is of merit to your code I can send a PR. Alternatively, Please suggest if there is a better way to achieve the desired functionality.

Simple system for writing HTML/XML as Go code. Better-performing replacement for html/template and text/template

Simple system for writing HTML as Go code. Use normal Go conditionals, loops and functions. Benefit from typing and code analysis. Better performance than templating. Tiny and dependency-free.

Dec 5, 2022
A handy, fast and powerful go template engine.
A handy, fast and powerful go template engine.

Hero Hero is a handy, fast and powerful go template engine, which pre-compiles the html templates to go code. It has been used in production environme

Dec 27, 2022
The world’s most powerful template engine and Go embeddable interpreter.
The world’s most powerful template engine and Go embeddable interpreter.

The world’s most powerful template engine and Go embeddable interpreter

Dec 23, 2022
Wrapper package for Go's template/html to allow for easy file-based template inheritance.

Extemplate Extemplate is a small wrapper package around html/template to allow for easy file-based template inheritance. File: templates/parent.tmpl <

Dec 6, 2022
Goview is a lightweight, minimalist and idiomatic template library based on golang html/template for building Go web application.

goview Goview is a lightweight, minimalist and idiomatic template library based on golang html/template for building Go web application. Contents Inst

Dec 25, 2022
A template to build dynamic web apps quickly using Go, html/template and javascript
A template to build dynamic web apps quickly using Go, html/template and javascript

gomodest-template A modest template to build dynamic web apps in Go, HTML and sprinkles and spots of javascript. Why ? Build dynamic websites using th

Dec 29, 2022
Made from template temporalio/money-transfer-project-template-go
Made from template temporalio/money-transfer-project-template-go

Temporal Go Project Template This is a simple project for demonstrating Temporal with the Go SDK. The full 20 minute guide is here: https://docs.tempo

Jan 6, 2022
Go-project-template - Template for a golang project

This is a template repository for golang project Usage Go to github: https://git

Oct 25, 2022
Go-api-template - A rough template to give you a starting point for your API

Golang API Template This is only a rough template to give you a starting point f

Jan 14, 2022
Api-go-template - A simple Go API template that uses a controller-service based model to build its routes

api-go-template This is a simple Go API template that uses a controller-service

Feb 18, 2022
⚗ The most advanced CLI template on earth! Featuring automatic releases, website generation and a custom CI-System out of the box.
⚗ The most advanced CLI template on earth! Featuring automatic releases, website generation and a custom CI-System out of the box.

cli-template ✨ ⚗ A template for beautiful, modern, cross-platform compatible CLI tools written with Go! Getting Started | Wiki This template features

Dec 4, 2022
HTML template engine for Go

Ace - HTML template engine for Go Overview Ace is an HTML template engine for Go. This is inspired by Slim and Jade. This is a refinement of Gold. Exa

Jan 4, 2023
Package damsel provides html outlining via css-selectors and common template functionality.

Damsel Markup language featuring html outlining via css-selectors, extensible via pkg html/template and others. Library This package expects to exist

Oct 23, 2022
Simple and fast template engine for Go

fasttemplate Simple and fast template engine for Go. Fasttemplate performs only a single task - it substitutes template placeholders with user-defined

Dec 30, 2022
Jet template engine

Jet Template Engine for Go Jet is a template engine developed to be easy to use, powerful, dynamic, yet secure and very fast. simple and familiar synt

Jan 4, 2023
A complete Liquid template engine in Go
A complete Liquid template engine in Go

Liquid Template Parser liquid is a pure Go implementation of Shopify Liquid templates. It was developed for use in the Gojekyll port of the Jekyll sta

Dec 15, 2022
The mustache template language in Go

Overview mustache.go is an implementation of the mustache template language in Go. It is better suited for website templates than Go's native pkg/temp

Dec 22, 2022
Useful template functions for Go templates.

Sprig: Template functions for Go templates The Go language comes with a built-in template language, but not very many template functions. Sprig is a l

Jan 4, 2023
comparing the performance of different template engines

goTemplateBenchmark comparing the performance of different template engines full featured template engines Ace Amber Go Handlebars removed - Kasia Mus

Nov 17, 2022