Compare commits

...

10 commits

Author SHA1 Message Date
DataHoarder e93c177d0c
Change go.mod name 2023-05-25 09:53:14 +02:00
DataHoarder 3cd159c7f0
Made token values pointers in lexer/parser 2023-05-25 09:52:54 +02:00
Tyler Sommer 6a90da394f
Fix default case handling in walkChild
A refreshed understanding has been etched in commentary.
2023-04-07 21:59:24 -06:00
Tyler Sommer a8aa579c7f
Update shopspring/decimal 2023-04-07 21:43:43 -06:00
Tyler Sommer 689e13d918
Fix omitted error handling 2023-04-07 21:42:17 -06:00
Tyler Sommer ea7f7a2e68
Add ExecuteSafe for avoiding partial output if an error occurs 2023-04-07 21:37:32 -06:00
Tyler Sommer d63ce1a09a
Fix CI badge in twig/README 2023-04-01 19:03:44 -06:00
Tyler Sommer 6388a7d5b7
Fix template format guessing in auto escaper 2023-04-01 19:02:24 -06:00
Tyler Sommer 1b3c7cdf21
Update README 2023-04-01 00:23:28 -06:00
Tyler Sommer 6d77def7dd
Refactor exec tests further 2023-03-31 23:59:16 -06:00
14 changed files with 204 additions and 165 deletions

View file

@ -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)

View file

@ -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 {

View file

@ -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
}

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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}
}

View file

@ -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
}

View file

@ -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

View file

@ -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:

View file

@ -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)

View file

@ -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

View file

@ -115,5 +115,5 @@ func (v *autoEscapeVisitor) guessTypeFromName(name string) string {
// Default to html
return "html"
}
return name[p:]
return name[p+1:]
}

View file

@ -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, &lt;a href=&#39;bad&#39;&gt;tyler-sommer&lt;/a&gt;!</body></html>"
actual := buf.String()
if actual != expected {
t.Errorf("expected output to be escaped, but got: %s", actual)
}
}