Skip to content

Commit

Permalink
Add passthrough attribute support.
Browse files Browse the repository at this point in the history
  • Loading branch information
benbjohnson committed Oct 13, 2018
1 parent bf2331d commit 02c2902
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 28 deletions.
49 changes: 39 additions & 10 deletions ego.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"go/parser"
"go/token"
"io"
"sort"
"strings"
)

Expand Down Expand Up @@ -84,9 +85,17 @@ func writeBlocksTo(buf *bytes.Buffer, blks []Block) {
fmt.Fprintf(buf, "EGO.%s = %s\n", field.Name, field.Value)
}

for _, attr := range blk.Attrs {
fmt.Fprintf(buf, "EGO.%s = func() {\n", attr.Name)
writeBlocksTo(buf, attr.Yield)
if len(blk.Attrs) > 0 {
fmt.Fprintf(buf, "EGO.Attrs = map[string]string{\n")
for _, attr := range blk.Attrs {
fmt.Fprintf(buf, " %q: fmt.Sprint(%s),\n", attr.Name, attr.Value)
}
fmt.Fprintf(buf, "}\n")
}

for _, attrBlock := range blk.AttrBlocks {
fmt.Fprintf(buf, "EGO.%s = func() {\n", attrBlock.Name)
writeBlocksTo(buf, attrBlock.Yield)
fmt.Fprint(buf, "}\n")
}

Expand Down Expand Up @@ -260,13 +269,14 @@ type RawPrintBlock struct {

// ComponentStartBlock represents the opening block of an ego component.
type ComponentStartBlock struct {
Pos Pos
Package string
Name string
Closed bool
Fields []*Field
Attrs []*AttrStartBlock
Yield []Block
Pos Pos
Package string
Name string
Closed bool
Fields []*Field
Attrs []*Attr
AttrBlocks []*AttrStartBlock
Yield []Block
}

// Namespace returns the block package, if defined. Otherwise returns "ego".
Expand Down Expand Up @@ -347,6 +357,15 @@ type Field struct {
ValuePos Pos
}

// Attr represents a key/value passthrough pair on a component.
type Attr struct {
Name string
NamePos Pos

Value string
ValuePos Pos
}

// Position returns the position of the block.
func Position(blk Block) Pos {
switch blk := blk.(type) {
Expand Down Expand Up @@ -390,3 +409,13 @@ type stackElem struct {
block Block
yield []Block
}

// AttrNames returns a sorted list of names for an attribute set.
func AttrNames(attrs map[string]interface{}) []string {
a := make([]string, 0, len(attrs))
for k := range attrs {
a = append(a, k)
}
sort.Strings(a)
return a
}
2 changes: 1 addition & 1 deletion parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func parseComponentBlock(s *Scanner, start *ComponentStartBlock) error {
if err := parseAttrBlock(s, blk); err != nil {
return err
}
start.Attrs = append(start.Attrs, blk)
start.AttrBlocks = append(start.AttrBlocks, blk)

case *AttrEndBlock:
return NewSyntaxError(blk.Pos, "Attribute end block found without start block: %s", shortComponentBlockString(blk))
Expand Down
72 changes: 69 additions & 3 deletions scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func (s *Scanner) scanComponentStartBlock() (_ *ComponentStartBlock, err error)
return nil, err
}

// Scan fields.
// Scan attributes & fields.
for {
s.skipWhitespace()
if ch := s.peek(); ch == '>' {
Expand All @@ -167,11 +167,20 @@ func (s *Scanner) scanComponentStartBlock() (_ *ComponentStartBlock, err error)
break
}

field, err := s.scanField()
if ch := s.peek(); unicode.IsUpper(ch) {
field, err := s.scanField()
if err != nil {
return nil, err
}
b.Fields = append(b.Fields, field)
continue
}

attr, err := s.scanAttr()
if err != nil {
return nil, err
}
b.Fields = append(b.Fields, field)
b.Attrs = append(b.Attrs, attr)
}

return b, nil
Expand Down Expand Up @@ -378,6 +387,45 @@ func (s *Scanner) scanField() (*Field, error) {
}, nil
}

func (s *Scanner) scanAttr() (*Attr, error) {
s.skipWhitespace()

// First scan is the HTML attribute name.
namePos := s.pos
name, err := s.scanAttrName()
if err != nil {
return nil, err
}
s.skipWhitespace()

// If we see an identifier or tag close then only save the name.
if ch := s.peek(); ch == '>' || isIdentStart(ch) {
return &Attr{Name: name, NamePos: namePos}, nil
} else if ch := s.peekN(2); ch == "/>" {
return &Attr{Name: name, NamePos: namePos}, nil
}

// Expect an equals sign next.
if ch := s.read(); ch != '=' {
return nil, NewSyntaxError(s.pos, "Expected '=', found %s", runeString(ch))
}
s.skipWhitespace()

// Parse expression.
valuePos := s.pos
value, err := s.scanExpr()
if err != nil {
return nil, err
}

return &Attr{
Name: name,
NamePos: namePos,
Value: value,
ValuePos: valuePos,
}, nil
}

func (s *Scanner) peekIdent() bool {
ident, _ := s.scanIdent()
return ident != ""
Expand All @@ -401,6 +449,24 @@ func (s *Scanner) scanIdent() (string, error) {
return buf.String(), nil
}

func (s *Scanner) scanAttrName() (string, error) {
var buf bytes.Buffer

// First rune must be a letter.
ch := s.read()
if !isIdentStart(ch) {
return "", NewSyntaxError(s.pos, "Expected identifier, found %s", runeString(ch))
}
buf.WriteRune(ch)

// Keep scanning while we have letters or digits.
for ch := s.peek(); unicode.IsLetter(ch) || unicode.IsDigit(ch) || ch == '_' || ch == ':' || ch == '.' || ch == '-'; ch = s.peek() {
buf.WriteRune(s.read())
}

return buf.String(), nil
}

func (s *Scanner) scanExpr() (string, error) {
var buf bytes.Buffer
pos := s.pos
Expand Down
Loading

0 comments on commit 02c2902

Please sign in to comment.