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) == "`), "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) {