Skip to content

Commit

Permalink
parser: support missing package instructions (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
a-h authored Mar 13, 2022
1 parent 17fe039 commit 4234940
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 124 deletions.
123 changes: 0 additions & 123 deletions parser/parser.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package parser

import (
"bufio"
"fmt"
"io"
"os"
"unicode"

"github.com/a-h/lexical/input"
"github.com/a-h/lexical/parse"
)

Expand Down Expand Up @@ -96,123 +93,3 @@ type ParseError struct {
func (pe ParseError) Error() string {
return fmt.Sprintf("%v at %v", pe.Message, pe.From)
}

// NewTemplateFileParser creates a new TemplateFileParser.
func NewTemplateFileParser() TemplateFileParser { return TemplateFileParser{} }

type TemplateFileParser struct {
}

func (p TemplateFileParser) Parse(pi parse.Input) parse.Result {
var tf TemplateFile
from := NewPositionFromInput(pi)

// Required package.
// {% package name %}
pr := newPackageParser().Parse(pi)
if pr.Error != nil {
return pr
}
if !pr.Success {
return parse.Failure("TemplateFileParser", newParseError("package not found", from, NewPositionFromInput(pi)))
}
tf.Package = pr.Item.(Package)

// Optional whitespace.
parse.Optional(parse.WithStringConcatCombiner, whitespaceParser)(pi)

// Optional imports.
// {% import "strings" %}
ip := newImportParser()
for {
ipr := ip.Parse(pi)
if ipr.Error != nil {
return ipr
}
if !ipr.Success {
break
}
tf.Imports = append(tf.Imports, ipr.Item.(Import))

// Eat optional whitespace.
parse.Optional(parse.WithStringConcatCombiner, whitespaceParser)(pi)
}

// Optional templates, CSS, and script templates.
// {% templ Name(p Parameter) %}
// {% css Name() %}
// {% script Name() %}
tp := newTemplateParser()
cssp := newCSSParser()
stp := newScriptTemplateParser()
for {
// Try for a template.
tpr := tp.Parse(pi)
if tpr.Error != nil && tpr.Error != io.EOF {
return tpr
}
if tpr.Success {
tf.Nodes = append(tf.Nodes, tpr.Item.(HTMLTemplate))
// Eat optional whitespace.
parse.Optional(parse.WithStringConcatCombiner, whitespaceParser)(pi)
continue
}
// Try for css.
cssr := cssp.Parse(pi)
if cssr.Error != nil && cssr.Error != io.EOF {
return cssr
}
if cssr.Success {
tf.Nodes = append(tf.Nodes, cssr.Item.(CSSTemplate))
// Eat optional whitespace.
parse.Optional(parse.WithStringConcatCombiner, whitespaceParser)(pi)
continue
}
// Try for script.
stpr := stp.Parse(pi)
if stpr.Error != nil && stpr.Error != io.EOF {
return stpr
}
if stpr.Success {
tf.Nodes = append(tf.Nodes, stpr.Item.(ScriptTemplate))
// Eat optional whitespace.
parse.Optional(parse.WithStringConcatCombiner, whitespaceParser)(pi)
continue
}
break
}

// Success.
return parse.Success("template file", tf, nil)
}

const maxBufferSize = 1024 * 1024 * 10 // 10MB

func Parse(fileName string) (TemplateFile, error) {
f, err := os.Open(fileName)
if err != nil {
return TemplateFile{}, err
}
fi, err := f.Stat()
if err != nil {
return TemplateFile{}, err
}
bufferSize := maxBufferSize
if fi.Size() < int64(bufferSize) {
bufferSize = int(fi.Size())
}
reader := bufio.NewReader(f)
tfr := NewTemplateFileParser().Parse(input.NewWithBufferSize(reader, bufferSize))
if tfr.Error != nil {
return TemplateFile{}, tfr.Error
}
return tfr.Item.(TemplateFile), nil
}

func ParseString(template string) (TemplateFile, error) {
tfr := NewTemplateFileParser().Parse(input.NewFromString(template))
if tfr.Error != nil {
return TemplateFile{}, tfr.Error
}
return tfr.Item.(TemplateFile), nil
}
2 changes: 1 addition & 1 deletion parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/google/go-cmp/cmp"
)

func TestParsers(t *testing.T) {
func TestWhitespace(t *testing.T) {
var tests = []struct {
name string
input string
Expand Down
163 changes: 163 additions & 0 deletions parser/templatefile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package parser

import (
"bufio"
"io"
"os"
"path/filepath"
"unicode"

"github.com/a-h/lexical/input"
"github.com/a-h/lexical/parse"
)

const maxBufferSize = 1024 * 1024 * 10 // 10MB

func Parse(fileName string) (TemplateFile, error) {
f, err := os.Open(fileName)
if err != nil {
return TemplateFile{}, err
}
fi, err := f.Stat()
if err != nil {
return TemplateFile{}, err
}
bufferSize := maxBufferSize
if fi.Size() < int64(bufferSize) {
bufferSize = int(fi.Size())
}
reader := bufio.NewReader(f)
tfr := NewTemplateFileParser(getDefaultPackageName(fileName)).Parse(input.NewWithBufferSize(reader, bufferSize))
if tfr.Error != nil {
return TemplateFile{}, tfr.Error
}
return tfr.Item.(TemplateFile), nil
}

func getDefaultPackageName(fileName string) (pkg string) {
parent := filepath.Base(filepath.Dir(fileName))
if !isGoIdentifier(parent) {
return "main"
}
return parent
}

func isGoIdentifier(s string) bool {
if len(s) == 0 {
return false
}
for i, r := range s {
if unicode.IsLetter(r) || r == '_' {
continue
}
if i > 0 && unicode.IsDigit(r) {
continue
}
return false
}
return true
}

func ParseString(template string) (TemplateFile, error) {
tfr := NewTemplateFileParser("main").Parse(input.NewFromString(template))
if tfr.Error != nil {
return TemplateFile{}, tfr.Error
}
return tfr.Item.(TemplateFile), nil
}

// NewTemplateFileParser creates a new TemplateFileParser.
func NewTemplateFileParser(pkg string) TemplateFileParser {
return TemplateFileParser{
DefaultPackage: pkg,
}
}

type TemplateFileParser struct {
DefaultPackage string
}

func (p TemplateFileParser) Parse(pi parse.Input) parse.Result {
var tf TemplateFile

// Required package.
// {% package name %}
pr := newPackageParser().Parse(pi)
if pr.Error != nil {
return pr
}
pkg, ok := pr.Item.(Package)
if !ok {
pkg = Package{
Expression: NewExpression(p.DefaultPackage, NewPosition(), NewPosition()),
}
}
tf.Package = pkg

// Optional whitespace.
parse.Optional(parse.WithStringConcatCombiner, whitespaceParser)(pi)

// Optional imports.
// {% import "strings" %}
ip := newImportParser()
for {
ipr := ip.Parse(pi)
if ipr.Error != nil {
return ipr
}
if !ipr.Success {
break
}
tf.Imports = append(tf.Imports, ipr.Item.(Import))

// Eat optional whitespace.
parse.Optional(parse.WithStringConcatCombiner, whitespaceParser)(pi)
}

// Optional templates, CSS, and script templates.
// {% templ Name(p Parameter) %}
// {% css Name() %}
// {% script Name() %}
tp := newTemplateParser()
cssp := newCSSParser()
stp := newScriptTemplateParser()
for {
// Try for a template.
tpr := tp.Parse(pi)
if tpr.Error != nil && tpr.Error != io.EOF {
return tpr
}
if tpr.Success {
tf.Nodes = append(tf.Nodes, tpr.Item.(HTMLTemplate))
// Eat optional whitespace.
parse.Optional(parse.WithStringConcatCombiner, whitespaceParser)(pi)
continue
}
// Try for css.
cssr := cssp.Parse(pi)
if cssr.Error != nil && cssr.Error != io.EOF {
return cssr
}
if cssr.Success {
tf.Nodes = append(tf.Nodes, cssr.Item.(CSSTemplate))
// Eat optional whitespace.
parse.Optional(parse.WithStringConcatCombiner, whitespaceParser)(pi)
continue
}
// Try for script.
stpr := stp.Parse(pi)
if stpr.Error != nil && stpr.Error != io.EOF {
return stpr
}
if stpr.Success {
tf.Nodes = append(tf.Nodes, stpr.Item.(ScriptTemplate))
// Eat optional whitespace.
parse.Optional(parse.WithStringConcatCombiner, whitespaceParser)(pi)
continue
}
break
}

// Success.
return parse.Success("template file", tf, nil)
}
72 changes: 72 additions & 0 deletions parser/templatefile_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package parser

import "testing"

func TestTemplateFileParser(t *testing.T) {
t.Run("does not require a package expression", func(t *testing.T) {
input := `{% templ Hello() %}
Hello
{% endtempl %}`
tf, err := ParseString(input)
if err != nil {
t.Fatalf("failed to parse template, with error: %v", err)
}
if len(tf.Nodes) != 1 {
t.Errorf("expected 1 node, got %+v", tf.Nodes)
}
if tf.Package.Expression.Value != "main" {
t.Errorf("expected the package to be 'main', because no context was provided, but got %v", tf.Package.Expression)
}
})
t.Run("but can accept a package expression, if one is provided", func(t *testing.T) {
input := `{% package main %}
{% templ Hello() %}
Hello
{% endtempl %}`
tf, err := ParseString(input)
if err != nil {
t.Fatalf("failed to parse template, with error: %v", err)
}
if len(tf.Nodes) != 1 {
t.Errorf("expected 1 node, got %+v", tf.Nodes)
}
})
}

func TestDefaultPackageName(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "standard filename",
input: "/files/on/disk/header.templ",
expected: "disk",
},
{
name: "path that starts with numbers",
input: "/files/on/123disk/header.templ",
expected: "main",
},
{
name: "path that includes hyphens",
input: "/files/on/disk-drive/header.templ",
expected: "main",
},
{
name: "relative path",
input: "header.templ",
expected: "main",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := getDefaultPackageName(tt.input)
if actual != tt.expected {
t.Errorf("expected %q got %q", tt.expected, actual)
}
})
}
}

0 comments on commit 4234940

Please sign in to comment.