Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add arbitrary go code support #713

Merged
merged 6 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"],
}
}
31 changes: 31 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,31 @@
# Raw Go

:::caution
This page describes functionality that is experimental, and not enabled by default.

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 to allow 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>
```
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