Skip to content

Instantly share code, notes, and snippets.

@a-h

a-h/main_test.go Secret

Created March 30, 2022 11:24
Show Gist options
  • Save a-h/c7c2a04afd5605c48496e5b346956455 to your computer and use it in GitHub Desktop.
Save a-h/c7c2a04afd5605c48496e5b346956455 to your computer and use it in GitHub Desktop.
Describe how templ could be modified to use { instead of {% and {%=
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