-
-
Save a-h/c7c2a04afd5605c48496e5b346956455 to your computer and use it in GitHub Desktop.
Describe how templ could be modified to use { instead of {% and {%=
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package templlang | |
import ( | |
"errors" | |
"io" | |
"strings" | |
"testing" | |
"unicode" | |
"github.com/a-h/lexical/input" | |
"github.com/a-h/lexical/parse" | |
) | |
func parseUntil(combiner parse.MultipleResultCombiner, p parse.Function, delimiter parse.Function) parse.Function { | |
return func(pi parse.Input) parse.Result { | |
name := "function until delimiter" | |
results := make([]interface{}, 0) | |
for { | |
current := pi.Index() | |
ds := delimiter(pi) | |
if ds.Success { | |
rewind(pi, int(pi.Index()-current)) | |
break | |
} | |
pr := p(pi) | |
if pr.Error != nil { | |
return parse.Failure(name, pr.Error) | |
} | |
if !pr.Success { | |
return parse.Failure(name+": failed to match function", nil) | |
} | |
results = append(results, pr.Item) | |
} | |
item, ok := combiner(results) | |
if !ok { | |
return parse.Failure("until", errors.New("failed to combine results")) | |
} | |
return parse.Success("until", item, nil) | |
} | |
} | |
func rewind(pi parse.Input, times int) (err error) { | |
for i := 0; i < times; i++ { | |
_, err = pi.Retreat() | |
if err != nil { | |
return | |
} | |
} | |
return | |
} | |
var openBrace = parse.Rune('{') | |
var closeBrace = parse.Rune('}') | |
var closeBraceWithOptionalPadding = parse.Then(parse.WithStringConcatCombiner, | |
parse.Optional(parse.WithStringConcatCombiner, | |
parse.RuneInRanges(unicode.White_Space), | |
), | |
closeBrace, | |
) | |
type expression string | |
type expressionParser struct { | |
startBraceCount int | |
} | |
func (p expressionParser) Parse(pi parse.Input) parse.Result { | |
braceCount := p.startBraceCount | |
var sb strings.Builder | |
var r rune | |
var err error | |
loop: | |
for { | |
// Try to read a string literal first. | |
result := string_lit(pi) | |
if result.Error != nil { | |
return result | |
} | |
if result.Success { | |
sb.WriteString(string(result.Item.(string))) | |
continue | |
} | |
// Also try for a rune literal. | |
result = rune_lit(pi) | |
if result.Error != nil { | |
return result | |
} | |
if result.Success { | |
sb.WriteString(string(result.Item.(string))) | |
continue | |
} | |
// Try opener. | |
result = openBrace(pi) | |
if result.Error != nil { | |
return result | |
} | |
if result.Success { | |
braceCount++ | |
sb.WriteRune(result.Item.(rune)) | |
continue | |
} | |
// Try closer. | |
result = closeBraceWithOptionalPadding(pi) | |
if result.Error != nil { | |
return result | |
} | |
if result.Success { | |
braceCount-- | |
if braceCount < 0 { | |
return parse.Failure("expression: too many closing braces", nil) | |
} | |
if braceCount == 0 { | |
break loop | |
} | |
sb.WriteString(result.Item.(string)) | |
continue | |
} | |
// Read anything else. | |
r, err = pi.Advance() | |
if err != nil { | |
if errors.Is(err, io.EOF) { | |
break loop | |
} | |
return parse.Failure("expression: failed to read", err) | |
} | |
if r == '\n' { | |
return parse.Failure("expression: unexpected end of line", nil) | |
} | |
sb.WriteRune(r) | |
} | |
if braceCount != 0 { | |
return parse.Failure("expression: unexpected brace count", nil) | |
} | |
return parse.Success("expression", expression(sb.String()), nil) | |
} | |
// # List of situations where a templ file could contain braces. | |
// Inside a HTML attribute. | |
// <a style="font-family: { arial }">That does not make sense, but still...</a> | |
// Inside a script tag. | |
// <script>var value = { test: 123 };</script> | |
// Inside a templ definition expression. | |
// { templ Name(data map[string]interface{}) } | |
// Inside a templ script. | |
// { script Name(data map[string]interface{}) } | |
// { something } | |
// { endscript } | |
// Inside a call to a template, passing some data. | |
// {! localisations(map[string]interface{} { "key": 123 }) } | |
// Inside a string. | |
// {! localisations("\"value{'data'}") } | |
// Inside a tick string. | |
// {! localisations(`value{'data'}`) } | |
// Parser logic... | |
// Read until ( ` | " | { | } | EOL/EOF ) | |
// If " handle any escaped quotes or ticks until the end of the string. | |
// If ` read until the closing tick. | |
// If { increment the brace count up | |
// If } increment the brace count down | |
// If brace count == 0, break | |
// If EOL, break | |
// If EOF, break | |
// If brace count != 0 throw an error | |
func TestRuneLiterals(t *testing.T) { | |
tests := []struct { | |
name string | |
input string | |
expected string | |
}{ | |
{ | |
name: "rune literal with escaped newline", | |
input: `'\n' `, | |
expected: `'\n'`, | |
}, | |
} | |
for _, tt := range tests { | |
t.Run(tt.name, func(t *testing.T) { | |
pr := rune_lit(input.NewFromString(tt.input)) | |
if pr.Error != nil && pr.Error != io.EOF { | |
t.Fatalf("unexpected error: %v", pr.Error) | |
} | |
if !pr.Success { | |
t.Fatalf("unexpected failure for input %q: %v", tt.input, pr) | |
} | |
actual, ok := pr.Item.(string) | |
if !ok { | |
t.Fatalf("unexpectedly not a string, got %t (%v) instead", pr.Item, pr.Item) | |
} | |
if string(actual) != tt.expected { | |
t.Errorf("expected vs got:\n%q\n%q", tt.expected, actual) | |
} | |
}) | |
} | |
} | |
func TestStringLiterals(t *testing.T) { | |
tests := []struct { | |
name string | |
input string | |
expected string | |
}{ | |
{ | |
name: "string literal with escaped newline", | |
input: `"\n" `, | |
expected: `"\n"`, | |
}, | |
{ | |
name: "raw literal with \n", | |
input: "`\\n` ", | |
expected: "`\\n`", | |
}, | |
} | |
for _, tt := range tests { | |
t.Run(tt.name, func(t *testing.T) { | |
pr := string_lit(input.NewFromString(tt.input)) | |
if pr.Error != nil && pr.Error != io.EOF { | |
t.Fatalf("unexpected error: %v", pr.Error) | |
} | |
if !pr.Success { | |
t.Fatalf("unexpected failure for input %q: %v", tt.input, pr) | |
} | |
actual, ok := pr.Item.(string) | |
if !ok { | |
t.Fatalf("unexpectedly not a string, got %t (%v) instead", pr.Item, pr.Item) | |
} | |
if string(actual) != tt.expected { | |
t.Errorf("expected vs got:\n%q\n%q", tt.expected, actual) | |
} | |
}) | |
} | |
} | |
func TestExpressions(t *testing.T) { | |
tests := []struct { | |
name string | |
input string | |
prefix string | |
startBraceCount int | |
expected string | |
}{ | |
{ | |
name: "templ: no parameters", | |
input: "{ templ TemplName() }\n", | |
prefix: "{ templ ", | |
startBraceCount: 1, | |
expected: "TemplName()", | |
}, | |
{ | |
name: "templ: string parameter", | |
input: `{ templ TemplName(a string) }`, | |
prefix: "{ templ ", | |
startBraceCount: 1, | |
expected: `TemplName(a string)`, | |
}, | |
{ | |
name: "templ: map parameter", | |
input: `{ templ TemplName(data map[string]interface{}) }`, | |
prefix: "{ templ ", | |
startBraceCount: 1, | |
expected: `TemplName(data map[string]interface{})`, | |
}, | |
{ | |
name: "call: string parameter", | |
input: `{! Header("test") }`, | |
prefix: "{! ", | |
startBraceCount: 1, | |
expected: `Header("test")`, | |
}, | |
{ | |
name: "call: string parameter with escaped values and mismatched braces", | |
input: `{! Header("\"}}") }`, | |
prefix: "{! ", | |
startBraceCount: 1, | |
expected: `Header("\"}}")`, | |
}, | |
{ | |
name: "call: string parameter, with rune literals", | |
input: `{! Header('\"') }`, | |
prefix: "{! ", | |
startBraceCount: 1, | |
expected: `Header('\"')`, | |
}, | |
{ | |
name: "call: map literal", | |
input: `{! Header(map[string]interface{}{ "test": 123 }) }`, | |
prefix: "{! ", | |
startBraceCount: 1, | |
expected: `Header(map[string]interface{}{ "test": 123 })`, | |
}, | |
{ | |
name: "call: rune and map literal", | |
input: `{! Header('\"', map[string]interface{}{ "test": 123 }) }`, | |
prefix: "{! ", | |
startBraceCount: 1, | |
expected: `Header('\"', map[string]interface{}{ "test": 123 })`, | |
}, | |
{ | |
name: "if: function call", | |
input: `{ if findOut("}") }`, | |
prefix: "{ if ", | |
startBraceCount: 1, | |
expected: `findOut("}")`, | |
}, | |
{ | |
name: "if: function call, tricky string/rune params", | |
input: `{ if findOut("}", '}', '\'') }`, | |
prefix: "{ if ", | |
startBraceCount: 1, | |
expected: `findOut("}", '}', '\'')`, | |
}, | |
{ | |
name: "if: function call, function param", | |
input: `{ if findOut(func() bool { return true }) }`, | |
prefix: "{ if ", | |
startBraceCount: 1, | |
expected: `findOut(func() bool { return true })`, | |
}, | |
{ | |
name: "attribute value: simple string", | |
// Used to be {%= "data" %}, but can be simplified, since the position | |
// of the node in the document defines how it can be used. | |
// As an attribute value, it must be a Go expression that returns a string. | |
input: `{ "data" }`, | |
prefix: "{ ", | |
startBraceCount: 1, | |
expected: `"data"`, | |
}, | |
} | |
for _, tt := range tests { | |
t.Run(tt.name, func(t *testing.T) { | |
ep := &expressionParser{ | |
startBraceCount: tt.startBraceCount, | |
} | |
expr := tt.input[len(tt.prefix):] | |
pr := ep.Parse(input.NewFromString(expr)) | |
if pr.Error != nil && pr.Error != io.EOF { | |
t.Fatalf("unexpected error: %v", pr.Error) | |
} | |
if !pr.Success { | |
t.Fatalf("unexpected failure for input %q: %v", expr, pr) | |
} | |
actual, ok := pr.Item.(expression) | |
if !ok { | |
t.Fatalf("unexpectedly not an expression, got %t (%v) instead", pr.Item, pr.Item) | |
} | |
if string(actual) != tt.expected { | |
t.Errorf("expected vs got:\n%q\n%q", tt.expected, actual) | |
} | |
}) | |
} | |
} | |
// Letters and digits | |
var octal_digit = parse.RuneIn("01234567") | |
var hex_digit = parse.RuneIn("0123456789ABCDEFabcdef") | |
// https://go.dev/ref/spec#Rune_literals | |
var rune_lit = parse.All(parse.WithStringConcatCombiner, | |
parse.Rune('\''), | |
parseUntil(parse.WithStringConcatCombiner, | |
parse.Or(unicode_value_rune, byte_value), | |
parse.Rune('\''), | |
), | |
parse.Rune('\''), | |
) | |
var unicode_value_rune = parse.Any(little_u_value, big_u_value, escaped_char, parse.RuneNotIn("'")) | |
//byte_value = octal_byte_value | hex_byte_value . | |
var byte_value = parse.Any(octal_byte_value, hex_byte_value) | |
//octal_byte_value = `\` octal_digit octal_digit octal_digit . | |
var octal_byte_value = parse.All(parse.WithStringConcatCombiner, | |
parse.String(`\`), | |
octal_digit, octal_digit, octal_digit, | |
) | |
//hex_byte_value = `\` "x" hex_digit hex_digit . | |
var hex_byte_value = parse.All(parse.WithStringConcatCombiner, | |
parse.String(`\x`), | |
hex_digit, hex_digit, | |
) | |
//little_u_value = `\` "u" hex_digit hex_digit hex_digit hex_digit . | |
var little_u_value = parse.All(parse.WithStringConcatCombiner, | |
parse.String(`\u`), | |
hex_digit, hex_digit, | |
hex_digit, hex_digit, | |
) | |
//big_u_value = `\` "U" hex_digit hex_digit hex_digit hex_digit | |
var big_u_value = parse.All(parse.WithStringConcatCombiner, | |
parse.String(`\U`), | |
hex_digit, hex_digit, hex_digit, hex_digit, | |
hex_digit, hex_digit, hex_digit, hex_digit, | |
) | |
//escaped_char = `\` ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | `\` | "'" | `"` ) . | |
var escaped_char = parse.All(parse.WithStringConcatCombiner, | |
parse.Rune('\\'), | |
parse.Any( | |
parse.Rune('a'), | |
parse.Rune('b'), | |
parse.Rune('f'), | |
parse.Rune('n'), | |
parse.Rune('r'), | |
parse.Rune('t'), | |
parse.Rune('v'), | |
parse.Rune('\\'), | |
parse.Rune('\''), | |
parse.Rune('"'), | |
), | |
) | |
// https://go.dev/ref/spec#String_literals | |
var string_lit = parse.Or(interpreted_string_lit, raw_string_lit) | |
var interpreted_string_lit = parse.All(parse.WithStringConcatCombiner, | |
parse.Rune('"'), | |
parseUntil(parse.WithStringConcatCombiner, | |
parse.Or(unicode_value_interpreted, byte_value), | |
parse.Rune('"'), | |
), | |
parse.Rune('"'), | |
) | |
var unicode_value_interpreted = parse.Any(little_u_value, big_u_value, escaped_char, parse.RuneNotIn("\n\"")) | |
var raw_string_lit = parse.All(parse.WithStringConcatCombiner, | |
parse.Rune('`'), | |
parseUntil(parse.WithStringConcatCombiner, | |
unicode_value_raw, | |
parse.Rune('`'), | |
), | |
parse.Rune('`'), | |
) | |
var unicode_value_raw = parse.Any(little_u_value, big_u_value, escaped_char, parse.RuneNotIn("`")) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment