Compare commits
10 commits
22492ee956
...
e93c177d0c
Author | SHA1 | Date | |
---|---|---|---|
DataHoarder | e93c177d0c | ||
DataHoarder | 3cd159c7f0 | ||
6a90da394f | |||
a8aa579c7f | |||
689e13d918 | |||
ea7f7a2e68 | |||
d63ce1a09a | |||
6388a7d5b7 | |||
1b3c7cdf21 | |||
6d77def7dd |
|
@ -1,7 +1,7 @@
|
|||
Stick
|
||||
=====
|
||||
|
||||
[![CircleCI](https://circleci.com/gh/tyler-sommer/stick/tree/master.svg?style=shield)](https://circleci.com/gh/tyler-sommer/stick/tree/master)
|
||||
[![CircleCI](https://circleci.com/gh/tyler-sommer/stick/tree/main.svg?style=shield)](https://circleci.com/gh/tyler-sommer/stick/tree/main)
|
||||
[![GoDoc](https://godoc.org/github.com/tyler-sommer/stick?status.svg)](https://godoc.org/github.com/tyler-sommer/stick)
|
||||
|
||||
A Go language port of the [Twig](http://twig.sensiolabs.org/) templating engine.
|
||||
|
@ -10,7 +10,7 @@ A Go language port of the [Twig](http://twig.sensiolabs.org/) templating engine.
|
|||
Overview
|
||||
--------
|
||||
|
||||
This project is split over two main parts.
|
||||
This project is split across two parts.
|
||||
|
||||
Package
|
||||
[`github.com/tyler-sommer/stick`](https://github.com/tyler-sommer/stick)
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
package stick_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"strconv"
|
||||
|
||||
"bytes"
|
||||
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/tyler-sommer/stick"
|
||||
)
|
||||
|
@ -25,6 +22,18 @@ func ExampleEnv_Execute() {
|
|||
// Output: Hello, World!
|
||||
}
|
||||
|
||||
// An example of executing a template that avoids writing any output if an error occurs.
|
||||
func ExampleEnv_ExecuteSafe() {
|
||||
env := stick.New(nil)
|
||||
|
||||
params := map[string]stick.Value{"name": "World"}
|
||||
err := env.ExecuteSafe(`Hello, {{ 'world' | fakefilter }}!`, os.Stdout, params)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
// Output: Undeclared filter "fakefilter"
|
||||
}
|
||||
|
||||
type exampleType struct{}
|
||||
|
||||
func (e exampleType) Boolean() bool {
|
||||
|
|
3
exec.go
3
exec.go
|
@ -343,6 +343,9 @@ func (s *state) walkChild(node parse.Node) error {
|
|||
}
|
||||
case *parse.UseNode:
|
||||
return s.walkUseNode(node)
|
||||
default:
|
||||
// No need to handle other nodes. This function only populates blocks from a
|
||||
// referenced template (in a use statement) and does not actually execute anything.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
234
exec_test.go
234
exec_test.go
|
@ -10,118 +10,113 @@ import (
|
|||
"github.com/tyler-sommer/stick/parse"
|
||||
)
|
||||
|
||||
// execTest is an extensible template execution test.
|
||||
type execTest interface {
|
||||
name() string
|
||||
tpl() string
|
||||
ctx() map[string]Value
|
||||
expected() expectedChecker
|
||||
// A testValidator checks that the result of a test execution is as expected.
|
||||
type testValidator func(actual string, err error) error
|
||||
|
||||
// execTest is a configurable template execution test.
|
||||
type execTest struct {
|
||||
name string
|
||||
tpl string
|
||||
ctx map[string]Value
|
||||
|
||||
checkResult testValidator
|
||||
|
||||
visitNode func(parse.Node) // When visitNode is set, it will be called after each node is parsed.
|
||||
}
|
||||
|
||||
// _execTest is the standard implementation of execTest.
|
||||
type _execTest struct {
|
||||
_name string
|
||||
_tpl string
|
||||
_ctx map[string]Value
|
||||
_expected expectedChecker
|
||||
type testOption func(t *execTest)
|
||||
|
||||
func newExecTest(name string, tpl string, v testValidator, opts ...testOption) execTest {
|
||||
t := execTest{name: name, tpl: tpl, checkResult: v}
|
||||
for _, o := range opts {
|
||||
o(&t)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (t _execTest) name() string { return t._name }
|
||||
func (t _execTest) tpl() string { return t._tpl }
|
||||
func (t _execTest) ctx() map[string]Value { return t._ctx }
|
||||
func (t _execTest) expected() expectedChecker { return t._expected }
|
||||
|
||||
func newExecTest(name string, tpl string, ctx map[string]Value, expected expectedChecker) execTest {
|
||||
return _execTest{name, tpl, ctx, expected}
|
||||
// withContext sets the context variables for the template.
|
||||
func withContext(ctx map[string]Value) testOption {
|
||||
return func(t *execTest) {
|
||||
t.ctx = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// execTestWithPatch is an execTest with a monkey patch function defined.
|
||||
type execTestWithPatch struct {
|
||||
execTest
|
||||
|
||||
patchNode func(parse.Node) // When patchNode is set, it will be called during parsing after each node is parsed.
|
||||
}
|
||||
|
||||
// withPatch enhances a test with the ability to monkey patch the parsed nodes.
|
||||
// withNodeVisitor enhances a test with the ability to inspect parsed nodes.
|
||||
//
|
||||
// Patching is used when a test needs to create alter the internal structure of a template.
|
||||
// This can be used to create an invalid tree that would not normally be parsed, but may be
|
||||
// necessary to test a certain error condition.
|
||||
func withPatch(t execTest, patch func(parse.Node)) execTestWithPatch {
|
||||
return execTestWithPatch{t, patch}
|
||||
// For the purposes of this testing, this provides the ability to muck around with the internal
|
||||
// structure of a template. This can be used to create an invalid tree that would not normally
|
||||
// be parsed, but may be necessary to test a certain error condition.
|
||||
func withNodeVisitor(visitFunc func(parse.Node)) testOption {
|
||||
return func(t *execTest) {
|
||||
t.visitNode = visitFunc
|
||||
}
|
||||
}
|
||||
|
||||
var tests = []execTest{
|
||||
newExecTest("Hello, World", "Hello, World!", nil, expect("Hello, World!")),
|
||||
newExecTest("Hello, Tyler!", "Hello, {{ name }}!", map[string]Value{"name": "Tyler"}, expect("Hello, Tyler!")),
|
||||
newExecTest("Simple if", `<li class="{% if active %}active{% endif %}">`, map[string]Value{"active": true}, expect(`<li class="active">`)),
|
||||
newExecTest("Simple inheritance", `{% extends 'Hello, {% block test %}universe{% endblock %}!' %}{% block test %}world{% endblock %}`, nil, expect(`Hello, world!`)),
|
||||
newExecTest("Simple include", `This is a test. {% include 'Hello, {{ name }}!' %} This concludes the test.`, map[string]Value{"name": "John"}, expect(`This is a test. Hello, John! This concludes the test.`)),
|
||||
newExecTest("Include with", `{% include 'Hello, {{ name }}{{ value }}' with vars %}`, map[string]Value{"value": "!", "vars": map[string]Value{"name": "Adam"}}, expect(`Hello, Adam!`)),
|
||||
newExecTest("Include with literal", `{% include 'Hello, {{ name }}{{ value }}' with {"name": "world", "value": "!"} only %}`, nil, expect(`Hello, world!`)),
|
||||
newExecTest("Embed", `Well. {% embed 'Hello, {% block name %}World{% endblock %}!' %}{% block name %}Tyler{% endblock %}{% endembed %}`, nil, expect(`Well. Hello, Tyler!`)),
|
||||
newExecTest("Constant null", `{% if test == null %}Yes{% else %}no{% endif %}`, map[string]Value{"test": nil}, expect(`Yes`)),
|
||||
newExecTest("Constant bool", `{% if test == true %}Yes{% else %}no{% endif %}`, map[string]Value{"test": false}, expect(`no`)),
|
||||
newExecTest("Chained attributes", `{{ entity.attr.Name }}`, map[string]Value{"entity": map[string]Value{"attr": struct{ Name string }{"Tyler"}}}, expect(`Tyler`)),
|
||||
newExecTest("Attribute method call", `{{ entity.Name('lower') }}`, map[string]Value{"entity": &fakePerson{"Johnny"}}, expect(`lowerJohnny`)),
|
||||
newExecTest("For loop", `{% for i in 1..3 %}{{ i }}{% endfor %}`, nil, expect(`123`)),
|
||||
newExecTest("Hello, World", "Hello, World!", expect("Hello, World!")),
|
||||
newExecTest("Hello, Tyler!", "Hello, {{ name }}!", expect("Hello, Tyler!"), withContext(map[string]Value{"name": "Tyler"})),
|
||||
newExecTest("Simple if", `<li class="{% if active %}active{% endif %}">`, expect(`<li class="active">`), withContext(map[string]Value{"active": true})),
|
||||
newExecTest("Simple inheritance", `{% extends 'Hello, {% block test %}universe{% endblock %}!' %}{% block test %}world{% endblock %}`, expect(`Hello, world!`)),
|
||||
newExecTest("Simple include", `This is a test. {% include 'Hello, {{ name }}!' %} This concludes the test.`, expect(`This is a test. Hello, John! This concludes the test.`), withContext(map[string]Value{"name": "John"})),
|
||||
newExecTest("Include with", `{% include 'Hello, {{ name }}{{ value }}' with vars %}`, expect(`Hello, Adam!`), withContext(map[string]Value{"value": "!", "vars": map[string]Value{"name": "Adam"}})),
|
||||
newExecTest("Include with literal", `{% include 'Hello, {{ name }}{{ value }}' with {"name": "world", "value": "!"} only %}`, expect(`Hello, world!`)),
|
||||
newExecTest("Embed", `Well. {% embed 'Hello, {% block name %}World{% endblock %}!' %}{% block name %}Tyler{% endblock %}{% endembed %}`, expect(`Well. Hello, Tyler!`)),
|
||||
newExecTest("Constant null", `{% if test == null %}Yes{% else %}no{% endif %}`, expect(`Yes`), withContext(map[string]Value{"test": nil})),
|
||||
newExecTest("Constant bool", `{% if test == true %}Yes{% else %}no{% endif %}`, expect(`no`), withContext(map[string]Value{"test": false})),
|
||||
newExecTest("Chained attributes", `{{ entity.attr.Name }}`, expect(`Tyler`), withContext(map[string]Value{"entity": map[string]Value{"attr": struct{ Name string }{"Tyler"}}})),
|
||||
newExecTest("Attribute method call", `{{ entity.Name('lower') }}`, expect(`lowerJohnny`), withContext(map[string]Value{"entity": &fakePerson{"Johnny"}})),
|
||||
newExecTest("For loop", `{% for i in 1..3 %}{{ i }}{% endfor %}`, expect(`123`)),
|
||||
newExecTest(
|
||||
"For loop with inner loop",
|
||||
`{% for i in test %}{% for j in i %}{{ j }}{{ loop.index }}{{ loop.parent.index }}{% if loop.first %},{% endif %}{% if loop.last %};{% endif %}{% endfor %}{% if loop.first %}f{% endif %}{% if loop.last %}l{% endif %}:{% endfor %}`,
|
||||
map[string]Value{
|
||||
expect(`111,221331;f:412,522632;:713,823933;l:`),
|
||||
withContext(map[string]Value{
|
||||
"test": [][]int{
|
||||
{1, 2, 3},
|
||||
{4, 5, 6},
|
||||
{7, 8, 9}},
|
||||
},
|
||||
expect(`111,221331;f:412,522632;:713,823933;l:`),
|
||||
}),
|
||||
),
|
||||
newExecTest(
|
||||
"For loop variables",
|
||||
`{% for i in 1..3 %}{{ i }}{{ loop.index }}{{ loop.index0 }}{{ loop.revindex }}{{ loop.revindex0 }}{{ loop.length }}{% if loop.first %}f{% endif %}{% if loop.last %}l{% endif %}{% endfor %}`,
|
||||
nil,
|
||||
expect(`110323f221213332103l`),
|
||||
),
|
||||
newExecTest("For else", `{% for i in emptySet %}{{ i }}{% else %}No results.{% endfor %}`, map[string]Value{"emptySet": []int{}}, expect(`No results.`)),
|
||||
newExecTest("For else", `{% for i in emptySet %}{{ i }}{% else %}No results.{% endfor %}`, expect(`No results.`), withContext(map[string]Value{"emptySet": []int{}})),
|
||||
newExecTest(
|
||||
"For map",
|
||||
`{% for k, v in data %}Record {{ loop.index }}: {{ k }}: {{ v }}{% if not loop.last %} - {% endif %}{% endfor %}`,
|
||||
map[string]Value{"data": map[string]float64{"Group A": 5.12, "Group B": 5.09}},
|
||||
expect(`Record 1: Group A: 5.12 - Record 2: Group B: 5.09`, `Record 1: Group B: 5.09 - Record 2: Group A: 5.12`),
|
||||
withContext(map[string]Value{"data": map[string]float64{"Group A": 5.12, "Group B": 5.09}}),
|
||||
),
|
||||
newExecTest(
|
||||
"Some operators",
|
||||
`{{ 4.5 * 10 }} - {{ 3 + true }} - {{ 3 + 4 == 7.0 }} - {{ 10 % 2 == 0 }} - {{ 10 ** 2 > 99.9 and 10 ** 2 <= 100 }}`,
|
||||
nil,
|
||||
expect(`45 - 4 - 1 - 1 - 1`),
|
||||
),
|
||||
newExecTest("In and not in", `{{ 5 in set and 4 not in set }}`, map[string]Value{"set": []int{5, 10}}, expect(`1`)),
|
||||
newExecTest("Function call", `{{ multiply(num, 5) }}`, map[string]Value{"num": 10}, expect(`50`)),
|
||||
newExecTest("Filter call", `Welcome, {{ name }}`, nil, expect(`Welcome, `)),
|
||||
newExecTest("Filter call", `Welcome, {{ name|default('User') }}`, map[string]Value{"name": nil}, expect(`Welcome, User`)),
|
||||
newExecTest("Filter call", `Welcome, {{ surname|default('User') }}`, map[string]Value{"name": nil}, expect(`Welcome, User`)),
|
||||
newExecTest("In and not in", `{{ 5 in set and 4 not in set }}`, expect(`1`), withContext(map[string]Value{"set": []int{5, 10}})),
|
||||
newExecTest("Function call", `{{ multiply(num, 5) }}`, expect(`50`), withContext(map[string]Value{"num": 10})),
|
||||
newExecTest("Filter call", `Welcome, {{ name }}`, expect(`Welcome, `)),
|
||||
newExecTest("Filter call", `Welcome, {{ name|default('User') }}`, expect(`Welcome, User`), withContext(map[string]Value{"name": nil})),
|
||||
newExecTest("Filter call", `Welcome, {{ surname|default('User') }}`, expect(`Welcome, User`), withContext(map[string]Value{"name": nil})),
|
||||
newExecTest(
|
||||
"Basic use statement",
|
||||
`{% extends '{% block message %}{% endblock %}' %}{% use '{% block message %}Hello{% endblock %}' %}`,
|
||||
nil,
|
||||
expect("Hello"),
|
||||
),
|
||||
newExecTest(
|
||||
"Extended use statement",
|
||||
`{% extends '{% block message %}{% endblock %}' %}{% use '{% block message %}Hello{% endblock %}' with message as base_message %}{% block message %}{{ block('base_message') }}, World!{% endblock %}`,
|
||||
nil,
|
||||
expect("Hello, World!"),
|
||||
),
|
||||
newExecTest(
|
||||
"Set statement",
|
||||
`{% set val = 'a value' %}{{ val }}`,
|
||||
nil,
|
||||
expect("a value"),
|
||||
),
|
||||
newExecTest(
|
||||
"Set statement with body",
|
||||
`{% set var1 = 'Hello,' %}{% set var2 %}{{ var1 }} World!{% endset %}{{ var2 }}`,
|
||||
nil,
|
||||
expect("Hello, World!"),
|
||||
),
|
||||
newExecTest(
|
||||
|
@ -135,107 +130,107 @@ var tests = []execTest{
|
|||
{{ var0 }}
|
||||
{{ var2 }}
|
||||
{{ var0 }}`,
|
||||
nil,
|
||||
expectContains("Hello, World!\n1\n\n\nHello, World!\n1"),
|
||||
),
|
||||
withPatch(newExecTest(
|
||||
newExecTest(
|
||||
"Set statement invalid expr type",
|
||||
`{% set v = 10 %}`,
|
||||
nil,
|
||||
expectErrorContains("unable to evaluate unsupported Expr type: *parse.TextNode"),
|
||||
), func(n parse.Node) {
|
||||
if sn, ok := n.(*parse.SetNode); ok {
|
||||
sn.X = parse.NewTextNode("Hello", sn.X.Start())
|
||||
}
|
||||
}),
|
||||
withNodeVisitor(func(n parse.Node) {
|
||||
if sn, ok := n.(*parse.SetNode); ok {
|
||||
sn.X = parse.NewTextNode("Hello", sn.X.Start())
|
||||
}
|
||||
}),
|
||||
),
|
||||
newExecTest(
|
||||
"Do statement",
|
||||
`{% do p.Name('Mister ') %}{{ p.Name('') }}`,
|
||||
map[string]Value{"p": &fakePerson{"Meeseeks"}},
|
||||
expect("Mister Meeseeks"),
|
||||
withContext(map[string]Value{"p": &fakePerson{"Meeseeks"}}),
|
||||
),
|
||||
newExecTest(
|
||||
"Filter statement",
|
||||
`{% filter upper %}hello, world!{% endfilter %}`,
|
||||
nil,
|
||||
expect("HELLO, WORLD!"),
|
||||
),
|
||||
newExecTest(
|
||||
"Import statement",
|
||||
`{% import 'macros.twig' as mac %}{{ mac.test("hi") }}`,
|
||||
nil,
|
||||
expect("test: hi"),
|
||||
),
|
||||
newExecTest(
|
||||
"From statement",
|
||||
`{% from 'macros.twig' import test, def as other %}{{ other("", "HI!") }}`,
|
||||
nil,
|
||||
expect("HI!"),
|
||||
),
|
||||
newExecTest(
|
||||
"Ternary if",
|
||||
`{{ false ? (true ? "Hello" : "World") : "Words" }}`,
|
||||
nil,
|
||||
expect("Words"),
|
||||
),
|
||||
newExecTest(
|
||||
"Hash literal",
|
||||
`{{ {"test": 1}["test"] }}`,
|
||||
nil,
|
||||
expect("1"),
|
||||
),
|
||||
newExecTest(
|
||||
"Another hash literal",
|
||||
`{% set v = {quadruple: "to the power of four!", 0: "ew", "0": "it's not that bad"} %}ew? {{ v.0 }} {{ v.quadruple }}`,
|
||||
nil,
|
||||
expect("ew? it's not that bad to the power of four!"),
|
||||
),
|
||||
newExecTest(
|
||||
"Array literal",
|
||||
`{{ ["test", 1, "bar"][2] }}`,
|
||||
nil,
|
||||
expect("bar"),
|
||||
),
|
||||
newExecTest(
|
||||
"Another Array literal",
|
||||
`{{ ["test", 1, "bar"].1 }}`,
|
||||
nil,
|
||||
expect("1"),
|
||||
),
|
||||
newExecTest(
|
||||
"Comparison with or",
|
||||
`{% if item1 == "banana" or item2 == "apple" %}At least one item is correct{% else %}neither item is correct{% endif %}`,
|
||||
map[string]Value{"item1": "orange", "item2": "apple"},
|
||||
expect("At least one item is correct"),
|
||||
withContext(map[string]Value{"item1": "orange", "item2": "apple"}),
|
||||
),
|
||||
newExecTest(
|
||||
"Non-existent map element without default",
|
||||
`{{ data.A }} {{ data.NotThere }} {{ data.B }}`,
|
||||
map[string]Value{"data": map[string]string{"A": "Foo", "B": "Bar"}},
|
||||
expect("Foo Bar"),
|
||||
withContext(map[string]Value{"data": map[string]string{"A": "Foo", "B": "Bar"}}),
|
||||
),
|
||||
newExecTest(
|
||||
"Non-existent map element with default",
|
||||
`{{ data.A }} {{ data.NotThere|default("default value") }} {{ data.B }}`,
|
||||
map[string]Value{"data": map[string]string{"A": "Foo", "B": "Bar"}},
|
||||
expect("Foo default value Bar"),
|
||||
withContext(map[string]Value{"data": map[string]string{"A": "Foo", "B": "Bar"}}),
|
||||
),
|
||||
newExecTest(
|
||||
"Accessing templateName on _self",
|
||||
`Template: {{ _self.templateName }}`,
|
||||
nil,
|
||||
expect("Template: Template: {{ _self.templateName }}"),
|
||||
),
|
||||
withPatch(_execTest{
|
||||
newExecTest(
|
||||
"Unsupported binary operator",
|
||||
`{{ 1 + 2 }}`,
|
||||
nil,
|
||||
expectErrorContains("unsupported binary operator: _"),
|
||||
}, func(n parse.Node) {
|
||||
if bn, ok := n.(*parse.BinaryExpr); ok {
|
||||
bn.Op = "_"
|
||||
}
|
||||
}),
|
||||
withNodeVisitor(func(n parse.Node) {
|
||||
if bn, ok := n.(*parse.BinaryExpr); ok {
|
||||
bn.Op = "_"
|
||||
}
|
||||
}),
|
||||
),
|
||||
newExecTest(
|
||||
"Unsupported binary operator",
|
||||
`{{ 1 + 2 }}`,
|
||||
expectErrorContains("unable to evaluate unsupported Expr type: *parse.TextNode"),
|
||||
withNodeVisitor(func(n parse.Node) {
|
||||
if bn, ok := n.(*parse.BinaryExpr); ok {
|
||||
bn.Right = parse.NewTextNode("foo", bn.Right.Start())
|
||||
}
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
func joinExpected(expected []string) string {
|
||||
|
@ -277,7 +272,7 @@ func (err *expectErrorMismatchError) Error() string {
|
|||
if len(err.expected) == 0 {
|
||||
if err.actual == nil {
|
||||
// shouldn't happen in practice, but technically possible
|
||||
return fmt.Sprint("expected error mismatch but there was no actual error and no error was expected! (bug?)")
|
||||
return fmt.Sprint("error mismatch expected but there was no actual error and no error was expected! (bug?)")
|
||||
}
|
||||
return fmt.Sprintf("unexpected error %#v", err.actual.Error())
|
||||
}
|
||||
|
@ -292,9 +287,7 @@ func newExpectErrorMismatchError(actual error, expected ...string) error {
|
|||
return &expectErrorMismatchError{actual, expected}
|
||||
}
|
||||
|
||||
type expectedChecker func(actual string, err error) error
|
||||
|
||||
func expectChained(expects ...expectedChecker) expectedChecker {
|
||||
func expectChained(expects ...testValidator) testValidator {
|
||||
return func(actual string, err error) error {
|
||||
for _, expect := range expects {
|
||||
if e := expect(actual, err); e != nil {
|
||||
|
@ -305,7 +298,7 @@ func expectChained(expects ...expectedChecker) expectedChecker {
|
|||
}
|
||||
}
|
||||
|
||||
func expect(expected ...string) expectedChecker {
|
||||
func expect(expected ...string) testValidator {
|
||||
return expectChained(expectNoError(), func(actual string, err error) error {
|
||||
for _, exp := range expected {
|
||||
if actual == exp {
|
||||
|
@ -316,7 +309,7 @@ func expect(expected ...string) expectedChecker {
|
|||
})
|
||||
}
|
||||
|
||||
func expectContains(matches string) expectedChecker {
|
||||
func expectContains(matches string) testValidator {
|
||||
return expectChained(expectNoError(), func(actual string, err error) error {
|
||||
if !strings.Contains(actual, matches) {
|
||||
return newExpectFailedError(true, actual, matches)
|
||||
|
@ -325,7 +318,7 @@ func expectContains(matches string) expectedChecker {
|
|||
})
|
||||
}
|
||||
|
||||
func expectNoError() expectedChecker {
|
||||
func expectNoError() testValidator {
|
||||
return func(actual string, err error) error {
|
||||
if err != nil {
|
||||
return newExpectErrorMismatchError(err)
|
||||
|
@ -334,7 +327,7 @@ func expectNoError() expectedChecker {
|
|||
}
|
||||
}
|
||||
|
||||
func expectErrorContains(expected ...string) expectedChecker {
|
||||
func expectErrorContains(expected ...string) testValidator {
|
||||
return func(_ string, err error) error {
|
||||
if err == nil && len(expected) == 0 {
|
||||
return nil
|
||||
|
@ -354,17 +347,6 @@ func expectErrorContains(expected ...string) expectedChecker {
|
|||
}
|
||||
}
|
||||
|
||||
func evaluateTest(t *testing.T, env *Env, test execTest) {
|
||||
w := &bytes.Buffer{}
|
||||
err := execute(test.tpl(), w, test.ctx(), env)
|
||||
check := test.expected()
|
||||
|
||||
out := w.String()
|
||||
if err := check(out, err); err != nil {
|
||||
t.Errorf("%s: %s", test.name(), err)
|
||||
}
|
||||
}
|
||||
|
||||
type testLoader struct {
|
||||
templates map[string]Template
|
||||
}
|
||||
|
@ -402,16 +384,29 @@ func (t *testLoader) Load(name string) (Template, error) {
|
|||
return tpl(name, name), nil
|
||||
}
|
||||
|
||||
// nodeMonkeyPatcher is a parse.NodeVisitor to enable tests to arbitrarily modify a parsed tree.
|
||||
type nodeMonkeyPatcher struct {
|
||||
patch func(parse.Node)
|
||||
// testVisitor is a parse.NodeVisitor to enable tests to arbitrarily modify a parsed tree.
|
||||
type testVisitor struct {
|
||||
// visit is called when the visitor leaves a node
|
||||
visit func(parse.Node)
|
||||
}
|
||||
|
||||
func (v *nodeMonkeyPatcher) Enter(parse.Node) {}
|
||||
func (v *testVisitor) Enter(parse.Node) {
|
||||
// only Leave is used for simplicity's sake
|
||||
}
|
||||
|
||||
func (v *nodeMonkeyPatcher) Leave(n parse.Node) {
|
||||
if v.patch != nil {
|
||||
v.patch(n)
|
||||
func (v *testVisitor) Leave(n parse.Node) {
|
||||
if v.visit != nil {
|
||||
v.visit(n)
|
||||
}
|
||||
}
|
||||
|
||||
func evaluateTest(t *testing.T, env *Env, test execTest) {
|
||||
w := &bytes.Buffer{}
|
||||
err := execute(test.tpl, w, test.ctx, env)
|
||||
|
||||
out := w.String()
|
||||
if err := test.checkResult(out, err); err != nil {
|
||||
t.Errorf("%s: %s", test.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -446,13 +441,10 @@ func TestExec(t *testing.T) {
|
|||
}
|
||||
return val
|
||||
}
|
||||
patcher := &nodeMonkeyPatcher{}
|
||||
env.Visitors = append(env.Visitors, patcher)
|
||||
tv := &testVisitor{}
|
||||
env.Visitors = append(env.Visitors, tv)
|
||||
for _, test := range tests {
|
||||
patcher.patch = nil
|
||||
if t, ok := test.(execTestWithPatch); ok {
|
||||
patcher.patch = t.patchNode
|
||||
}
|
||||
tv.visit = test.visitNode
|
||||
evaluateTest(t, env, test)
|
||||
}
|
||||
}
|
||||
|
|
4
go.mod
4
go.mod
|
@ -1,5 +1,5 @@
|
|||
module github.com/tyler-sommer/stick
|
||||
module git.gammaspectra.live/P2Pool/stick
|
||||
|
||||
go 1.12
|
||||
|
||||
require github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24
|
||||
require github.com/shopspring/decimal v1.3.1
|
||||
|
|
4
go.sum
4
go.sum
|
@ -1,2 +1,2 @@
|
|||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
|
|
|
@ -47,7 +47,7 @@ func (e *parseError) sprintf(format string, a ...interface{}) string {
|
|||
// is not of the expected type.
|
||||
type UnexpectedTokenError struct {
|
||||
baseError
|
||||
actual token
|
||||
actual *token
|
||||
expected []tokenType
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ func (e *UnexpectedTokenError) Error() string {
|
|||
}
|
||||
|
||||
// newUnexpectedTokenError returns a new UnexpectedTokenError
|
||||
func newUnexpectedTokenError(actual token, expected ...tokenType) error {
|
||||
func newUnexpectedTokenError(actual *token, expected ...tokenType) error {
|
||||
return &UnexpectedTokenError{newBaseError(actual.Pos), actual, expected}
|
||||
}
|
||||
|
||||
|
@ -100,14 +100,14 @@ func (e *UnexpectedEOFError) Error() string {
|
|||
}
|
||||
|
||||
// newUnexpectedEOFError returns a new UnexpectedEOFError
|
||||
func newUnexpectedEOFError(tok token) error {
|
||||
func newUnexpectedEOFError(tok *token) error {
|
||||
return &UnexpectedEOFError{newBaseError(tok.Pos)}
|
||||
}
|
||||
|
||||
// UnexpectedValueError describes an invalid or unexpected value inside a token.
|
||||
type UnexpectedValueError struct {
|
||||
baseError
|
||||
tok token // The actual token.
|
||||
tok *token // The actual token.
|
||||
val string // The expected value.
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,7 @@ func (e *UnexpectedValueError) Error() string {
|
|||
}
|
||||
|
||||
// newUnexpectedValueError returns a new UnexpectedPunctuationError
|
||||
func newUnexpectedValueError(tok token, expected string) error {
|
||||
func newUnexpectedValueError(tok *token, expected string) error {
|
||||
return &UnexpectedValueError{newBaseError(tok.Pos), tok, expected}
|
||||
}
|
||||
|
||||
|
|
14
parse/lex.go
14
parse/lex.go
|
@ -112,15 +112,15 @@ type lexer struct {
|
|||
line int // The current line number
|
||||
offset int // The current character offset on the current line
|
||||
input string
|
||||
tokens chan token
|
||||
tokens chan *token
|
||||
state stateFn
|
||||
mode mode
|
||||
last token // The last emitted token
|
||||
parens int // Number of open parenthesis
|
||||
last *token // The last emitted token
|
||||
parens int // Number of open parenthesis
|
||||
}
|
||||
|
||||
// nextToken returns the next token emitted by the lexer.
|
||||
func (l *lexer) nextToken() token {
|
||||
func (l *lexer) nextToken() *token {
|
||||
for v, ok := <-l.tokens; ok; {
|
||||
l.last = v
|
||||
return v
|
||||
|
@ -140,7 +140,7 @@ func (l *lexer) tokenize() {
|
|||
func newLexer(input io.Reader) *lexer {
|
||||
// TODO: lexer should use the reader.
|
||||
i, _ := ioutil.ReadAll(input)
|
||||
return &lexer{0, 0, 1, 0, string(i), make(chan token), nil, modeNormal, token{}, 0}
|
||||
return &lexer{0, 0, 1, 0, string(i), make(chan *token), nil, modeNormal, &token{}, 0}
|
||||
}
|
||||
|
||||
func (l *lexer) next() (val string) {
|
||||
|
@ -184,7 +184,7 @@ func (l *lexer) emit(t tokenType) {
|
|||
l.offset += len(val)
|
||||
}
|
||||
|
||||
l.tokens <- tok
|
||||
l.tokens <- &tok
|
||||
l.start = l.pos
|
||||
if tok.tokenType == tokenEOF {
|
||||
close(l.tokens)
|
||||
|
@ -194,7 +194,7 @@ func (l *lexer) emit(t tokenType) {
|
|||
|
||||
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
|
||||
tok := token{fmt.Sprintf(format, args...), tokenError, Pos{l.line, l.offset}}
|
||||
l.tokens <- tok
|
||||
l.tokens <- &tok
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -21,8 +21,8 @@ type Tree struct {
|
|||
blocks []map[string]*BlockNode // Contains each block available to this template.
|
||||
macros map[string]*MacroNode // All macros defined on this template.
|
||||
|
||||
unread []token // Any tokens received by the lexer but not yet read.
|
||||
read []token // Tokens that have already been read.
|
||||
unread []*token // Any tokens received by the lexer but not yet read.
|
||||
read []*token // Tokens that have already been read.
|
||||
|
||||
Name string // A name identifying this tree; the template name.
|
||||
|
||||
|
@ -43,8 +43,8 @@ func NewNamedTree(name string, input io.Reader) *Tree {
|
|||
blocks: []map[string]*BlockNode{make(map[string]*BlockNode)},
|
||||
macros: make(map[string]*MacroNode),
|
||||
|
||||
unread: make([]token, 0),
|
||||
read: make([]token, 0),
|
||||
unread: make([]*token, 0),
|
||||
read: make([]*token, 0),
|
||||
|
||||
Name: name,
|
||||
Visitors: make([]NodeVisitor, 0),
|
||||
|
@ -88,7 +88,7 @@ func (t *Tree) enrichError(err error) error {
|
|||
}
|
||||
|
||||
// peek returns the next unread token without advancing the internal cursor.
|
||||
func (t *Tree) peek() token {
|
||||
func (t *Tree) peek() *token {
|
||||
tok := t.next()
|
||||
t.backup()
|
||||
|
||||
|
@ -96,8 +96,8 @@ func (t *Tree) peek() token {
|
|||
}
|
||||
|
||||
// peek returns the next unread, non-space token without advancing the internal cursor.
|
||||
func (t *Tree) peekNonSpace() token {
|
||||
var next token
|
||||
func (t *Tree) peekNonSpace() *token {
|
||||
var next *token
|
||||
for {
|
||||
next = t.next()
|
||||
if next.tokenType != tokenWhitespace {
|
||||
|
@ -109,7 +109,7 @@ func (t *Tree) peekNonSpace() token {
|
|||
|
||||
// backup pushes the last read token back onto the unread stack and reduces the internal cursor by one.
|
||||
func (t *Tree) backup() {
|
||||
var tok token
|
||||
var tok *token
|
||||
tok, t.read = t.read[len(t.read)-1], t.read[:len(t.read)-1]
|
||||
t.unread = append(t.unread, tok)
|
||||
}
|
||||
|
@ -126,8 +126,8 @@ func (t *Tree) backup3() {
|
|||
}
|
||||
|
||||
// next returns the next unread token and advances the internal cursor by one.
|
||||
func (t *Tree) next() token {
|
||||
var tok token
|
||||
func (t *Tree) next() *token {
|
||||
var tok *token
|
||||
if len(t.unread) > 0 {
|
||||
tok, t.unread = t.unread[len(t.unread)-1], t.unread[:len(t.unread)-1]
|
||||
} else {
|
||||
|
@ -140,8 +140,8 @@ func (t *Tree) next() token {
|
|||
}
|
||||
|
||||
// nextNonSpace returns the next non-whitespace token.
|
||||
func (t *Tree) nextNonSpace() token {
|
||||
var next token
|
||||
func (t *Tree) nextNonSpace() *token {
|
||||
var next *token
|
||||
for {
|
||||
next = t.next()
|
||||
if next.tokenType != tokenWhitespace {
|
||||
|
@ -152,7 +152,7 @@ func (t *Tree) nextNonSpace() token {
|
|||
|
||||
// expect returns the next non-space token. Additionally, if the token is not of one of the expected types,
|
||||
// an UnexpectedTokenError is returned.
|
||||
func (t *Tree) expect(typs ...tokenType) (token, error) {
|
||||
func (t *Tree) expect(typs ...tokenType) (*token, error) {
|
||||
tok := t.nextNonSpace()
|
||||
for _, typ := range typs {
|
||||
if tok.tokenType == typ {
|
||||
|
@ -166,7 +166,7 @@ func (t *Tree) expect(typs ...tokenType) (token, error) {
|
|||
// expectValue returns the next non-space token, with additional checks on the value of the token.
|
||||
// If the token is not of the expected type, an UnexpectedTokenError is returned. If the token is not the
|
||||
// expected value, an UnexpectedValueError is returned.
|
||||
func (t *Tree) expectValue(typ tokenType, val string) (token, error) {
|
||||
func (t *Tree) expectValue(typ tokenType, val string) (*token, error) {
|
||||
tok, err := t.expect(typ)
|
||||
if err != nil {
|
||||
return tok, err
|
||||
|
|
|
@ -688,7 +688,9 @@ func parseVerbatim(t *Tree, start Pos) (Node, error) {
|
|||
|
||||
body := bytes.Buffer{}
|
||||
|
||||
t.expect(tokenTagClose)
|
||||
if _, err := t.expect(tokenTagClose); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
switch tok := t.peek(); tok.tokenType {
|
||||
case tokenEOF:
|
||||
|
@ -700,7 +702,9 @@ func parseVerbatim(t *Tree, start Pos) (Node, error) {
|
|||
return nil, err
|
||||
}
|
||||
if tok.value == "end"+tagName {
|
||||
t.expect(tokenTagClose)
|
||||
if _, err := t.expect(tokenTagClose); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewTextNode(body.String(), start), nil
|
||||
}
|
||||
default:
|
||||
|
|
11
stick.go
11
stick.go
|
@ -1,6 +1,7 @@
|
|||
package stick // import "github.com/tyler-sommer/stick"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
|
||||
"github.com/tyler-sommer/stick/parse"
|
||||
|
@ -104,6 +105,16 @@ func (env *Env) Execute(tpl string, out io.Writer, ctx map[string]Value) error {
|
|||
return execute(tpl, out, ctx, env)
|
||||
}
|
||||
|
||||
// ExecuteSafe executes the template but does not output anything if an error occurs.
|
||||
func (env *Env) ExecuteSafe(tpl string, out io.Writer, ctx map[string]Value) error {
|
||||
buf := &bytes.Buffer{}
|
||||
if err := env.Execute(tpl, buf, ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := io.Copy(out, buf)
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse loads and parses the given template.
|
||||
func (env *Env) Parse(name string) (*parse.Tree, error) {
|
||||
return env.load(name)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Twig
|
||||
====
|
||||
|
||||
[![Build Status](https://travis-ci.org/tyler-sommer/stick.svg?branch=master)](https://travis-ci.org/tyler-sommer/stick)
|
||||
[![CircleCI](https://circleci.com/gh/tyler-sommer/stick/tree/main.svg?style=shield)](https://circleci.com/gh/tyler-sommer/stick/tree/main)
|
||||
[![GoDoc](https://godoc.org/github.com/tyler-sommer/stick/twig?status.svg)](https://godoc.org/github.com/tyler-sommer/stick/twig)
|
||||
|
||||
Provides [Twig-compatibility](http://twig.sensiolabs.org/) for the stick
|
||||
|
|
|
@ -115,5 +115,5 @@ func (v *autoEscapeVisitor) guessTypeFromName(name string) string {
|
|||
// Default to html
|
||||
return "html"
|
||||
}
|
||||
return name[p:]
|
||||
return name[p+1:]
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package twig_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/tyler-sommer/stick"
|
||||
"github.com/tyler-sommer/stick/parse"
|
||||
|
@ -75,3 +75,23 @@ func TestAutoEscapeVisitor(t *testing.T) {
|
|||
t.Errorf("expected 'text', got %s", fv)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutoEscapeExtension(t *testing.T) {
|
||||
env := twig.New(&stick.MemoryLoader{Templates: map[string]string{
|
||||
"index.html.twig": "<html><script>{% include 'utils.js.twig' %}</script><body>Hello, {{ user }}!</body></html>",
|
||||
"utils.js.twig": `console.log("{{ message }}");`,
|
||||
}})
|
||||
buf := bytes.Buffer{}
|
||||
err := env.Execute("index.html.twig", &buf, map[string]stick.Value{
|
||||
"user": "<a href='bad'>tyler-sommer</a>",
|
||||
"message": "bad '\" message",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error executing template: %s", err)
|
||||
}
|
||||
expected := "<html><script>console.log(\"bad\\u0020\\u0027\\u0022\\u0020message\");</script><body>Hello, <a href='bad'>tyler-sommer</a>!</body></html>"
|
||||
actual := buf.String()
|
||||
if actual != expected {
|
||||
t.Errorf("expected output to be escaped, but got: %s", actual)
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue