diff --git a/cmd/ego/main.go b/cmd/ego/main.go
index d17cb08..73813a6 100644
--- a/cmd/ego/main.go
+++ b/cmd/ego/main.go
@@ -62,11 +62,6 @@ func run(args []string) error {
continue
}
- // Ignore files without an .ego extension.
- if filepath.Ext(path) != ".ego" {
- continue
- }
-
// Process individual file.
if err := processFile(path); err != nil {
return err
@@ -82,7 +77,14 @@ func processDir(path string) error {
return err
}
for _, fi := range fis {
- if err := processFile(filepath.Join(path, fi.Name())); err != nil {
+ filePath := filepath.Join(path, fi.Name())
+ if fi.IsDir() {
+ if err := processDir(filePath); err != nil {
+ return err
+ }
+ continue
+ }
+ if err := processFile(filePath); err != nil {
return err
}
}
diff --git a/ego.go b/ego.go
index 75ed3aa..06ca60e 100644
--- a/ego.go
+++ b/ego.go
@@ -270,12 +270,15 @@ type RawPrintBlock struct {
// ComponentStartBlock represents the opening block of an ego component.
type ComponentStartBlock struct {
Pos Pos
+ StartI int
+ EndI int
Package string
Name string
Closed bool
Fields []*Field
Attrs []*Attr
AttrBlocks []*AttrStartBlock
+ XMLNS []string
Yield []Block
}
@@ -290,6 +293,8 @@ func (blk *ComponentStartBlock) Namespace() string {
// ComponentEndBlock represents the closing block of an ego component.
type ComponentEndBlock struct {
Pos Pos
+ StartI int
+ EndI int
Package string
Name string
}
diff --git a/parse.go b/parse.go
index 8c00702..71347b0 100644
--- a/parse.go
+++ b/parse.go
@@ -28,9 +28,12 @@ func Parse(r io.Reader, path string) (*Template, error) {
return nil, err
}
+ var blocks []Block
+
switch blk := blk.(type) {
case *ComponentStartBlock:
- if err := parseComponentBlock(s, blk); err != nil {
+ blocks, err = parseComponentBlock(s, blk, blk.XMLNS)
+ if err != nil {
return nil, err
}
case *ComponentEndBlock:
@@ -39,50 +42,69 @@ func Parse(r io.Reader, path string) (*Template, error) {
return nil, NewSyntaxError(blk.Pos, "Attribute start block found outside of component: %s", shortComponentBlockString(blk))
case *AttrEndBlock:
return nil, NewSyntaxError(blk.Pos, "Attribute end block found outside of component: %s", shortComponentBlockString(blk))
+ default:
+ blocks = []Block{blk}
}
- t.Blocks = append(t.Blocks, blk)
+ t.Blocks = append(t.Blocks, blocks...)
}
t.Blocks = normalizeBlocks(t.Blocks)
return t, nil
}
-func parseComponentBlock(s *Scanner, start *ComponentStartBlock) error {
+func parseComponentBlock(s *Scanner, start *ComponentStartBlock, xmlns []string) ([]Block, error) {
+ isXMLNS := xmlnsMatches(start, xmlns)
+
if start.Closed {
+ if isXMLNS {
+ return parseComponentBlockXMLNS(s, start)
+ }
start.Yield = normalizeBlocks(start.Yield)
- return nil
+ return []Block{start}, nil
}
for {
blk, err := s.Scan()
if err == io.EOF {
- return NewSyntaxError(start.Pos, "Expected component close tag, found EOF: %s", shortComponentBlockString(start))
+ return nil, NewSyntaxError(start.Pos, "Expected component close tag, found EOF: %s", shortComponentBlockString(start))
} else if err != nil {
- return err
+ return nil, err
}
switch blk := blk.(type) {
case *ComponentStartBlock:
- if err := parseComponentBlock(s, blk); err != nil {
- return err
+ blocks, err := parseComponentBlock(s, blk, append(xmlns, blk.XMLNS...))
+ if err != nil {
+ return nil, err
}
- start.Yield = append(start.Yield, blk)
+ start.Yield = append(start.Yield, blocks...)
case *ComponentEndBlock:
if blk.Name != start.Name {
- return NewSyntaxError(blk.Pos, "Component end block mismatch: %s != %s", shortComponentBlockString(start), shortComponentBlockString(blk))
+ return nil, NewSyntaxError(blk.Pos, "Component end block mismatch: %s != %s", shortComponentBlockString(start), shortComponentBlockString(blk))
+ }
+ if isXMLNS {
+ startBlocks, err := parseComponentBlockXMLNS(s, start)
+ if err != nil {
+ return nil, err
+ }
+ return append(append(startBlocks, start.Yield...), s.componentEndBlockToTextBlock(blk)), nil
}
start.Yield = normalizeBlocks(start.Yield)
- return nil
+ return []Block{start}, nil
case *AttrStartBlock:
- if err := parseAttrBlock(s, blk); err != nil {
- return err
+ if isXMLNS {
+ return nil, NewSyntaxError(blk.Pos, "Attribute start block found outside of component: %s", shortComponentBlockString(blk))
+ }
+
+ if err := parseAttrBlock(s, blk, xmlns); err != nil {
+ return nil, err
}
start.AttrBlocks = append(start.AttrBlocks, blk)
case *AttrEndBlock:
- return NewSyntaxError(blk.Pos, "Attribute end block found without start block: %s", shortComponentBlockString(blk))
+ return nil, NewSyntaxError(blk.Pos, "Attribute end block found without start block: %s", shortComponentBlockString(blk))
default:
start.Yield = append(start.Yield, blk)
@@ -90,7 +112,26 @@ func parseComponentBlock(s *Scanner, start *ComponentStartBlock) error {
}
}
-func parseAttrBlock(s *Scanner, start *AttrStartBlock) error {
+func parseComponentBlockXMLNS(s *Scanner, start *ComponentStartBlock) ([]Block, error) {
+ var blocks []Block
+
+ textScanner := s.componentStartBlockScanner(start)
+
+ for {
+ blk, err := textScanner.Scan()
+ if err == io.EOF {
+ break
+ } else if err != nil {
+ return nil, err
+ }
+
+ blocks = append(blocks, blk)
+ }
+
+ return normalizeBlocks(blocks), nil
+}
+
+func parseAttrBlock(s *Scanner, start *AttrStartBlock, xmlns []string) error {
for {
blk, err := s.Scan()
if err == io.EOF {
@@ -101,10 +142,11 @@ func parseAttrBlock(s *Scanner, start *AttrStartBlock) error {
switch blk := blk.(type) {
case *ComponentStartBlock:
- if err := parseComponentBlock(s, blk); err != nil {
+ blocks, err := parseComponentBlock(s, blk, append(xmlns, blk.XMLNS...))
+ if err != nil {
return err
}
- start.Yield = append(start.Yield, blk)
+ start.Yield = append(start.Yield, blocks...)
case *ComponentEndBlock:
return NewSyntaxError(blk.Pos, "Expected attribute close block, found %s", shortComponentBlockString(blk))
@@ -124,3 +166,12 @@ func parseAttrBlock(s *Scanner, start *AttrStartBlock) error {
}
}
}
+
+func xmlnsMatches(blk *ComponentStartBlock, xmlns []string) bool {
+ for _, ns := range xmlns {
+ if blk.Package == ns {
+ return true
+ }
+ }
+ return false
+}
diff --git a/parse_test.go b/parse_test.go
new file mode 100644
index 0000000..f4e662f
--- /dev/null
+++ b/parse_test.go
@@ -0,0 +1,66 @@
+package ego_test
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/benbjohnson/ego"
+)
+
+// Ensure that a text block can be parsed.
+func TestParse(t *testing.T) {
+ t.Run("ComponentStartBlock", func(t *testing.T) {
+ t.Run("XMLNS", func(t *testing.T) {
+ tmpl, err := ego.Parse(bytes.NewBufferString(``), "tmpl.ego")
+ if err != nil {
+ t.Fatal(err)
+ } else if len(tmpl.Blocks) != 5 {
+ t.Fatalf("unexpected blocks count: %d", len(tmpl.Blocks))
+ } else if blk0, ok := tmpl.Blocks[0].(*ego.TextBlock); !ok {
+ t.Fatalf("unexpected block type [0]: %T", tmpl.Blocks[0])
+ } else if blk0.Content != `` {
+ t.Fatalf("unexpected content [2]: %T", blk2.Content)
+ } else if blk2, ok := tmpl.Blocks[4].(*ego.TextBlock); !ok {
+ t.Fatalf("unexpected block type [4]: %T", tmpl.Blocks[4])
+ } else if blk2.Content != `` {
+ t.Fatalf("unexpected content [4]: %T", blk2.Content)
+ }
+ })
+
+ t.Run("XMLNSNested", func(t *testing.T) {
+ tmpl, err := ego.Parse(bytes.NewBufferString(``), "tmpl.ego")
+ if err != nil {
+ t.Fatal(err)
+ } else if len(tmpl.Blocks) != 3 {
+ t.Fatalf("unexpected blocks count: %d", len(tmpl.Blocks))
+ } else if blk0, ok := tmpl.Blocks[0].(*ego.TextBlock); !ok {
+ t.Fatalf("unexpected block type [0]: %T", tmpl.Blocks[0])
+ } else if blk0.Content != `` {
+ t.Fatalf("unexpected content [0]: %s", blk0.Content)
+ } else if blk2, ok := tmpl.Blocks[2].(*ego.TextBlock); !ok {
+ t.Fatalf("unexpected block type [2]: %T", tmpl.Blocks[2])
+ } else if blk2.Content != `` {
+ t.Fatalf("unexpected content [2]: %s", blk2.Content)
+ }
+ })
+
+ t.Run("XMLNSClosed", func(t *testing.T) {
+ tmpl, err := ego.Parse(bytes.NewBufferString(``), "tmpl.ego")
+ if err != nil {
+ t.Fatal(err)
+ } else if len(tmpl.Blocks) != 1 {
+ t.Fatalf("unexpected blocks count: %d", len(tmpl.Blocks))
+ } else if blk, ok := tmpl.Blocks[0].(*ego.TextBlock); !ok {
+ t.Fatalf("unexpected block type [0]: %T", tmpl.Blocks[0])
+ } else if blk.Content != `` {
+ t.Fatalf("unexpected content [0]: %T", blk.Content)
+ }
+ })
+ })
+}
diff --git a/scanner.go b/scanner.go
index 480c574..9b4dd49 100644
--- a/scanner.go
+++ b/scanner.go
@@ -6,6 +6,7 @@ import (
"go/parser"
"io"
"io/ioutil"
+ "strings"
"unicode"
"unicode/utf8"
)
@@ -20,6 +21,8 @@ type Scanner struct {
i int
pos Pos
+
+ textOnly bool
}
// NewScanner initializes a new scanner with a given reader.
@@ -41,15 +44,17 @@ func (s *Scanner) Scan() (Block, error) {
switch s.peek() {
case '<':
- // Special handling for component/attr blocks.
- if s.peekComponentStartBlock() {
- return s.scanComponentStartBlock()
- } else if s.peekComponentEndBlock() {
- return s.scanComponentEndBlock()
- } else if s.peekAttrStartBlock() {
- return s.scanAttrStartBlock()
- } else if s.peekAttrEndBlock() {
- return s.scanAttrEndBlock()
+ if !s.textOnly {
+ // Special handling for component/attr blocks.
+ if s.peekComponentStartBlock() {
+ return s.scanComponentStartBlock()
+ } else if s.peekComponentEndBlock() {
+ return s.scanComponentEndBlock()
+ } else if s.peekAttrStartBlock() {
+ return s.scanAttrStartBlock()
+ } else if s.peekAttrEndBlock() {
+ return s.scanAttrEndBlock()
+ }
}
// Special handling for ego blocks.
@@ -67,6 +72,20 @@ func (s *Scanner) Scan() (Block, error) {
return s.scanTextBlock()
}
+func (s *Scanner) componentStartBlockScanner(b *ComponentStartBlock) *Scanner {
+ return &Scanner{
+ r: s.r,
+ b: s.b[:b.EndI],
+ i: b.StartI,
+ pos: b.Pos,
+ textOnly: true,
+ }
+}
+
+func (s *Scanner) componentEndBlockToTextBlock(b *ComponentEndBlock) *TextBlock {
+ return &TextBlock{Pos: s.pos, Content: string(s.b[b.StartI:b.EndI])}
+}
+
func (s *Scanner) scanTextBlock() (*TextBlock, error) {
buf := bytes.NewBufferString(s.readN(1))
b := &TextBlock{Pos: s.pos}
@@ -137,8 +156,9 @@ func (s *Scanner) peekComponentStartBlock() bool {
}
func (s *Scanner) scanComponentStartBlock() (_ *ComponentStartBlock, err error) {
- b := &ComponentStartBlock{Pos: s.pos}
+ b := &ComponentStartBlock{Pos: s.pos, StartI: s.i}
assert(s.read() == '<')
+ defer func() { b.EndI = s.i }()
// Scan package name. The ego package is reserved for local types.
if b.Package, err = s.scanIdent(); err != nil {
@@ -180,6 +200,9 @@ func (s *Scanner) scanComponentStartBlock() (_ *ComponentStartBlock, err error)
if err != nil {
return nil, err
}
+ if strings.HasPrefix(attr.Name, "xmlns:") {
+ b.XMLNS = append(b.XMLNS, attr.Name[6:])
+ }
b.Attrs = append(b.Attrs, attr)
}
@@ -205,8 +228,9 @@ func (s *Scanner) peekComponentEndBlock() bool {
}
func (s *Scanner) scanComponentEndBlock() (_ *ComponentEndBlock, err error) {
- b := &ComponentEndBlock{Pos: s.pos}
+ b := &ComponentEndBlock{Pos: s.pos, StartI: s.i}
assert(s.readN(2) == "")
+ defer func() { b.EndI = s.i }()
// Scan package name.
if b.Package, err = s.scanIdent(); err != nil {
diff --git a/scanner_test.go b/scanner_test.go
index e931769..7f4f5f4 100644
--- a/scanner_test.go
+++ b/scanner_test.go
@@ -462,6 +462,26 @@ func TestScanner(t *testing.T) {
})
})
})
+
+ t.Run("XMLNS", func(t *testing.T) {
+ s := ego.NewScanner(bytes.NewBufferString(``), "tmpl.ego")
+ if blk, err := s.Scan(); err != nil {
+ t.Fatal(err)
+ } else if blk, ok := blk.(*ego.ComponentStartBlock); !ok {
+ t.Fatalf("unexpected block type: %T", blk)
+ } else if len(blk.Attrs) != 1 {
+ t.Fatalf("unexpected attr count: %d", len(blk.Attrs))
+ } else if !reflect.DeepEqual(blk.Attrs[0], &ego.Attr{
+ Name: "xmlns:v",
+ NamePos: ego.Pos{Path: "tmpl.ego", LineNo: 1},
+ Value: `"urn:schemas-microsoft-com:vml"`,
+ ValuePos: ego.Pos{Path: "tmpl.ego", LineNo: 1}},
+ ) {
+ t.Fatalf("unexpected attr: %#v", blk.Attrs[0])
+ } else if !reflect.DeepEqual(blk.XMLNS, []string{"v"}) {
+ t.Fatalf("unexpected XMLNS: %v", blk.XMLNS)
+ }
+ })
})
t.Run("ComponentEndBlock", func(t *testing.T) {