attribute.go

  1package parser
  2
  3import (
  4	"bytes"
  5	"io"
  6	"strconv"
  7
  8	"github.com/yuin/goldmark/text"
  9	"github.com/yuin/goldmark/util"
 10)
 11
 12var attrNameID = []byte("id")
 13var attrNameClass = []byte("class")
 14
 15// An Attribute is an attribute of the markdown elements.
 16type Attribute struct {
 17	Name  []byte
 18	Value interface{}
 19}
 20
 21// An Attributes is a collection of attributes.
 22type Attributes []Attribute
 23
 24// Find returns a (value, true) if an attribute correspond with given name is found, otherwise (nil, false).
 25func (as Attributes) Find(name []byte) (interface{}, bool) {
 26	for _, a := range as {
 27		if bytes.Equal(a.Name, name) {
 28			return a.Value, true
 29		}
 30	}
 31	return nil, false
 32}
 33
 34func (as Attributes) findUpdate(name []byte, cb func(v interface{}) interface{}) bool {
 35	for i, a := range as {
 36		if bytes.Equal(a.Name, name) {
 37			as[i].Value = cb(a.Value)
 38			return true
 39		}
 40	}
 41	return false
 42}
 43
 44// ParseAttributes parses attributes into a map.
 45// ParseAttributes returns a parsed attributes and true if could parse
 46// attributes, otherwise nil and false.
 47func ParseAttributes(reader text.Reader) (Attributes, bool) {
 48	savedLine, savedPosition := reader.Position()
 49	reader.SkipSpaces()
 50	if reader.Peek() != '{' {
 51		reader.SetPosition(savedLine, savedPosition)
 52		return nil, false
 53	}
 54	reader.Advance(1)
 55	attrs := Attributes{}
 56	for {
 57		if reader.Peek() == '}' {
 58			reader.Advance(1)
 59			return attrs, true
 60		}
 61		attr, ok := parseAttribute(reader)
 62		if !ok {
 63			reader.SetPosition(savedLine, savedPosition)
 64			return nil, false
 65		}
 66		if bytes.Equal(attr.Name, attrNameClass) {
 67			if !attrs.findUpdate(attrNameClass, func(v interface{}) interface{} {
 68				ret := make([]byte, 0, len(v.([]byte))+1+len(attr.Value.([]byte)))
 69				ret = append(ret, v.([]byte)...)
 70				return append(append(ret, ' '), attr.Value.([]byte)...)
 71			}) {
 72				attrs = append(attrs, attr)
 73			}
 74		} else {
 75			attrs = append(attrs, attr)
 76		}
 77		reader.SkipSpaces()
 78		if reader.Peek() == ',' {
 79			reader.Advance(1)
 80			reader.SkipSpaces()
 81		}
 82	}
 83}
 84
 85func parseAttribute(reader text.Reader) (Attribute, bool) {
 86	reader.SkipSpaces()
 87	c := reader.Peek()
 88	if c == '#' || c == '.' {
 89		reader.Advance(1)
 90		line, _ := reader.PeekLine()
 91		i := 0
 92		// HTML5 allows any kind of characters as id, but XHTML restricts characters for id.
 93		// CommonMark is basically defined for XHTML(even though it is legacy).
 94		// So we restrict id characters.
 95		for ; i < len(line) && !util.IsSpace(line[i]) &&
 96			(!util.IsPunct(line[i]) || line[i] == '_' ||
 97				line[i] == '-' || line[i] == ':' || line[i] == '.'); i++ {
 98		}
 99		name := attrNameClass
100		if c == '#' {
101			name = attrNameID
102		}
103		reader.Advance(i)
104		return Attribute{Name: name, Value: line[0:i]}, true
105	}
106	line, _ := reader.PeekLine()
107	if len(line) == 0 {
108		return Attribute{}, false
109	}
110	c = line[0]
111	if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
112		c == '_' || c == ':') {
113		return Attribute{}, false
114	}
115	i := 0
116	for ; i < len(line); i++ {
117		c = line[i]
118		if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
119			(c >= '0' && c <= '9') ||
120			c == '_' || c == ':' || c == '.' || c == '-') {
121			break
122		}
123	}
124	name := line[:i]
125	reader.Advance(i)
126	reader.SkipSpaces()
127	c = reader.Peek()
128	if c != '=' {
129		return Attribute{}, false
130	}
131	reader.Advance(1)
132	reader.SkipSpaces()
133	value, ok := parseAttributeValue(reader)
134	if !ok {
135		return Attribute{}, false
136	}
137	if bytes.Equal(name, attrNameClass) {
138		if _, ok = value.([]byte); !ok {
139			return Attribute{}, false
140		}
141	}
142	return Attribute{Name: name, Value: value}, true
143}
144
145func parseAttributeValue(reader text.Reader) (interface{}, bool) {
146	reader.SkipSpaces()
147	c := reader.Peek()
148	var value interface{}
149	var ok bool
150	switch c {
151	case text.EOF:
152		return Attribute{}, false
153	case '{':
154		value, ok = ParseAttributes(reader)
155	case '[':
156		value, ok = parseAttributeArray(reader)
157	case '"':
158		value, ok = parseAttributeString(reader)
159	default:
160		if c == '-' || c == '+' || util.IsNumeric(c) {
161			value, ok = parseAttributeNumber(reader)
162		} else {
163			value, ok = parseAttributeOthers(reader)
164		}
165	}
166	if !ok {
167		return nil, false
168	}
169	return value, true
170}
171
172func parseAttributeArray(reader text.Reader) ([]interface{}, bool) {
173	reader.Advance(1) // skip [
174	ret := []interface{}{}
175	for i := 0; ; i++ {
176		c := reader.Peek()
177		comma := false
178		if i != 0 && c == ',' {
179			reader.Advance(1)
180			comma = true
181		}
182		if c == ']' {
183			if !comma {
184				reader.Advance(1)
185				return ret, true
186			}
187			return nil, false
188		}
189		reader.SkipSpaces()
190		value, ok := parseAttributeValue(reader)
191		if !ok {
192			return nil, false
193		}
194		ret = append(ret, value)
195		reader.SkipSpaces()
196	}
197}
198
199func parseAttributeString(reader text.Reader) ([]byte, bool) {
200	reader.Advance(1) // skip "
201	line, _ := reader.PeekLine()
202	i := 0
203	l := len(line)
204	var buf bytes.Buffer
205	for i < l {
206		c := line[i]
207		if c == '\\' && i != l-1 {
208			n := line[i+1]
209			switch n {
210			case '"', '/', '\\':
211				buf.WriteByte(n)
212				i += 2
213			case 'b':
214				buf.WriteString("\b")
215				i += 2
216			case 'f':
217				buf.WriteString("\f")
218				i += 2
219			case 'n':
220				buf.WriteString("\n")
221				i += 2
222			case 'r':
223				buf.WriteString("\r")
224				i += 2
225			case 't':
226				buf.WriteString("\t")
227				i += 2
228			default:
229				buf.WriteByte('\\')
230				i++
231			}
232			continue
233		}
234		if c == '"' {
235			reader.Advance(i + 1)
236			return buf.Bytes(), true
237		}
238		buf.WriteByte(c)
239		i++
240	}
241	return nil, false
242}
243
244func scanAttributeDecimal(reader text.Reader, w io.ByteWriter) {
245	for {
246		c := reader.Peek()
247		if util.IsNumeric(c) {
248			_ = w.WriteByte(c)
249		} else {
250			return
251		}
252		reader.Advance(1)
253	}
254}
255
256func parseAttributeNumber(reader text.Reader) (float64, bool) {
257	sign := 1
258	c := reader.Peek()
259	if c == '-' {
260		sign = -1
261		reader.Advance(1)
262	} else if c == '+' {
263		reader.Advance(1)
264	}
265	var buf bytes.Buffer
266	if !util.IsNumeric(reader.Peek()) {
267		return 0, false
268	}
269	scanAttributeDecimal(reader, &buf)
270	if buf.Len() == 0 {
271		return 0, false
272	}
273	c = reader.Peek()
274	if c == '.' {
275		buf.WriteByte(c)
276		reader.Advance(1)
277		scanAttributeDecimal(reader, &buf)
278	}
279	c = reader.Peek()
280	if c == 'e' || c == 'E' {
281		buf.WriteByte(c)
282		reader.Advance(1)
283		c = reader.Peek()
284		if c == '-' || c == '+' {
285			buf.WriteByte(c)
286			reader.Advance(1)
287		}
288		scanAttributeDecimal(reader, &buf)
289	}
290	f, err := strconv.ParseFloat(buf.String(), 64)
291	if err != nil {
292		return 0, false
293	}
294	return float64(sign) * f, true
295}
296
297var bytesTrue = []byte("true")
298var bytesFalse = []byte("false")
299var bytesNull = []byte("null")
300
301func parseAttributeOthers(reader text.Reader) (interface{}, bool) {
302	line, _ := reader.PeekLine()
303	c := line[0]
304	if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
305		c == '_' || c == ':') {
306		return nil, false
307	}
308	i := 0
309	for ; i < len(line); i++ {
310		c := line[i]
311		if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
312			(c >= '0' && c <= '9') ||
313			c == '_' || c == ':' || c == '.' || c == '-') {
314			break
315		}
316	}
317	value := line[:i]
318	reader.Advance(i)
319	if bytes.Equal(value, bytesTrue) {
320		return true, true
321	}
322	if bytes.Equal(value, bytesFalse) {
323		return false, true
324	}
325	if bytes.Equal(value, bytesNull) {
326		return nil, true
327	}
328	return value, true
329}