├── .github └── workflows │ ├── standard-go-test.yml │ └── standard-stale.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── ast ├── array_literal.go ├── assign_expression.go ├── ast.go ├── ast_test.go ├── block_statement.go ├── boolean.go ├── break_expression.go ├── call_expression.go ├── continue_expression.go ├── expression_statement.go ├── float_literal.go ├── for_expression.go ├── function_literal.go ├── hash_literal.go ├── html_literal.go ├── identifier.go ├── if_expression.go ├── index_expression.go ├── infix_expression.go ├── integer_literal.go ├── let_statement.go ├── prefix_expression.go ├── program.go ├── return_statement.go └── string_literal.go ├── comments_test.go ├── compiler.go ├── context.go ├── context_test.go ├── error_test.go ├── escape_test.go ├── example_test.go ├── for_test.go ├── functions_test.go ├── go.mod ├── go.sum ├── hashes_test.go ├── helper_context.go ├── helpers.go ├── helpers ├── content │ ├── content.go │ ├── for.go │ ├── for_test.go │ ├── of.go │ └── of_test.go ├── debug │ ├── debug.go │ ├── debug_test.go │ ├── inspect.go │ └── inspect_test.go ├── encoders │ ├── encoders.go │ ├── json.go │ ├── json_test.go │ ├── raw.go │ └── raw_test.go ├── env │ ├── env.go │ └── env_test.go ├── escapes │ ├── escapes.go │ ├── html.go │ ├── html_test.go │ └── js.go ├── hctx │ ├── context.go │ ├── map.go │ └── map_test.go ├── helpers.go ├── helptest │ └── context.go ├── inflections │ └── inflections.go ├── iterators │ ├── between.go │ ├── group_by.go │ ├── group_by_test.go │ ├── iterator.go │ ├── iterators.go │ ├── range.go │ └── until.go ├── map.go ├── meta │ ├── len.go │ ├── len_test.go │ └── meta.go ├── paths │ ├── path_for.go │ ├── path_for_test.go │ └── paths.go └── text │ ├── text.go │ ├── truncate.go │ └── truncate_test.go ├── helpers_test.go ├── if_test.go ├── intern.go ├── intern_test.go ├── iterators.go ├── iterators_test.go ├── lexer ├── lexer.go └── lexer_test.go ├── line_number_test.go ├── math_test.go ├── numbers_test.go ├── objects.go ├── parser ├── errors.go ├── parser.go ├── parser_test.go └── precedences.go ├── partial_helper.go ├── partial_helper_test.go ├── plush.go ├── plush_test.go ├── quotes_test.go ├── return_exit_test.go ├── script_test.go ├── struct_test.go ├── symbol_table.go ├── symbol_table_test.go ├── template.go ├── template_test.go ├── time_test.go ├── token ├── const.go └── token.go ├── user_function.go ├── variables_test.go └── variadic_test.go /.github/workflows/standard-go-test.yml: -------------------------------------------------------------------------------- 1 | name: Standard Test 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | dependency-review: 10 | if: ${{ github.event_name == 'pull_request' }} 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Dependency Review 15 | uses: actions/dependency-review-action@v1 16 | 17 | standard-go-test: 18 | name: go${{ matrix.go-version }}/${{ matrix.os }} 19 | runs-on: ${{ matrix.os }} 20 | strategy: 21 | matrix: 22 | go-version: 23 | - "1.21" 24 | - "1.22" 25 | os: 26 | - "ubuntu-latest" 27 | - "macos-latest" 28 | - "windows-latest" 29 | 30 | steps: 31 | - uses: actions/checkout@v3 32 | 33 | - name: Set up Go 34 | uses: actions/setup-go@v5 35 | with: 36 | go-version: ${{ matrix.go-version }} 37 | 38 | - name: Test 39 | if: ${{ matrix.os != 'windows-latest' }} 40 | env: 41 | YARN_ENABLE_IMMUTABLE_INSTALLS: 0 42 | run: | 43 | go test -v -p 1 -race -cover -tags "sqlite,integration" ./... 44 | 45 | - name: Short Test 46 | if: ${{ matrix.os == 'windows-latest' }} 47 | env: 48 | YARN_ENABLE_IMMUTABLE_INSTALLS: 0 49 | run: | 50 | go test -v -p 1 -tags "sqlite,integration" ./... 51 | -------------------------------------------------------------------------------- /.github/workflows/standard-stale.yml: -------------------------------------------------------------------------------- 1 | name: Standard Autocloser 2 | 3 | on: 4 | schedule: 5 | - cron: "30 1 * * *" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write # for actions/stale to close stale issues 12 | pull-requests: write # for actions/stale to close stale PRs 13 | 14 | steps: 15 | - uses: actions/stale@v6 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | days-before-issue-stale: 30 19 | days-before-issue-close: 7 20 | days-before-pr-stale: 45 21 | days-before-pr-close: 7 22 | stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment. Otherwise, this will be closed in 7 days." 23 | stale-pr-message: "This PR is stale because it has been open 45 days with no activity. Remove stale label or comment. Otherwise, this will be closed in 7 days." 24 | close-issue-message: "This issue was closed because it has been stalled for 30+7 days with no activity." 25 | close-pr-message: "This PR was closed because it has been stalled for 45+7 days with no activity." 26 | stale-issue-label: "stale" 27 | stale-pr-label: "stale" 28 | close-issue-label: "s: closed" 29 | close-pr-label: "s: closed" 30 | exempt-issue-labels: "bug,security,s: accepted,s: blocked,s: hold" 31 | exempt-pr-labels: "bug,security,s: accepted,s: blocked,s: hold" 32 | exempt-all-milestones: true 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | doc 4 | tmp 5 | pkg 6 | *.gem 7 | *.pid 8 | coverage 9 | coverage.data 10 | build/* 11 | *.pbxuser 12 | *.mode1v3 13 | .svn 14 | profile 15 | .console_history 16 | .sass-cache/* 17 | .rake_tasks~ 18 | *.log.lck 19 | solr/ 20 | .jhw-cache/ 21 | jhw.* 22 | *.sublime* 23 | node_modules/ 24 | dist/ 25 | generated/ 26 | .vendor/ 27 | bin/* 28 | gin-bin 29 | .idea/ 30 | cover.out 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Mark Bates 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test -failfast -short -cover ./... 3 | go mod tidy -v 4 | 5 | cov: 6 | go test -short -coverprofile cover.out ./... 7 | go tool cover -html cover.out 8 | go mod tidy -v 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Plush 2 | 3 | [](https://github.com/gobuffalo/plush/actions/workflows/standard-go-test.yml) 4 | [](https://pkg.go.dev/github.com/gobuffalo/plush/v5) 5 | 6 | Plush is the templating system that [Go](http://golang.org) both needs _and_ deserves. Powerful, flexible, and extendable, Plush is there to make writing your templates that much easier. 7 | 8 | **[Introduction Video](https://blog.gobuffalo.io/introduction-to-plush-82a8a12cf98a#.y9t0g4xq2)** 9 | 10 | ## Installation 11 | 12 | ```text 13 | $ go get -u github.com/gobuffalo/plush 14 | ``` 15 | 16 | ## Usage 17 | 18 | Plush allows for the embedding of dynamic code inside of your templates. Take the following example: 19 | 20 | ```erb 21 | 22 |
<%= "plush is great" %>
23 | 24 | 25 |plush is great
26 | ``` 27 | 28 | ### Controlling Output 29 | 30 | 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 `` tags. 31 | 32 | 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: 33 | 34 | ```erb 35 | 36 |<% "plush is great" %>
37 | 38 | 39 | 40 | ``` 41 | 42 | By using the `<% %>` tags we can create variables (and functions!) inside of templates to use later: 43 | 44 | ```erb 45 | 46 | <% 47 | let h = {name: "mark"} 48 | let greet = fn(n) { 49 | return "hi " + n 50 | } 51 | %> 52 | 53 |<%= one() %>
313 |<%= greet("mark")%>
314 | <%= can("update") { %> 315 |i can update
316 | <% } %> 317 | <%= can("destroy") { %> 318 |i can destroy
319 | <% } %> 320 | ` 321 | 322 | ctx := NewContext() 323 | 324 | // one() #=> 1 325 | ctx.Set("one", func() int { 326 | return 1 327 | }) 328 | 329 | // greet("mark") #=> "Hi mark" 330 | ctx.Set("greet", func(s string) string { 331 | return fmt.Sprintf("Hi %s", s) 332 | }) 333 | 334 | // can("update") #=> returns the block associated with it 335 | // can("adsf") #=> "" 336 | ctx.Set("can", func(s string, help HelperContext) (template.HTML, error) { 337 | if s == "update" { 338 | h, err := help.Block() 339 | return template.HTML(h), err 340 | } 341 | return "", nil 342 | }) 343 | 344 | s, err := Render(html, ctx) 345 | if err != nil { 346 | log.Fatal(err) 347 | } 348 | fmt.Print(s) 349 | // output:1
350 | //Hi mark
351 | //i can update
352 | ``` 353 | 354 | ### Special Thanks 355 | 356 | This package absolutely 100% could not have been written without the help of Thorsten Ball's incredible book, [Writing an Interpreter in Go](https://interpreterbook.com). 357 | 358 | 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. 359 | 360 | If you have yet to read Thorsten's book, I can't recommend it enough. Please go and buy it! 361 | -------------------------------------------------------------------------------- /ast/array_literal.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | type ArrayLiteral struct { 9 | TokenAble 10 | Elements []Expression 11 | } 12 | 13 | var _ Expression = &ArrayLiteral{} 14 | 15 | func (al *ArrayLiteral) expressionNode() {} 16 | 17 | func (al *ArrayLiteral) String() string { 18 | var out bytes.Buffer 19 | 20 | elements := []string{} 21 | for _, el := range al.Elements { 22 | elements = append(elements, el.String()) 23 | } 24 | 25 | out.WriteString("[") 26 | out.WriteString(strings.Join(elements, ", ")) 27 | out.WriteString("]") 28 | 29 | return out.String() 30 | } 31 | -------------------------------------------------------------------------------- /ast/assign_expression.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import "fmt" 4 | 5 | type AssignExpression struct { 6 | TokenAble 7 | Name *Identifier 8 | Value Expression 9 | } 10 | 11 | var _ Expression = &AssignExpression{} 12 | 13 | func (ae *AssignExpression) expressionNode() {} 14 | 15 | func (ae *AssignExpression) String() string { 16 | n, v := "?", "?" 17 | 18 | if ae.Name != nil { 19 | n = ae.Name.String() 20 | } 21 | 22 | if ae.Value != nil { 23 | v = ae.Value.String() 24 | } 25 | 26 | return fmt.Sprintf("%s = %s", n, v) 27 | } 28 | -------------------------------------------------------------------------------- /ast/ast.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import "github.com/gobuffalo/plush/v5/token" 4 | 5 | type TokenAble struct { 6 | token.Token 7 | } 8 | 9 | func (t TokenAble) T() token.Token { 10 | return t.Token 11 | } 12 | 13 | func (t TokenAble) TokenLiteral() string { 14 | return t.Token.Literal 15 | } 16 | 17 | type Printable interface { 18 | Printable() bool 19 | } 20 | 21 | // The base Node interface 22 | type Node interface { 23 | T() token.Token 24 | TokenLiteral() string 25 | String() string 26 | } 27 | 28 | // All statement nodes implement this 29 | type Statement interface { 30 | Node 31 | statementNode() 32 | } 33 | 34 | // All expression nodes implement this 35 | type Expression interface { 36 | Node 37 | expressionNode() 38 | } 39 | 40 | type Comparable interface { 41 | // TODO: not sure what is the purpose of this interface. 42 | // The only method of this interface is validIfCondition that returns 43 | // true always for all implementations. Need to check but it could be 44 | // something like isCondition or isComparable of Expression interface. 45 | validIfCondition() bool 46 | } 47 | -------------------------------------------------------------------------------- /ast/ast_test.go: -------------------------------------------------------------------------------- 1 | package ast_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/plush/v5/ast" 7 | "github.com/gobuffalo/plush/v5/token" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Program_String(t *testing.T) { 12 | r := require.New(t) 13 | program := &ast.Program{ 14 | Statements: []ast.Statement{ 15 | &ast.LetStatement{ 16 | TokenAble: ast.TokenAble{token.Token{Type: token.LET, Literal: "let"}}, 17 | Name: &ast.Identifier{ 18 | TokenAble: ast.TokenAble{token.Token{Type: token.IDENT, Literal: "myVar"}}, 19 | Value: "myVar", 20 | }, 21 | Value: &ast.Identifier{ 22 | TokenAble: ast.TokenAble{token.Token{Type: token.IDENT, Literal: "anotherVar"}}, 23 | Value: "anotherVar", 24 | }, 25 | }, 26 | }, 27 | } 28 | 29 | r.Equal("let myVar = anotherVar;", program.String()) 30 | } 31 | -------------------------------------------------------------------------------- /ast/block_statement.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | // BlockStatement is a list of statements grouped in a context surrounded by braces. 8 | type BlockStatement struct { 9 | TokenAble 10 | Statements []Statement 11 | } 12 | 13 | var _ Statement = &BlockStatement{} 14 | 15 | func (bs *BlockStatement) statementNode() {} 16 | 17 | // InnerText gets the raw string representation of the block's contents. 18 | func (bs *BlockStatement) InnerText() string { 19 | var out bytes.Buffer 20 | for _, s := range bs.Statements { 21 | out.WriteString(s.String()) 22 | } 23 | return out.String() 24 | } 25 | 26 | func (bs *BlockStatement) String() string { 27 | var out bytes.Buffer 28 | for _, s := range bs.Statements { 29 | out.WriteString("\t" + s.String() + "\n") 30 | } 31 | return out.String() 32 | } 33 | -------------------------------------------------------------------------------- /ast/boolean.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | type Boolean struct { 4 | TokenAble 5 | Value bool 6 | } 7 | 8 | var _ Comparable = &Boolean{} 9 | var _ Expression = &Boolean{} 10 | 11 | func (b *Boolean) validIfCondition() bool { return true } 12 | 13 | func (b *Boolean) expressionNode() {} 14 | 15 | func (b *Boolean) String() string { 16 | return b.Token.Literal 17 | } 18 | -------------------------------------------------------------------------------- /ast/break_expression.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | type BreakExpression struct { 4 | TokenAble 5 | } 6 | 7 | var _ Expression = &BreakExpression{} 8 | 9 | func (ce *BreakExpression) expressionNode() {} 10 | 11 | func (ce *BreakExpression) String() string { 12 | return ce.Token.Literal 13 | } 14 | -------------------------------------------------------------------------------- /ast/call_expression.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | type CallExpression struct { 9 | TokenAble 10 | Callee Expression 11 | ChainCallee Expression 12 | Function Expression 13 | Arguments []Expression 14 | Block *BlockStatement 15 | ElseBlock *BlockStatement 16 | } 17 | 18 | var _ Comparable = &CallExpression{} 19 | var _ Expression = &CallExpression{} 20 | 21 | func (ce *CallExpression) validIfCondition() bool { return true } 22 | 23 | func (ce *CallExpression) expressionNode() {} 24 | 25 | func (ce *CallExpression) String() string { 26 | var out bytes.Buffer 27 | args := []string{} 28 | 29 | for _, a := range ce.Arguments { 30 | if a != nil { 31 | args = append(args, a.String()) 32 | } 33 | } 34 | 35 | out.WriteString(ce.Function.String()) 36 | out.WriteString("(") 37 | out.WriteString(strings.Join(args, ", ")) 38 | out.WriteString(")") 39 | 40 | if ce.Block != nil { 41 | out.WriteString(" {\n") 42 | out.WriteString(ce.Block.String()) 43 | out.WriteString("}") 44 | } 45 | 46 | if ce.ElseBlock != nil { 47 | out.WriteString(" else { ") 48 | out.WriteString(ce.ElseBlock.String()) 49 | out.WriteString(" }") 50 | } 51 | 52 | return out.String() 53 | } 54 | -------------------------------------------------------------------------------- /ast/continue_expression.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | type ContinueExpression struct { 4 | TokenAble 5 | } 6 | 7 | var _ Expression = &ContinueExpression{} 8 | 9 | func (ce *ContinueExpression) expressionNode() {} 10 | 11 | func (ce *ContinueExpression) String() string { 12 | return ce.Token.Literal 13 | } 14 | -------------------------------------------------------------------------------- /ast/expression_statement.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | type ExpressionStatement struct { 4 | TokenAble 5 | Expression Expression 6 | } 7 | 8 | var _ Statement = &ExpressionStatement{} 9 | 10 | func (es *ExpressionStatement) statementNode() {} 11 | 12 | func (es *ExpressionStatement) String() string { 13 | if es.Expression != nil { 14 | return es.Expression.String() 15 | } 16 | return "" 17 | } 18 | -------------------------------------------------------------------------------- /ast/float_literal.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | type FloatLiteral struct { 4 | TokenAble 5 | Value float64 6 | } 7 | 8 | var _ Comparable = &FloatLiteral{} 9 | var _ Expression = &FloatLiteral{} 10 | 11 | func (il *FloatLiteral) validIfCondition() bool { return true } 12 | 13 | func (il *FloatLiteral) expressionNode() {} 14 | 15 | func (il *FloatLiteral) String() string { 16 | return il.Token.Literal 17 | } 18 | -------------------------------------------------------------------------------- /ast/for_expression.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type ForExpression struct { 8 | TokenAble 9 | KeyName string 10 | ValueName string 11 | Block *BlockStatement 12 | Iterable Expression 13 | } 14 | 15 | var _ Expression = &ForExpression{} 16 | 17 | func (fe *ForExpression) expressionNode() {} 18 | 19 | func (fe *ForExpression) String() string { 20 | var out bytes.Buffer 21 | 22 | out.WriteString("for (") 23 | out.WriteString(fe.KeyName) 24 | out.WriteString(", ") 25 | out.WriteString(fe.ValueName) 26 | out.WriteString(") in ") 27 | out.WriteString(fe.Iterable.String()) 28 | out.WriteString(" { ") 29 | 30 | if fe.Block != nil { 31 | out.WriteString(fe.Block.String()) 32 | } 33 | 34 | out.WriteString(" }") 35 | 36 | return out.String() 37 | } 38 | -------------------------------------------------------------------------------- /ast/function_literal.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | type FunctionLiteral struct { 9 | TokenAble 10 | Parameters []*Identifier 11 | Block *BlockStatement 12 | } 13 | 14 | var _ Expression = &FunctionLiteral{} 15 | 16 | func (fl *FunctionLiteral) expressionNode() {} 17 | 18 | func (fl *FunctionLiteral) String() string { 19 | var out bytes.Buffer 20 | 21 | params := []string{} 22 | for _, p := range fl.Parameters { 23 | params = append(params, p.String()) 24 | } 25 | 26 | out.WriteString(fl.TokenLiteral()) 27 | out.WriteString("(") 28 | out.WriteString(strings.Join(params, ", ")) 29 | out.WriteString(") ") 30 | out.WriteString(fl.Block.String()) 31 | 32 | return out.String() 33 | } 34 | -------------------------------------------------------------------------------- /ast/hash_literal.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | ) 7 | 8 | type HashLiteral struct { 9 | TokenAble 10 | Order []Expression 11 | Pairs map[Expression]Expression 12 | } 13 | 14 | var _ Expression = &HashLiteral{} 15 | 16 | func (hl *HashLiteral) expressionNode() {} 17 | 18 | func (hl *HashLiteral) String() string { 19 | var out bytes.Buffer 20 | 21 | pairs := []string{} 22 | for _, key := range hl.Order { 23 | p := hl.Pairs[key] 24 | pairs = append(pairs, key.String()+": "+p.String()) 25 | } 26 | 27 | out.WriteString("{") 28 | out.WriteString(strings.Join(pairs, ", ")) 29 | out.WriteString("}") 30 | 31 | return out.String() 32 | } 33 | -------------------------------------------------------------------------------- /ast/html_literal.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | type HTMLLiteral struct { 4 | TokenAble 5 | Value string 6 | } 7 | 8 | var _ Printable = &HTMLLiteral{} 9 | var _ Expression = &HTMLLiteral{} 10 | 11 | func (hl *HTMLLiteral) Printable() bool { 12 | return true 13 | } 14 | 15 | func (hl *HTMLLiteral) expressionNode() {} 16 | 17 | func (hl *HTMLLiteral) String() string { 18 | return hl.Token.Literal 19 | } 20 | -------------------------------------------------------------------------------- /ast/identifier.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type Identifier struct { 8 | TokenAble 9 | Callee *Identifier 10 | Value string 11 | OriginalCallee *Identifier // So robot.Avatar.Name the OriginalCallee will be robot 12 | } 13 | 14 | var _ Comparable = &Identifier{} 15 | var _ Expression = &Identifier{} 16 | 17 | func (il *Identifier) validIfCondition() bool { return true } 18 | 19 | func (i *Identifier) expressionNode() {} 20 | 21 | func (i *Identifier) String() string { 22 | out := &bytes.Buffer{} 23 | 24 | if i.Callee != nil { 25 | out.WriteString(i.Callee.String()) 26 | out.WriteString(".") 27 | } 28 | 29 | out.WriteString(i.Value) 30 | return out.String() 31 | } 32 | -------------------------------------------------------------------------------- /ast/if_expression.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type IfExpression struct { 8 | TokenAble 9 | Condition Expression 10 | Block *BlockStatement 11 | ElseIf []*ElseIfExpression 12 | ElseBlock *BlockStatement 13 | } 14 | 15 | var _ Expression = &IfExpression{} 16 | 17 | type ElseIfExpression struct { 18 | TokenAble 19 | Condition Expression 20 | Block *BlockStatement 21 | } 22 | 23 | func (ie *IfExpression) expressionNode() {} 24 | 25 | func (ie *IfExpression) String() string { 26 | var out bytes.Buffer 27 | 28 | out.WriteString("if (") 29 | out.WriteString(ie.Condition.String()) 30 | out.WriteString(") { ") 31 | out.WriteString(ie.Block.String()) 32 | out.WriteString(" }") 33 | 34 | for _, elseIf := range ie.ElseIf { 35 | out.WriteString(" } else if (") 36 | out.WriteString(elseIf.Condition.String()) 37 | out.WriteString(") { ") 38 | out.WriteString(elseIf.Block.String()) 39 | out.WriteString(" }") 40 | } 41 | 42 | if ie.ElseBlock != nil { 43 | out.WriteString(" } else { ") 44 | out.WriteString(ie.ElseBlock.String()) 45 | out.WriteString(" }") 46 | } 47 | 48 | return out.String() 49 | } 50 | -------------------------------------------------------------------------------- /ast/index_expression.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type IndexExpression struct { 8 | TokenAble 9 | Left Expression 10 | Index Expression 11 | Value Expression 12 | Callee Expression 13 | } 14 | 15 | var _ Comparable = &IndexExpression{} 16 | var _ Expression = &IndexExpression{} 17 | 18 | func (ie *IndexExpression) validIfCondition() bool { return true } 19 | 20 | func (ie *IndexExpression) expressionNode() {} 21 | 22 | func (ie *IndexExpression) String() string { 23 | var out bytes.Buffer 24 | 25 | out.WriteString("(") 26 | out.WriteString(ie.Left.String()) 27 | out.WriteString("[") 28 | out.WriteString(ie.Index.String()) 29 | 30 | if ie.Callee != nil { 31 | out.WriteString("]") 32 | out.WriteString("." + ie.Callee.String()) 33 | out.WriteString(")") 34 | } else { 35 | 36 | out.WriteString("])") 37 | } 38 | 39 | if ie.Value != nil { 40 | out.WriteString("=") 41 | out.WriteString(ie.Value.String()) 42 | } 43 | 44 | return out.String() 45 | } 46 | -------------------------------------------------------------------------------- /ast/infix_expression.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type InfixExpression struct { 8 | TokenAble 9 | Left Expression 10 | Operator string 11 | Right Expression 12 | } 13 | 14 | var _ Comparable = &InfixExpression{} 15 | var _ Expression = &InfixExpression{} 16 | 17 | func (oe *InfixExpression) validIfCondition() bool { return true } 18 | 19 | func (oe *InfixExpression) expressionNode() {} 20 | 21 | func (oe *InfixExpression) String() string { 22 | var out bytes.Buffer 23 | 24 | out.WriteString("(") 25 | 26 | if oe.Left != nil { 27 | out.WriteString(oe.Left.String()) 28 | } 29 | 30 | out.WriteString(" " + oe.Operator + " ") 31 | 32 | if oe.Right != nil { 33 | out.WriteString(oe.Right.String()) 34 | } else { 35 | out.WriteString(" !!MISSING '%>'!!") 36 | } 37 | 38 | out.WriteString(")") 39 | 40 | return out.String() 41 | } 42 | -------------------------------------------------------------------------------- /ast/integer_literal.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | type IntegerLiteral struct { 4 | TokenAble 5 | Value int 6 | } 7 | 8 | var _ Comparable = &IntegerLiteral{} 9 | var _ Expression = &IntegerLiteral{} 10 | 11 | func (il *IntegerLiteral) validIfCondition() bool { return true } 12 | 13 | func (il *IntegerLiteral) expressionNode() {} 14 | 15 | func (il *IntegerLiteral) String() string { 16 | return il.Token.Literal 17 | } 18 | -------------------------------------------------------------------------------- /ast/let_statement.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type LetStatement struct { 8 | TokenAble 9 | Name *Identifier 10 | Value Expression 11 | } 12 | 13 | var _ Statement = &LetStatement{} 14 | 15 | func (ls *LetStatement) statementNode() {} 16 | 17 | func (ls *LetStatement) String() string { 18 | var out bytes.Buffer 19 | 20 | out.WriteString(ls.TokenLiteral() + " ") 21 | if ls.Name != nil { 22 | out.WriteString(ls.Name.String()) 23 | } 24 | out.WriteString(" = ") 25 | 26 | if ls.Value != nil { 27 | out.WriteString(ls.Value.String()) 28 | } 29 | 30 | out.WriteString(";") 31 | 32 | return out.String() 33 | } 34 | -------------------------------------------------------------------------------- /ast/prefix_expression.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type PrefixExpression struct { 8 | TokenAble 9 | Operator string 10 | Right Expression 11 | } 12 | 13 | var _ Comparable = &PrefixExpression{} 14 | var _ Expression = &PrefixExpression{} 15 | 16 | func (ce *PrefixExpression) validIfCondition() bool { return true } 17 | 18 | func (pe *PrefixExpression) expressionNode() {} 19 | 20 | func (pe *PrefixExpression) String() string { 21 | var out bytes.Buffer 22 | 23 | out.WriteString("(") 24 | out.WriteString(pe.Operator) 25 | 26 | if pe.Right != nil { 27 | out.WriteString(pe.Right.String()) 28 | } else { 29 | out.WriteString("<%= "" %>
` 15 | s, err := plush.Render(input, plush.NewContext()) 16 | r.NoError(err) 17 | r.Equal("<script>alert('pwned')</script>
", s) 18 | } 19 | 20 | func Test_Render_HTML_Escape(t *testing.T) { 21 | r := require.New(t) 22 | 23 | input := `<%= escapedHTML() %>|<%= unescapedHTML() %>|<%= raw("unsafe") %>` 24 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 25 | "escapedHTML": func() string { 26 | return "unsafe" 27 | }, 28 | "unescapedHTML": func() template.HTML { 29 | return "unsafe" 30 | }, 31 | })) 32 | r.NoError(err) 33 | r.Equal("<b>unsafe</b>|unsafe|unsafe", s) 34 | } 35 | 36 | func Test_Escaping_EscapeExpression(t *testing.T) { 37 | r := require.New(t) 38 | input := `C:\\<%= "temp" %>` 39 | 40 | s, err := plush.Render(input, plush.NewContext()) 41 | r.NoError(err) 42 | r.Equal(`C:\temp`, s) 43 | } 44 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "log" 7 | 8 | "github.com/gobuffalo/plush/v5" 9 | ) 10 | 11 | // ExampleRender using `if`, `for`, `else`, functions, etc... 12 | func ExampleRender() { 13 | html := ` 14 | <%= if (names && len(names) > 0) { %> 15 |<%= one() %>
70 |<%= greet("mark")%>
71 | <%= can("update") { %> 72 |i can update
73 | <% } %> 74 | <%= can("destroy") { %> 75 |i can destroy
76 | <% } %> 77 | ` 78 | 79 | ctx := plush.NewContext() 80 | ctx.Set("one", func() int { 81 | return 1 82 | }) 83 | ctx.Set("greet", func(s string) string { 84 | return fmt.Sprintf("Hi %s", s) 85 | }) 86 | ctx.Set("can", func(s string, help plush.HelperContext) (template.HTML, error) { 87 | if s == "update" { 88 | h, err := help.Block() 89 | return template.HTML(h), err 90 | } 91 | return "", nil 92 | }) 93 | 94 | s, err := plush.Render(html, ctx) 95 | if err != nil { 96 | log.Fatal(err) 97 | } 98 | fmt.Print(s) 99 | // output:1
100 | //Hi mark
101 | // 102 | //i can update
103 | } 104 | 105 | func ExampleRender_forIterator() { 106 | html := `<%= for (v) in between(3,6) { %><%=v%><% } %>` 107 | 108 | s, err := plush.Render(html, plush.NewContext()) 109 | if err != nil { 110 | log.Fatal(err) 111 | } 112 | fmt.Print(s) 113 | // output: 45 114 | } 115 | -------------------------------------------------------------------------------- /for_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/gobuffalo/plush/v5" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Render_For_Array(t *testing.T) { 12 | r := require.New(t) 13 | input := `<% for (i,v) in ["a", "b", "c"] {return v} %>` 14 | s, err := plush.Render(input, plush.NewContext()) 15 | r.NoError(err) 16 | r.Equal("", s) 17 | } 18 | 19 | func Test_Render_For_Update_Global_Scope(t *testing.T) { 20 | r := require.New(t) 21 | input := `<% let varTest = "" %><% for (i,v) in ["a", "b", "c"] {varTest = v} %><%= varTest %>` 22 | s, err := plush.Render(input, plush.NewContext()) 23 | r.NoError(err) 24 | r.Equal("c", s) 25 | } 26 | 27 | func Test_Render_For_Hash(t *testing.T) { 28 | r := require.New(t) 29 | input := `<%= for (k,v) in myMap { %><%= k + ":" + v%><% } %>` 30 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 31 | "myMap": map[string]string{ 32 | "a": "A", 33 | "b": "B", 34 | }, 35 | })) 36 | r.NoError(err) 37 | r.Contains(s, "a:A") 38 | r.Contains(s, "b:B") 39 | } 40 | 41 | func Test_Render_For_Global_Scope_Key_Access(t *testing.T) { 42 | r := require.New(t) 43 | input := `<%= for (k,v) in myMap { %><%= k + ":" + v%><% } %> <%= k %>` 44 | _, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 45 | "myMap": map[string]string{ 46 | "a": "A", 47 | }, 48 | })) 49 | r.Error(err) 50 | r.Errorf(err, `line 1: "k": unknown identifier`) 51 | } 52 | 53 | func Test_Render_For_Global_Scope_Value_Access(t *testing.T) { 54 | r := require.New(t) 55 | input := `<%= for (k,v) in myMap { %><%= k + ":" + v%><% } %> <%= v %>` 56 | _, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 57 | "myMap": map[string]string{ 58 | "a": "A", 59 | }, 60 | })) 61 | r.Error(err) 62 | r.Errorf(err, `line 1: "v": unknown identifier`) 63 | } 64 | 65 | func Test_Render_For_Nested_For_With_Same_Iterators_Keys(t *testing.T) { 66 | r := require.New(t) 67 | input := `<%= for (k,v) in myMap { %><%= for (k,v) in myMap2 { %><%= k + ":" + v%><% } %>%><%}%>` 68 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 69 | "myMap": map[string]string{ 70 | "a": "A", 71 | }, 72 | "myMap2": map[string]string{ 73 | "b": "B", 74 | }, 75 | })) 76 | r.NoError(err) 77 | r.Contains(s, "b:B") 78 | } 79 | 80 | func Test_Render_For_Array_Return(t *testing.T) { 81 | r := require.New(t) 82 | input := `<%= for (i,v) in ["a", "b", "c"] {return v} %>` 83 | s, err := plush.Render(input, plush.NewContext()) 84 | r.NoError(err) 85 | r.Equal("abc", s) 86 | } 87 | 88 | func Test_Render_For_Array_Continue(t *testing.T) { 89 | r := require.New(t) 90 | input := `<%= for (i,v) in [1, 2, 3,4,5,6,7,8,9,10] { 91 | %>Start<% 92 | if (v == 1 || v ==3 || v == 5 || v == 7 || v == 9) { 93 | 94 | 95 | %>Odd<% 96 | continue 97 | } 98 | 99 | return v 100 | } %>` 101 | s, err := plush.Render(input, plush.NewContext()) 102 | 103 | r.NoError(err) 104 | r.Equal("StartOddStart2StartOddStart4StartOddStart6StartOddStart8StartOddStart10", s) 105 | } 106 | 107 | func Test_Render_For_Array_WithNoOutput(t *testing.T) { 108 | r := require.New(t) 109 | input := `<%= for (i,v) in [1, 2, 3,4,5,6,7,8,9,10] { 110 | 111 | if (v == 1 || v == 2 || v ==3 || v == 4|| v == 5 || v == 6 || v == 7 || v == 8 || v == 9 || v == 10) { 112 | 113 | continue 114 | } 115 | 116 | return v 117 | } %>` 118 | s, err := plush.Render(input, plush.NewContext()) 119 | 120 | r.NoError(err) 121 | r.Equal("", s) 122 | } 123 | 124 | func Test_Render_For_Array_WithoutContinue(t *testing.T) { 125 | r := require.New(t) 126 | input := `<%= for (i,v) in [1, 2, 3,4,5,6,7,8,9,10] { 127 | if (v == 1 || v ==3 || v == 5 || v == 7 || v == 9) { 128 | } 129 | return v 130 | } %>` 131 | s, err := plush.Render(input, plush.NewContext()) 132 | 133 | r.NoError(err) 134 | r.Equal("12345678910", s) 135 | } 136 | 137 | func Test_Render_For_Array_ContinueNoControl(t *testing.T) { 138 | r := require.New(t) 139 | input := `<%= for (i,v) in [1, 2, 3,4,5,6,7,8,9,10] { 140 | continue 141 | return v 142 | } %>` 143 | s, err := plush.Render(input, plush.NewContext()) 144 | 145 | r.NoError(err) 146 | r.Equal("", s) 147 | } 148 | 149 | func Test_Render_For_Array_Break_String(t *testing.T) { 150 | r := require.New(t) 151 | input := `<%= for (i,v) in [1, 2, 3,4,5,6,7,8,9,10] { 152 | %>Start<% 153 | if (v == 5) { 154 | 155 | 156 | %>Odd<% 157 | break 158 | } 159 | 160 | return v 161 | } %>` 162 | s, err := plush.Render(input, plush.NewContext()) 163 | 164 | r.NoError(err) 165 | r.Equal("Start1Start2Start3Start4StartOdd", s) 166 | } 167 | 168 | func Test_Render_For_Array_WithBreakFirstValue(t *testing.T) { 169 | r := require.New(t) 170 | input := `<%= for (i,v) in [1, 2, 3,4,5,6,7,8,9,10] { 171 | if (v == 1 || v ==3 || v == 5 || v == 7 || v == 9) { 172 | break 173 | } 174 | return v 175 | } %>` 176 | s, err := plush.Render(input, plush.NewContext()) 177 | 178 | r.NoError(err) 179 | r.Equal("", s) 180 | } 181 | 182 | func Test_Render_For_Array_WithBreakFirstValueWithReturn(t *testing.T) { 183 | r := require.New(t) 184 | input := `<%= for (i,v) in [1, 2, 3,4,5,6,7,8,9,10] { 185 | if (v == 1 || v ==3 || v == 5 || v == 7 || v == 9) { 186 | %><%=v%><% 187 | break 188 | } 189 | return v 190 | } %>` 191 | s, err := plush.Render(input, plush.NewContext()) 192 | 193 | r.NoError(err) 194 | r.Equal("1", s) 195 | } 196 | func Test_Render_For_Array_Break(t *testing.T) { 197 | r := require.New(t) 198 | input := `<%= for (i,v) in [1, 2, 3,4,5,6,7,8,9,10] { 199 | break 200 | return v 201 | } %>` 202 | s, err := plush.Render(input, plush.NewContext()) 203 | 204 | r.NoError(err) 205 | r.Equal("", s) 206 | } 207 | 208 | func Test_Render_For_Array_Key_Only(t *testing.T) { 209 | r := require.New(t) 210 | input := `<%= for (v) in ["a", "b", "c"] {%><%=v%><%} %>` 211 | s, err := plush.Render(input, plush.NewContext()) 212 | r.NoError(err) 213 | r.Equal("abc", s) 214 | } 215 | 216 | func Test_Render_For_Func_Range(t *testing.T) { 217 | r := require.New(t) 218 | input := `<%= for (v) in range(3,5) { %><%=v%><% } %>` 219 | s, err := plush.Render(input, plush.NewContext()) 220 | r.NoError(err) 221 | r.Equal("345", s) 222 | } 223 | 224 | func Test_Render_For_Func_Between(t *testing.T) { 225 | r := require.New(t) 226 | input := `<%= for (v) in between(3,6) { %><%=v%><% } %>` 227 | s, err := plush.Render(input, plush.NewContext()) 228 | r.NoError(err) 229 | r.Equal("45", s) 230 | } 231 | 232 | func Test_Render_For_Func_Until(t *testing.T) { 233 | r := require.New(t) 234 | input := `<%= for (v) in until(3) { %><%=v%><% } %>` 235 | s, err := plush.Render(input, plush.NewContext()) 236 | r.NoError(err) 237 | r.Equal("012", s) 238 | } 239 | 240 | func Test_Render_For_Array_Key_Value(t *testing.T) { 241 | r := require.New(t) 242 | input := `<%= for (i,v) in ["a", "b", "c"] {%><%=i%><%=v%><%} %>` 243 | s, err := plush.Render(input, plush.NewContext()) 244 | r.NoError(err) 245 | r.Equal("0a1b2c", s) 246 | } 247 | 248 | func Test_Render_For_Array_Key_Global_Scope_Same_Identifier(t *testing.T) { 249 | r := require.New(t) 250 | input := `<% let i = 10000 %><%= for (i,v) in ["a", "b", "c"] {%><%=i%><%=v%><%} %><%= i %>` 251 | s, err := plush.Render(input, plush.NewContext()) 252 | r.NoError(err) 253 | r.Equal("0a1b2c10000", s) 254 | } 255 | 256 | func Test_Render_For_Array_Key_Not_Defined(t *testing.T) { 257 | r := require.New(t) 258 | input := `<%= for (i,v) in ["a", "b", "c"] {%><%=i%><%=v%><%} %><%= i %>` 259 | _, err := plush.Render(input, plush.NewContext()) 260 | r.Error(err) 261 | r.Errorf(err, `line 1: "i": unknown identifier`) 262 | } 263 | 264 | func Test_Render_For_Array_Value_Global_Scope(t *testing.T) { 265 | r := require.New(t) 266 | input := ` <%= for (i,v) in ["a", "b", "c"] {%><%=i%><%=v%><%} %><%= v %>` 267 | _, err := plush.Render(input, plush.NewContext()) 268 | r.Error(err) 269 | r.Errorf(err, `line 1: "v": unknown identifier`) 270 | } 271 | 272 | func Test_Render_For_Nil(t *testing.T) { 273 | r := require.New(t) 274 | input := `<% for (i,v) in nilValue {return v} %>` 275 | ctx := plush.NewContext() 276 | ctx.Set("nilValue", nil) 277 | s, err := plush.Render(input, ctx) 278 | r.Error(err) 279 | r.Equal("", s) 280 | } 281 | 282 | func Test_Render_For_Map_Nil_Value(t *testing.T) { 283 | r := require.New(t) 284 | input := ` 285 | <%= for (k, v) in flash["errors"] { %> 286 | Flash: 287 | <%= k %>:<%= v %> 288 | <% } %> 289 | ` 290 | ctx := plush.NewContext() 291 | ctx.Set("flash", map[string][]string{}) 292 | s, err := plush.Render(input, ctx) 293 | r.NoError(err) 294 | r.Equal("", strings.TrimSpace(s)) 295 | } 296 | 297 | type Category struct { 298 | Products []Product 299 | } 300 | type Product struct { 301 | Name []string 302 | } 303 | 304 | func Test_Render_For_Array_OutofBoundIndex(t *testing.T) { 305 | r := require.New(t) 306 | ctx := plush.NewContext() 307 | product_listing := Category{} 308 | ctx.Set("product_listing", product_listing) 309 | input := `<%= for (i, names) in product_listing.Products[0].Name { %> 310 | <%= splt %> 311 | <% } %>` 312 | _, err := plush.Render(input, ctx) 313 | r.Error(err) 314 | } 315 | -------------------------------------------------------------------------------- /functions_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "html/template" 7 | "testing" 8 | 9 | "github.com/gobuffalo/plush/v5" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func Test_Render_Function_Call(t *testing.T) { 14 | r := require.New(t) 15 | 16 | input := `<%= f() %>
` 17 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 18 | "f": func() string { 19 | return "hi!" 20 | }, 21 | })) 22 | r.NoError(err) 23 | r.Equal("hi!
", s) 24 | } 25 | 26 | func Test_Render_Unknown_Function_Call(t *testing.T) { 27 | r := require.New(t) 28 | 29 | input := `<%= f() %>
` 30 | _, err := plush.Render(input, plush.NewContext()) 31 | r.Error(err) 32 | } 33 | 34 | func Test_Render_Function_Call_With_Arg(t *testing.T) { 35 | r := require.New(t) 36 | 37 | input := `<%= f("mark") %>
` 38 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 39 | "f": func(s string) string { 40 | return fmt.Sprintf("hi %s!", s) 41 | }, 42 | })) 43 | r.NoError(err) 44 | r.Equal("hi mark!
", s) 45 | } 46 | 47 | func Test_Render_Function_Call_With_Variable_Arg(t *testing.T) { 48 | r := require.New(t) 49 | 50 | input := `<%= f(name) %>
` 51 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 52 | "f": func(s string) string { 53 | return fmt.Sprintf("hi %s!", s) 54 | }, 55 | "name": "mark", 56 | })) 57 | r.NoError(err) 58 | r.Equal("hi mark!
", s) 59 | } 60 | 61 | func Test_Render_Function_Call_With_Hash(t *testing.T) { 62 | r := require.New(t) 63 | 64 | input := `<%= f({name: name}) %>
` 65 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 66 | "f": func(m map[string]interface{}) string { 67 | return fmt.Sprintf("hi %s!", m["name"]) 68 | }, 69 | "name": "mark", 70 | })) 71 | r.NoError(err) 72 | r.Equal("hi mark!
", s) 73 | } 74 | 75 | func Test_Render_Function_Call_With_Syntax_Error_Hash(t *testing.T) { 76 | r := require.New(t) 77 | 78 | input := `<%= f({name: name) %>
` 79 | _, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 80 | "f": func(m map[string]interface{}) string { 81 | return fmt.Sprintf("hi %s!", m["name"]) 82 | }, 83 | "name": "mark", 84 | })) 85 | r.Error(err) 86 | 87 | } 88 | 89 | func Test_Render_Function_Call_With_Error(t *testing.T) { 90 | r := require.New(t) 91 | 92 | input := `<%= f() %>
` 93 | _, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 94 | "f": func() (string, error) { 95 | return "hi!", errors.New("oops") 96 | }, 97 | })) 98 | r.Error(err) 99 | } 100 | 101 | func Test_Render_Function_Call_With_Block(t *testing.T) { 102 | r := require.New(t) 103 | 104 | input := `<%= f() { %>hello<% } %>
` 105 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 106 | "f": func(h plush.HelperContext) string { 107 | s, _ := h.Block() 108 | return s 109 | }, 110 | })) 111 | r.NoError(err) 112 | r.Equal("hello
", s) 113 | } 114 | 115 | func Test_Render_Function_Call_With_Block_Update_Global_Scope(t *testing.T) { 116 | r := require.New(t) 117 | 118 | input := `<% let i = "hello-world" <%= f() { i = "bye" } %><%= i %>
` 119 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 120 | "f": func(h plush.HelperContext) string { 121 | s, _ := h.Block() 122 | return s 123 | }, 124 | })) 125 | r.NoError(err) 126 | r.Equal("bye
", s) 127 | } 128 | 129 | func Test_Render_Function_Call_With_Block_Update_Local_Scope(t *testing.T) { 130 | r := require.New(t) 131 | 132 | input := `<%= f() { let i = "hello-world" } %><%= i %>
` 133 | _, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 134 | "f": func(h plush.HelperContext) string { 135 | s, _ := h.Block() 136 | return s 137 | }, 138 | })) 139 | 140 | r.Error(err) 141 | r.Errorf(err, `line 1: "i": unknown identifier`) 142 | } 143 | 144 | type greeter struct{} 145 | 146 | func (g greeter) Greet(s string) string { 147 | return fmt.Sprintf("hi %s!", s) 148 | } 149 | 150 | func Test_Render_Function_Call_On_Callee(t *testing.T) { 151 | r := require.New(t) 152 | 153 | input := `<%= g.Greet("mark") %>
` 154 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 155 | "g": greeter{}, 156 | })) 157 | r.NoError(err) 158 | r.Equal(`hi mark!
`, s) 159 | } 160 | 161 | func Test_Render_Function_Optional_Map(t *testing.T) { 162 | r := require.New(t) 163 | input := `<%= foo() %>|<%= bar({a: "A"}) %>` 164 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 165 | "foo": func(opts map[string]interface{}, help plush.HelperContext) string { 166 | return "foo" 167 | }, 168 | "bar": func(opts map[string]interface{}) string { 169 | return opts["a"].(string) 170 | }, 171 | })) 172 | r.NoError(err) 173 | r.Equal("foo|A", s) 174 | } 175 | 176 | func Test_Render_Function_With_Backticks_And_Quotes(t *testing.T) { 177 | // From https://github.com/gobuffalo/pop/issues/168 178 | r := require.New(t) 179 | input := "<%= raw(`" + `CREATE MATERIALIZED VIEW view_papers AS 180 | SELECT papers.created_at, 181 | papers.updated_at, 182 | papers.id, 183 | papers.name, 184 | ( setweight(to_tsvector(papers.name::text), 'A'::"char") || 185 | setweight(to_tsvector(papers.author_name), 'B'::"char") 186 | ) || setweight(to_tsvector(papers.description), 'C'::"char") 187 | AS paper_vector 188 | FROM 189 | ( SELECT papers.id, string_agg(categories.code, ',') as categories 190 | FROM papers 191 | LEFT JOIN paper_categories ON paper_categories.paper_id=papers.id LEFT JOIN (select * from categories order by weight asc) categories ON categories.id=paper_categories.category_id 192 | GROUP BY papers.id 193 | ) a 194 | LEFT JOIN papers on a.id=papers.id 195 | WHERE (papers.doc_status = ANY (ARRAY[1, 3])) AND papers.status = 1 196 | WITH DATA` + "`) %>" 197 | 198 | output := `CREATE MATERIALIZED VIEW view_papers AS 199 | SELECT papers.created_at, 200 | papers.updated_at, 201 | papers.id, 202 | papers.name, 203 | ( setweight(to_tsvector(papers.name::text), 'A'::"char") || 204 | setweight(to_tsvector(papers.author_name), 'B'::"char") 205 | ) || setweight(to_tsvector(papers.description), 'C'::"char") 206 | AS paper_vector 207 | FROM 208 | ( SELECT papers.id, string_agg(categories.code, ',') as categories 209 | FROM papers 210 | LEFT JOIN paper_categories ON paper_categories.paper_id=papers.id LEFT JOIN (select * from categories order by weight asc) categories ON categories.id=paper_categories.category_id 211 | GROUP BY papers.id 212 | ) a 213 | LEFT JOIN papers on a.id=papers.id 214 | WHERE (papers.doc_status = ANY (ARRAY[1, 3])) AND papers.status = 1 215 | WITH DATA` 216 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 217 | "raw": func(arg string) template.HTML { 218 | return template.HTML(arg) 219 | }, 220 | })) 221 | r.NoError(err) 222 | r.Equal(output, s) 223 | } 224 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gobuffalo/plush/v5 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/gobuffalo/flect v1.0.2 7 | github.com/gobuffalo/tags/v3 v3.1.4 8 | github.com/stretchr/testify v1.9.0 9 | golang.org/x/sync v0.8.0 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/fatih/structs v1.1.0 // indirect 15 | github.com/kr/pretty v0.1.0 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 5 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 6 | github.com/gobuffalo/flect v0.3.0/go.mod h1:5pf3aGnsvqvCj50AVni7mJJF8ICxGZ8HomberC3pXLE= 7 | github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= 8 | github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= 9 | github.com/gobuffalo/tags/v3 v3.1.4 h1:X/ydLLPhgXV4h04Hp2xlbI2oc5MDaa7eub6zw8oHjsM= 10 | github.com/gobuffalo/tags/v3 v3.1.4/go.mod h1:ArRNo3ErlHO8BtdA0REaZxijuWnWzF6PUXngmMXd2I0= 11 | github.com/gobuffalo/validate/v3 v3.3.3/go.mod h1:YC7FsbJ/9hW/VjQdmXPvFqvRis4vrRYFxr69WiNZw6g= 12 | github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 13 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 14 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 15 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 16 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 17 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 21 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 22 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 23 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 25 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 26 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 27 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 28 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 29 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 31 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 32 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 33 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 35 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 36 | -------------------------------------------------------------------------------- /hashes_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/plush/v5" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_Render_Hash_Key_Interface(t *testing.T) { 11 | r := require.New(t) 12 | 13 | input := `<%= m["first"]%>` 14 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 15 | 16 | "m": map[interface{}]bool{"first": true}, 17 | })) 18 | r.NoError(err) 19 | r.Equal("true", s) 20 | } 21 | 22 | func Test_Render_Hash_Key_Int_With_String_Index(t *testing.T) { 23 | r := require.New(t) 24 | 25 | input := `<%= m["first"]%>` 26 | _, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 27 | 28 | "m": map[int]bool{0: true}, 29 | })) 30 | 31 | errStr := "line 1: cannot use first (string constant) as int value in map index" 32 | r.Error(err) 33 | r.Equal(errStr, err.Error()) 34 | 35 | } 36 | 37 | func Test_Render_Hash_Array_Index(t *testing.T) { 38 | r := require.New(t) 39 | 40 | input := `<%= m["first"] + " " + m["last"] %>|<%= a[0+1] %>` 41 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 42 | "m": map[string]string{"first": "Mark", "last": "Bates"}, 43 | "a": []string{"john", "paul"}, 44 | })) 45 | r.NoError(err) 46 | r.Equal("Mark Bates|paul", s) 47 | } 48 | 49 | func Test_Render_HashCall(t *testing.T) { 50 | r := require.New(t) 51 | input := `<%= m["a"] %>` 52 | ctx := plush.NewContext() 53 | ctx.Set("m", map[string]string{ 54 | "a": "A", 55 | }) 56 | s, err := plush.Render(input, ctx) 57 | r.NoError(err) 58 | r.Equal("A", s) 59 | } 60 | 61 | func Test_Render_HashCall_OnAttribute(t *testing.T) { 62 | r := require.New(t) 63 | input := `<%= m.MyMap[key] %>` 64 | ctx := plush.NewContext() 65 | ctx.Set("m", struct { 66 | MyMap map[string]string 67 | }{ 68 | MyMap: map[string]string{"a": "A"}, 69 | }) 70 | ctx.Set("key", "a") 71 | s, err := plush.Render(input, ctx) 72 | r.NoError(err) 73 | r.Equal("A", s) 74 | } 75 | 76 | func Test_Render_HashCall_OnAttribute_IntoFunction(t *testing.T) { 77 | r := require.New(t) 78 | input := `<%= debug(m.MyMap[key]) %>` 79 | ctx := plush.NewContext() 80 | ctx.Set("m", struct { 81 | MyMap map[string]string 82 | }{ 83 | MyMap: map[string]string{"a": "A"}, 84 | }) 85 | ctx.Set("key", "a") 86 | s, err := plush.Render(input, ctx) 87 | r.NoError(err) 88 | r.Equal("A", s) 89 | } 90 | -------------------------------------------------------------------------------- /helper_context.go: -------------------------------------------------------------------------------- 1 | package plush 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/gobuffalo/plush/v5/ast" 8 | "github.com/gobuffalo/plush/v5/helpers/hctx" 9 | ) 10 | 11 | var _ hctx.HelperContext = &HelperContext{} 12 | 13 | // HelperContext is an optional last argument to helpers 14 | // that provides the current context of the call, and access 15 | // to an optional "block" of code that can be executed from 16 | // within the helper. 17 | type HelperContext struct { 18 | hctx.Context 19 | compiler *compiler 20 | block *ast.BlockStatement 21 | } 22 | 23 | const helperContextKind = "HelperContext" 24 | 25 | // Render a string with the current context 26 | func (h HelperContext) Render(s string) (string, error) { 27 | return Render(s, h.Context) 28 | } 29 | 30 | // HasBlock returns true if a block is associated with the helper function 31 | func (h HelperContext) HasBlock() bool { 32 | return h.block != nil 33 | } 34 | 35 | // Block executes the block of template associated with 36 | // the helper, think the block inside of an "if" or "each" 37 | // statement. 38 | func (h HelperContext) Block() (string, error) { 39 | return h.BlockWith(h.Context) 40 | } 41 | 42 | // BlockWith executes the block of template associated with 43 | // the helper, think the block inside of an "if" or "each" 44 | // statement, but with it's own context. 45 | func (h HelperContext) BlockWith(hc hctx.Context) (string, error) { 46 | ctx, ok := hc.(*Context) 47 | if !ok { 48 | return "", fmt.Errorf("expected *Context, got %T", hc) 49 | } 50 | 51 | octx := h.compiler.ctx 52 | defer func() { h.compiler.ctx = octx }() 53 | 54 | h.compiler.ctx = ctx.New() 55 | 56 | if h.block == nil { 57 | return "", fmt.Errorf("no block defined") 58 | } 59 | 60 | i, err := h.compiler.evalBlockStatement(h.block) 61 | if err != nil { 62 | return "", err 63 | } 64 | 65 | bb := &strings.Builder{} 66 | h.compiler.write(bb, i) 67 | 68 | return bb.String(), nil 69 | } 70 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package plush 2 | 3 | import ( 4 | "github.com/gobuffalo/plush/v5/helpers" 5 | ) 6 | 7 | // Helpers contains all of the default helpers for 8 | // These will be available to all templates. You should add 9 | // any custom global helpers to this list. 10 | var Helpers = helpers.NewMap(map[string]interface{}{}) 11 | 12 | func init() { 13 | Helpers.AddMany(helpers.Base) 14 | Helpers.Add("partial", PartialHelper) 15 | } 16 | -------------------------------------------------------------------------------- /helpers/content/content.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import "github.com/gobuffalo/plush/v5/helpers/hctx" 4 | 5 | // Keys to be used in templates for the functions in this package. 6 | const ( 7 | OfKey = "contentOf" 8 | ForKey = "contentFor" 9 | ) 10 | 11 | // New returns a map of the helpers within this package. 12 | func New() hctx.Map { 13 | return hctx.Map{ 14 | OfKey: ContentOf, 15 | ForKey: ContentFor, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /helpers/content/for.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "html/template" 5 | 6 | "github.com/gobuffalo/plush/v5/helpers/hctx" 7 | ) 8 | 9 | // ContentFor stores a block of templating code to be re-used later in the template 10 | // via the contentOf helper. 11 | // An optional map of values can be passed to contentOf, 12 | // which are made available to the contentFor block. 13 | /* 14 | <% contentFor("buttons") { %> 15 | 16 | <% } %> 17 | */ 18 | func ContentFor(name string, help hctx.HelperContext) { 19 | help.Set("contentFor:"+name, func(data hctx.Map) (template.HTML, error) { 20 | hctx := help.New() 21 | for k, v := range data { 22 | hctx.Set(k, v) 23 | } 24 | body, err := help.BlockWith(hctx) 25 | if err != nil { 26 | return "", err 27 | } 28 | return template.HTML(body), nil 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /helpers/content/for_test.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/gobuffalo/plush/v5/helpers/hctx" 8 | "github.com/gobuffalo/plush/v5/helpers/helptest" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_ContentFor(t *testing.T) { 13 | r := require.New(t) 14 | 15 | in := "" 16 | hc := helptest.NewContext() 17 | hc.BlockContextFn = func(c hctx.Context) (string, error) { 18 | return in, nil 19 | } 20 | 21 | cf := hc.New().(*helptest.HelperContext) 22 | ContentFor("buttons", hc) 23 | s, err := ContentOf("buttons", hctx.Map{}, cf) 24 | r.NoError(err) 25 | r.Contains(s, in) 26 | } 27 | 28 | func Test_ContentFor_Fail(t *testing.T) { 29 | r := require.New(t) 30 | 31 | hc := helptest.NewContext() 32 | hc.BlockContextFn = func(c hctx.Context) (string, error) { 33 | return "", errors.New("nope") 34 | } 35 | 36 | cf := hc.New().(*helptest.HelperContext) 37 | ContentFor("buttons", hc) 38 | s, err := ContentOf("buttons", hctx.Map{}, cf) 39 | r.Error(err) 40 | r.Empty(s) 41 | } 42 | -------------------------------------------------------------------------------- /helpers/content/of.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "errors" 5 | "html/template" 6 | 7 | "github.com/gobuffalo/plush/v5/helpers/hctx" 8 | ) 9 | 10 | // ContentOf retrieves a stored block for templating and renders it. 11 | // You can pass an optional map of fields that will be set. 12 | /* 13 | <%= contentOf("buttons") %> 14 | <%= contentOf("buttons", {"label": "Click me"}) %> 15 | */ 16 | func ContentOf(name string, data hctx.Map, help hctx.HelperContext) (template.HTML, error) { 17 | fn, ok := help.Value("contentFor:" + name).(func(data hctx.Map) (template.HTML, error)) 18 | if !ok { 19 | if !help.HasBlock() { 20 | return template.HTML(""), errors.New("missing contentOf block: " + name) 21 | } 22 | 23 | hc := help.New() 24 | for k, v := range data { 25 | hc.Set(k, v) 26 | } 27 | body, err := help.BlockWith(hc) 28 | if err != nil { 29 | return template.HTML(""), err 30 | } 31 | 32 | return template.HTML(body), nil 33 | } 34 | return fn(data) 35 | } 36 | -------------------------------------------------------------------------------- /helpers/content/of_test.go: -------------------------------------------------------------------------------- 1 | package content 2 | 3 | import ( 4 | "html/template" 5 | "testing" 6 | 7 | "github.com/gobuffalo/plush/v5/helpers/hctx" 8 | "github.com/gobuffalo/plush/v5/helpers/helptest" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_ContentOf_MissingBlock(t *testing.T) { 13 | r := require.New(t) 14 | 15 | cf := helptest.NewContext() 16 | s, err := ContentOf("buttons", hctx.Map{}, cf) 17 | r.Error(err) 18 | r.Empty(s) 19 | } 20 | 21 | func Test_ContentOf_MissingBlock_DefaultBlock(t *testing.T) { 22 | r := require.New(t) 23 | 24 | cf := helptest.NewContext() 25 | cf.BlockContextFn = func(hctx.Context) (string, error) { 26 | return "default", nil 27 | } 28 | 29 | s, err := ContentOf("buttons", hctx.Map{}, cf) 30 | r.NoError(err) 31 | r.Equal(s, template.HTML("default")) 32 | } 33 | 34 | func Test_ContentOf(t *testing.T) { 35 | r := require.New(t) 36 | 37 | cf := helptest.NewContext() 38 | cf.BlockContextFn = func(hctx.Context) (string, error) { 39 | return "default", nil 40 | } 41 | 42 | name := "testing" 43 | cf.Set("contentFor:"+name, func(data hctx.Map) (template.HTML, error) { 44 | return template.HTML("body"), nil 45 | }) 46 | 47 | s, err := ContentOf(name, hctx.Map{}, cf) 48 | r.NoError(err) 49 | r.Equal(s, template.HTML("body")) 50 | } 51 | -------------------------------------------------------------------------------- /helpers/debug/debug.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | 7 | "github.com/gobuffalo/plush/v5/helpers/hctx" 8 | ) 9 | 10 | // Keys to be used in templates for the functions in this package. 11 | const ( 12 | DebugKey = "debug" 13 | InspectKey = "inspect" 14 | ) 15 | 16 | // New returns a map of the helpers within this package. 17 | func New() hctx.Map { 18 | return hctx.Map{ 19 | DebugKey: Debug, 20 | InspectKey: Inspect, 21 | } 22 | } 23 | 24 | // Debug by verbosely printing out using 'pre' tags. 25 | func Debug(v interface{}) template.HTML { 26 | return template.HTML(fmt.Sprintf("
%s", Inspect(v))) 27 | } 28 | -------------------------------------------------------------------------------- /helpers/debug/debug_test.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Debug(t *testing.T) { 12 | table := []struct { 13 | in interface{} 14 | out string 15 | }{ 16 | {"foo", "foo"}, 17 | } 18 | 19 | for _, tt := range table { 20 | t.Run(fmt.Sprint(tt.in), func(st *testing.T) { 21 | r := require.New(st) 22 | out := fmt.Sprintf("
%s", tt.out) 23 | r.Equal(template.HTML(out), Debug(tt.in)) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /helpers/debug/inspect.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Inspect the interface using the `%+v` formatter 8 | func Inspect(v interface{}) string { 9 | return fmt.Sprintf("%+v", v) 10 | } 11 | -------------------------------------------------------------------------------- /helpers/debug/inspect_test.go: -------------------------------------------------------------------------------- 1 | package debug 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_Inspect(t *testing.T) { 10 | r := require.New(t) 11 | s := struct { 12 | Name string 13 | }{"Ringo"} 14 | 15 | o := Inspect(s) 16 | r.Contains(o, "Ringo") 17 | } 18 | -------------------------------------------------------------------------------- /helpers/encoders/encoders.go: -------------------------------------------------------------------------------- 1 | package encoders 2 | 3 | import "github.com/gobuffalo/plush/v5/helpers/hctx" 4 | 5 | // Keys to be used in templates for the functions in this package. 6 | const ( 7 | ToJSONKey = "toJSON" 8 | RawKey = "raw" 9 | ) 10 | 11 | // New returns a map of the helpers within this package. 12 | func New() hctx.Map { 13 | return hctx.Map{ 14 | "json": ToJSON, 15 | RawKey: Raw, 16 | ToJSONKey: ToJSON, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /helpers/encoders/json.go: -------------------------------------------------------------------------------- 1 | package encoders 2 | 3 | import ( 4 | "encoding/json" 5 | "html/template" 6 | ) 7 | 8 | // ToJSON marshals the interface{} and returns it 9 | // as template.HTML 10 | func ToJSON(v interface{}) (template.HTML, error) { 11 | b, err := json.Marshal(v) 12 | if err != nil { 13 | return "", err 14 | } 15 | return template.HTML(b), nil 16 | } 17 | -------------------------------------------------------------------------------- /helpers/encoders/json_test.go: -------------------------------------------------------------------------------- 1 | package encoders 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_ToJSON(t *testing.T) { 12 | x := struct { 13 | A string 14 | B int 15 | C bool 16 | }{"A", 42, true} 17 | 18 | f := func() {} 19 | 20 | table := []struct { 21 | in interface{} 22 | out string 23 | err bool 24 | }{ 25 | {"foo", `"foo"`, false}, 26 | {[]string{"foo", "bar"}, `["foo","bar"]`, false}, 27 | {x, `{"A":"A","B":42,"C":true}`, false}, 28 | {nil, "null", false}, 29 | {f, "", true}, 30 | } 31 | 32 | for _, tt := range table { 33 | t.Run(fmt.Sprint(tt.in), func(st *testing.T) { 34 | r := require.New(st) 35 | h, err := ToJSON(tt.in) 36 | if tt.err { 37 | r.Error(err) 38 | return 39 | } 40 | r.NoError(err) 41 | r.Equal(template.HTML(tt.out), h) 42 | }) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /helpers/encoders/raw.go: -------------------------------------------------------------------------------- 1 | package encoders 2 | 3 | import "html/template" 4 | 5 | // Raw converts a `string` to a `template.HTML` 6 | func Raw(s string) template.HTML { 7 | return template.HTML(s) 8 | } 9 | -------------------------------------------------------------------------------- /helpers/encoders/raw_test.go: -------------------------------------------------------------------------------- 1 | package encoders 2 | 3 | import ( 4 | "html/template" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_Raw(t *testing.T) { 11 | r := require.New(t) 12 | r.Equal(template.HTML("A"), Raw("A")) 13 | } 14 | -------------------------------------------------------------------------------- /helpers/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/gobuffalo/plush/v5/helpers/hctx" 8 | ) 9 | 10 | // Keys to be used in templates for the functions in this package. 11 | const ( 12 | EnvKey = "env" 13 | EnvOrKey = "envOr" 14 | ) 15 | 16 | // New returns a map of the helpers within this package. 17 | func New() hctx.Map { 18 | return hctx.Map{ 19 | EnvKey: Env, 20 | EnvOrKey: EnvOr, 21 | } 22 | } 23 | 24 | // Env will return the specified environment variable, 25 | // or an error if it can not be found 26 | // 27 | // <%= env("GOPATH") %> 28 | func Env(key string) (string, error) { 29 | s := os.Getenv(key) 30 | if len(s) == 0 { 31 | return "", fmt.Errorf("could not find ENV %q", key) 32 | } 33 | return s, nil 34 | } 35 | 36 | // Env will return the specified environment variable, 37 | // or the second argument, if not found 38 | // 39 | // <%= envOr("GOPATH", "~/go") %> 40 | func EnvOr(key string, def string) string { 41 | s := os.Getenv(key) 42 | if len(s) == 0 { 43 | return def 44 | } 45 | return s 46 | } 47 | -------------------------------------------------------------------------------- /helpers/env/env_test.go: -------------------------------------------------------------------------------- 1 | package env 2 | -------------------------------------------------------------------------------- /helpers/escapes/escapes.go: -------------------------------------------------------------------------------- 1 | package escapes 2 | 3 | import "github.com/gobuffalo/plush/v5/helpers/hctx" 4 | 5 | // Keys to be used in templates for the functions in this package. 6 | const ( 7 | JSEscapeKey = "jsEscape" 8 | HTMLEscapeKey = "htmlEscape" 9 | ) 10 | 11 | // New returns a map of the helpers within this package. 12 | func New() hctx.Map { 13 | return hctx.Map{ 14 | JSEscapeKey: JSEscape, 15 | HTMLEscapeKey: HTMLEscape, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /helpers/escapes/html.go: -------------------------------------------------------------------------------- 1 | package escapes 2 | 3 | import ( 4 | "html/template" 5 | 6 | "github.com/gobuffalo/plush/v5/helpers/hctx" 7 | ) 8 | 9 | // HTMLEscape will escape a string for HTML 10 | func HTMLEscape(s string, help hctx.HelperContext) (string, error) { 11 | var err error 12 | if help.HasBlock() { 13 | s, err = help.Block() 14 | } 15 | if err != nil { 16 | return "", err 17 | } 18 | return template.HTMLEscapeString(s), nil 19 | } 20 | -------------------------------------------------------------------------------- /helpers/escapes/html_test.go: -------------------------------------------------------------------------------- 1 | package escapes 2 | 3 | import ( 4 | "errors" 5 | "html/template" 6 | "testing" 7 | 8 | "github.com/gobuffalo/plush/v5/helpers/helptest" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_HTMLEscape(t *testing.T) { 13 | r := require.New(t) 14 | 15 | in := `foo` 16 | hc := helptest.NewContext() 17 | s, err := HTMLEscape(in, hc) 18 | r.NoError(err) 19 | r.Equal(template.HTMLEscapeString(in), s) 20 | } 21 | 22 | func Test_HTMLEscape_Block(t *testing.T) { 23 | r := require.New(t) 24 | 25 | in := `foo` 26 | hc := helptest.NewContext() 27 | hc.BlockFn = func() (string, error) { 28 | return in, nil 29 | } 30 | s, err := HTMLEscape("", hc) 31 | r.NoError(err) 32 | r.Equal(template.HTMLEscapeString(in), s) 33 | 34 | hc2 := helptest.NewContext() 35 | hc2.BlockFn = func() (string, error) { 36 | return "", errors.New("nope") 37 | } 38 | s2, err2 := HTMLEscape("", hc2) 39 | r.Error(err2) 40 | r.Empty(s2) 41 | } 42 | -------------------------------------------------------------------------------- /helpers/escapes/js.go: -------------------------------------------------------------------------------- 1 | package escapes 2 | 3 | import "html/template" 4 | 5 | // JSEscape will escape a string for Javascript 6 | var JSEscape = template.JSEscapeString 7 | -------------------------------------------------------------------------------- /helpers/hctx/context.go: -------------------------------------------------------------------------------- 1 | package hctx 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type Context interface { 8 | context.Context 9 | New() Context 10 | Has(key string) bool 11 | Update(key string, value interface{}) bool 12 | Set(key string, value interface{}) 13 | } 14 | 15 | type HelperContext interface { 16 | Context 17 | Block() (string, error) 18 | BlockWith(Context) (string, error) 19 | HasBlock() bool 20 | Render(s string) (string, error) 21 | } 22 | -------------------------------------------------------------------------------- /helpers/hctx/map.go: -------------------------------------------------------------------------------- 1 | package hctx 2 | 3 | // Map is a standard map[string]interface{} 4 | // for use throughout the helper packages. 5 | type Map map[string]interface{} 6 | 7 | // Merge creates a single Map from any 8 | // number of Maps. Latter key/value pairs 9 | // will overwrite earlier pairs. 10 | func Merge(maps ...Map) Map { 11 | mx := map[string]interface{}{} 12 | for _, m := range maps { 13 | for k, v := range m { 14 | mx[k] = v 15 | } 16 | } 17 | return mx 18 | } 19 | -------------------------------------------------------------------------------- /helpers/hctx/map_test.go: -------------------------------------------------------------------------------- 1 | package hctx 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestMerge(t *testing.T) { 9 | map1 := Map{ 10 | "Test": 1, 11 | "Take": 2, 12 | } 13 | map2 := Map{ 14 | "Testing": '1', 15 | "Taking": "2", 16 | } 17 | mapM := Map{ 18 | "Test": 1, 19 | "Take": 2, 20 | "Testing": '1', 21 | "Taking": "2", 22 | } 23 | tests := []struct { 24 | name string 25 | maps []Map 26 | want Map 27 | }{ 28 | {"good single", []Map{map1}, map1}, 29 | {"good together", []Map{map1, map2}, mapM}, 30 | } 31 | for _, tt := range tests { 32 | t.Run(tt.name, func(t *testing.T) { 33 | if got := Merge(tt.maps...); !reflect.DeepEqual(got, tt.want) { 34 | t.Errorf("Merge() = %v, want %v", got, tt.want) 35 | } 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /helpers/helpers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "github.com/gobuffalo/plush/v5/helpers/content" 5 | "github.com/gobuffalo/plush/v5/helpers/debug" 6 | "github.com/gobuffalo/plush/v5/helpers/encoders" 7 | "github.com/gobuffalo/plush/v5/helpers/env" 8 | "github.com/gobuffalo/plush/v5/helpers/escapes" 9 | "github.com/gobuffalo/plush/v5/helpers/hctx" 10 | "github.com/gobuffalo/plush/v5/helpers/inflections" 11 | "github.com/gobuffalo/plush/v5/helpers/iterators" 12 | "github.com/gobuffalo/plush/v5/helpers/meta" 13 | "github.com/gobuffalo/plush/v5/helpers/paths" 14 | "github.com/gobuffalo/plush/v5/helpers/text" 15 | ) 16 | 17 | var Content = content.New() 18 | var Debug = debug.New() 19 | var Encoders = encoders.New() 20 | var Env = env.New() 21 | var Escapes = escapes.New() 22 | var Inflections = inflections.New() 23 | var Iterators = iterators.New() 24 | var Meta = meta.New() 25 | var Paths = paths.New() 26 | var Text = text.New() 27 | 28 | var Base = hctx.Merge( 29 | Content, 30 | Debug, 31 | Encoders, 32 | Env, 33 | Escapes, 34 | Inflections, 35 | Iterators, 36 | Meta, 37 | Paths, 38 | Text, 39 | ) 40 | -------------------------------------------------------------------------------- /helpers/helptest/context.go: -------------------------------------------------------------------------------- 1 | package helptest 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | 8 | "github.com/gobuffalo/plush/v5/helpers/hctx" 9 | ) 10 | 11 | var _ hctx.HelperContext = NewContext() 12 | 13 | func NewContext() *HelperContext { 14 | return &HelperContext{ 15 | Context: context.Background(), 16 | } 17 | } 18 | 19 | type HelperContext struct { 20 | context.Context 21 | data sync.Map 22 | BlockFn func() (string, error) 23 | BlockContextFn func(hctx.Context) (string, error) 24 | RenderFn func(string) (string, error) 25 | } 26 | 27 | func (f HelperContext) New() hctx.Context { 28 | fhc := NewContext() 29 | fhc.Context = f.Context 30 | f.data.Range(func(k, v interface{}) bool { 31 | fhc.data.Store(k, v) 32 | return true 33 | }) 34 | fhc.BlockFn = f.BlockFn 35 | fhc.BlockContextFn = f.BlockContextFn 36 | fhc.RenderFn = f.RenderFn 37 | return fhc 38 | } 39 | 40 | func (f HelperContext) Data() sync.Map { 41 | var m sync.Map 42 | f.data.Range(func(k, v interface{}) bool { 43 | m.Store(k, v) 44 | return true 45 | }) 46 | 47 | return m 48 | } 49 | 50 | func (f HelperContext) Value(key interface{}) interface{} { 51 | v, ok := f.data.Load(key) 52 | if ok { 53 | return v 54 | } 55 | return f.Context.Value(key) 56 | } 57 | func (f *HelperContext) Update(key string, value interface{}) (returnData bool) { 58 | return 59 | } 60 | func (f *HelperContext) Set(key string, value interface{}) { 61 | f.data.Store(key, value) 62 | } 63 | 64 | func (f HelperContext) Block() (string, error) { 65 | if f.BlockFn == nil { 66 | return "", errors.New("no block given") 67 | } 68 | return f.BlockFn() 69 | } 70 | 71 | func (f HelperContext) BlockWith(c hctx.Context) (string, error) { 72 | if f.BlockContextFn == nil { 73 | return "", errors.New("no block given") 74 | } 75 | return f.BlockContextFn(c) 76 | } 77 | 78 | func (f HelperContext) HasBlock() bool { 79 | return f.BlockFn != nil || f.BlockContextFn != nil 80 | } 81 | 82 | func (f HelperContext) Render(s string) (string, error) { 83 | if f.RenderFn == nil { 84 | return "", errors.New("render is not available") 85 | } 86 | return f.RenderFn(s) 87 | } 88 | 89 | // Has checks the existence of the key in the context. 90 | func (c HelperContext) Has(key string) bool { 91 | return c.Value(key) != nil 92 | } 93 | -------------------------------------------------------------------------------- /helpers/inflections/inflections.go: -------------------------------------------------------------------------------- 1 | package inflections 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gobuffalo/flect" 7 | "github.com/gobuffalo/plush/v5/helpers/hctx" 8 | ) 9 | 10 | // Keys to be used in templates for the functions in this package. 11 | const ( 12 | CamelizeKey = "camelize" 13 | CapitalizeKey = "capitalize" 14 | DasherizeKey = "dasherize" 15 | OrdinalizeKey = "ordinalize" 16 | PluralizeKey = "pluralize" 17 | SingularizeKey = "singularize" 18 | UnderscoreKey = "underscore" 19 | UpcaseKey = "upcase" 20 | DowncaseKey = "downcase" 21 | ) 22 | 23 | // New returns a map of the helpers within this package. 24 | func New() hctx.Map { 25 | return hctx.Map{ 26 | CamelizeKey: Camelize, 27 | "camelize_down_first": Camelize, // Deprecated 28 | CapitalizeKey: Capitalize, 29 | DasherizeKey: Dasherize, 30 | OrdinalizeKey: Ordinalize, 31 | PluralizeKey: Pluralize, 32 | SingularizeKey: Singularize, 33 | UnderscoreKey: Underscore, 34 | DowncaseKey: Downcase, 35 | UpcaseKey: Upcase, 36 | 37 | // "asciffy": Asciify, 38 | // "humanize": Humanize, 39 | // "parameterize": Parameterize, 40 | // "pluralize_with_size": PluralizeWithSize, 41 | // "tableize": Tableize, 42 | // "typeify": Typeify, 43 | } 44 | } 45 | 46 | var Upcase = strings.ToUpper 47 | var Downcase = strings.ToLower 48 | var Camelize = flect.Camelize 49 | var Pascalize = flect.Pascalize 50 | var Capitalize = flect.Capitalize 51 | var Dasherize = flect.Dasherize 52 | var Ordinalize = flect.Ordinalize 53 | var Pluralize = flect.Pluralize 54 | var Singularize = flect.Singularize 55 | var Underscore = flect.Underscore 56 | -------------------------------------------------------------------------------- /helpers/iterators/between.go: -------------------------------------------------------------------------------- 1 | package iterators 2 | 3 | // Between will iterate up to, but not including `b` 4 | // Between(0,10) // 0,1,2,3,4,5,6,7,8,9 5 | func Between(a, b int) Iterator { 6 | return &ranger{pos: a, end: b - 1} 7 | } 8 | -------------------------------------------------------------------------------- /helpers/iterators/group_by.go: -------------------------------------------------------------------------------- 1 | package iterators 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | // GroupBy creates an iterator of groups or sub-slices of the underlying 10 | // Array or Slice entered where each group is of length 11 | // Len(underlying) / size. If Len(underlying) == size it will 12 | // return an iterator with only a single group. 13 | func GroupBy(size int, underlying interface{}) (Iterator, error) { 14 | if size <= 0 { 15 | return nil, errors.New("size must be greater than zero") 16 | } 17 | u := reflect.Indirect(reflect.ValueOf(underlying)) 18 | 19 | group := []reflect.Value{} 20 | switch u.Kind() { 21 | case reflect.Array, reflect.Slice: 22 | if u.Len() == size { 23 | return &groupBy{ 24 | group: []reflect.Value{u}, 25 | }, nil 26 | } 27 | 28 | groupSize := u.Len() / size 29 | if u.Len()%size != 0 { 30 | groupSize++ 31 | } 32 | 33 | pos := 0 34 | for pos < u.Len() { 35 | e := pos + groupSize 36 | if e > u.Len() { 37 | e = u.Len() 38 | } 39 | group = append(group, u.Slice(pos, e)) 40 | pos += groupSize 41 | } 42 | default: 43 | return nil, fmt.Errorf("can not use %T in groupBy", underlying) 44 | } 45 | g := &groupBy{ 46 | group: group, 47 | } 48 | return g, nil 49 | } 50 | 51 | type groupBy struct { 52 | pos int 53 | group []reflect.Value 54 | } 55 | 56 | // Next returns the next group from the GroupBy 57 | func (g *groupBy) Next() interface{} { 58 | if g.pos >= len(g.group) { 59 | return nil 60 | } 61 | v := g.group[g.pos] 62 | g.pos++ 63 | return v.Interface() 64 | } 65 | -------------------------------------------------------------------------------- /helpers/iterators/group_by_test.go: -------------------------------------------------------------------------------- 1 | package iterators 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func Test_GroupBy(t *testing.T) { 10 | r := require.New(t) 11 | g, err := GroupBy(2, []string{"a", "b", "c", "d", "e"}) 12 | r.NoError(err) 13 | g1 := g.Next() 14 | r.Equal([]string{"a", "b", "c"}, g1) 15 | g2 := g.Next() 16 | r.Equal([]string{"d", "e"}, g2) 17 | r.Nil(g.Next()) 18 | } 19 | 20 | func Test_GroupBy_Exact(t *testing.T) { 21 | r := require.New(t) 22 | g, err := GroupBy(2, []string{"a", "b"}) 23 | r.NoError(err) 24 | g1 := g.Next() 25 | r.Equal([]string{"a", "b"}, g1) 26 | r.Nil(g.Next()) 27 | } 28 | 29 | func Test_GroupBy_Pointer(t *testing.T) { 30 | r := require.New(t) 31 | g, err := GroupBy(2, &[]string{"a", "b", "c", "d", "e"}) 32 | r.NoError(err) 33 | g1 := g.Next() 34 | r.Equal([]string{"a", "b", "c"}, g1) 35 | g2 := g.Next() 36 | r.Equal([]string{"d", "e"}, g2) 37 | r.Nil(g.Next()) 38 | } 39 | 40 | func Test_GroupBy_SmallGroup(t *testing.T) { 41 | r := require.New(t) 42 | g, err := GroupBy(1, []string{"a", "b", "c", "d", "e"}) 43 | r.NoError(err) 44 | g1 := g.Next() 45 | r.Equal([]string{"a", "b", "c", "d", "e"}, g1) 46 | r.Nil(g.Next()) 47 | } 48 | 49 | func Test_GroupBy_NonGroupable(t *testing.T) { 50 | r := require.New(t) 51 | _, err := GroupBy(1, 1) 52 | r.Error(err) 53 | } 54 | 55 | func Test_GroupBy_ZeroSize(t *testing.T) { 56 | r := require.New(t) 57 | _, err := GroupBy(0, []string{"a"}) 58 | r.Error(err) 59 | } 60 | -------------------------------------------------------------------------------- /helpers/iterators/iterator.go: -------------------------------------------------------------------------------- 1 | package iterators 2 | 3 | // Iterator type can be implemented and used by the `for` command to build loops in templates 4 | type Iterator interface { 5 | Next() interface{} 6 | } 7 | -------------------------------------------------------------------------------- /helpers/iterators/iterators.go: -------------------------------------------------------------------------------- 1 | package iterators 2 | 3 | import "github.com/gobuffalo/plush/v5/helpers/hctx" 4 | 5 | // Keys to be used in templates for the functions in this package. 6 | const ( 7 | RangeKey = "range" 8 | BetweenKey = "between" 9 | UntilKey = "until" 10 | GroupByKey = "groupBy" 11 | ) 12 | 13 | // New returns a map of the helpers within this package. 14 | func New() hctx.Map { 15 | return hctx.Map{ 16 | RangeKey: Range, 17 | BetweenKey: Between, 18 | UntilKey: Until, 19 | GroupByKey: GroupBy, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /helpers/iterators/range.go: -------------------------------------------------------------------------------- 1 | package iterators 2 | 3 | // Range creates an Iterator that will 4 | // iterate numbers from a to b, including b. 5 | func Range(a, b int) Iterator { 6 | return &ranger{pos: a - 1, end: b} 7 | } 8 | 9 | type ranger struct { 10 | pos int 11 | end int 12 | } 13 | 14 | // Next returns the next number in the Range or nil 15 | func (r *ranger) Next() interface{} { 16 | if r.pos < r.end { 17 | r.pos++ 18 | return r.pos 19 | } 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /helpers/iterators/until.go: -------------------------------------------------------------------------------- 1 | package iterators 2 | 3 | // Until will iterate up to, but not including `a` 4 | // Until(3) // 0,1,2 5 | func Until(a int) Iterator { 6 | return &ranger{pos: -1, end: a - 1} 7 | } 8 | -------------------------------------------------------------------------------- /helpers/map.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import "sync" 4 | 5 | // HelperMap holds onto helpers and validates they are properly formed. 6 | type HelperMap struct { 7 | helpers map[string]interface{} 8 | moot *sync.Mutex 9 | } 10 | 11 | // NewHelperMap containing all of the "default" helpers from "plush.Helpers". 12 | func NewMap(helpers map[string]interface{}) HelperMap { 13 | hm := HelperMap{ 14 | helpers: helpers, 15 | moot: &sync.Mutex{}, 16 | } 17 | 18 | return hm 19 | } 20 | 21 | // Add a new helper to the map. New Helpers will be validated to ensure they 22 | // meet the requirements for a helper: 23 | func (h *HelperMap) Add(key string, helper interface{}) error { 24 | h.moot.Lock() 25 | defer h.moot.Unlock() 26 | 27 | if h.helpers == nil { 28 | h.helpers = map[string]interface{}{} 29 | } 30 | 31 | h.helpers[key] = helper 32 | 33 | return nil 34 | } 35 | 36 | // AddMany helpers at the same time. 37 | func (h *HelperMap) AddMany(helpers map[string]interface{}) error { 38 | for k, v := range helpers { 39 | err := h.Add(k, v) 40 | if err != nil { 41 | return err 42 | } 43 | } 44 | 45 | return nil 46 | } 47 | 48 | // Helpers returns the underlying list of helpers from the map 49 | func (h HelperMap) Helpers() map[string]interface{} { 50 | return h.helpers 51 | } 52 | 53 | func (h HelperMap) All() map[string]interface{} { 54 | return h.helpers 55 | } 56 | -------------------------------------------------------------------------------- /helpers/meta/len.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // Len returns the length of v 8 | func Len(v interface{}) int { 9 | if v == nil { 10 | return 0 11 | } 12 | rv := reflect.ValueOf(v) 13 | if rv.Kind() == reflect.Ptr { 14 | rv = rv.Elem() 15 | } 16 | return rv.Len() 17 | } 18 | -------------------------------------------------------------------------------- /helpers/meta/len_test.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_Len(t *testing.T) { 11 | table := []struct { 12 | in interface{} 13 | out int 14 | }{ 15 | {"foo", 3}, 16 | {[]string{"a", "b"}, 2}, 17 | {nil, 0}, 18 | {map[string]int{"a": 1, "b": 2, "c": 3, "d": 4}, 4}, 19 | } 20 | 21 | for _, tt := range table { 22 | t.Run(fmt.Sprint(tt.in), func(st *testing.T) { 23 | r := require.New(st) 24 | r.Equal(Len(tt.in), tt.out) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /helpers/meta/meta.go: -------------------------------------------------------------------------------- 1 | package meta 2 | 3 | import ( 4 | "github.com/gobuffalo/plush/v5/helpers/hctx" 5 | ) 6 | 7 | // Keys to be used in templates for the functions in this package. 8 | const ( 9 | LenKey = "len" 10 | ) 11 | 12 | // New returns a map of the helpers within this package. 13 | func New() hctx.Map { 14 | return hctx.Map{ 15 | LenKey: Len, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /helpers/paths/path_for.go: -------------------------------------------------------------------------------- 1 | package paths 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "html/template" 7 | "net/url" 8 | "path" 9 | "reflect" 10 | "strings" 11 | 12 | "github.com/gobuffalo/flect/name" 13 | ) 14 | 15 | type Pathable interface { 16 | ToPath() string 17 | } 18 | 19 | type Paramable interface { 20 | ToParam() string 21 | } 22 | 23 | // PathFor takes an `interface{}`, or a `slice` of them, 24 | // and tries to convert it to a `/foos/{id}` style URL path. 25 | // Rules: 26 | // * if `string` it is returned as is 27 | // * if `Pathable` the `ToPath` method is returned 28 | // * if `slice` or an `array` each element is run through the helper then joined 29 | // * if `struct` the name of the struct, pluralized is used for the name 30 | // * if `Paramable` the `ToParam` method is used to fill the `{id}` slot 31 | // * if `struct.Slug` the slug is used to fill the `{id}` slot of the URL 32 | // * if `struct.ID` the ID is used to fill the `{id}` slot of the URL 33 | func PathFor(in interface{}) (string, error) { 34 | if in == nil { 35 | return "", errors.New("can not calculate path to nil") 36 | } 37 | 38 | switch s := in.(type) { 39 | case string: 40 | return join(s), nil 41 | case template.HTML: 42 | return join(string(s)), nil 43 | case Pathable: 44 | return join(s.ToPath()), nil 45 | } 46 | 47 | ni, err := name.Interface(in) 48 | if err != nil { 49 | return "", err 50 | } 51 | 52 | rv := reflect.Indirect(reflect.ValueOf(in)) 53 | 54 | to := rv.Type() 55 | k := to.Kind() 56 | switch k { 57 | case reflect.Struct: 58 | f := rv.FieldByName("Slug") 59 | if f.IsValid() { 60 | return byField(ni, f) 61 | } 62 | f = rv.FieldByName("ID") 63 | if f.IsValid() { 64 | return byField(ni, f) 65 | } 66 | case reflect.Slice, reflect.Array: 67 | var paths []string 68 | for i := 0; i < rv.Len(); i++ { 69 | xrv := rv.Index(i) 70 | s, err := PathFor(xrv.Interface()) 71 | if err != nil { 72 | return "", err 73 | } 74 | paths = append(paths, s) 75 | } 76 | return join(paths...), nil 77 | } 78 | 79 | if s, ok := in.(Paramable); ok { 80 | return join(ni.URL().String(), s.ToParam()), nil 81 | } 82 | 83 | return "", fmt.Errorf("could not convert %T to path", in) 84 | } 85 | 86 | func byField(ni name.Ident, f reflect.Value) (string, error) { 87 | ii := f.Interface() 88 | if ii == nil { 89 | return "", nil 90 | } 91 | 92 | zero := reflect.DeepEqual(ii, reflect.Zero(reflect.TypeOf(ii)).Interface()) 93 | if zero { 94 | return join(ni.URL().String()), nil 95 | } 96 | return join(ni.URL().String(), fmt.Sprint(ii)), nil 97 | } 98 | 99 | func join(s ...string) string { 100 | //In case is a full valid url it will return the same url without modification 101 | if len(s) == 1 { 102 | if _, err := url.ParseRequestURI(s[0]); err == nil { 103 | return s[0] 104 | } 105 | } 106 | 107 | p := path.Join(s...) 108 | if !strings.HasPrefix(p, "/") { 109 | p = "/" + p 110 | } 111 | 112 | return p 113 | } 114 | -------------------------------------------------------------------------------- /helpers/paths/path_for_test.go: -------------------------------------------------------------------------------- 1 | package paths 2 | 3 | import ( 4 | "html/template" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | type Car struct { 11 | ID int 12 | } 13 | 14 | type boat struct { 15 | Slug string 16 | } 17 | 18 | type plane struct{} 19 | 20 | func (plane) ToParam() string { 21 | return "aeroplane" 22 | } 23 | 24 | type truck struct{} 25 | 26 | func (truck) ToPath() string { 27 | return "/a/truck" 28 | } 29 | 30 | type BadCar struct { 31 | IDs int 32 | } 33 | 34 | func Test_PathFor(t *testing.T) { 35 | table := []struct { 36 | in interface{} 37 | out string 38 | err bool 39 | }{ 40 | {Car{1}, "/cars/1", false}, 41 | {Car{}, "/cars", false}, 42 | {&Car{}, "/cars", false}, 43 | {boat{"titanic"}, "/boats/titanic", false}, 44 | {plane{}, "/planes/aeroplane", false}, 45 | {truck{}, "/a/truck", false}, 46 | {[]interface{}{truck{}, plane{}}, "/a/truck/planes/aeroplane", false}, 47 | {"foo", "/foo", false}, 48 | {template.HTML("foo"), "/foo", false}, 49 | {map[int]int{}, "", true}, 50 | {nil, "", true}, 51 | {[]interface{}{truck{}, nil}, "", true}, 52 | {BadCar{}, "", true}, 53 | {"https://www.google.com", "https://www.google.com", false}, 54 | } 55 | 56 | for _, tt := range table { 57 | t.Run(tt.out, func(st *testing.T) { 58 | r := require.New(st) 59 | s, err := PathFor(tt.in) 60 | if tt.err { 61 | r.Error(err) 62 | return 63 | } 64 | r.NoError(err) 65 | r.Equal(tt.out, s) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /helpers/paths/paths.go: -------------------------------------------------------------------------------- 1 | package paths 2 | 3 | import "github.com/gobuffalo/plush/v5/helpers/hctx" 4 | 5 | // Keys to be used in templates for the functions in this package. 6 | const ( 7 | PathForKey = "pathFor" 8 | ) 9 | 10 | // New returns a map of the helpers within this package. 11 | func New() hctx.Map { 12 | return hctx.Map{ 13 | PathForKey: PathFor, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /helpers/text/text.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import "github.com/gobuffalo/plush/v5/helpers/hctx" 4 | 5 | // Keys to be used in templates for the functions in this package. 6 | const ( 7 | TruncateKey = "truncate" 8 | ) 9 | 10 | // New returns a map of the helpers within this package. 11 | func New() hctx.Map { 12 | return hctx.Map{ 13 | TruncateKey: Truncate, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /helpers/text/truncate.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import "github.com/gobuffalo/plush/v5/helpers/hctx" 4 | 5 | // Truncate will try to return a string that is no longer 6 | // than `size`, which defaults to 50. If given 7 | // a `trail` option the returned string will have 8 | // that appended at the end, while still trying to make 9 | // sure that the returned string is no longer than 10 | // `size` characters long. However, if `trail` is longer 11 | // than or equal to `size`, `trail` will be returned 12 | // completely as is. Defaults to a `trail` of `...`. 13 | func Truncate(s string, opts hctx.Map) string { 14 | if opts["size"] == nil { 15 | opts["size"] = 50 16 | } 17 | if opts["trail"] == nil { 18 | opts["trail"] = "..." 19 | } 20 | runesS := []rune(s) 21 | size := opts["size"].(int) 22 | if len(runesS) <= size { 23 | return s 24 | } 25 | trail := opts["trail"].(string) 26 | runesTrail := []rune(trail) 27 | if len(runesTrail) >= size { 28 | return trail 29 | } 30 | return string(runesS[:size-len(runesTrail)]) + trail 31 | } 32 | -------------------------------------------------------------------------------- /helpers/text/truncate_test.go: -------------------------------------------------------------------------------- 1 | package text 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/plush/v5/helpers/hctx" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_Truncate(t *testing.T) { 11 | var runesS []rune 12 | var runesX []rune 13 | 14 | r := require.New(t) 15 | x := "世界uFHyyImKUMhSkSolLqgqevKQNZUjpSZokrGbZqnUrUnWrTDwi" 16 | s := Truncate(x, hctx.Map{}) 17 | runesS = []rune(s) 18 | r.Equal(len(runesS), 50) 19 | r.Equal("...", string(runesS[47:])) 20 | 21 | s = Truncate(x, hctx.Map{ 22 | "size": 10, 23 | }) 24 | runesS = []rune(s) 25 | r.Equal(len(runesS), 10) 26 | r.Equal("...", string(runesS[7:])) 27 | 28 | s = Truncate(x, hctx.Map{ 29 | "size": 10, 30 | "trail": "世界re", 31 | }) 32 | runesS = []rune(s) 33 | r.Equal(len(runesS), 10) 34 | r.Equal("世界re", string(runesS[6:])) 35 | 36 | // Case size < len(trail) 37 | s = Truncate(x, hctx.Map{ 38 | "size": 3, 39 | "trail": "世界re", 40 | }) 41 | runesS = []rune(s) 42 | r.Equal(len(runesS), 4) 43 | r.Equal("世界re", s) 44 | 45 | // Case size >= len(string) 46 | s = Truncate(x, hctx.Map{ 47 | "size": len(x), 48 | }) 49 | runesS = []rune(s) 50 | runesX = []rune(x) 51 | r.Equal(len(runesS), len(runesX)) 52 | r.Equal(string(runesX[48:]), string(runesS[48:])) 53 | } 54 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/gobuffalo/plush/v5" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Helpers_WithoutData(t *testing.T) { 12 | type data map[string]interface{} 13 | r := require.New(t) 14 | 15 | table := []struct { 16 | I string 17 | E string 18 | Err error 19 | }{ 20 | {I: `<%= foo() {return bar + name} %>`, E: "BARunknown"}, 21 | {I: `<%= foo({name: "mark"}) {return bar + name} %>`, E: "BARmark"}, 22 | {I: `<%= foo({name: "mark", bbb: "hello-world"}) {return bar + name} %><%= bbb %>`, Err: errors.New(`line 1: "bbb": unknown identifier`)}, 23 | } 24 | 25 | for _, tt := range table { 26 | ctx := plush.NewContext() 27 | ctx.Set("name", "unknown") 28 | ctx.Set("bar", "BAR") 29 | ctx.Set("foo", func(d data, help plush.HelperContext) (string, error) { 30 | c := help.New() 31 | if n, ok := d["name"]; ok { 32 | c.Set("name", n) 33 | } 34 | if n, ok := d["bbb"]; ok { 35 | c.Set("bbb", n) 36 | } 37 | return help.BlockWith(c) 38 | }) 39 | s, err := plush.Render(tt.I, ctx) 40 | if tt.Err == nil { 41 | r.NoError(err) 42 | r.Equal(tt.E, s) 43 | } else { 44 | r.Error(err) 45 | r.EqualError(err, tt.Err.Error()) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /if_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/plush/v5" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_If_Condition(t *testing.T) { 11 | tests := []struct { 12 | input string 13 | expected string 14 | name string 15 | success bool 16 | }{ 17 | {`<%= if (true) { return "good"} else { return "bad"} %>`, "good", "if_else_true", true}, 18 | {`<%= if (false) { return "good"} else { return "bad"} %>`, "bad", "if_else_false", true}, 19 | {`<% if (true) { return "good"} else { return "bad"} %>`, "", "missing=", true}, 20 | {`<%= if (true) { %>good<% } %>`, "good", "value_from_template_html", true}, 21 | {`<%= if (false) { return "good"} %>`, "", "if_false", true}, 22 | {`<%= if (!false) { return "good"} %>`, "good", "if_bang_false", true}, 23 | {`<%= if (true == true) { return "good"} else { return "bad"} %>`, "good", "bool_true_is_true", true}, 24 | {`<%= if (true != true) { return "good"} else { return "bad"} %>`, "bad", "bool_true_is_not_true", true}, 25 | {`<% let test = true %><%= if (test == false) { return "good"} %>`, "", "let_var_is_false", true}, 26 | {`<% let test = true %><%= if (test == true) { return "good"} %>`, "good", "let_var_is_true", true}, 27 | {`<% let test = true %><%= if (test != true) { return "good"} %>`, "", "let_var_is_not_true", true}, 28 | {`<% let test = 1 %><%= if (test != true) { return "good"} %>`, "line 1: unable to operate (!=) on int and bool", "let_var_is_not_bool", false}, 29 | {`<% let test = 1 %><%= if (test == 1) { return "good"} %>`, "good", "let_var_is_1", true}, 30 | {`<%= if (false && true) { %>good<% } %>`, "", "logical_false_and_true", true}, 31 | {`<%= if (2 == 2 && 1 == 1) { %>good<% } %>`, "good", "logical_true_and_true", true}, 32 | {`<%= if (false || true) { %>good<% } %>`, "good", "logical_false_or_true", true}, 33 | {`<%= if (1 == 2 || 2 == 1) { %>good<% } %>`, "", "logical_false_or_false", true}, 34 | {`<%= if (names && len(names) >= 1) { %>good<% } %>`, "", "nil_and", true}, 35 | {`<%= if (names && len(names) >= 1) { %>good<% } else { %>else<% } %>`, "else", "nil_and_else", true}, 36 | {`<%= if (names) { %>good<% } %>`, "", "nil", true}, 37 | {`<%= if (!names) { %>good<% } %>`, "good", "not_nil", true}, 38 | {`<%= if (1 == 2) { %>good<% } %>`, "", "compare_equal_to", true}, 39 | {`<%= if (1 != 2) { %>good<% } %>`, "good", "compare_not_equal_to", true}, 40 | {`<%= if (1 < 2) { %>good<% } %>`, "good", "compare_less_than", true}, 41 | {`<%= if (1 <= 2) { %>good<% } %>`, "good", "compare_less_than_equal_to", true}, 42 | {`<%= if (1 > 2) { %>good<% } %>`, "", "compare_greater_than", true}, 43 | {`<%= if (1 >= 2) { %>good<% } %>`, "", "compare_greater_than_equal_to", true}, 44 | {`<%= if ("foo" ~= "bar") { %>good<% } %>`, "", "if_match_foo_bar", true}, 45 | {`<%= if ("foo" ~= "^fo") { %>good<% } %>`, "good", "if_match_foo_^fo", true}, 46 | } 47 | 48 | for _, tc := range tests { 49 | t.Run(tc.name, func(t *testing.T) { 50 | r := require.New(t) 51 | s, err := plush.Render(tc.input, plush.NewContext()) 52 | if tc.success { 53 | r.NoError(err) 54 | r.Equal(tc.expected, s) 55 | } else { 56 | r.Error(err, tc.expected) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func Test_Condition_Only(t *testing.T) { 63 | ctx_empty := plush.NewContext() 64 | 65 | ctx_with_paths := plush.NewContext() 66 | ctx_with_paths.Set("paths", "cart") 67 | 68 | tests := []struct { 69 | input string 70 | expected string 71 | name string 72 | success bool 73 | context *plush.Context 74 | }{ 75 | {`<%= paths == nil %>`, "true", "unknown_equal_to_nil", true, ctx_empty}, 76 | {`<%= nil == paths %>`, "true", "nil_equal_to_unknown", true, ctx_empty}, 77 | 78 | {`<%= !paths %>`, "false", "NOT SET", true, ctx_with_paths}, 79 | {`<%= !pages %>`, "true", "NOT UNKNOWN", true, ctx_with_paths}, 80 | 81 | {`<%= paths || pages %>`, "true", "SET_or_unknown", true, ctx_with_paths}, // fast return 82 | {`<%= pages || paths %>`, "true", "unknown_or_SET", true, ctx_with_paths}, 83 | {`<%= paths || pages == "cart" %>`, "true", "SET_or_unknown_equal", true, ctx_with_paths}, // fast return 84 | {`<%= pages == "cart" || paths %>`, "true", "unknown_equal_or_SET", true, ctx_with_paths}, 85 | {`<%= paths == "cart" || pages %>`, "true", "EQUAL_or_unknown", true, ctx_with_paths}, // fast return 86 | {`<%= pages || paths == "cart" %>`, "true", "unknown_or_EQUAL", true, ctx_with_paths}, 87 | {`<%= paths == "cart" || pages == "cart" %>`, "true", "EQUAL_or_unknown_equal", true, ctx_with_paths}, // fast return 88 | {`<%= pages == "cart" || paths == "cart" %>`, "true", "unknown_equal_or_EQUAL", true, ctx_with_paths}, 89 | 90 | {`<%= paths && pages %>`, "false", "set_and_UNKNOWN==false", true, ctx_with_paths}, 91 | {`<%= pages && paths %>`, "false", "UNKNOWN_and_set==false", true, ctx_with_paths}, // fast return 92 | {`<%= paths && pages == "cart" %>`, "false", "set_and_UNKNOWN_equal==false", true, ctx_with_paths}, 93 | {`<%= pages == "cart" && paths %>`, "false", "UNKNOWN_equal_and_set==false", true, ctx_with_paths}, // fast return 94 | {`<%= paths == "cart" && pages %>`, "false", "equal_and_UNKNOWN", true, ctx_with_paths}, 95 | {`<%= pages && paths == "cart" %>`, "false", "UNKNOWN_and_equal", true, ctx_with_paths}, // fast return 96 | {`<%= paths == "cart" && pages == "cart" %>`, "false", "equal_and_UNKNOWN_equal", true, ctx_with_paths}, 97 | {`<%= pages == "cart" && paths == "cart" %>`, "false", "UNKNOWN_equal_and_equal", true, ctx_with_paths}, // fast return 98 | } 99 | 100 | for _, tc := range tests { 101 | t.Run(tc.name, func(t *testing.T) { 102 | r := require.New(t) 103 | s, err := plush.Render(tc.input, tc.context) 104 | if tc.success { 105 | r.NoError(err) 106 | r.Equal(tc.expected, s) 107 | } else { 108 | r.Error(err, tc.expected) 109 | } 110 | }) 111 | } 112 | } 113 | 114 | func Test_If_Else_If_Else_True(t *testing.T) { 115 | r := require.New(t) 116 | ctx := plush.NewContext() 117 | input := `
<%= if (state == "foo") { %>hi foo<% } else if (state == "bar") { %>hi bar<% } else if (state == "fizz") { %>hi fizz<% } else { %>hi buzz<% } %>
` 118 | 119 | ctx.Set("state", "foo") 120 | s, err := plush.Render(input, ctx) 121 | r.NoError(err) 122 | r.Equal("hi foo
", s) 123 | 124 | ctx.Set("state", "bar") 125 | s, err = plush.Render(input, ctx) 126 | r.NoError(err) 127 | r.Equal("hi bar
", s) 128 | 129 | ctx.Set("state", "fizz") 130 | s, err = plush.Render(input, ctx) 131 | r.NoError(err) 132 | r.Equal("hi fizz
", s) 133 | 134 | ctx.Set("state", "buzz") 135 | s, err = plush.Render(input, ctx) 136 | r.NoError(err) 137 | r.Equal("hi buzz
", s) 138 | } 139 | 140 | func Test_If_String_Truthy(t *testing.T) { 141 | r := require.New(t) 142 | 143 | ctx := plush.NewContext() 144 | ctx.Set("username", "") 145 | 146 | input := `<%= if (username && username != "") { return "hi" } else { return "bye" } %>
` 147 | s, err := plush.Render(input, ctx) 148 | r.NoError(err) 149 | r.Equal("bye
", s) 150 | 151 | ctx.Set("username", "foo") 152 | s, err = plush.Render(input, ctx) 153 | r.NoError(err) 154 | r.Equal("hi
", s) 155 | } 156 | 157 | func Test_If_Updating_Variable_Inside_IF_Scope(t *testing.T) { 158 | r := require.New(t) 159 | 160 | ctx := plush.NewContext() 161 | ctx.Set("username", "") 162 | 163 | input := `<%= if (username && username != "") { username = "hi" } else { username= "bye" } %><%= username %>
` 164 | s, err := plush.Render(input, ctx) 165 | r.NoError(err) 166 | r.Equal("bye
", s) 167 | 168 | ctx.Set("username", "foo") 169 | s, err = plush.Render(input, ctx) 170 | r.NoError(err) 171 | r.Equal("hi
", s) 172 | } 173 | 174 | func Test_If_UserName_Is_Declared(t *testing.T) { 175 | r := require.New(t) 176 | 177 | ctx := plush.NewContext() 178 | 179 | input := `<%= if (username && username != "") { username = "hi" } else { username= "bye" } %><%= username %>
` 180 | s, err := plush.Render(input, ctx) 181 | r.Error(err) 182 | r.EqualError(err, `line 1: "username": unknown identifier`) 183 | r.Empty(s) 184 | } 185 | 186 | func Test_If_BlockScope_Declare(t *testing.T) { 187 | r := require.New(t) 188 | 189 | ctx := plush.NewContext() 190 | 191 | input := `<% let username = "Hello World" %><%= if (username && username != "") { 192 | let username = "hi" 193 | username = "hi" 194 | } else { 195 | let username = "bye" 196 | username = "1" 197 | }%><%= username %>
` 198 | s, err := plush.Render(input, ctx) 199 | r.NoError(err) 200 | r.Equal("Hello World
", s) 201 | } 202 | 203 | func Test_If_BlockScope_Nested_Declare(t *testing.T) { 204 | r := require.New(t) 205 | 206 | ctx := plush.NewContext() 207 | 208 | input := `<% let username = "Hello World" %><%= if (username && username != "") { 209 | let username = "hi" 210 | username = "hi" 211 | if (username == "hi"){ 212 | username = "hi2" 213 | } 214 | } else { 215 | let username = "bye" 216 | username = "1" 217 | }%><%= username %>
` 218 | s, err := plush.Render(input, ctx) 219 | r.NoError(err) 220 | r.Equal("Hello World
", s) 221 | } 222 | 223 | func Test_If_BlockScope_Nested_Overwrite(t *testing.T) { 224 | r := require.New(t) 225 | 226 | ctx := plush.NewContext() 227 | 228 | input := `<% let username = "Hello World" %><%= if (username && username != "") { 229 | username = "hi" 230 | if (username == "hi"){ 231 | username = "hi2" 232 | } 233 | } else { 234 | let username = "bye" 235 | username = "1" 236 | }%><%= username %>
` 237 | s, err := plush.Render(input, ctx) 238 | r.NoError(err) 239 | r.Equal("hi2
", s) 240 | } 241 | 242 | func Test_If_Variable_Not_Set_But_Or_Condition_Is_True_Complex(t *testing.T) { 243 | r := require.New(t) 244 | ctx := plush.NewContext() 245 | ctx.Set("path", "cart") 246 | ctx.Set("paths", "cart") 247 | input := `<%= if ( paths == "cart" || (page && page.PageTitle != "cafe") || paths == "cart") { %>hi<%} %>` 248 | 249 | s, err := plush.Render(input, ctx) 250 | r.NoError(err) 251 | r.Equal("hi", s) 252 | } 253 | 254 | func Test_If_Syntax_Error_On_Last_Node(t *testing.T) { 255 | r := require.New(t) 256 | ctx := plush.NewContext() 257 | ctx.Set("paths", "cart") 258 | input := `<%= if ( paths == "cart" || pages ^^^ ) { %>hi<%} %>` 259 | 260 | _, err := plush.Render(input, ctx) 261 | r.Error(err) 262 | } 263 | 264 | func Test_If_Syntax_Error_On_First_Node(t *testing.T) { 265 | r := require.New(t) 266 | ctx := plush.NewContext() 267 | ctx.Set("paths", "cart") 268 | input := `<%= if ( paths @#@# "cart" || pages) { %>hi<%} %>` 269 | 270 | _, err := plush.Render(input, ctx) 271 | r.Error(err) 272 | } 273 | -------------------------------------------------------------------------------- /intern.go: -------------------------------------------------------------------------------- 1 | package plush 2 | 3 | type InternTable struct { 4 | stringToID map[string]int 5 | idToString []string 6 | } 7 | 8 | func NewInternTable() *InternTable { 9 | return &InternTable{ 10 | stringToID: make(map[string]int), 11 | idToString: []string{}, 12 | } 13 | } 14 | 15 | func (it *InternTable) Intern(name string) int { 16 | if id, ok := it.stringToID[name]; ok { 17 | return id 18 | } 19 | id := len(it.idToString) 20 | it.stringToID[name] = id 21 | it.idToString = append(it.idToString, name) 22 | return id 23 | } 24 | 25 | func (it *InternTable) Lookup(name string) (int, bool) { 26 | id, ok := it.stringToID[name] 27 | return id, ok 28 | } 29 | 30 | func (it *InternTable) SymbolName(id int) string { 31 | if id < len(it.idToString) { 32 | return it.idToString[id] 33 | } 34 | return "\<%= 1 %>
` 78 | tests := []struct { 79 | tokenType token.Type 80 | tokenLiteral string 81 | }{ 82 | {token.HTML, `<%= 1 %>
`}, 83 | } 84 | 85 | l := lexer.New(input) 86 | for _, tt := range tests { 87 | tok := l.NextToken() 88 | r.Equal(tt.tokenType, tok.Type) 89 | r.Equal(tt.tokenLiteral, tok.Literal) 90 | } 91 | } 92 | 93 | func Test_Escaping_EscapeExpression(t *testing.T) { 94 | r := require.New(t) 95 | input := `C:\\<%= "temp" %>` 96 | l := lexer.New(input) 97 | 98 | tests := []struct { 99 | tokenType token.Type 100 | tokenLiteral string 101 | }{ 102 | {token.HTML, `C:\`}, 103 | {token.E_START, "<%="}, 104 | {token.STRING, `temp`}, 105 | {token.E_END, "%>"}, 106 | } 107 | 108 | for _, tt := range tests { 109 | tok := l.NextToken() 110 | r.Equal(tt.tokenType, tok.Type) 111 | r.Equal(tt.tokenLiteral, tok.Literal) 112 | } 113 | } 114 | 115 | func Test_NextToken_WithHTML(t *testing.T) { 116 | r := require.New(t) 117 | input := `<%= 1 %>
` 118 | tests := []struct { 119 | tokenType token.Type 120 | tokenLiteral string 121 | }{ 122 | {token.HTML, ``}, 123 | {token.E_START, "<%="}, 124 | {token.INT, "1"}, 125 | {token.E_END, "%>"}, 126 | {token.HTML, `
`}, 127 | } 128 | 129 | l := lexer.New(input) 130 | for _, tt := range tests { 131 | tok := l.NextToken() 132 | r.Equal(tt.tokenType, tok.Type) 133 | r.Equal(tt.tokenLiteral, tok.Literal) 134 | } 135 | } 136 | func Test_NextToken_Complete(t *testing.T) { 137 | r := require.New(t) 138 | input := `<% break 139 | continue 140 | let five = 5; 141 | let ten = 10; 142 | 143 | let add = fn(x, y) { 144 | x + y; 145 | }; 146 | 147 | let result = add(five, ten); 148 | !-/*5; 149 | 5 < 10 > 5; 150 | .23 151 | 23.2343 152 | 23.23.23 153 | 154 | if (5 < 10) { 155 | return true; 156 | } else { 157 | return false; 158 | } 159 | 160 | 10 == 10; 161 | 10 != 9; 162 | "foobar" 163 | "foo bar" 164 | [1, 2]; 165 | {"foo": "bar"} 166 | let fl = 1.23 %> 167 | <%= 1 %> 168 | <%# 2 %> 169 | <% 3 %> 170 | <% for (i, v) in myArray { 171 | } 172 | a && b 173 | c || d 174 | for (x) in range(1,3){return x} 175 | myvar1 176 | my-helper() 177 | %> 178 | ` 179 | 180 | tests := []struct { 181 | expectedType token.Type 182 | expectedLiteral string 183 | }{ 184 | {token.S_START, "<%"}, 185 | {token.BREAK, "break"}, 186 | {token.CONTINUE, "continue"}, 187 | {token.LET, "let"}, 188 | {token.IDENT, "five"}, 189 | {token.ASSIGN, "="}, 190 | {token.INT, "5"}, 191 | {token.SEMICOLON, ";"}, 192 | {token.LET, "let"}, 193 | {token.IDENT, "ten"}, 194 | {token.ASSIGN, "="}, 195 | {token.INT, "10"}, 196 | {token.SEMICOLON, ";"}, 197 | {token.LET, "let"}, 198 | {token.IDENT, "add"}, 199 | {token.ASSIGN, "="}, 200 | {token.FUNCTION, "fn"}, 201 | {token.LPAREN, "("}, 202 | {token.IDENT, "x"}, 203 | {token.COMMA, ","}, 204 | {token.IDENT, "y"}, 205 | {token.RPAREN, ")"}, 206 | {token.LBRACE, "{"}, 207 | {token.IDENT, "x"}, 208 | {token.PLUS, "+"}, 209 | {token.IDENT, "y"}, 210 | {token.SEMICOLON, ";"}, 211 | {token.RBRACE, "}"}, 212 | {token.SEMICOLON, ";"}, 213 | {token.LET, "let"}, 214 | {token.IDENT, "result"}, 215 | {token.ASSIGN, "="}, 216 | {token.IDENT, "add"}, 217 | {token.LPAREN, "("}, 218 | {token.IDENT, "five"}, 219 | {token.COMMA, ","}, 220 | {token.IDENT, "ten"}, 221 | {token.RPAREN, ")"}, 222 | {token.SEMICOLON, ";"}, 223 | {token.BANG, "!"}, 224 | {token.MINUS, "-"}, 225 | {token.SLASH, "/"}, 226 | {token.ASTERISK, "*"}, 227 | {token.INT, "5"}, 228 | {token.SEMICOLON, ";"}, 229 | {token.INT, "5"}, 230 | {token.LT, "<"}, 231 | {token.INT, "10"}, 232 | {token.GT, ">"}, 233 | {token.INT, "5"}, 234 | {token.SEMICOLON, ";"}, 235 | {token.FLOAT, ".23"}, 236 | {token.FLOAT, "23.2343"}, 237 | {token.ILLEGAL, "23.23.23"}, 238 | {token.IF, "if"}, 239 | {token.LPAREN, "("}, 240 | {token.INT, "5"}, 241 | {token.LT, "<"}, 242 | {token.INT, "10"}, 243 | {token.RPAREN, ")"}, 244 | {token.LBRACE, "{"}, 245 | {token.RETURN, "return"}, 246 | {token.TRUE, "true"}, 247 | {token.SEMICOLON, ";"}, 248 | {token.RBRACE, "}"}, 249 | {token.ELSE, "else"}, 250 | {token.LBRACE, "{"}, 251 | {token.RETURN, "return"}, 252 | {token.FALSE, "false"}, 253 | {token.SEMICOLON, ";"}, 254 | {token.RBRACE, "}"}, 255 | {token.INT, "10"}, 256 | {token.EQ, "=="}, 257 | {token.INT, "10"}, 258 | {token.SEMICOLON, ";"}, 259 | {token.INT, "10"}, 260 | {token.NOT_EQ, "!="}, 261 | {token.INT, "9"}, 262 | {token.SEMICOLON, ";"}, 263 | {token.STRING, "foobar"}, 264 | {token.STRING, "foo bar"}, 265 | {token.LBRACKET, "["}, 266 | {token.INT, "1"}, 267 | {token.COMMA, ","}, 268 | {token.INT, "2"}, 269 | {token.RBRACKET, "]"}, 270 | {token.SEMICOLON, ";"}, 271 | {token.LBRACE, "{"}, 272 | {token.STRING, "foo"}, 273 | {token.COLON, ":"}, 274 | {token.STRING, "bar"}, 275 | {token.RBRACE, "}"}, 276 | {token.LET, "let"}, 277 | {token.IDENT, "fl"}, 278 | {token.ASSIGN, "="}, 279 | {token.FLOAT, "1.23"}, 280 | {token.E_END, "%>"}, 281 | {token.HTML, "\n"}, 282 | {token.E_START, "<%="}, 283 | {token.INT, "1"}, 284 | {token.E_END, "%>"}, 285 | {token.HTML, "\n"}, 286 | {token.C_START, "<%#"}, 287 | {token.INT, "2"}, 288 | {token.E_END, "%>"}, 289 | {token.HTML, "\n"}, 290 | {token.S_START, "<%"}, 291 | {token.INT, "3"}, 292 | {token.E_END, "%>"}, 293 | {token.HTML, "\n"}, 294 | {token.S_START, "<%"}, 295 | {token.FOR, "for"}, 296 | {token.LPAREN, "("}, 297 | {token.IDENT, "i"}, 298 | {token.COMMA, ","}, 299 | {token.IDENT, "v"}, 300 | {token.RPAREN, ")"}, 301 | {token.IN, "in"}, 302 | {token.IDENT, "myArray"}, 303 | {token.LBRACE, "{"}, 304 | {token.RBRACE, "}"}, 305 | {token.IDENT, "a"}, 306 | {token.AND, "&&"}, 307 | {token.IDENT, "b"}, 308 | {token.IDENT, "c"}, 309 | {token.OR, "||"}, 310 | {token.IDENT, "d"}, 311 | {token.FOR, "for"}, 312 | {token.LPAREN, "("}, 313 | {token.IDENT, "x"}, 314 | {token.RPAREN, ")"}, 315 | {token.IN, "in"}, 316 | {token.IDENT, "range"}, 317 | {token.LPAREN, "("}, 318 | {token.INT, "1"}, 319 | {token.COMMA, ","}, 320 | {token.INT, "3"}, 321 | {token.RPAREN, ")"}, 322 | {token.LBRACE, "{"}, 323 | {token.RETURN, "return"}, 324 | {token.IDENT, "x"}, 325 | {token.RBRACE, "}"}, 326 | {token.IDENT, "myvar1"}, 327 | {token.IDENT, "my-helper"}, 328 | {token.LPAREN, "("}, 329 | {token.RPAREN, ")"}, 330 | {token.E_END, "%>"}, 331 | {token.HTML, "\n"}, 332 | {token.EOF, ""}, 333 | } 334 | 335 | l := lexer.New(input) 336 | 337 | for _, tt := range tests { 338 | tok := l.NextToken() 339 | 340 | r.Equal(tt.expectedLiteral, tok.Literal) 341 | r.Equal(tt.expectedType, tok.Type) 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /line_number_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/plush/v5" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_LineNumberErrors(t *testing.T) { 11 | r := require.New(t) 12 | input := `13 | <%= f.Foo %> 14 |
` 15 | 16 | _, err := plush.Render(input, plush.NewContext()) 17 | r.Error(err) 18 | r.Contains(err.Error(), "line 2:") 19 | } 20 | 21 | func Test_LineNumberErrors_ForLoop(t *testing.T) { 22 | r := require.New(t) 23 | input := ` 24 | <%= for (n) in numbers.Foo { %> 25 | <%= n %> 26 | <% } %> 27 | ` 28 | 29 | _, err := plush.Render(input, plush.NewContext()) 30 | r.Error(err) 31 | r.Contains(err.Error(), "line 2:") 32 | } 33 | 34 | func Test_LineNumberErrors_ForLoop2(t *testing.T) { 35 | r := require.New(t) 36 | input := ` 37 | <%= for (n in numbers.Foo { %> 38 | <%= if (n == 3) { %> 39 | <%= n %> 40 | <% } %> 41 | <% } %> 42 | ` 43 | 44 | _, err := plush.Parse(input) 45 | r.Error(err) 46 | r.Contains(err.Error(), "line 2:") 47 | } 48 | 49 | func Test_LineNumberErrors_InsideForLoop(t *testing.T) { 50 | r := require.New(t) 51 | input := ` 52 | <%= for (n) in numbers { %> 53 | <%= n.Foo %> 54 | <% } %> 55 | ` 56 | ctx := plush.NewContext() 57 | ctx.Set("numbers", []int{1, 2}) 58 | _, err := plush.Render(input, ctx) 59 | r.Error(err) 60 | r.Contains(err.Error(), "line 3:") 61 | } 62 | 63 | func Test_LineNumberErrors_MissingKeyword(t *testing.T) { 64 | r := require.New(t) 65 | input := ` 66 | 67 | 68 | 69 | 70 | <%= (n) in numbers { %> 71 | <%= n %> 72 | <% } %> 73 | ` 74 | ctx := plush.NewContext() 75 | ctx.Set("numbers", []int{1, 2}) 76 | _, err := plush.Render(input, ctx) 77 | r.Error(err) 78 | r.Contains(err.Error(), "line 6:") 79 | } 80 | -------------------------------------------------------------------------------- /math_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gobuffalo/plush/v5" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Render_Int_Math_Division_By_Zero(t *testing.T) { 12 | r := require.New(t) 13 | input := `<%= 10 / 0 %>` 14 | s, err := plush.Render(input, plush.NewContext()) 15 | r.Error(err) 16 | r.Empty(s) 17 | r.Contains(err.Error(), "division by zero 10 / 0") 18 | } 19 | 20 | func Test_Render_Int_Float_Division_By_Zero(t *testing.T) { 21 | r := require.New(t) 22 | input := `<%= 10.5 / 0.0 %>` 23 | s, err := plush.Render(input, plush.NewContext()) 24 | r.Error(err) 25 | r.Empty(s) 26 | r.Contains(err.Error(), "division by zero 10.5 / 0") 27 | } 28 | 29 | func Test_Render_Int_Math(t *testing.T) { 30 | r := require.New(t) 31 | 32 | tests := []struct { 33 | a int 34 | b int 35 | op string 36 | res string 37 | }{ 38 | {1, 3, "+", "4"}, 39 | {3, 1, "-", "2"}, 40 | {10, 2, "/", "5"}, 41 | {10, 2, "*", "20"}, 42 | {10, 2, ">", "true"}, 43 | {10, 2, ">=", "true"}, 44 | {10, 10, ">=", "true"}, 45 | {2, 2, "<=", "true"}, 46 | {10, 2, "<", "false"}, 47 | {10, 2, "<=", "false"}, 48 | {2, 2, "==", "true"}, 49 | {1, 2, "!=", "true"}, 50 | } 51 | for _, tt := range tests { 52 | input := fmt.Sprintf("<%%= %d %s %d %%>", tt.a, tt.op, tt.b) 53 | s, err := plush.Render(input, plush.NewContext()) 54 | r.NoError(err) 55 | r.Equal(tt.res, s) 56 | } 57 | } 58 | 59 | func Test_Render_Float_Math(t *testing.T) { 60 | r := require.New(t) 61 | 62 | tests := []struct { 63 | a float64 64 | b float64 65 | op string 66 | res string 67 | }{ 68 | {1, 3, "+", "4"}, 69 | {3, 1, "-", "2"}, 70 | {10, 2, "/", "5"}, 71 | {10, 2, "*", "20"}, 72 | {10, 2, ">", "true"}, 73 | {10, 2, ">=", "true"}, 74 | {10, 10, ">=", "true"}, 75 | {2, 2, "<=", "true"}, 76 | {10, 2, "<", "false"}, 77 | {10, 2, "<=", "false"}, 78 | {2, 2, "==", "true"}, 79 | {1, 2, "!=", "true"}, 80 | } 81 | for _, tt := range tests { 82 | input := fmt.Sprintf("<%%= %f %s %f %%>", tt.a, tt.op, tt.b) 83 | s, err := plush.Render(input, plush.NewContext()) 84 | r.NoError(err) 85 | r.Equal(tt.res, s) 86 | } 87 | } 88 | 89 | func Test_Render_String_Math(t *testing.T) { 90 | r := require.New(t) 91 | 92 | tests := []struct { 93 | a string 94 | b string 95 | op string 96 | res string 97 | }{ 98 | {"a", "b", "+", "ab"}, 99 | {"a", "b", "!=", "true"}, 100 | {"a", "a", "==", "true"}, 101 | {"a", "b", "==", "false"}, 102 | {"a", "b", ">", "false"}, 103 | {"a", "b", ">=", "false"}, 104 | {"a", "b", "<=", "true"}, 105 | } 106 | 107 | for _, tt := range tests { 108 | input := fmt.Sprintf("<%%= %q %s %q %%>", tt.a, tt.op, tt.b) 109 | s, err := plush.Render(input, plush.NewContext()) 110 | r.NoError(err) 111 | r.Equal(tt.res, s) 112 | } 113 | } 114 | 115 | func Test_Render_Operator_UndefinedVar(t *testing.T) { 116 | tests := []struct { 117 | operator string 118 | result interface{} 119 | errorExpected bool 120 | }{ 121 | {"+", "", true}, 122 | {"-", "", true}, 123 | {"/", "", true}, 124 | {"*", "", true}, 125 | {">", "", true}, 126 | {">=", "", true}, 127 | {"<=", "", true}, 128 | {"<", "", true}, 129 | {"==", "false", false}, 130 | {"!=", "true", false}, 131 | } 132 | for _, tc := range tests { 133 | t.Run(tc.operator, func(t *testing.T) { 134 | r := require.New(t) 135 | input := fmt.Sprintf("<%%= undefined %s 3 %%>", tc.operator) 136 | s, err := plush.Render(input, plush.NewContext()) 137 | if tc.errorExpected { 138 | r.Error(err, "undefined %s 3 --> '%v'", tc.operator, tc.result) 139 | } else { 140 | r.NoError(err, "undefined %s 3 --> '%v'", tc.operator, tc.result) 141 | } 142 | r.Equal(tc.result, s, "undefined %s 3", tc.operator) 143 | 144 | input = fmt.Sprintf("<%%= 3 %s unknown %%>", tc.operator) 145 | s, err = plush.Render(input, plush.NewContext()) 146 | if tc.errorExpected { 147 | r.Error(err, "3 %s undefined --> '%v'", tc.operator, tc.result) 148 | } else { 149 | r.NoError(err, "3 %s undefined --> '%v'", tc.operator, tc.result) 150 | } 151 | r.Equal(tc.result, s, "undefined %s 3", tc.operator) 152 | }) 153 | } 154 | } 155 | 156 | func Test_Render_String_Concat_Multiple(t *testing.T) { 157 | r := require.New(t) 158 | 159 | input := `<%= "a" + "b" + "c" %>` 160 | s, err := plush.Render(input, plush.NewContext()) 161 | r.NoError(err) 162 | r.Equal("abc", s) 163 | } 164 | 165 | func Test_Render_String_Int_Concat(t *testing.T) { 166 | r := require.New(t) 167 | 168 | input := `<%= "a" + 1 %>` 169 | s, err := plush.Render(input, plush.NewContext()) 170 | r.NoError(err) 171 | r.Equal("a1", s) 172 | } 173 | 174 | func Test_Render_Bool_Concat(t *testing.T) { 175 | r := require.New(t) 176 | 177 | input := `<%= true + 1 %>` 178 | s, err := plush.Render(input, plush.NewContext()) 179 | r.Equal("true", s) 180 | r.NoError(err) 181 | } 182 | -------------------------------------------------------------------------------- /numbers_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/plush/v5" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | // support identifiers containing digits, but not starting with a digits 11 | func Test_Identifiers_With_Digits(t *testing.T) { 12 | r := require.New(t) 13 | input := `<%= my123greet %> <%= name3 %>` 14 | 15 | ctx := plush.NewContext() 16 | ctx.Set("my123greet", "hi") 17 | ctx.Set("name3", "mark") 18 | 19 | s, err := plush.Render(input, ctx) 20 | r.NoError(err) 21 | r.Equal("hi mark", s) 22 | } 23 | 24 | func Test_Render_Var_ends_in_Number(t *testing.T) { 25 | r := require.New(t) 26 | ctx := plush.NewContextWith(map[string]interface{}{ 27 | "myvar1": []string{"john", "paul"}, 28 | }) 29 | s, err := plush.Render(`<%= for (n) in myvar1 {return n}`, ctx) 30 | r.NoError(err) 31 | r.Equal("johnpaul", s) 32 | } 33 | 34 | func Test_Render_AllowsManyNumericTypes(t *testing.T) { 35 | r := require.New(t) 36 | input := `<%= i32 %> <%= u32 %> <%= i8 %>` 37 | 38 | ctx := plush.NewContext() 39 | ctx.Set("i32", int32(1)) 40 | ctx.Set("u32", uint32(2)) 41 | ctx.Set("i8", int8(3)) 42 | 43 | s, err := plush.Render(input, ctx) 44 | r.NoError(err) 45 | r.Equal("1 2 3", s) 46 | } 47 | -------------------------------------------------------------------------------- /objects.go: -------------------------------------------------------------------------------- 1 | package plush 2 | 3 | type exitBlockStatment interface { 4 | exitBlock() 5 | } 6 | 7 | type returnObject struct { 8 | exitBlockStatment 9 | Value []interface{} 10 | } 11 | 12 | type continueObject struct { 13 | exitBlockStatment 14 | Value []interface{} 15 | } 16 | 17 | type breakObject struct { 18 | exitBlockStatment 19 | Value []interface{} 20 | } 21 | -------------------------------------------------------------------------------- /parser/errors.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "strings" 4 | 5 | type errSlice []string 6 | 7 | func (e errSlice) Error() string { 8 | return strings.Join(e, "\n") 9 | } 10 | -------------------------------------------------------------------------------- /parser/precedences.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import "github.com/gobuffalo/plush/v5/token" 4 | 5 | const ( 6 | _ int = iota 7 | LOWEST // 8 | ANDOR // || or && 9 | EQUALS // == 10 | LESSGREATER // > or < 11 | SUM // + 12 | PRODUCT // * 13 | PREFIX // -X or !X 14 | CALL // myFunction(X) 15 | INDEX // array[index] 16 | ) 17 | 18 | var precedences = map[token.Type]int{ 19 | token.EQ: EQUALS, 20 | token.NOT_EQ: EQUALS, 21 | token.MATCHES: EQUALS, 22 | token.AND: ANDOR, 23 | token.OR: ANDOR, 24 | token.LT: LESSGREATER, 25 | token.LTEQ: LESSGREATER, 26 | token.GT: LESSGREATER, 27 | token.GTEQ: LESSGREATER, 28 | token.PLUS: SUM, 29 | token.MINUS: SUM, 30 | token.SLASH: PRODUCT, 31 | token.ASTERISK: PRODUCT, 32 | token.LPAREN: CALL, 33 | token.LBRACKET: INDEX, 34 | } 35 | -------------------------------------------------------------------------------- /partial_helper.go: -------------------------------------------------------------------------------- 1 | package plush 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | // PartialFeeder is callback function should implemented on application side. 11 | type PartialFeeder func(string) (string, error) 12 | 13 | func PartialHelper(name string, data map[string]interface{}, help HelperContext) (template.HTML, error) { 14 | if help.Context == nil { 15 | return "", fmt.Errorf("invalid context. abort") 16 | } 17 | 18 | help.Context = help.New() 19 | for k, v := range data { 20 | help.Set(k, v) 21 | } 22 | 23 | pf, ok := help.Value("partialFeeder").(func(string) (string, error)) 24 | if !ok { 25 | return "", fmt.Errorf("could not found partial feeder from helpers") 26 | } 27 | 28 | var part string 29 | var err error 30 | if part, err = pf(name); err != nil { 31 | return "", err 32 | } 33 | 34 | if part, err = Render(part, help.Context); err != nil { 35 | return "", err 36 | } 37 | 38 | if ct, ok := help.Value("contentType").(string); ok { 39 | ext := filepath.Ext(name) 40 | if strings.Contains(ct, "javascript") && ext != ".js" && ext != "" { 41 | part = template.JSEscapeString(string(part)) 42 | } 43 | } 44 | 45 | if layout, ok := data["layout"].(string); ok { 46 | return PartialHelper( 47 | layout, 48 | map[string]interface{}{"yield": template.HTML(part)}, 49 | help) 50 | } 51 | 52 | return template.HTML(part), err 53 | } 54 | -------------------------------------------------------------------------------- /partial_helper_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gobuffalo/plush/v5" 8 | "github.com/gobuffalo/plush/v5/helpers/hctx" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_PartialHelper_Nil_Context(t *testing.T) { 13 | r := require.New(t) 14 | 15 | name := "index" 16 | data := map[string]interface{}{} 17 | help := plush.HelperContext{} 18 | 19 | html, err := plush.PartialHelper(name, data, help) 20 | r.Error(err) 21 | r.Contains(err.Error(), "invalid context") 22 | r.Equal("", string(html)) 23 | } 24 | 25 | func Test_PartialHelper_Blank_Context(t *testing.T) { 26 | r := require.New(t) 27 | 28 | name := "index" 29 | data := map[string]interface{}{} 30 | help := plush.HelperContext{Context: plush.NewContext()} 31 | 32 | html, err := plush.PartialHelper(name, data, help) 33 | r.Error(err) 34 | r.Contains(err.Error(), "could not found") 35 | r.Equal("", string(html)) 36 | } 37 | 38 | func Test_PartialHelper_Invalid_Feeder(t *testing.T) { 39 | r := require.New(t) 40 | 41 | name := "index" 42 | data := map[string]interface{}{} 43 | help := plush.HelperContext{Context: plush.NewContext()} 44 | help.Set("partialFeeder", "me-rong") 45 | 46 | html, err := plush.PartialHelper(name, data, help) 47 | r.Error(err) 48 | r.Contains(err.Error(), "could not found") 49 | r.Equal("", string(html)) 50 | } 51 | 52 | func Test_PartialHelper_Invalid_FeederFunction(t *testing.T) { 53 | r := require.New(t) 54 | 55 | name := "index" 56 | data := map[string]interface{}{} 57 | help := plush.HelperContext{Context: plush.NewContext()} 58 | help.Set("partialFeeder", func(string) string { 59 | return "me-rong" 60 | }) 61 | 62 | html, err := plush.PartialHelper(name, data, help) 63 | r.Error(err) 64 | r.Contains(err.Error(), "could not found") 65 | r.Equal("", string(html)) 66 | } 67 | 68 | func Test_PartialHelper_Feeder_Error(t *testing.T) { 69 | r := require.New(t) 70 | 71 | name := "index" 72 | data := map[string]interface{}{} 73 | help := plush.HelperContext{Context: plush.NewContext()} 74 | help.Set("partialFeeder", func(string) (string, error) { 75 | return "", fmt.Errorf("me-rong") 76 | }) 77 | 78 | _, err := plush.PartialHelper(name, data, help) 79 | r.Error(err) 80 | r.Contains(err.Error(), "me-rong") 81 | } 82 | 83 | func Test_PartialHelper_Good(t *testing.T) { 84 | r := require.New(t) 85 | 86 | name := "index" 87 | data := map[string]interface{}{} 88 | help := plush.HelperContext{Context: plush.NewContext()} 89 | help.Set("partialFeeder", func(string) (string, error) { 90 | return `Hi
` 14 | s, err := plush.Render(input, plush.NewContext()) 15 | r.NoError(err) 16 | r.Equal(input, s) 17 | } 18 | 19 | func Test_Render_Keeps_Spacing(t *testing.T) { 20 | r := require.New(t) 21 | input := `<%= greet %> <%= name %>` 22 | 23 | ctx := plush.NewContext() 24 | ctx.Set("greet", "hi") 25 | ctx.Set("name", "mark") 26 | 27 | s, err := plush.Render(input, ctx) 28 | r.NoError(err) 29 | r.Equal("hi mark", s) 30 | } 31 | 32 | func Test_Render_HTML_InjectedString(t *testing.T) { 33 | r := require.New(t) 34 | 35 | input := `<%= "mark" %>
` 36 | s, err := plush.Render(input, plush.NewContext()) 37 | r.NoError(err) 38 | r.Equal("mark
", s) 39 | } 40 | 41 | func Test_Render_Injected_Variable(t *testing.T) { 42 | r := require.New(t) 43 | 44 | input := `<%= name %>
` 45 | s, err := plush.Render(input, plush.NewContextWith(map[string]interface{}{ 46 | "name": "Mark", 47 | })) 48 | r.NoError(err) 49 | r.Equal("Mark
", s) 50 | } 51 | 52 | func Test_Render_Missing_Variable(t *testing.T) { 53 | r := require.New(t) 54 | 55 | input := `<%= name %>
` 56 | _, err := plush.Render(input, plush.NewContext()) 57 | r.Error(err) 58 | } 59 | 60 | func Test_Render_ShowNoShow(t *testing.T) { 61 | r := require.New(t) 62 | input := `<%= "shown" %><% "notshown" %>` 63 | s, err := plush.Render(input, plush.NewContext()) 64 | r.NoError(err) 65 | r.Equal("shown", s) 66 | } 67 | 68 | func Test_Render_ScriptFunction(t *testing.T) { 69 | r := require.New(t) 70 | 71 | input := `<% let add = fn(x) { return x + 2; }; %><%= add(2) %>` 72 | 73 | s, err := plush.Render(input, plush.NewContext()) 74 | r.NoError(err) 75 | r.Equal("4", s) 76 | } 77 | 78 | func Test_Render_HasBlock(t *testing.T) { 79 | r := require.New(t) 80 | ctx := plush.NewContext() 81 | ctx.Set("blockCheck", func(help plush.HelperContext) string { 82 | if help.HasBlock() { 83 | s, _ := help.Block() 84 | return s 85 | } 86 | return "no block" 87 | }) 88 | input := `<%= blockCheck() {return "block"} %>|<%= blockCheck() %>` 89 | s, err := plush.Render(input, ctx) 90 | r.NoError(err) 91 | r.Equal("block|no block", s) 92 | } 93 | 94 | func Test_Render_Dash_in_Helper(t *testing.T) { 95 | r := require.New(t) 96 | ctx := plush.NewContextWith(map[string]interface{}{ 97 | "my-helper": func() string { 98 | return "hello" 99 | }, 100 | }) 101 | s, err := plush.Render(`<%= my-helper() %>`, ctx) 102 | r.NoError(err) 103 | r.Equal("hello", s) 104 | } 105 | 106 | func Test_BuffaloRenderer(t *testing.T) { 107 | r := require.New(t) 108 | input := `<%= foo() %><%= name %>` 109 | data := map[string]interface{}{ 110 | "name": "Ringo", 111 | } 112 | helpers := map[string]interface{}{ 113 | "foo": func() string { 114 | return "George" 115 | }, 116 | } 117 | s, err := plush.BuffaloRenderer(input, data, helpers) 118 | r.NoError(err) 119 | r.Equal("GeorgeRingo", s) 120 | } 121 | 122 | func Test_BuffaloRenderer_Data_Persistence(t *testing.T) { 123 | r := require.New(t) 124 | input := `<%= contentFor("name") { %>MD<% } %>` 125 | data := map[string]interface{}{} 126 | s, err := plush.BuffaloRenderer(input, data, map[string]interface{}{}) 127 | r.NoError(err) 128 | r.Empty(s) 129 | r.Contains(data, "contentFor:name") 130 | } 131 | 132 | func Test_Helper_Nil_Arg(t *testing.T) { 133 | r := require.New(t) 134 | input := `<%= foo(nil, "k") %><%= foo(one, "k") %>` 135 | ctx := plush.NewContextWith(map[string]interface{}{ 136 | "one": map[string]string{ 137 | "k": "test", 138 | }, 139 | "foo": func(a map[string]string, b string) string { 140 | if a != nil { 141 | return a[b] 142 | } 143 | return "" 144 | }, 145 | }) 146 | s, err := plush.Render(input, ctx) 147 | r.NoError(err) 148 | r.Equal("test", s) 149 | } 150 | 151 | func Test_UndefinedArg(t *testing.T) { 152 | r := require.New(t) 153 | input := `<%= foo(bar) %>` 154 | ctx := plush.NewContext() 155 | ctx.Set("foo", func(string) {}) 156 | 157 | _, err := plush.Render(input, ctx) 158 | r.Error(err) 159 | r.Equal(`line 1: "bar": unknown identifier`, err.Error()) 160 | } 161 | 162 | func Test_Caching(t *testing.T) { 163 | r := require.New(t) 164 | 165 | template, err := plush.NewTemplate("<%= \"AA\" %>") 166 | r.NoError(err) 167 | 168 | plush.CacheSet("<%= a %>", template) 169 | plush.CacheEnabled = true 170 | 171 | tc, err := plush.Parse("<%= a %>") 172 | r.NoError(err) 173 | r.Equal(tc, template) 174 | 175 | plush.CacheEnabled = false 176 | tc, err = plush.Parse("<%= a %>") 177 | r.NoError(err) 178 | r.NotEqual(tc, template) 179 | } 180 | -------------------------------------------------------------------------------- /quotes_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/plush/v5" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_Quote_Missing(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | input string 14 | }{ 15 | {"case1", `<%= foo("asdf) %>`}, 16 | {"case2", `<%= foo("test) %>".`}, 17 | {"case3", `<%= title("Running Migrations) %>(default "./migrations")`}, 18 | } 19 | 20 | for _, tc := range tests { 21 | t.Run(tc.name, func(t *testing.T) { 22 | r := require.New(t) 23 | ctx := plush.NewContext() 24 | ctx.Set("foo", func(string) {}) 25 | _, err := plush.Render(tc.input, ctx) 26 | r.Error(err) 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /return_exit_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/gobuffalo/plush/v5" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Return_Exit_With__InfixExpression(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | success bool 15 | expected string 16 | input string 17 | }{ 18 | {"infix_expression", true, "2", `<% 19 | let numberify = fn(arg) { 20 | if (arg == "one") { 21 | return 1+1; 22 | } 23 | if (arg == "two") { 24 | return 44; 25 | } 26 | if (arg == "three") { 27 | return 2; 28 | } 29 | return "unsupported" 30 | } %> 31 | <%= numberify("one") %>`}, 32 | {"simple_return", true, "445", `<% 33 | let numberify = fn(arg) { 34 | if (arg == "one") { 35 | return 1; 36 | } 37 | if (arg == "two") { 38 | return 445; 39 | } 40 | if (arg == "three") { 41 | return 3; 42 | } 43 | return "unsupported" 44 | } %> 45 | <%= numberify("two") %>`}, 46 | {"default_return", true, "default value", `<% 47 | let numberify = fn(arg) { 48 | if (arg == "one") { 49 | return 1; 50 | } 51 | if (arg == "two") { 52 | return 445; 53 | } 54 | if (arg == "three") { 55 | return 3; 56 | } 57 | return "default value" 58 | } %> 59 | <%= numberify("six") %>`}, 60 | } 61 | for _, tc := range tests { 62 | t.Run(tc.name, func(t *testing.T) { 63 | r := require.New(t) 64 | 65 | s, err := plush.Render(tc.input, plush.NewContext()) 66 | if tc.success { 67 | r.NoError(err) 68 | } else { 69 | r.Error(err) 70 | } 71 | r.Equal(tc.expected, strings.TrimSpace(s)) 72 | }) 73 | } 74 | } 75 | 76 | func Test_User_Function_Return(t *testing.T) { 77 | r := require.New(t) 78 | ctx := plush.NewContext() 79 | in := `<% 80 | let print = fn(obj) { 81 | if (obj.Secret) { 82 | if (obj.GiveHint) { 83 | return truncate(obj.String, {size: 12, trail: "****"}) 84 | } 85 | return "**********" 86 | } 87 | return obj.String 88 | } 89 | %>You are: <%= print(data) %>.` 90 | 91 | type obj struct { 92 | Secret bool 93 | GiveHint bool 94 | String string 95 | } 96 | 97 | ctx.Set("data", obj{Secret: true, String: "your royal highness"}) 98 | out, err := plush.Render(in, ctx) 99 | r.NoError(err, "Render") 100 | r.Equal(`You are: **********.`, out) 101 | 102 | ctx.Set("data", obj{Secret: true, GiveHint: true, String: "your royal highness"}) 103 | out, err = plush.Render(in, ctx) 104 | r.NoError(err, "Render") 105 | r.Equal(`You are: your roy****.`, out) 106 | 107 | ctx.Set("data", obj{Secret: false, String: "your royal highness"}) 108 | out, err = plush.Render(in, ctx) 109 | r.NoError(err, "Render") 110 | r.Equal(`You are: your royal highness.`, out) 111 | } 112 | -------------------------------------------------------------------------------- /script_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/gobuffalo/plush/v5" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_RunScript(t *testing.T) { 13 | r := require.New(t) 14 | bb := &bytes.Buffer{} 15 | ctx := plush.NewContextWith(map[string]interface{}{ 16 | "out": func(i interface{}) { 17 | bb.WriteString(fmt.Sprint(i)) 18 | }, 19 | }) 20 | err := plush.RunScript(script, ctx) 21 | r.NoError(err) 22 | r.Equal("3hiasdfasdf", bb.String()) 23 | } 24 | 25 | const script = `let x = "foo" 26 | 27 | let a = 1 28 | let b = 2 29 | let c = a + b 30 | 31 | out(c) 32 | 33 | if (c == 3) { 34 | out("hi") 35 | } 36 | 37 | let x = fn(f) { 38 | f() 39 | } 40 | 41 | x(fn() { 42 | out("asdfasdf") 43 | })` 44 | -------------------------------------------------------------------------------- /struct_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "time" 7 | 8 | "github.com/gobuffalo/plush/v5" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_Render_Struct_Attribute(t *testing.T) { 13 | r := require.New(t) 14 | input := `<%= f.Name %>` 15 | ctx := plush.NewContext() 16 | f := struct { 17 | Name string 18 | }{"Mark"} 19 | ctx.Set("f", f) 20 | s, err := plush.Render(input, ctx) 21 | r.NoError(err) 22 | r.Equal("Mark", s) 23 | } 24 | 25 | func Test_Render_UnknownAttribute_on_Callee(t *testing.T) { 26 | r := require.New(t) 27 | ctx := plush.NewContext() 28 | ctx.Set("m", struct{}{}) 29 | input := `<%= m.Foo %>` 30 | _, err := plush.Render(input, ctx) 31 | r.Error(err) 32 | r.Contains(err.Error(), "'m' does not have a field or method named 'Foo' (m.Foo)") 33 | } 34 | 35 | type Robot struct { 36 | Avatar Avatar 37 | name string 38 | } 39 | 40 | type Avatar string 41 | 42 | func (a Avatar) URL() string { 43 | return strings.ToUpper(string(a)) 44 | } 45 | 46 | func (r *Robot) Name() string { 47 | return r.name 48 | } 49 | 50 | func Test_Render_Function_on_sub_Struct(t *testing.T) { 51 | r := require.New(t) 52 | ctx := plush.NewContext() 53 | bender := Robot{ 54 | Avatar: Avatar("bender.jpg"), 55 | } 56 | ctx.Set("robot", bender) 57 | input := `<%= robot.Avatar.URL() %>` 58 | s, err := plush.Render(input, ctx) 59 | r.NoError(err) 60 | r.Equal("BENDER.JPG", s) 61 | } 62 | 63 | func Test_Render_Struct_PointerMethod(t *testing.T) { 64 | r := require.New(t) 65 | ctx := plush.NewContext() 66 | robot := Robot{name: "robot"} 67 | 68 | t.Run("ByValue", func(t *testing.T) { 69 | ctx.Set("robot", robot) 70 | input := `<%= robot.Name() %>` 71 | s, err := plush.Render(input, ctx) 72 | r.NoError(err) 73 | r.Equal("robot", s) 74 | }) 75 | t.Run("ByPointer", func(t *testing.T) { 76 | ctx.Set("robot", &robot) 77 | input := `<%= robot.Name() %>` 78 | s, err := plush.Render(input, ctx) 79 | r.NoError(err) 80 | r.Equal("robot", s) 81 | }) 82 | } 83 | 84 | func Test_Render_Struct_PointerMethod_IsNil(t *testing.T) { 85 | r := require.New(t) 86 | 87 | type mylist struct { 88 | N int 89 | Next *mylist 90 | } 91 | 92 | input := `Current number is <%= p.N %>.<%= if (p.Next) { %> Next up is <%= p.Next.N %>.<% } %>` 93 | first := &mylist{N: 0} 94 | last := first 95 | 96 | for i := 0; i < 5; i++ { 97 | last.Next = &mylist{N: i + 1} 98 | last = last.Next 99 | } 100 | 101 | resE := []string{ 102 | "Current number is 0. Next up is 1.", 103 | "Current number is 1. Next up is 2.", 104 | "Current number is 2. Next up is 3.", 105 | "Current number is 3. Next up is 4.", 106 | "Current number is 4. Next up is 5.", 107 | "Current number is 5.", 108 | } 109 | 110 | for p := first; p != nil; p = p.Next { 111 | ctx := plush.NewContextWith(map[string]interface{}{"p": p}) 112 | res, err := plush.Render(input, ctx) 113 | r.NoError(err) 114 | r.Equal(resE[p.N], res) 115 | 116 | } 117 | } 118 | 119 | func Test_Render_Struct_PointerValue_Nil(t *testing.T) { 120 | r := require.New(t) 121 | 122 | type user struct { 123 | Name string 124 | Image *string 125 | } 126 | 127 | u := user{ 128 | Name: "Garn Clapstick", 129 | Image: nil, 130 | } 131 | ctx := plush.NewContextWith(map[string]interface{}{ 132 | "user": u, 133 | }) 134 | input := `<%= user.Name %>: <%= user.Image %>` 135 | res, err := plush.Render(input, ctx) 136 | 137 | r.NoError(err) 138 | r.Equal(`Garn Clapstick: `, res) 139 | } 140 | 141 | func Test_Render_Struct_PointerValue_NonNil(t *testing.T) { 142 | r := require.New(t) 143 | 144 | type user struct { 145 | Name string 146 | Image *string 147 | } 148 | 149 | image := "bicep.png" 150 | u := user{ 151 | Name: "Scrinch Archipeligo", 152 | Image: &image, 153 | } 154 | ctx := plush.NewContextWith(map[string]interface{}{ 155 | "user": u, 156 | }) 157 | input := `<%= user.Name %>: <%= user.Image %>` 158 | res, err := plush.Render(input, ctx) 159 | 160 | r.NoError(err) 161 | r.Equal(`Scrinch Archipeligo: bicep.png`, res) 162 | } 163 | 164 | func Test_Render_Struct_Multiple_Access(t *testing.T) { 165 | r := require.New(t) 166 | 167 | type mylist struct { 168 | Name string 169 | } 170 | 171 | input := `<%= myarray[0].Name %> <%= myarray[1].Name %>` 172 | 173 | gg := make([]mylist, 3) 174 | gg[0].Name = "John" 175 | gg[1].Name = "Doe" 176 | ctx := plush.NewContext() 177 | ctx.Set("myarray", gg) 178 | res, err := plush.Render(input, ctx) 179 | r.NoError(err) 180 | r.Equal("John Doe", res) 181 | 182 | } 183 | 184 | func Test_Render_Nested_Structs_Start_With_Slice(t *testing.T) { 185 | r := require.New(t) 186 | 187 | type b struct { 188 | Final string 189 | } 190 | type mylist struct { 191 | Name b 192 | } 193 | 194 | input := `<%= myarray[0].Name.Final %>` 195 | 196 | gg := make([]mylist, 3) 197 | gg[0].Name.Final = "Hello World" 198 | ctx := plush.NewContext() 199 | ctx.Set("myarray", gg) 200 | res, err := plush.Render(input, ctx) 201 | r.NoError(err) 202 | r.Equal("Hello World", res) 203 | 204 | } 205 | 206 | func Test_Render_Nested_Structs_Start_With_Slice_End_With_Slice(t *testing.T) { 207 | r := require.New(t) 208 | type b struct { 209 | A []string 210 | } 211 | type mylist struct { 212 | Name b 213 | } 214 | 215 | input := `<%= myarray[0].Name.A[0] %>` 216 | 217 | gg := make([]mylist, 3) 218 | gg[0].Name = b{[]string{"Hello World"}} 219 | ctx := plush.NewContext() 220 | ctx.Set("myarray", gg) 221 | res, err := plush.Render(input, ctx) 222 | r.NoError(err) 223 | r.Equal("Hello World", res) 224 | 225 | } 226 | func Test_Render_Nested_Structs_Ends_With_Slice(t *testing.T) { 227 | r := require.New(t) 228 | ctx := plush.NewContext() 229 | 230 | type c struct { 231 | Final []string 232 | } 233 | 234 | type b struct { 235 | B c 236 | } 237 | type mylist struct { 238 | Name b 239 | } 240 | 241 | bender := mylist{ 242 | Name: b{ 243 | B: c{ 244 | Final: []string{"bendser.jpg"}, 245 | }, 246 | }, 247 | } 248 | ctx.Set("robot", bender) 249 | input := `<%= robot.Name.B.Final[0] %>` 250 | s, err := plush.Render(input, ctx) 251 | r.NoError(err) 252 | r.Equal("bendser.jpg", s) 253 | } 254 | 255 | func Test_Render_Nested_Structs(t *testing.T) { 256 | r := require.New(t) 257 | ctx := plush.NewContext() 258 | 259 | type c struct { 260 | Final string 261 | } 262 | 263 | type b struct { 264 | B c 265 | } 266 | type mylist struct { 267 | Name b 268 | } 269 | 270 | bender := mylist{ 271 | Name: b{ 272 | B: c{ 273 | Final: "bendser.jpg", 274 | }, 275 | }, 276 | } 277 | ctx.Set("robot", bender) 278 | input := `<%= robot.Name.B.Final %>` 279 | s, err := plush.Render(input, ctx) 280 | r.NoError(err) 281 | r.Equal("bendser.jpg", s) 282 | } 283 | func Test_Render_Struct_Access_Slice_Field(t *testing.T) { 284 | r := require.New(t) 285 | ctx := plush.NewContext() 286 | type D struct { 287 | Final string 288 | } 289 | type c struct { 290 | Final []D 291 | } 292 | 293 | type b struct { 294 | B c 295 | } 296 | type mylist struct { 297 | Name b 298 | } 299 | 300 | bender := mylist{ 301 | Name: b{ 302 | B: c{ 303 | Final: []D{ 304 | {Final: "String"}, 305 | }, 306 | }, 307 | }, 308 | } 309 | ctx.Set("robot", bender) 310 | input := `<%= robot.Name.B.Final[0].Final %>` 311 | s, err := plush.Render(input, ctx) 312 | r.NoError(err) 313 | r.Equal("String", s) 314 | } 315 | 316 | func Test_Render_Struct_Nested_Slice_Access(t *testing.T) { 317 | r := require.New(t) 318 | type d struct { 319 | Final string 320 | } 321 | type c struct { 322 | He []d 323 | } 324 | type b struct { 325 | A []c 326 | } 327 | type mylist struct { 328 | Name []b 329 | } 330 | 331 | input := `<%= myarray[0].Name[0].A[1].He[2].Final %>` 332 | 333 | gg := make([]mylist, 3) 334 | 335 | var bc b 336 | var ca c 337 | ca.He = make([]d, 3) 338 | ca.He[2] = d{"Hello World"} 339 | bc.A = make([]c, 3) 340 | bc.A[1] = ca 341 | gg[0].Name = []b{bc} 342 | 343 | ctx := plush.NewContext() 344 | ctx.Set("myarray", gg) 345 | res, err := plush.Render(input, ctx) 346 | r.NoError(err) 347 | r.Equal("Hello World", res) 348 | 349 | } 350 | 351 | func Test_Render_Struct_Nested_Map_Slice_Access(t *testing.T) { 352 | r := require.New(t) 353 | type d struct { 354 | Final string 355 | } 356 | type c struct { 357 | He map[string]d 358 | } 359 | type b struct { 360 | A []c 361 | } 362 | type mylist struct { 363 | Name []b 364 | } 365 | 366 | input := `<%= myarray[0].Name[0].A[1].He["test"].Final %>` 367 | 368 | gg := make([]mylist, 3) 369 | 370 | var bc b 371 | var ca c 372 | ca.He = make(map[string]d) 373 | ca.He["test"] = d{"Hello World"} 374 | bc.A = make([]c, 3) 375 | bc.A[1] = ca 376 | gg[0].Name = []b{bc} 377 | 378 | ctx := plush.NewContext() 379 | ctx.Set("myarray", gg) 380 | res, err := plush.Render(input, ctx) 381 | r.NoError(err) 382 | r.Equal("Hello World", res) 383 | 384 | } 385 | 386 | func Test_Render_Struct_Nested_With_Unexported_Fields(t *testing.T) { 387 | r := require.New(t) 388 | type people struct { 389 | FirstName string 390 | LastName string 391 | } 392 | type employee struct { 393 | employee []people 394 | } 395 | departments := make(map[string]employee) 396 | var joh people 397 | 398 | joh.FirstName = "John" 399 | joh.LastName = "Doe" 400 | 401 | var jane people 402 | 403 | jane.FirstName = "Jane" 404 | jane.LastName = "Dolittle" 405 | 406 | var employees employee 407 | employees.employee = []people{joh, jane} 408 | departments["HR"] = employees 409 | 410 | input := `<%= departments["HR"].employee[0].FirstName %>` 411 | ctx := plush.NewContext() 412 | ctx.Set("departments", departments) 413 | _, err := plush.Render(input, ctx) 414 | r.Error(err) 415 | //r.Equal("John", res) 416 | 417 | } 418 | 419 | func Test_Render_Struct_Nested_Map_Access(t *testing.T) { 420 | r := require.New(t) 421 | type people struct { 422 | FirstName string 423 | LastName string 424 | } 425 | type employee struct { 426 | Employee []people 427 | } 428 | departments := make(map[string]employee) 429 | var joh people 430 | 431 | joh.FirstName = "John" 432 | joh.LastName = "Doe" 433 | 434 | var jane people 435 | 436 | jane.FirstName = "Jane" 437 | jane.LastName = "Dolittle" 438 | 439 | var employees employee 440 | employees.Employee = []people{joh, jane} 441 | departments["HR"] = employees 442 | 443 | input := `<%= departments["HR"].Employee[0].FirstName %> <%= departments["HR"].Employee[0].LastName %>` 444 | ctx := plush.NewContext() 445 | ctx.Set("departments", departments) 446 | res, err := plush.Render(input, ctx) 447 | r.NoError(err) 448 | r.Equal("John Doe", res) 449 | 450 | input = `<%= departments["HR"].Employee[1].FirstName %> <%= departments["HR"].Employee[1].LastName %>` 451 | res, err = plush.Render(input, ctx) 452 | r.NoError(err) 453 | r.Equal("Jane Dolittle", res) 454 | 455 | input = `<%= departments["HR"].Employee[1].FirstName %> <%= departments["HR"].Employee[0].LastName %>` 456 | res, err = plush.Render(input, ctx) 457 | r.NoError(err) 458 | r.Equal("Jane Doe", res) 459 | 460 | input = `<%= departments["HR"].Employee[0].FirstName %> <%= departments["HR"].Employee[1].LastName %>` 461 | res, err = plush.Render(input, ctx) 462 | r.NoError(err) 463 | r.Equal("John Dolittle", res) 464 | } 465 | 466 | type person struct { 467 | likes []string 468 | hates []string 469 | born time.Time 470 | } 471 | 472 | func (a person) GetAge() time.Duration { 473 | return time.Since(a.born) 474 | } 475 | 476 | func (a person) GetBorn() time.Time { 477 | return a.born 478 | } 479 | 480 | func (a person) Hates() []string { 481 | return a.hates 482 | } 483 | func (a person) Likes() []string { 484 | return a.likes 485 | } 486 | 487 | func Test_Render_Struct_With_ChainingFunction_ArrayAccess(t *testing.T) { 488 | r := require.New(t) 489 | 490 | tt := person{likes: []string{"pringles", "galaxy", "carrot cake", "world pendant", "gold braclet"}, 491 | hates: []string{"boiled eggs", "coconut"}} 492 | input := `<%= nour.Likes()[0] %>` 493 | ctx := plush.NewContext() 494 | ctx.Set("nour", tt) 495 | res, err := plush.Render(input, ctx) 496 | r.NoError(err) 497 | r.Equal("pringles", res) 498 | } 499 | 500 | func Test_Render_Struct_With_ChainingFunction_ArrayAccess_Outofbound(t *testing.T) { 501 | r := require.New(t) 502 | 503 | tt := person{likes: []string{"pringles", "galaxy", "carrot cake", "world pendant", "gold bracelet"}, 504 | hates: []string{"boiled eggs", "coconut"}} 505 | input := `<%= nour.Hates()[30] %>` 506 | ctx := plush.NewContext() 507 | ctx.Set("nour", tt) 508 | _, err := plush.Render(input, ctx) 509 | r.Error(err) 510 | } 511 | 512 | func Test_Render_Struct_With_ChainingFunction_FunctionCall(t *testing.T) { 513 | r := require.New(t) 514 | 515 | tt := person{born: time.Date(2024, time.January, 11, 0, 0, 0, 0, time.UTC).AddDate(-31, 0, 0)} 516 | input := `<%= nour.GetBorn().Format("Jan 2, 2006") %>` 517 | ctx := plush.NewContext() 518 | ctx.Set("nour", tt) 519 | res, err := plush.Render(input, ctx) 520 | r.NoError(err) 521 | r.Equal("Jan 11, 1993", res) 522 | } 523 | 524 | func Test_Render_Struct_With_ChainingFunction_UndefinedStructProperty(t *testing.T) { 525 | r := require.New(t) 526 | 527 | tt := person{born: time.Now()} 528 | input := `<%= nour.GetBorn().TEST %>` 529 | ctx := plush.NewContext() 530 | ctx.Set("nour", tt) 531 | _, err := plush.Render(input, ctx) 532 | r.Error(err) 533 | 534 | } 535 | 536 | func Test_Render_Struct_With_ChainingFunction_InvalidFunctionCall(t *testing.T) { 537 | r := require.New(t) 538 | 539 | tt := person{born: time.Now()} 540 | input := `<%= nour.GetBorn().TEST("Jan 2, 2006") %>` 541 | ctx := plush.NewContext() 542 | ctx.Set("nour", tt) 543 | _, err := plush.Render(input, ctx) 544 | r.Error(err) 545 | r.Contains(err.Error(), "'nour.GetBorn' does not have a method named 'TEST' (nour.GetBorn.TEST)") 546 | } 547 | 548 | func Test_Render_Function_on_Invalid_Function_Struct(t *testing.T) { 549 | r := require.New(t) 550 | ctx := plush.NewContext() 551 | bender := Robot{ 552 | Avatar: Avatar("bender.jpg"), 553 | } 554 | ctx.Set("robot", bender) 555 | input := `<%= robot.Avatar.URL2() %>` 556 | _, err := plush.Render(input, ctx) 557 | r.Error(err) 558 | } 559 | 560 | func Test_Render_Struct_Nested_Slice_Access_Out_Of_Range(t *testing.T) { 561 | r := require.New(t) 562 | type d struct { 563 | Final string 564 | } 565 | type c struct { 566 | He []d 567 | } 568 | type b struct { 569 | A []c 570 | } 571 | type mylist struct { 572 | Name []b 573 | } 574 | 575 | input := `<%= myarray[0].Name[0].A[1].He[2].Final %>` 576 | 577 | gg := make([]mylist, 3) 578 | 579 | var bc b 580 | 581 | gg[0].Name = []b{bc} 582 | 583 | ctx := plush.NewContext() 584 | ctx.Set("myarray", gg) 585 | res, err := plush.Render(input, ctx) 586 | r.Error(err) 587 | r.Empty(res) 588 | r.Error(err, "line 1: array index out of bounds, got index 1, while array size is 0") 589 | } 590 | -------------------------------------------------------------------------------- /symbol_table.go: -------------------------------------------------------------------------------- 1 | package plush 2 | 3 | // SymbolTable represents a scope 4 | type SymbolTable struct { 5 | vars map[int]interface{} 6 | parent *SymbolTable 7 | // Interning system 8 | localInterner *InternTable 9 | globalInterner *InternTable 10 | } 11 | 12 | // NewScope creates a new scope with an optional parent 13 | func NewScope(parent *SymbolTable) *SymbolTable { 14 | if parent == nil { 15 | global := NewInternTable() 16 | local := NewInternTable() 17 | return &SymbolTable{ 18 | vars: make(map[int]interface{}), 19 | parent: nil, 20 | globalInterner: global, 21 | localInterner: local, 22 | } 23 | } 24 | 25 | // Inherit interning from parent 26 | return &SymbolTable{ 27 | vars: make(map[int]interface{}), 28 | parent: parent, 29 | globalInterner: parent.globalInterner, 30 | localInterner: parent.localInterner, 31 | } 32 | } 33 | 34 | // Declare adds or updates a variable in the current scope 35 | func (s *SymbolTable) Declare(name string, value interface{}) { 36 | if value == nil { 37 | return 38 | } 39 | id := s.localInterner.Intern(name) 40 | s.vars[id] = value 41 | } 42 | 43 | // Assign searches outer scopes and updates an existing variable 44 | func (s *SymbolTable) Assign(name string, value interface{}) bool { 45 | var id int 46 | var ok bool 47 | 48 | isLocal := false 49 | 50 | // Try local interner first 51 | if id, ok = s.localInterner.Lookup(name); !ok { 52 | // Then try global interner 53 | if id, ok = s.globalInterner.Lookup(name); !ok { 54 | return false 55 | } 56 | } else { 57 | isLocal = true 58 | } 59 | 60 | firstK := 0 61 | for curr := s; curr != nil; curr = curr.parent { 62 | //Skip if we know it's not in the first local scope 63 | if !isLocal && firstK == 0 { 64 | firstK += 1 65 | continue 66 | } 67 | if _, exists := curr.vars[id]; exists { 68 | curr.vars[id] = value 69 | return true 70 | } 71 | } 72 | 73 | return false 74 | } 75 | 76 | // Has finds the value of a variable 77 | func (s *SymbolTable) Has(name string) bool { 78 | var id int 79 | var ok bool 80 | 81 | isLocal := false 82 | // Try local first 83 | if id, ok = s.localInterner.Lookup(name); !ok { 84 | // Try global if not found locally 85 | if id, ok = s.globalInterner.Lookup(name); !ok { 86 | return false 87 | } 88 | } else { 89 | isLocal = true 90 | } 91 | 92 | firstK := 0 93 | // Only one walk through the scope chain, using the ID we found 94 | for curr := s; curr != nil; curr = curr.parent { 95 | //Skip if we know it's not in the first local scope 96 | if !isLocal && firstK == 0 { 97 | firstK += 1 98 | continue 99 | } 100 | if _, exists := curr.vars[id]; exists { 101 | return true 102 | } 103 | } 104 | 105 | return false 106 | } 107 | 108 | // Resolve finds the value of a variable 109 | func (s *SymbolTable) Resolve(name string) (interface{}, bool) { 110 | var id int 111 | var ok bool 112 | 113 | isLocal := false 114 | // Try local first 115 | if id, ok = s.localInterner.Lookup(name); !ok { 116 | // Try global if not found locally 117 | if id, ok = s.globalInterner.Lookup(name); !ok { 118 | return nil, false 119 | } 120 | } else { 121 | isLocal = true 122 | } 123 | 124 | firstK := 0 125 | // Only one walk through the scope chain, using the ID we found 126 | for curr := s; curr != nil; curr = curr.parent { 127 | //Skip if we know it's not in the first local scope 128 | if !isLocal && firstK == 0 { 129 | firstK += 1 130 | continue 131 | } 132 | if val, exists := curr.vars[id]; exists { 133 | return val, true 134 | } 135 | } 136 | 137 | return nil, false 138 | } 139 | -------------------------------------------------------------------------------- /symbol_table_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/plush/v5" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestSymbolTable_NewSymbolTable(t *testing.T) { 11 | r := require.New(t) 12 | 13 | scope := plush.NewScope(nil) 14 | 15 | r.NotNil(scope) 16 | } 17 | 18 | func TestSymbolTable_Declare_And_Resolve(t *testing.T) { 19 | r := require.New(t) 20 | 21 | scope := plush.NewScope(nil) 22 | r.NotNil(scope) 23 | scope.Declare("x", 42) 24 | 25 | val, ok := scope.Resolve("x") 26 | 27 | r.True(ok) 28 | r.Equal(42, val) 29 | } 30 | 31 | func TestSymbolTable_Declare_And_Has(t *testing.T) { 32 | r := require.New(t) 33 | 34 | scope := plush.NewScope(nil) 35 | r.NotNil(scope) 36 | scope.Declare("x", 42) 37 | 38 | ok := scope.Has("x") 39 | 40 | r.True(ok) 41 | } 42 | 43 | func TestSymbolTable_Declare_And_Has_Child(t *testing.T) { 44 | r := require.New(t) 45 | 46 | scope := plush.NewScope(nil) 47 | r.NotNil(scope) 48 | scope.Declare("x", 42) 49 | childA := plush.NewScope(scope) 50 | r.NotNil(scope) 51 | childA.Declare("y", 42) 52 | ok := childA.Has("x") 53 | 54 | r.True(ok) 55 | 56 | ok = childA.Has("y") 57 | r.True(ok) 58 | 59 | ok = childA.Has("d") 60 | r.False(ok) 61 | } 62 | func TestSymbolTable_Resolve_From_Parent_Scope(t *testing.T) { 63 | r := require.New(t) 64 | 65 | parent := plush.NewScope(nil) 66 | 67 | r.NotNil(parent) 68 | 69 | parent.Declare("y", "hello") 70 | 71 | child := plush.NewScope(parent) 72 | 73 | r.NotNil(child) 74 | val, ok := child.Resolve("y") 75 | 76 | r.Equal("hello", val) 77 | r.True(ok) 78 | } 79 | 80 | func TestSymbolTable_Assign_To_ParentScope(t *testing.T) { 81 | r := require.New(t) 82 | parent := plush.NewScope(nil) 83 | 84 | r.NotNil(parent) 85 | 86 | parent.Declare("z", 100) 87 | 88 | child := plush.NewScope(parent) 89 | 90 | r.NotNil(child) 91 | 92 | assigned := child.Assign("z", 200) 93 | 94 | r.True(assigned) 95 | 96 | valC, okC := child.Resolve("z") 97 | 98 | r.True(okC) 99 | r.Equal(200, valC) 100 | 101 | val, ok := parent.Resolve("z") 102 | 103 | r.True(ok) 104 | r.Equal(200, val) 105 | } 106 | 107 | func TestSymbolTable_Assign_Non_Existent_Fails(t *testing.T) { 108 | r := require.New(t) 109 | 110 | scope := plush.NewScope(nil) 111 | 112 | r.NotNil(scope) 113 | 114 | assigned := scope.Assign("nonexistent", 123) 115 | 116 | r.False(assigned) 117 | } 118 | 119 | func TestSymbolTable_Declare_Nil_Ignored(t *testing.T) { 120 | r := require.New(t) 121 | scope := plush.NewScope(nil) 122 | 123 | r.NotNil(scope) 124 | 125 | scope.Declare("a", nil) 126 | 127 | _, ok := scope.Resolve("a") 128 | r.False(ok) 129 | } 130 | 131 | func TestSymbolTable_Shadowing(t *testing.T) { 132 | 133 | r := require.New(t) 134 | 135 | root := plush.NewScope(nil) 136 | 137 | r.NotNil(root) 138 | 139 | root.Declare("v", 1) 140 | 141 | child := plush.NewScope(root) 142 | 143 | r.NotNil(child) 144 | child.Declare("v", 2) 145 | 146 | valRoot, okRoot := root.Resolve("v") 147 | valChild, okChild := child.Resolve("v") 148 | 149 | r.True(okRoot) 150 | r.Equal(1, valRoot) 151 | 152 | r.True(okChild) 153 | r.Equal(2, valChild) 154 | } 155 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | package plush 2 | 3 | import ( 4 | "github.com/gobuffalo/plush/v5/ast" 5 | "github.com/gobuffalo/plush/v5/helpers/hctx" 6 | 7 | "github.com/gobuffalo/plush/v5/parser" 8 | ) 9 | 10 | // Template represents an input and helpers to be used 11 | // to evaluate and render the input. 12 | type Template struct { 13 | Input string 14 | program *ast.Program 15 | } 16 | 17 | // NewTemplate from the input string. Adds all of the 18 | // global helper functions from "Helpers", this function does not 19 | // cache the template. 20 | func NewTemplate(input string) (*Template, error) { 21 | t := &Template{ 22 | Input: input, 23 | } 24 | 25 | err := t.Parse() 26 | if err != nil { 27 | return t, err 28 | } 29 | 30 | return t, nil 31 | } 32 | 33 | // Parse the template this can be called many times 34 | // as a successful result is cached and is used on subsequent 35 | // uses. 36 | func (t *Template) Parse() error { 37 | if t.program != nil { 38 | return nil 39 | } 40 | 41 | program, err := parser.Parse(t.Input) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | t.program = program 47 | return nil 48 | } 49 | 50 | // Exec the template using the content and return the results 51 | func (t *Template) Exec(ctx hctx.Context) (string, error) { 52 | err := t.Parse() 53 | if err != nil { 54 | return "", err 55 | } 56 | 57 | ev := compiler{ 58 | ctx: ctx, 59 | program: t.program, 60 | } 61 | 62 | s, err := ev.compile() 63 | return s, err 64 | } 65 | 66 | // Clone a template. This is useful for defining helpers on per "instance" of the template. 67 | func (t *Template) Clone() *Template { 68 | t2 := &Template{ 69 | Input: t.Input, 70 | program: t.program, 71 | } 72 | return t2 73 | } 74 | -------------------------------------------------------------------------------- /template_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "golang.org/x/sync/errgroup" 7 | 8 | "github.com/gobuffalo/plush/v5" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func Test_Template_Exec_Concurrency(t *testing.T) { 13 | r := require.New(t) 14 | tmpl, err := plush.NewTemplate(``) 15 | r.NoError(err) 16 | exec := func() error { 17 | _, e := tmpl.Exec(plush.NewContext()) 18 | return e 19 | } 20 | wg := errgroup.Group{} 21 | wg.Go(exec) 22 | wg.Go(exec) 23 | wg.Go(exec) 24 | err = wg.Wait() 25 | r.NoError(err) 26 | } 27 | -------------------------------------------------------------------------------- /time_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/gobuffalo/plush/v5" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func Test_Default_Time_Format(t *testing.T) { 12 | r := require.New(t) 13 | 14 | shortForm := "2006-Jan-02" 15 | tm, err := time.Parse(shortForm, "2013-Feb-03") 16 | r.NoError(err) 17 | ctx := plush.NewContext() 18 | ctx.Set("tm", tm) 19 | 20 | input := `<%= tm %>` 21 | 22 | s, err := plush.Render(input, ctx) 23 | r.NoError(err) 24 | r.Equal("February 03, 2013 00:00:00 +0000", s) 25 | 26 | ctx.Set("TIME_FORMAT", "2006-02-Jan") 27 | s, err = plush.Render(input, ctx) 28 | r.NoError(err) 29 | r.Equal("2013-03-Feb", s) 30 | 31 | ctx.Set("tm", &tm) 32 | s, err = plush.Render(input, ctx) 33 | r.NoError(err) 34 | r.Equal("2013-03-Feb", s) 35 | } 36 | -------------------------------------------------------------------------------- /token/const.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | const ( 4 | ILLEGAL = "ILLEGAL" 5 | EOF = "EOF" 6 | 7 | // Identifiers + literals 8 | IDENT = "IDENT" // add, foobar, x, y, ... 9 | INT = "INT" // 1343456 10 | FLOAT = "FLOAT" // 12.34 11 | STRING = "STRING" // "foobar" 12 | B_STRING = "B_STRING" // `foobar` 13 | HTML = "HTML" //adf
14 | DOT = "DOT" // .23 15 | 16 | // Operators 17 | ASSIGN = "=" 18 | PLUS = "+" 19 | MINUS = "-" 20 | BANG = "!" 21 | ASTERISK = "*" 22 | SLASH = "/" 23 | PERCENT = "%" 24 | 25 | LT = "<" 26 | LTEQ = "<=" 27 | GT = ">" 28 | GTEQ = ">=" 29 | 30 | EQ = "==" 31 | NOT_EQ = "!=" 32 | AND = "&&" 33 | OR = "||" 34 | MATCHES = "~=" 35 | 36 | // Delimiters 37 | 38 | S_START = "<%" 39 | C_START = "<%#" 40 | E_START = "<%=" 41 | E_END = "%>" 42 | 43 | COMMA = "," 44 | SEMICOLON = ";" 45 | COLON = ":" 46 | 47 | LPAREN = "(" 48 | RPAREN = ")" 49 | LBRACE = "{" 50 | RBRACE = "}" 51 | LBRACKET = "[" 52 | RBRACKET = "]" 53 | 54 | // Keywords 55 | FUNCTION = "FUNCTION" 56 | LET = "LET" 57 | TRUE = "TRUE" 58 | FALSE = "FALSE" 59 | IF = "IF" 60 | ELSE = "ELSE" 61 | RETURN = "RETURN" 62 | FOR = "FOR" 63 | IN = "IN" 64 | CONTINUE = "CONTINUE" 65 | BREAK = "BREAK" 66 | ) 67 | -------------------------------------------------------------------------------- /token/token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | // Type represents each type of token. 4 | type Type string 5 | 6 | // Token of a section of input source. 7 | type Token struct { 8 | Type Type 9 | Literal string 10 | LineNumber int 11 | } 12 | 13 | var keywords = map[string]Type{ 14 | "fn": FUNCTION, 15 | "func": FUNCTION, 16 | "let": LET, 17 | "true": TRUE, 18 | "false": FALSE, 19 | "if": IF, 20 | "else": ELSE, 21 | "return": RETURN, 22 | "for": FOR, 23 | "in": IN, 24 | "continue": CONTINUE, 25 | "break": BREAK, 26 | } 27 | 28 | // LookupIdent an ident and return a keyword type, or a plain ident 29 | func LookupIdent(ident string) Type { 30 | if tok, ok := keywords[ident]; ok { 31 | return tok 32 | } 33 | return IDENT 34 | } 35 | -------------------------------------------------------------------------------- /user_function.go: -------------------------------------------------------------------------------- 1 | package plush 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | 7 | "github.com/gobuffalo/plush/v5/ast" 8 | ) 9 | 10 | type userFunction struct { 11 | Parameters []*ast.Identifier 12 | Block *ast.BlockStatement 13 | } 14 | 15 | func (f *userFunction) String() string { 16 | var out bytes.Buffer 17 | 18 | params := []string{} 19 | for _, p := range f.Parameters { 20 | params = append(params, p.String()) 21 | } 22 | 23 | out.WriteString("fn") 24 | out.WriteString("(") 25 | out.WriteString(strings.Join(params, ", ")) 26 | out.WriteString(") {\n") 27 | out.WriteString(f.Block.String()) 28 | out.WriteString("\n}") 29 | 30 | return out.String() 31 | } 32 | -------------------------------------------------------------------------------- /variables_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "html/template" 5 | "log" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/gobuffalo/plush/v5" 10 | "github.com/gobuffalo/tags/v3" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func Test_Let_Reassignment(t *testing.T) { 15 | r := require.New(t) 16 | input := `<% let foo = "bar" %> 17 | <%= for (a) in myArray { %> 18 | <%= foo %> 19 | <% if (foo != "baz") { %> 20 | <% foo = "baz" %> 21 | <% } %> 22 | <% } %> 23 | <% } %>` 24 | 25 | ctx := plush.NewContext() 26 | ctx.Set("myArray", []string{"a", "b"}) 27 | 28 | s, err := plush.Render(input, ctx) 29 | r.NoError(err) 30 | r.Equal("bar\n \n \nbaz", strings.TrimSpace(s)) 31 | } 32 | 33 | func Test_Let_SyntaxError_NoEqualSign(t *testing.T) { 34 | r := require.New(t) 35 | input := `<% let foo %>` 36 | 37 | ctx := plush.NewContext() 38 | 39 | _, err := plush.Render(input, ctx) 40 | r.ErrorContains(err, "expected next token to be =") 41 | } 42 | 43 | func Test_Let_SyntaxError_NoIdentifier(t *testing.T) { 44 | r := require.New(t) 45 | input := `<% let = %>` 46 | 47 | ctx := plush.NewContext() 48 | 49 | _, err := plush.Render(input, ctx) 50 | r.ErrorContains(err, "expected next token to be IDENT") 51 | } 52 | 53 | func Test_Let_Reassignment_UnknownIdent(t *testing.T) { 54 | r := require.New(t) 55 | input := `<% foo = "baz" %>` 56 | 57 | ctx := plush.NewContext() 58 | ctx.Set("myArray", []string{"a", "b"}) 59 | 60 | _, err := plush.Render(input, ctx) 61 | r.ErrorContains(err, "\"foo\": unknown identifier") 62 | } 63 | 64 | func Test_Let_Inside_Helper(t *testing.T) { 65 | r := require.New(t) 66 | ctx := plush.NewContextWith(map[string]interface{}{ 67 | "divwrapper": func(opts map[string]interface{}, helper plush.HelperContext) (template.HTML, error) { 68 | body, err := helper.Block() 69 | if err != nil { 70 | return template.HTML(""), err 71 | } 72 | t := tags.New("div", opts) 73 | t.Append(body) 74 | return t.HTML(), nil 75 | }, 76 | }) 77 | 78 | input := `<%= divwrapper({"class": "myclass"}) { %> 79 |<% let h = {"a": "A"} %><%= h["a"] %>
`, "A
"}, 103 | {"assign", true, `<% let h = {"a": "A"} %><% h["a"] = "C" %><%= h["a"] %>
`, "C
"}, 104 | {"assign", true, `<% let h = {"a": "A"} %><% h["b"] = "D" %><%= h["b"] %>
`, "D
"}, 105 | {"intvar", true, `<% let h = {"a": "A"} %><% h["b"] = 3 %><%= h["b"] %>
`, "3
"}, 106 | {"invalid", true, `<% let h = {"a": "A"} %><% h["b"] = 3 %><%= h["c"] %>
`, ""}, 107 | } 108 | for _, tc := range tests { 109 | t.Run(tc.name, func(t *testing.T) { 110 | r := require.New(t) 111 | s, err := plush.Render(tc.input, plush.NewContext()) 112 | if tc.success { 113 | r.NoError(err) 114 | } else { 115 | r.Error(err) 116 | } 117 | r.Equal(tc.expected, s) 118 | }) 119 | } 120 | } 121 | 122 | func Test_Render_Let_Array(t *testing.T) { 123 | tests := []struct { 124 | name string 125 | success bool 126 | input string 127 | expected string 128 | }{ 129 | {"success", true, `<% let a = [1, 2, "three", "four", 3.75] %><% a[0] = 3 %><%= a[0] %>
`, "3
"}, 130 | {"addition", true, `<% let a = [1, 2, "three", "four", 3.75] %><% a[4] = 3 %><%= a[4] + 2 %>
`, "5
"}, 131 | {"invalid_key", false, `<% let a = [1, 2, "three", "four", 3.75] %><% a["b"] = 3 %><%= a["c"] %>
`, ""}, 132 | {"outofbounds_assign", false, `<% let a = [1, 2, "three", "four", 3.75] %><% a[5] = 3 %><%= a[4] + 2 %>
`, ""}, 133 | {"outofbounds_access", false, `<% let a = [1, 2, "three", "four", 3.75] %><%= a[5] %>
`, ""}, 134 | } 135 | for _, tc := range tests { 136 | t.Run(tc.name, func(t *testing.T) { 137 | r := require.New(t) 138 | s, err := plush.Render(tc.input, plush.NewContext()) 139 | if tc.success { 140 | r.NoError(err) 141 | } else { 142 | r.Error(err) 143 | } 144 | r.Equal(tc.expected, s) 145 | }) 146 | } 147 | } 148 | 149 | func Test_Render_Let_ArrayAsssign_Unassignable(t *testing.T) { 150 | r := require.New(t) 151 | ctx := plush.NewContext() 152 | 153 | type tt struct { 154 | P string 155 | } 156 | 157 | ctx.Set("myArray", []tt{tt{P: "t"}}) 158 | input := `<% let a = myArray %>
<% a[0] = "HELLO WORLD" %>` 159 | _, err := plush.Render(input, ctx) 160 | r.Error(err) 161 | r.Contains(err.Error(), "cannot use 'HELLO WORLD' (untyped string constant) as plush_test.tt value in assignment") 162 | } 163 | 164 | func Test_Render_Let_ArrayAsssign_AssignableToArrayInterface(t *testing.T) { 165 | r := require.New(t) 166 | ctx := plush.NewContext() 167 | 168 | type tt struct { 169 | P string 170 | } 171 | 172 | ctx.Set("myArray", []interface{}{tt{P: "t"}, tt{P: "g"}}) 173 | input := `<% let a = myArray %>
<% a[0] = "HELLO WORLD" %>` 174 | _, err := plush.Render(input, ctx) 175 | r.NoError(err) 176 | } 177 | 178 | func Test_Render_AppendArray_WithTypeIntArrayTypeString(t *testing.T) { 179 | r := require.New(t) 180 | ctx := plush.NewContext() 181 | 182 | ctx.Set("myArray", []string{"a", "b"}) 183 | input := `<% let a = myArray %><% a = a + 1 %><%= a %>` 184 | _, err := plush.Render(input, ctx) 185 | r.Error(err) 186 | r.Contains(err.Error(), "cannot append '1' (untyped int constant) as string value in assignment") 187 | } 188 | 189 | func Test_Render_AppendArray_CreatedInPlush(t *testing.T) { 190 | r := require.New(t) 191 | 192 | input := `<% let a = [1,2,"HelloWorld"] %><% a = a + 2.2 %><%= a %>` 193 | s, err := plush.Render(input, plush.NewContext()) 194 | r.NoError(err) 195 | r.Equal(s, "12HelloWorld2.2") 196 | } 197 | func Test_Render_AppendArray_WithTypeInterface(t *testing.T) { 198 | r := require.New(t) 199 | ctx := plush.NewContext() 200 | 201 | ctx.Set("myArray", []interface{}{"a", "b"}) 202 | input := `<% let a = myArray %><% a = a + 1 %><%= a %>` 203 | s, err := plush.Render(input, ctx) 204 | log.Println(s) 205 | r.NoError(err) 206 | r.Equal("ab1", s) 207 | } 208 | 209 | type Category1 struct { 210 | Products []Product1 211 | } 212 | type Product1 struct { 213 | Name []string 214 | } 215 | 216 | func Test_Render_Access_CalleeArray(t *testing.T) { 217 | tests := []struct { 218 | name string 219 | success bool 220 | expected string 221 | data Category1 222 | }{ 223 | {"success", true, "Buffalo", Category1{ 224 | []Product1{ 225 | {Name: []string{"Buffalo"}}, 226 | }, 227 | }}, 228 | {"outofbounds", false, "", Category1{}}, 229 | } 230 | 231 | for _, tc := range tests { 232 | t.Run(tc.name, func(t *testing.T) { 233 | r := require.New(t) 234 | input := `<% let a = product_listing.Products[0].Name[0] %><%= a %>` 235 | 236 | ctx := plush.NewContext() 237 | ctx.Set("product_listing", tc.data) 238 | 239 | s, err := plush.Render(input, ctx) 240 | if tc.success { 241 | r.NoError(err) 242 | } else { 243 | r.Error(err) 244 | } 245 | r.Equal(tc.expected, s) 246 | }) 247 | } 248 | 249 | } 250 | -------------------------------------------------------------------------------- /variadic_test.go: -------------------------------------------------------------------------------- 1 | package plush_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gobuffalo/plush/v5" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func Test_VariadicHelper(t *testing.T) { 11 | r := require.New(t) 12 | input := `<%= foo(1, 2, 3) %>` 13 | ctx := plush.NewContext() 14 | ctx.Set("foo", func(args ...int) int { 15 | return len(args) 16 | }) 17 | 18 | s, err := plush.Render(input, ctx) 19 | r.NoError(err) 20 | r.Equal("3", s) 21 | } 22 | 23 | func Test_VariadicHelper_SecondArg(t *testing.T) { 24 | r := require.New(t) 25 | input := `<%= foo("hello") %>` 26 | ctx := plush.NewContext() 27 | ctx.Set("foo", func(s string, args ...interface{}) string { 28 | return s 29 | }) 30 | 31 | s, err := plush.Render(input, ctx) 32 | r.NoError(err) 33 | r.Equal("hello", s) 34 | } 35 | 36 | func Test_VariadicHelperNoParam(t *testing.T) { 37 | r := require.New(t) 38 | input := `<%= foo() %>` 39 | ctx := plush.NewContext() 40 | ctx.Set("foo", func(args ...int) int { 41 | return len(args) 42 | }) 43 | 44 | s, err := plush.Render(input, ctx) 45 | r.NoError(err) 46 | r.Equal("0", s) 47 | } 48 | 49 | func Test_VariadicHelperNoVariadicParam(t *testing.T) { 50 | r := require.New(t) 51 | input := `<%= foo(1) %>` 52 | ctx := plush.NewContext() 53 | ctx.Set("foo", func(a int, args ...int) int { 54 | return a + len(args) 55 | }) 56 | 57 | s, err := plush.Render(input, ctx) 58 | r.NoError(err) 59 | r.Equal("1", s) 60 | } 61 | 62 | func Test_VariadicHelperWithWrongParam(t *testing.T) { 63 | r := require.New(t) 64 | input := `<%= foo(1, 2, "test") %>` 65 | ctx := plush.NewContext() 66 | ctx.Set("foo", func(args ...int) int { 67 | return len(args) 68 | }) 69 | 70 | _, err := plush.Render(input, ctx) 71 | r.Error(err) 72 | r.Contains(err.Error(), "test (string) is an invalid argument for foo at pos 2: expected (int)") 73 | } 74 | --------------------------------------------------------------------------------