diff --git a/gwirl-lsp/template_utils.go b/gwirl-lsp/template_utils.go
index 4bd6100..77b0be6 100644
--- a/gwirl-lsp/template_utils.go
+++ b/gwirl-lsp/template_utils.go
@@ -251,13 +251,20 @@ func absTokensForContent(tt []parser.TemplateTree2) []absToken {
case parser.TT2GoExp:
length := len(t.Text)
var atToken absToken
- if t.Metadata.Has(parser.TTMDEscape) {
+ if t.Metadata.Has(parser.TTMDSafe) && !t.Metadata.Has(parser.TTMDEscape) {
+ atToken = NewAbsToken(startLine, startCol-2, 2, lsp.SemanticTokenOperator)
+ } else if t.Metadata.Has(parser.TTMDEscape) && t.Metadata.Has(parser.TTMDSafe) {
+ atToken = NewAbsToken(startLine, startCol-3, 3, lsp.SemanticTokenOperator)
+ } else if t.Metadata.Has(parser.TTMDEscape) {
atToken = NewAbsToken(startLine, startCol-2, 2, lsp.SemanticTokenOperator)
} else {
atToken = NewAbsToken(startLine, startCol-1, 1, lsp.SemanticTokenOperator)
}
token := NewAbsToken(startLine, startCol, length, lsp.SemanticTokenParameter)
tokens = append(tokens, atToken, token)
+ if t.Metadata.Has(parser.TTMDSafe) {
+ tokens = append(tokens, NewAbsToken(startLine, startCol + uint32(length), 1, lsp.SemanticTokenOperator))
+ }
if t.Children == nil {
continue
}
diff --git a/internal/parser/nodesv2.go b/internal/parser/nodesv2.go
index 922ac04..c44c824 100644
--- a/internal/parser/nodesv2.go
+++ b/internal/parser/nodesv2.go
@@ -99,6 +99,7 @@ type MetadataFlag int
const (
TTMDEscape MetadataFlag = 1 << iota
+ TTMDSafe = 2
)
func (f MetadataFlag) Has(flag MetadataFlag) bool { return f&flag != 0 }
@@ -188,6 +189,20 @@ func NewTT2GoExp(content string, escape bool, transclusions [][]TemplateTree2) T
}
}
+func NewTT2GoExpSafe(content string, escape bool) TemplateTree2 {
+ var metadata MetadataFlag
+ metadata.Set(TTMDSafe)
+ if escape {
+ metadata.Set(TTMDEscape)
+ }
+ return TemplateTree2{
+ Type: TT2GoExp,
+ Text: content,
+ Metadata: metadata,
+ Children: [][]TemplateTree2{},
+ }
+}
+
func NewTT2BlockComment(content string) TemplateTree2 {
return TemplateTree2{
Type: TT2BlockComment,
diff --git a/internal/parser/parse_expressions_test.go b/internal/parser/parse_expressions_test.go
new file mode 100644
index 0000000..89a04ce
--- /dev/null
+++ b/internal/parser/parse_expressions_test.go
@@ -0,0 +1,68 @@
+package parser_test
+
+import (
+ "testing"
+
+ "github.com/gamebox/gwirl/internal/parser"
+)
+
+var expressionTests = []ParsingTest{
+ {"simple expression", "@foobar\"", parser.NewTT2GoExp("foobar", false, noChildren)},
+ {"simple method", "@foobar()\"", parser.NewTT2GoExp("foobar()", false, noChildren)},
+ {"complex expression", "@foo.bar\"", parser.NewTT2GoExp("foo.bar", false, noChildren)},
+ {"complex method", "@foo.bar()\"", parser.NewTT2GoExp("foo.bar()", false, noChildren)},
+ {"complex method with params", "@foo.bar(param1, param2)\"", parser.NewTT2GoExp("foo.bar(param1, param2)", false, noChildren)},
+ {"complex method with literal params", "@foo.bar(\"hello\", 123)\"", parser.NewTT2GoExp("foo.bar(\"hello\", 123)", false, noChildren)},
+ {"complex method with struct literal param", "@foo.bar(MyStruct{something, else}, 123)\"", parser.NewTT2GoExp("foo.bar(MyStruct{something, else}, 123)", false, noChildren)},
+ {"complex method with chaining", "@foo.bar().something.else\"", parser.NewTT2GoExp("foo.bar().something.else", false, noChildren)},
+ {"complex method with params with chaining", "@foo.bar(param1, param2).something.else\"", parser.NewTT2GoExp("foo.bar(param1, param2).something.else", false, noChildren)},
+ {"complex method with literal params with chaining", "@foo.bar(\"hello\", 123).something.else\"", parser.NewTT2GoExp("foo.bar(\"hello\", 123).something.else", false, noChildren)},
+
+ // Transclusion tests
+ {
+ "simple method with transclusion",
+ "@foobar() {\n\t
Hello
\n}",
+ parser.NewTT2GoExp(
+ "foobar()",
+ false,
+ simpleTransclusionChildren,
+ ),
+ },
+
+ {
+ "complex method with transclusion",
+ "@foo.bar() {\n\tHello
\n}",
+ parser.NewTT2GoExp(
+ "foo.bar()",
+ false,
+ simpleTransclusionChildren,
+ ),
+ },
+
+ {
+ "complex method with param with transclusion",
+ "@foo.bar(param1, param2) {\n\tHello
\n}",
+ parser.NewTT2GoExp(
+ "foo.bar(param1, param2)",
+ false,
+ simpleTransclusionChildren,
+ ),
+ },
+
+ {
+ "complex method with literal params with transclusion",
+ "@foo.bar(\"hello\", 123) {\n\tHello
\n}",
+ parser.NewTT2GoExp(
+ "foo.bar(\"hello\", 123)",
+ false,
+ simpleTransclusionChildren,
+ ),
+ },
+}
+
+func TestExpressionParsing(t *testing.T) {
+ runParserTest(expressionTests, t, func (p *parser.Parser2) *parser.TemplateTree2 {
+ return p.Expression()
+ },"")
+}
+
diff --git a/internal/parser/parse_safe_expression_test.go b/internal/parser/parse_safe_expression_test.go
new file mode 100644
index 0000000..8d92d8d
--- /dev/null
+++ b/internal/parser/parse_safe_expression_test.go
@@ -0,0 +1,26 @@
+package parser_test
+
+import (
+ "testing"
+
+ "github.com/gamebox/gwirl/internal/parser"
+)
+
+var safeExpressionTests = []ParsingTest{
+ {"simple expression", "@(foobar)a", parser.NewTT2GoExpSafe("foobar", false)},
+ {"complex expression", "@(foo.bar)a", parser.NewTT2GoExpSafe("foo.bar", false)},
+ {"complex method with chaining", "@(foo.bar().something.else)a", parser.NewTT2GoExpSafe("foo.bar().something.else", false)},
+ {"complex method with params with chaining", "@(foo.bar(param1, param2).something.else)a", parser.NewTT2GoExpSafe("foo.bar(param1, param2).something.else", false)},
+ {"complex method with literal params with chaining", "@(foo.bar(\"hello\", 123).something.else)a", parser.NewTT2GoExpSafe("foo.bar(\"hello\", 123).something.else", false)},
+ {"escaped simple expression", "@!(foobar)a", parser.NewTT2GoExpSafe("foobar", true)},
+ {"escaped complex expression", "@!(foo.bar)a", parser.NewTT2GoExpSafe("foo.bar", true)},
+ {"escaped complex method with chaining", "@!(foo.bar().something.else)a", parser.NewTT2GoExpSafe("foo.bar().something.else", true)},
+ {"escaped complex method with params with chaining", "@!(foo.bar(param1, param2).something.else)a", parser.NewTT2GoExpSafe("foo.bar(param1, param2).something.else", true)},
+ {"escaped complex method with literal params with chaining", "@!(foo.bar(\"hello\", 123).something.else)a", parser.NewTT2GoExpSafe("foo.bar(\"hello\", 123).something.else", true)},
+}
+
+func TestParseSafeExpression(t *testing.T) {
+ runParserTest(safeExpressionTests, t, func(p *parser.Parser2) *parser.TemplateTree2 {
+ return p.SafeExpression()
+ }, "")
+}
diff --git a/internal/parser/parserv2.go b/internal/parser/parserv2.go
index 8c3842b..2b3821e 100644
--- a/internal/parser/parserv2.go
+++ b/internal/parser/parserv2.go
@@ -374,7 +374,28 @@ func expressionContainsKeyword(expressionCode string) (string, bool) {
return "", false
}
-func (p *Parser2) expression() *TemplateTree2 {
+func (p *Parser2) SafeExpression() *TemplateTree2 {
+ p.log("SafeExpression")
+ escape := p.checkStr("@!(")
+ if !escape && !p.checkStr("@(") {
+ return nil
+ }
+ var t *TemplateTree2 = nil
+ p.input.regress(1)
+ pos := p.input.offset() + 1
+ code := p.parentheses(true)
+ if code == nil {
+ return t
+ }
+ content := strings.TrimPrefix(strings.TrimSuffix(*code, ")"), "(")
+ exp := NewTT2GoExpSafe(content, escape)
+ t = &exp
+ p.position(t, pos)
+
+ return t
+}
+
+func (p *Parser2) Expression() *TemplateTree2 {
p.log("expression")
if !p.checkStr("@") {
return nil
@@ -622,8 +643,14 @@ func (p *Parser2) Mixed() *TemplateTree2 {
p.logf("mixedOpt1: got plain: %v", plain)
return plain
}
+ p.logf("mixedOpt1: trying SafeExpression")
+ safeExp := p.SafeExpression()
+ if safeExp != nil {
+ p.logf("SafeExpression was not null: %v\n", safeExp)
+ return safeExp
+ }
p.logf("mixedOpt1: trying expression")
- exp := p.expression()
+ exp := p.Expression()
if exp != nil {
p.logf("expression was not null: %v\n", exp)
return exp
diff --git a/internal/parser/testdata/testAll.html.gwirl b/internal/parser/testdata/testAll.html.gwirl
index b413177..54a08b6 100644
--- a/internal/parser/testdata/testAll.html.gwirl
+++ b/internal/parser/testdata/testAll.html.gwirl
@@ -15,5 +15,6 @@
B.O.B.
} @else {
@name
+ This is a example of a need for a @(name)Safe expression
}
diff --git a/internal/parser/testing_fixtures_test.go b/internal/parser/testing_fixtures_test.go
new file mode 100644
index 0000000..3a9713b
--- /dev/null
+++ b/internal/parser/testing_fixtures_test.go
@@ -0,0 +1,79 @@
+package parser_test
+
+import (
+ "os"
+ "testing"
+
+ "github.com/gamebox/gwirl/internal/parser"
+)
+
+type ParsingTest struct {
+ name string
+ input string
+ expected parser.TemplateTree2
+}
+var noChildren = [][]parser.TemplateTree2{}
+var simpleTransclusionChildren = [][]parser.TemplateTree2{
+ {
+ parser.NewTT2Plain("\n\tHello
\n"),
+ },
+}
+
+func compareTrees(a parser.TemplateTree2, b parser.TemplateTree2, t *testing.T) {
+ if a.Text != b.Text {
+ t.Fatalf("Expected \"%s\" but got \"%s\"", a.Text, b.Text)
+ }
+ if a.Type != b.Type {
+ t.Fatalf("Expected type %d but got %d", a.Type, b.Type)
+ }
+ if a.Metadata != b.Metadata {
+ t.Fatalf("Expected metadata %d but got %d", a.Metadata, b.Metadata)
+ }
+ if a.Children == nil && b.Children != nil {
+ t.Fatal("Expected the children to be nil")
+ }
+ if a.Children != nil && b.Children == nil {
+ t.Fatal("Expected the children to not be nil")
+ }
+ if len(a.Children) != len(b.Children) {
+ t.Fatalf("Expected %d children, got %d children", len(a.Children), len(b.Children))
+ }
+ for i := range a.Children {
+ aChildTree, bChildtree := a.Children[i], b.Children[i]
+ if aChildTree == nil && bChildtree != nil {
+ t.Fatal("Expected the children to be nil")
+ }
+ if aChildTree != nil && bChildtree == nil {
+ t.Fatal("Expected the children to not be nil")
+ }
+ if len(aChildTree) != len(bChildtree) {
+ t.Fatalf("Expected child to have %d trees, got %d trees", len(a.Children), len(b.Children))
+ }
+ for childIdx := range aChildTree {
+ compareTrees(aChildTree[childIdx], bChildtree[childIdx], t)
+ }
+ }
+}
+
+func runParserTest(tests []ParsingTest, t *testing.T, parseFn func(*parser.Parser2) *parser.TemplateTree2, debug string) {
+ for i := range tests {
+ test := tests[i]
+ if debug != "" && debug != test.name {
+ continue
+ }
+ success := t.Run(test.name, func(t *testing.T) {
+ p := parser.NewParser2(test.input)
+ if debug != "" {
+ p.SetLogger(os.Stdout)
+ }
+ res := parseFn(&p)
+ if res == nil {
+ t.Fatal("Expected a result, got nil")
+ }
+ compareTrees(test.expected, *res, t)
+ })
+ if !success {
+ t.FailNow()
+ }
+ }
+}