Skip to content

Commit

Permalink
feat: add experimental arbitrary Go code support (#713)
Browse files Browse the repository at this point in the history
Co-authored-by: Adrian Hesketh <adrianhesketh@hushmail.com>
  • Loading branch information
joerdav and a-h authored May 12, 2024
1 parent 6a9dbff commit 750f91e
Show file tree
Hide file tree
Showing 18 changed files with 275 additions and 1 deletion.
2 changes: 1 addition & 1 deletion .version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.2.688
0.2.692
25 changes: 25 additions & 0 deletions cfg/cfg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// This package is inspired by the GOEXPERIMENT approach of allowing feature flags for experimenting with breaking changes.
package cfg

import (
"os"
"strings"
)

type Flags struct {
// RawGo will enable the support of arbibrary Go code in templates.
RawGo bool
}

var Experiment = parse()

func parse() *Flags {
m := map[string]bool{}
for _, f := range strings.Split(os.Getenv("TEMPL_EXPERIMENT"), ",") {
m[strings.ToLower(f)] = true
}

return &Flags{
RawGo: m["rawgo"],
}
}
32 changes: 32 additions & 0 deletions docs/docs/03-syntax-and-usage/09-raw-go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Raw Go

:::caution
This page describes functionality that is experimental, not enabled by default, and may change or be removed in future versions.

To enable this feature run the generation step with the `rawgo` experiment flag: `TEMPL_EXPERIMENT=rawgo templ generate`

You will also need to set the `TEMPL_EXPERIMENT=rawgo` environment variable at your system level or within your editor to enable LSP behavior.
:::

For some more advanced use cases it may be useful to write Go code statements in your template.

Use the `{{ ... }}` syntax for this.

## Variable declarations

Scoped variables can be created using this syntax, to reduce the need for multiple function calls.

```templ title="component.templ"
package main
templ nameList(items []Item) {
{{ first := items[0] }}
<p>
{ first.Name }
</p>
}
```

```html title="Output"
<p>A</p>
```
File renamed without changes.
14 changes: 14 additions & 0 deletions generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,8 @@ func (g *generator) writeNode(indentLevel int, current parser.Node, next parser.
err = g.writeSwitchExpression(indentLevel, n, next)
case parser.StringExpression:
err = g.writeStringExpression(indentLevel, n.Expression)
case parser.GoCode:
err = g.writeGoCode(indentLevel, n.Expression)
case parser.Whitespace:
err = g.writeWhitespace(indentLevel, n)
case parser.Text:
Expand Down Expand Up @@ -1317,6 +1319,18 @@ func (g *generator) createVariableName() string {
return "templ_7745c5c3_Var" + strconv.Itoa(g.variableID)
}

func (g *generator) writeGoCode(indentLevel int, e parser.Expression) (err error) {
if strings.TrimSpace(e.Value) == "" {
return
}
var r parser.Range
if r, err = g.w.WriteIndent(indentLevel, e.Value+"\n"); err != nil {
return err
}
g.sourceMap.Add(e, r)
return nil
}

func (g *generator) writeStringExpression(indentLevel int, e parser.Expression) (err error) {
if strings.TrimSpace(e.Value) == "" {
return
Expand Down
3 changes: 3 additions & 0 deletions parser/v2/expressionparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ var openBraceWithOptionalPadding = parse.Any(openBraceWithPadding, openBrace)
var closeBrace = parse.String("}")
var closeBraceWithOptionalPadding = parse.StringFrom(optionalSpaces, closeBrace)

var dblCloseBrace = parse.String("}}")
var dblCloseBraceWithOptionalPadding = parse.StringFrom(optionalSpaces, dblCloseBrace)

var openBracket = parse.String("(")
var closeBracket = parse.String(")")

Expand Down
49 changes: 49 additions & 0 deletions parser/v2/gocodeparser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package parser

import (
"github.com/a-h/parse"
"github.com/a-h/templ/cfg"
"github.com/a-h/templ/parser/v2/goexpression"
)

var goCode = parse.Func(func(pi *parse.Input) (n Node, ok bool, err error) {
if !cfg.Experiment.RawGo {
return
}
// Check the prefix first.
if _, ok, err = parse.Or(parse.String("{{ "), parse.String("{{")).Parse(pi); err != nil || !ok {
return
}

// Once we have a prefix, we must have an expression that returns a string, with optional err.
l := pi.Position().Line
var r GoCode
if r.Expression, err = parseGo("go code", pi, goexpression.Expression); err != nil {
return r, false, err
}

if l != pi.Position().Line {
r.Multiline = true
}

// Clear any optional whitespace.
_, _, _ = parse.OptionalWhitespace.Parse(pi)

// }}
if _, ok, err = dblCloseBraceWithOptionalPadding.Parse(pi); err != nil || !ok {
err = parse.Error("go code: missing close braces", pi.Position())
return
}

// Parse trailing whitespace.
ws, _, err := parse.Whitespace.Parse(pi)
if err != nil {
return r, false, err
}
r.TrailingSpace, err = NewTrailingSpace(ws)
if err != nil {
return r, false, err
}

return r, true, nil
})
118 changes: 118 additions & 0 deletions parser/v2/gocodeparser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package parser

import (
"testing"

"github.com/a-h/parse"
"github.com/a-h/templ/cfg"
"github.com/google/go-cmp/cmp"
)

func TestGoCodeParser(t *testing.T) {
flagVal := cfg.Experiment.RawGo
cfg.Experiment.RawGo = true
defer func() {
cfg.Experiment.RawGo = flagVal
}()

tests := []struct {
name string
input string
expected GoCode
}{
{
name: "basic expression",
input: `{{ p := "this" }}`,
expected: GoCode{
Expression: Expression{
Value: `p := "this"`,
Range: Range{
From: Position{
Index: 3,
Line: 0,
Col: 3,
},
To: Position{
Index: 14,
Line: 0,
Col: 14,
},
},
},
},
},
{
name: "basic expression, no space",
input: `{{p:="this"}}`,
expected: GoCode{
Expression: Expression{
Value: `p:="this"`,
Range: Range{
From: Position{
Index: 2,
Line: 0,
Col: 2,
},
To: Position{
Index: 11,
Line: 0,
Col: 11,
},
},
},
},
},
{
name: "multiline function decl",
input: `{{
p := func() {
dosomething()
}
}}`,
expected: GoCode{
Expression: Expression{
Value: `
p := func() {
dosomething()
}`,
Range: Range{
From: Position{
Index: 2,
Line: 0,
Col: 2,
},
To: Position{
Index: 45,
Line: 3,
Col: 5,
},
},
},
Multiline: true,
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
input := parse.NewInput(tt.input)
an, ok, err := goCode.Parse(input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !ok {
t.Fatalf("unexpected failure for input %q", tt.input)
}
actual := an.(GoCode)
if diff := cmp.Diff(tt.expected, actual); diff != "" {
t.Error(diff)
}

// Check the index.
cut := tt.input[actual.Expression.Range.From.Index:actual.Expression.Range.To.Index]
if tt.expected.Expression.Value != cut {
t.Errorf("range, expected %q, got %q", tt.expected.Expression.Value, cut)
}
})
}
}
1 change: 1 addition & 0 deletions parser/v2/structure.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var (
_ Node = SwitchExpression{}
_ Node = ForExpression{}
_ Node = StringExpression{}
_ Node = GoCode{}
_ Node = Whitespace{}
_ Node = DocType{}
)
Expand Down
1 change: 1 addition & 0 deletions parser/v2/templateparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ var templateNodeParsers = []parse.Parser[Node]{
callTemplateExpression, // {! TemplateName(a, b, c) }
templElementExpression, // @TemplateName(a, b, c) { <div>Children</div> }
childrenExpression, // { children... }
goCode, // {{ myval := x.myval }}
stringExpression, // { "abc" }
whitespaceExpression, // { " " }
textParser, // anything &amp; everything accepted...
Expand Down
31 changes: 31 additions & 0 deletions parser/v2/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,37 @@ func (fe ForExpression) Write(w io.Writer, indent int) error {
return nil
}

// GoCode is used within HTML elements, and allows arbitrary go code.
// {{ ... }}
type GoCode struct {
Expression Expression
// TrailingSpace lists what happens after the expression.
TrailingSpace TrailingSpace
Multiline bool
}

func (gc GoCode) Trailing() TrailingSpace {
return gc.TrailingSpace
}

func (gc GoCode) IsNode() bool { return true }
func (gc GoCode) Write(w io.Writer, indent int) error {
if isWhitespace(gc.Expression.Value) {
gc.Expression.Value = ""
}
if !gc.Multiline {
return writeIndent(w, indent, `{{ `, gc.Expression.Value, ` }}`)
}
formatted, err := format.Source([]byte(gc.Expression.Value))
if err != nil {
return err
}
if err := writeIndent(w, indent, "{{"+string(formatted)+"\n"); err != nil {
return err
}
return writeIndent(w, indent, "}}")
}

// StringExpression is used within HTML elements, and for style values.
// { ... }
type StringExpression struct {
Expand Down

0 comments on commit 750f91e

Please sign in to comment.