stick

package module
v1.0.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 1, 2023 License: MIT Imports: 13 Imported by: 31

README

Stick

CircleCI GoDoc

A Go language port of the Twig templating engine.

Overview

This project is split over two main parts.

Package github.com/tyler-sommer/stick is a Twig template parser and executor. It provides the core functionality and offers many of the same extension points as Twig like functions, filters, node visitors, etc.

Package github.com/tyler-sommer/stick/twig contains extensions to provide the most Twig-like experience for template writers. It aims to feature the same functions, filters, etc. to be closely Twig-compatible.

Current status
Stable, mostly feature complete

Stick itself is mostly feature-complete, with the exception of whitespace control, and better error handling in places.

Stick is made up of three main parts: a lexer, a parser, and a template executor. Stick's lexer and parser are complete. Template execution is under development, but essentially complete.

See the to do list for additional information.

Alternatives

These alternatives are worth checking out if you're considering using Stick.

Installation

Stick is intended to be used as a library. The recommended way to install the library is using go get.

go get -u github.com/tyler-sommer/stick

Usage

Execute a simple Stick template.

package main

import (
	"log"
	"os"
    
	"github.com/tyler-sommer/stick"
)

func main() {
	env := stick.New(nil)
	if err := env.Execute("Hello, {{ name }}!", os.Stdout, map[string]stick.Value{"name": "Tyler"}); err != nil {
		log.Fatal(err)
	}
}

See godoc for more information.

To do

Further

Documentation

Overview

Package stick is a Go language port of the Twig templating engine.

Stick executes Twig templates and allows users to define custom Functions, Filters, and Tests. The parser allows parse-time node inspection with NodeVisitors, and a template Loader to load named templates from any source.

Twig compatibility

Stick itself is a parser and template executor. If you're looking for Twig compatibility, check out package https://pkg.go.dev/github.com/tyler-sommer/stick/twig

For additional information on Twig, check http://twig.sensiolabs.org/

Basic usage

Obligatory "Hello, World!" example:

env := stick.New(nil);    // A nil loader means stick will simply execute
                          // the string passed into env.Execute.

// Templates receive a map of string to any value.
p := map[string]stick.Value{"name": "World"}

// Substitute os.Stdout with any io.Writer.
env.Execute("Hello, {{ name }}!", os.Stdout, p)

Another example, using a FilesystemLoader and responding to an HTTP request:

import "net/http"

// ...

fsRoot := os.Getwd() // Templates are loaded relative to this directory.
env := stick.New(stick.NewFilesystemLoader(fsRoot))
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
	env.Execute("bar.html.twig", w, nil) // Loads "bar.html.twig" relative to fsRoot.
})
http.ListenAndServe(":80", nil)

Types and values

Any user value in Stick is represented by a stick.Value. There are three main types in Stick when it comes to built-in operations: strings, numbers, and booleans. Of note, numbers are represented by float64 as this matches regular Twig behavior most closely.

Stick makes no restriction on what is stored in a stick.Value, but some built-in operators will try to coerce a value into a boolean, string, or number depending on the operation.

Additionally, custom types that implement specific interfaces can be coerced. Stick defines three interfaces: Stringer, Number, and Boolean. Each interface defines a single method that should convert a custom type into the specified type.

type myType struct {
	// ...
}

func (t *myType) String() string {
	return fmt.Sprintf("%v", t.someField)
}

func (t *myType) Number() float64 {
	return t.someFloatField
}

func (t *myType) Boolean() bool {
	return t.someValue != nil
}

On a final note, there exists three functions to coerce any type into a string, number, or boolean, respectively.

// Coerce any value to a string
v := stick.CoerceString(anything)

// Coerce any value to a float64
f := stick.CoerceNumber(anything)

// Coerce any vale to a boolean
b := stick.CoerceBool(anything)

User defined helpers

It is possible to define custom Filters, Functions, and boolean Tests available to your Stick templates. Each user-defined type is simply a function with a specific signature.

A Func represents a user-defined function.

type Func func(e *Env, args ...Value) Value

Functions can be called anywhere expressions are allowed. Functions may take any number of arguments.

A Filter is a user-defined filter.

type Filter func(e *Env, val Value, args ...Value) Value

Filters receive a value and modify it in some way. Filters also accept zero or more arguments beyond the value to be filtered.

A Test represents a user-defined boolean test.

type Test func(e *Env, val Value, args ...Value) bool

Tests are used to make some comparisons more expressive. Tests also accept zero to any number of arguments, and Test names can contain up to one space.

User-defined types are added to an Env after it is created. For example:

env := stick.New(nil)
env.Functions["form_valid"] = func(e *stick.Env, args ...stick.Value) stick.Value {
	// Do something useful..
	return true
}
env.Filters["number_format"] = func(e *stick.Env, val stick.Value, args ...stick.Value) stick.Value {
	v := stick.CoerceNumber(val)
	// Do some formatting.
	return fmt.Sprintf("%.2d", v)
}
env.Tests["empty"] = func(e *stick.Env, val stick.Value, args ...stick.Value) bool {
	// Probably not that useful.
	return stick.CoerceBool(val) == false
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func CoerceBool

func CoerceBool(v Value) bool

CoerceBool coerces the given value into a boolean. Boolean false is returned if the value cannot be coerced.

Example

This example demonstrates how various values are coerced to boolean.

package main

import (
	"fmt"

	"github.com/tyler-sommer/stick"
)

func main() {
	v0 := ""
	v1 := "some string"
	v2 := 0
	v3 := 3.14
	fmt.Printf("%t %t %t %t", stick.CoerceBool(v0), stick.CoerceBool(v1), stick.CoerceBool(v2), stick.CoerceBool(v3))
}
Output:

false true false true

func CoerceNumber

func CoerceNumber(v Value) float64

CoerceNumber coerces the given value into a number. Zero (0) is returned if the value cannot be coerced.

Example

This example demonstrates how various values are coerced to number.

package main

import (
	"fmt"

	"github.com/tyler-sommer/stick"
)

func main() {
	v0 := true
	v1 := ""
	v2 := "54"
	v3 := "1.33"
	fmt.Printf("%.f %.f %.f %.2f", stick.CoerceNumber(v0), stick.CoerceNumber(v1), stick.CoerceNumber(v2), stick.CoerceNumber(v3))
}
Output:

1 0 54 1.33

func CoerceString

func CoerceString(v Value) string

CoerceString coerces the given value into a string. An empty string is returned if the value cannot be coerced.

Example

This demonstrates how various values are coerced to string.

package main

import (
	"fmt"

	"github.com/tyler-sommer/stick"
)

func main() {
	v0 := true
	v1 := false // Coerces into ""
	v2 := 54
	v3 := 1.33
	v4 := 0
	fmt.Printf("%s '%s' %s %s %s", stick.CoerceString(v0), stick.CoerceString(v1), stick.CoerceString(v2), stick.CoerceString(v3), stick.CoerceString(v4))
}
Output:

1 '' 54 1.33 0

func Contains

func Contains(haystack Value, needle Value) (bool, error)

Contains returns true if the haystack Value contains needle.

func Equal

func Equal(left Value, right Value) bool

Equal returns true if the two Values are considered equal.

func IsArray

func IsArray(val Value) bool

IsArray returns true if the given Value is a slice or array.

func IsIterable

func IsIterable(val Value) bool

IsIterable returns true if the given Value is a slice, array, or map.

func IsMap

func IsMap(val Value) bool

IsMap returns true if the given Value is a map.

func Iterate

func Iterate(val Value, it Iteratee) (int, error)

Iterate calls the Iteratee func for every item in the Value.

func Len

func Len(val Value) (int, error)

Len returns the Length of Value.

Types

type Boolean

type Boolean interface {
	// Boolean returns a boolean representation of the type.
	Boolean() bool
}

Boolean is implemented by any value that has a Boolean method.

Example

This demonstrates how a type can be coerced to a boolean. The struct in this example has the Boolean method implemented.

func (e exampleType) Boolean() bool {
	return true
}
package main

import (
	"fmt"

	"github.com/tyler-sommer/stick"
)

type exampleType struct{}

func (e exampleType) Boolean() bool {
	return true
}

func (e exampleType) Number() float64 {
	return 3.14
}

func (e exampleType) String() string {
	return "some kinda string"
}

func main() {
	v := exampleType{}
	fmt.Printf("%t", stick.CoerceBool(v))
}
Output:

true

type Context

type Context interface {
	Name() string          // The name of the template being executed.
	Meta() ContextMetadata // Runtime metadata about the template.
	Scope() ContextScope   // All defined root-level names.
	Env() *Env
	// contains filtered or unexported methods
}

A Context represents the execution context of a template.

The Context is passed to all user-defined functions, filters, tests, and node visitors. It can be used to affect and inspect the local environment while a template is executing.

type ContextMetadata

type ContextMetadata interface {
	All() map[string]string         // Returns a map of all attributes and values.
	Set(name, val string)           // Set a metadata attribute on the context.
	Get(name string) (string, bool) // Get a metadata attribute on the context.
	// contains filtered or unexported methods
}

ContextMetadata contains additional, unstructured runtime attributes about the template being executed.

type ContextScope

type ContextScope interface {
	All() map[string]Value    // Returns a map of all values defined in the scope.
	Get(string) (Value, bool) // Get a value defined in the scope.
	Set(string, Value)        // Set a value in the scope.
	// contains filtered or unexported methods
}

ContextScope provides an interface with the currently executing template's scope.

type Env

type Env struct {
	Loader    Loader              // Template loader.
	Functions map[string]Func     // User-defined functions.
	Filters   map[string]Filter   // User-defined filters.
	Tests     map[string]Test     // User-defined tests.
	Visitors  []parse.NodeVisitor // User-defined node visitors.
}

Env represents a configured Stick environment.

func New

func New(loader Loader) *Env

New creates an empty Env. If nil is passed as loader, a StringLoader is used.

func (*Env) Execute

func (env *Env) Execute(tpl string, out io.Writer, ctx map[string]Value) error

Execute parses and executes the given template.

Example

An example of executing a template in the simplest possible manner.

package main

import (
	"fmt"
	"os"

	"github.com/tyler-sommer/stick"
)

func main() {
	env := stick.New(nil)

	params := map[string]stick.Value{"name": "World"}
	err := env.Execute(`Hello, {{ name }}!`, os.Stdout, params)
	if err != nil {
		fmt.Println(err)
	}
}
Output:

Hello, World!
Example (FilesystemLoader)

An example showing the use of the provided FilesystemLoader.

This example makes use of templates in the testdata folder. In particular, this example shows vertical (via extends) and horizontal reuse (via use).

package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/tyler-sommer/stick"
)

func main() {
	d, _ := os.Getwd()
	env := stick.New(stick.NewFilesystemLoader(filepath.Join(d, "testdata")))

	params := map[string]stick.Value{"name": "World"}
	err := env.Execute("main.txt.twig", os.Stdout, params)
	if err != nil {
		fmt.Println(err)
	}
}
Output:

This is a document.

Hello

An introduction to the topic.

The body of this topic.

Another section

Some extra information.

Still nobody knows.

Some kind of footer.
Example (Macro)

An example of macro definition and usage.

This example uses a macro to list the values, also showing two ways to import macros. Check the templates in the testdata folder for more information.

package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/tyler-sommer/stick"
)

func main() {
	d, _ := os.Getwd()
	env := stick.New(stick.NewFilesystemLoader(filepath.Join(d, "testdata")))

	params := map[string]stick.Value{
		"title_first": "Hello",
		"value_first": []struct{ Key, Value string }{
			{"item1", "something about item1"},
			{"item2", "something about item2"},
		},
		"title_second": "Responses",
		"value_second": []struct{ Key, Value string }{
			{"please", "no, thank you"},
			{"why not", "cause"},
		},
	}
	err := env.Execute("other.txt.twig", os.Stdout, params)
	if err != nil {
		fmt.Println(err)
	}
}
Output:

Hello

* item1: something about item1 (0)

* item2: something about item2 (1)

Responses

* please: no, thank you (0)

* why not: cause (1)
Example (Parent)

An example of macro definition and usage.

This example uses a macro to list the values, also showing two ways to import macros. Check the templates in the testdata folder for more information.

package main

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/tyler-sommer/stick"
)

func main() {
	d, _ := os.Getwd()
	env := stick.New(stick.NewFilesystemLoader(filepath.Join(d, "testdata")))

	err := env.Execute("parent.txt.twig", os.Stdout, nil)
	if err != nil {
		fmt.Println(err)
	}
}
Output:

This is a document.

Not A title

Testing parent()

This is a test

Another section

Some extra information.

func (*Env) Parse

func (env *Env) Parse(name string) (*parse.Tree, error)

Parse loads and parses the given template.

func (*Env) Register

func (env *Env) Register(e Extension) error

Register adds the given Extension to the Env.

type Extension

type Extension interface {
	// Init is the entry-point for an extension to modify the Env.
	Init(*Env) error
}

An Extension is used to group related functions, filters, visitors, etc.

type FilesystemLoader

type FilesystemLoader struct {
	// contains filtered or unexported fields
}

A FilesystemLoader loads templates from a filesystem.

func NewFilesystemLoader

func NewFilesystemLoader(rootDir string) *FilesystemLoader

NewFilesystemLoader creates a new FilesystemLoader with the specified root directory.

func (*FilesystemLoader) Load

func (l *FilesystemLoader) Load(name string) (Template, error)

Load on a FileSystemLoader attempts to load the given file, relative to the configured root directory.

type Filter

type Filter func(ctx Context, val Value, args ...Value) Value

A Filter is a user-defined filter. Filters receive a value and modify it in some way. Filters also accept parameters.

Example

A simple user-defined filter.

package main

import (
	"fmt"
	"os"

	"github.com/tyler-sommer/stick"
)

func main() {
	env := stick.New(nil)
	env.Filters["raw"] = func(ctx stick.Context, val stick.Value, args ...stick.Value) stick.Value {
		return stick.NewSafeValue(val)
	}

	err := env.Execute(
		`{{ name|raw }}`,
		os.Stdout,
		map[string]stick.Value{"name": "<name>"},
	)
	if err != nil {
		fmt.Println(err)
	}
}
Output:

<name>
Example (WithParam)

A simple user-defined filter that accepts a parameter.

package main

import (
	"fmt"
	"os"

	"strconv"

	"github.com/tyler-sommer/stick"
)

func main() {
	env := stick.New(nil)
	env.Filters["number_format"] = func(ctx stick.Context, val stick.Value, args ...stick.Value) stick.Value {
		var d float64
		if len(args) > 0 {
			d = stick.CoerceNumber(args[0])
		}
		return strconv.FormatFloat(stick.CoerceNumber(val), 'f', int(d), 64)
	}

	err := env.Execute(
		`${{ price|number_format(2) }}`,
		os.Stdout,
		map[string]stick.Value{"price": 4.99},
	)
	if err != nil {
		fmt.Println(err)
	}
}
Output:

$4.99

type Func

type Func func(ctx Context, args ...Value) Value

A Func represents a user-defined function. Functions can be called anywhere expressions are allowed and take any number of arguments.

Example

A contrived example of a user-defined function.

package main

import (
	"fmt"
	"os"

	"github.com/tyler-sommer/stick"
)

func main() {
	env := stick.New(nil)
	env.Functions["get_post"] = func(ctx stick.Context, args ...stick.Value) stick.Value {
		if len(args) == 0 {
			return nil
		}
		return struct {
			Title string
			ID    float64
		}{"A post", stick.CoerceNumber(args[0])}
	}

	err := env.Execute(
		`{% set post = get_post(123) %}{{ post.Title }} (# {{ post.ID }})`,
		os.Stdout,
		nil,
	)
	if err != nil {
		fmt.Println(err)
	}
}
Output:

A post (# 123)
Example (UsingContext)
package main

import (
	"fmt"

	"bytes"

	"io/ioutil"

	"github.com/tyler-sommer/stick"
)

func main() {
	env := stick.New(&stick.MemoryLoader{
		Templates: map[string]string{
			"base.html.twig": `<!doctype html>
<html>
<head>
	<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% block nav %}{% endblock %}
#3 base: {{ current_template() }}
{% block content %}{% endblock %}
</body>
</html>
`,
			"side.html.twig": `{% block nav %}#2 side: {{ current_template() }}{% endblock %}`,
			"child.html.twig": `{% extends 'base.html.twig' %}
{% use 'side.html.twig' %}
{% block title %}#1 child: {{ current_template() }}{% endblock %}
{% block content %}#4 child: {{ current_template() }}{% endblock %}`,
		},
	})
	buf := &bytes.Buffer{}
	env.Functions["current_template"] = func(ctx stick.Context, args ...stick.Value) stick.Value {
		// Reading persistent metadata
		v, _ := ctx.Meta().Get("current_template_calls")
		nc := stick.CoerceNumber(v)
		nc++

		fmt.Fprintf(buf, "#%.0f Current Template: %s\n", nc, ctx.Name())

		// Writing persistent metadata
		ctx.Meta().Set("current_template_calls", stick.CoerceString(nc))

		return nil
	}

	// Notice that we discard the actual output. We only care about what the
	// current_template function writes to buf.
	err := env.Execute(
		`child.html.twig`,
		ioutil.Discard,
		nil,
	)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(buf.String())
}
Output:

#1 Current Template: child.html.twig
#2 Current Template: side.html.twig
#3 Current Template: base.html.twig
#4 Current Template: child.html.twig

type Iteratee

type Iteratee func(k, v Value, l Loop) (brk bool, err error)

An Iteratee is called for each step in a loop.

type Loader

type Loader interface {
	// Load attempts to load the specified template, returning a Template or an error.
	Load(name string) (Template, error)
}

Loader defines a type that can load Stick templates using the given name.

type Loop

type Loop struct {
	Last      bool
	Index     int
	Index0    int
	Revindex  int
	Revindex0 int
	First     bool
	Length    int
}

Loop contains metadata about the current state of a loop.

type MemoryLoader

type MemoryLoader struct {
	Templates map[string]string
}

MemoryLoader loads templates from an in-memory map.

func (*MemoryLoader) Load

func (l *MemoryLoader) Load(name string) (Template, error)

Load tries to load the template from the in-memory map.

type Number

type Number interface {
	// Number returns a float64 representation of the type.
	Number() float64
}

Number is implemented by any value that has a Number method.

Example

This demonstrates how a type can be coerced to a number. The struct in this example has the Number method implemented.

func (e exampleType) Number() float64 {
	return 3.14
}
package main

import (
	"fmt"

	"github.com/tyler-sommer/stick"
)

type exampleType struct{}

func (e exampleType) Boolean() bool {
	return true
}

func (e exampleType) Number() float64 {
	return 3.14
}

func (e exampleType) String() string {
	return "some kinda string"
}

func main() {
	v := exampleType{}
	fmt.Printf("%.2f", stick.CoerceNumber(v))
}
Output:

3.14

type SafeValue

type SafeValue interface {
	// Value returns the value stored in the SafeValue.
	Value() Value

	// IsSafe returns true if the value is safely escaped for content of type typ.
	IsSafe(typ string) bool

	// SafeFor returns the content types this value is safe for.
	SafeFor() []string
}

A SafeValue represents a value that has already been sanitized and escaped.

func NewSafeValue

func NewSafeValue(val Value, types ...string) SafeValue

NewSafeValue wraps the given value and returns a SafeValue.

type StringLoader

type StringLoader struct{}

StringLoader is intended to be used to load Stick templates directly from a string.

func (*StringLoader) Load

func (l *StringLoader) Load(name string) (Template, error)

Load on a StringLoader simply returns the name that is passed in.

type Stringer

type Stringer interface {
	fmt.Stringer
}

Stringer is implemented by any value that has a String method.

Example

This example demonstrates how a type can be coerced to a string. The struct in this example has the String method implemented.

func (e exampleType) String() string {
	return "some kinda string"
}
package main

import (
	"fmt"
)

type exampleType struct{}

func (e exampleType) Boolean() bool {
	return true
}

func (e exampleType) Number() float64 {
	return 3.14
}

func (e exampleType) String() string {
	return "some kinda string"
}

func main() {
	v := exampleType{}
	fmt.Printf("%s", v)
}
Output:

some kinda string

type Template

type Template interface {
	// Name returns the name of this Template.
	Name() string

	// Contents returns an io.Reader for reading the Template contents.
	Contents() io.Reader
}

A Template represents a named template and its contents.

type Test

type Test func(ctx Context, val Value, args ...Value) bool

A Test represents a user-defined test. Tests are used to make some comparisons more expressive. Tests also accept arguments and can consist of two words.

Example

A simple test to check if a value is empty

package main

import (
	"fmt"
	"os"

	"github.com/tyler-sommer/stick"
)

func main() {
	env := stick.New(nil)
	env.Tests["empty"] = func(ctx stick.Context, val stick.Value, args ...stick.Value) bool {
		return stick.CoerceBool(val) == false
	}

	err := env.Execute(
		`{{ (false is empty) ? 'empty' : 'not empty' }} - {{ ("a string" is empty) ? 'empty' : 'not empty' }}`,
		os.Stdout,
		nil,
	)
	if err != nil {
		fmt.Println(err)
	}
}
Output:

empty - not empty
Example (TwoWordsWithArgs)

A test made up of two words that takes an argument.

package main

import (
	"fmt"
	"os"

	"github.com/tyler-sommer/stick"
)

func main() {
	env := stick.New(nil)
	env.Tests["divisible by"] = func(ctx stick.Context, val stick.Value, args ...stick.Value) bool {
		if len(args) != 1 {
			return false
		}
		i := stick.CoerceNumber(args[0])
		if i == 0 {
			return false
		}
		v := stick.CoerceNumber(val)
		return int(v)%int(i) == 0
	}

	err := env.Execute(
		`{{ ('something' is divisible by(3)) ? "yep, 'something' evals to 0" : 'nope'  }} - {{ (9 is divisible by(3)) ? 'sure' : 'nope' }} - {{ (4 is divisible by(3)) ? 'sure' : 'nope' }}`,
		os.Stdout,
		nil,
	)
	if err != nil {
		fmt.Println(err)
	}
}
Output:

yep, 'something' evals to 0 - sure - nope

type Value

type Value interface{}

A Value represents some value, scalar or otherwise, able to be passed into and used by a Stick template.

func GetAttr

func GetAttr(v Value, attr Value, args ...Value) (Value, error)

GetAttr attempts to access the given value and return the specified attribute.

Directories

Path Synopsis
Package parse handles transforming Stick source code into AST for further processing.
Package parse handles transforming Stick source code into AST for further processing.
Package twig provides Twig 1.x compatible template parsing and executing.
Package twig provides Twig 1.x compatible template parsing and executing.
escape
Package escape provides Twig-compatible escape functions.
Package escape provides Twig-compatible escape functions.
filter
Package filter provides built-in filters for Twig-compatibility.
Package filter provides built-in filters for Twig-compatibility.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL