package parser import ( "bytes" "io" "strconv" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" ) var attrNameID = []byte("id") var attrNameClass = []byte("class") // An Attribute is an attribute of the markdown elements type Attribute struct { Name []byte Value interface{} } // An Attributes is a collection of attributes. type Attributes []Attribute // Find returns a (value, true) if an attribute correspond with given name is found, otherwise (nil, false). func (as Attributes) Find(name []byte) (interface{}, bool) { for _, a := range as { if bytes.Equal(a.Name, name) { return a.Value, true } } return nil, false } func (as Attributes) findUpdate(name []byte, cb func(v interface{}) interface{}) bool { for i, a := range as { if bytes.Equal(a.Name, name) { as[i].Value = cb(a.Value) return true } } return false } // ParseAttributes parses attributes into a map. // ParseAttributes returns a parsed attributes and true if could parse // attributes, otherwise nil and false. func ParseAttributes(reader text.Reader) (Attributes, bool) { savedLine, savedPosition := reader.Position() reader.SkipSpaces() if reader.Peek() != '{' { reader.SetPosition(savedLine, savedPosition) return nil, false } reader.Advance(1) attrs := Attributes{} for { if reader.Peek() == '}' { reader.Advance(1) return attrs, true } attr, ok := parseAttribute(reader) if !ok { reader.SetPosition(savedLine, savedPosition) return nil, false } if bytes.Equal(attr.Name, attrNameClass) { if !attrs.findUpdate(attrNameClass, func(v interface{}) interface{} { ret := make([]byte, 0, len(v.([]byte))+1+len(attr.Value.([]byte))) ret = append(ret, v.([]byte)...) return append(append(ret, ' '), attr.Value.([]byte)...) }) { attrs = append(attrs, attr) } } else { attrs = append(attrs, attr) } reader.SkipSpaces() if reader.Peek() == ',' { reader.Advance(1) reader.SkipSpaces() } } } func parseAttribute(reader text.Reader) (Attribute, bool) { reader.SkipSpaces() c := reader.Peek() if c == '#' || c == '.' { reader.Advance(1) line, _ := reader.PeekLine() i := 0 // HTML5 allows any kind of characters as id, but XHTML restricts characters for id. // CommonMark is basically defined for XHTML(even though it is legacy). // So we restrict id characters. for ; i < len(line) && !util.IsSpace(line[i]) && (!util.IsPunct(line[i]) || line[i] == '_' || line[i] == '-' || line[i] == ':' || line[i] == '.'); i++ { } name := attrNameClass if c == '#' { name = attrNameID } reader.Advance(i) return Attribute{Name: name, Value: line[0:i]}, true } line, _ := reader.PeekLine() if len(line) == 0 { return Attribute{}, false } c = line[0] if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == ':') { return Attribute{}, false } i := 0 for ; i < len(line); i++ { c = line[i] if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == ':' || c == '.' || c == '-') { break } } name := line[:i] reader.Advance(i) reader.SkipSpaces() c = reader.Peek() if c != '=' { return Attribute{}, false } reader.Advance(1) reader.SkipSpaces() value, ok := parseAttributeValue(reader) if !ok { return Attribute{}, false } if bytes.Equal(name, attrNameClass) { if _, ok = value.([]byte); !ok { return Attribute{}, false } } return Attribute{Name: name, Value: value}, true } func parseAttributeValue(reader text.Reader) (interface{}, bool) { reader.SkipSpaces() c := reader.Peek() var value interface{} ok := false switch c { case text.EOF: return Attribute{}, false case '{': value, ok = ParseAttributes(reader) case '[': value, ok = parseAttributeArray(reader) case '"': value, ok = parseAttributeString(reader) default: if c == '-' || c == '+' || util.IsNumeric(c) { value, ok = parseAttributeNumber(reader) } else { value, ok = parseAttributeOthers(reader) } } if !ok { return nil, false } return value, true } func parseAttributeArray(reader text.Reader) ([]interface{}, bool) { reader.Advance(1) // skip [ ret := []interface{}{} for i := 0; ; i++ { c := reader.Peek() comma := false if i != 0 && c == ',' { reader.Advance(1) comma = true } if c == ']' { if !comma { reader.Advance(1) return ret, true } return nil, false } reader.SkipSpaces() value, ok := parseAttributeValue(reader) if !ok { return nil, false } ret = append(ret, value) reader.SkipSpaces() } } func parseAttributeString(reader text.Reader) ([]byte, bool) { reader.Advance(1) // skip " line, _ := reader.PeekLine() i := 0 l := len(line) var buf bytes.Buffer for i < l { c := line[i] if c == '\\' && i != l-1 { n := line[i+1] switch n { case '"', '/', '\\': buf.WriteByte(n) i += 2 case 'b': buf.WriteString("\b") i += 2 case 'f': buf.WriteString("\f") i += 2 case 'n': buf.WriteString("\n") i += 2 case 'r': buf.WriteString("\r") i += 2 case 't': buf.WriteString("\t") i += 2 default: buf.WriteByte('\\') i++ } continue } if c == '"' { reader.Advance(i + 1) return buf.Bytes(), true } buf.WriteByte(c) i++ } return nil, false } func scanAttributeDecimal(reader text.Reader, w io.ByteWriter) { for { c := reader.Peek() if util.IsNumeric(c) { w.WriteByte(c) } else { return } reader.Advance(1) } } func parseAttributeNumber(reader text.Reader) (float64, bool) { sign := 1 c := reader.Peek() if c == '-' { sign = -1 reader.Advance(1) } else if c == '+' { reader.Advance(1) } var buf bytes.Buffer if !util.IsNumeric(reader.Peek()) { return 0, false } scanAttributeDecimal(reader, &buf) if buf.Len() == 0 { return 0, false } c = reader.Peek() if c == '.' { buf.WriteByte(c) reader.Advance(1) scanAttributeDecimal(reader, &buf) } c = reader.Peek() if c == 'e' || c == 'E' { buf.WriteByte(c) reader.Advance(1) c = reader.Peek() if c == '-' || c == '+' { buf.WriteByte(c) reader.Advance(1) } scanAttributeDecimal(reader, &buf) } f, err := strconv.ParseFloat(buf.String(), 10) if err != nil { return 0, false } return float64(sign) * f, true } var bytesTrue = []byte("true") var bytesFalse = []byte("false") var bytesNull = []byte("null") func parseAttributeOthers(reader text.Reader) (interface{}, bool) { line, _ := reader.PeekLine() c := line[0] if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == ':') { return nil, false } i := 0 for ; i < len(line); i++ { c := line[i] if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == ':' || c == '.' || c == '-') { break } } value := line[:i] reader.Advance(i) if bytes.Equal(value, bytesTrue) { return true, true } if bytes.Equal(value, bytesFalse) { return false, true } if bytes.Equal(value, bytesNull) { return nil, true } return value, true }