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] == '_' || line[i] == '-' || line[i] == ':' || line[i] == '.'); i++ {
 97		}
 98		name := attrNameClass
 99		if c == '#' {
100			name = attrNameID
101		}
102		reader.Advance(i)
103		return Attribute{Name: name, Value: line[0:i]}, true
104	}
105	line, _ := reader.PeekLine()
106	if len(line) == 0 {
107		return Attribute{}, false
108	}
109	c = line[0]
110	if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
111		c == '_' || c == ':') {
112		return Attribute{}, false
113	}
114	i := 0
115	for ; i < len(line); i++ {
116		c = line[i]
117		if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
118			(c >= '0' && c <= '9') ||
119			c == '_' || c == ':' || c == '.' || c == '-') {
120			break
121		}
122	}
123	name := line[:i]
124	reader.Advance(i)
125	reader.SkipSpaces()
126	c = reader.Peek()
127	if c != '=' {
128		return Attribute{}, false
129	}
130	reader.Advance(1)
131	reader.SkipSpaces()
132	value, ok := parseAttributeValue(reader)
133	if !ok {
134		return Attribute{}, false
135	}
136	if bytes.Equal(name, attrNameClass) {
137		if _, ok = value.([]byte); !ok {
138			return Attribute{}, false
139		}
140	}
141	return Attribute{Name: name, Value: value}, true
142}
143
144func parseAttributeValue(reader text.Reader) (interface{}, bool) {
145	reader.SkipSpaces()
146	c := reader.Peek()
147	var value interface{}
148	ok := false
149	switch c {
150	case text.EOF:
151		return Attribute{}, false
152	case '{':
153		value, ok = ParseAttributes(reader)
154	case '[':
155		value, ok = parseAttributeArray(reader)
156	case '"':
157		value, ok = parseAttributeString(reader)
158	default:
159		if c == '-' || c == '+' || util.IsNumeric(c) {
160			value, ok = parseAttributeNumber(reader)
161		} else {
162			value, ok = parseAttributeOthers(reader)
163		}
164	}
165	if !ok {
166		return nil, false
167	}
168	return value, true
169}
170
171func parseAttributeArray(reader text.Reader) ([]interface{}, bool) {
172	reader.Advance(1) // skip [
173	ret := []interface{}{}
174	for i := 0; ; i++ {
175		c := reader.Peek()
176		comma := false
177		if i != 0 && c == ',' {
178			reader.Advance(1)
179			comma = true
180		}
181		if c == ']' {
182			if !comma {
183				reader.Advance(1)
184				return ret, true
185			}
186			return nil, false
187		}
188		reader.SkipSpaces()
189		value, ok := parseAttributeValue(reader)
190		if !ok {
191			return nil, false
192		}
193		ret = append(ret, value)
194		reader.SkipSpaces()
195	}
196}
197
198func parseAttributeString(reader text.Reader) ([]byte, bool) {
199	reader.Advance(1) // skip "
200	line, _ := reader.PeekLine()
201	i := 0
202	l := len(line)
203	var buf bytes.Buffer
204	for i < l {
205		c := line[i]
206		if c == '\\' && i != l-1 {
207			n := line[i+1]
208			switch n {
209			case '"', '/', '\\':
210				buf.WriteByte(n)
211				i += 2
212			case 'b':
213				buf.WriteString("\b")
214				i += 2
215			case 'f':
216				buf.WriteString("\f")
217				i += 2
218			case 'n':
219				buf.WriteString("\n")
220				i += 2
221			case 'r':
222				buf.WriteString("\r")
223				i += 2
224			case 't':
225				buf.WriteString("\t")
226				i += 2
227			default:
228				buf.WriteByte('\\')
229				i++
230			}
231			continue
232		}
233		if c == '"' {
234			reader.Advance(i + 1)
235			return buf.Bytes(), true
236		}
237		buf.WriteByte(c)
238		i++
239	}
240	return nil, false
241}
242
243func scanAttributeDecimal(reader text.Reader, w io.ByteWriter) {
244	for {
245		c := reader.Peek()
246		if util.IsNumeric(c) {
247			w.WriteByte(c)
248		} else {
249			return
250		}
251		reader.Advance(1)
252	}
253}
254
255func parseAttributeNumber(reader text.Reader) (float64, bool) {
256	sign := 1
257	c := reader.Peek()
258	if c == '-' {
259		sign = -1
260		reader.Advance(1)
261	} else if c == '+' {
262		reader.Advance(1)
263	}
264	var buf bytes.Buffer
265	if !util.IsNumeric(reader.Peek()) {
266		return 0, false
267	}
268	scanAttributeDecimal(reader, &buf)
269	if buf.Len() == 0 {
270		return 0, false
271	}
272	c = reader.Peek()
273	if c == '.' {
274		buf.WriteByte(c)
275		reader.Advance(1)
276		scanAttributeDecimal(reader, &buf)
277	}
278	c = reader.Peek()
279	if c == 'e' || c == 'E' {
280		buf.WriteByte(c)
281		reader.Advance(1)
282		c = reader.Peek()
283		if c == '-' || c == '+' {
284			buf.WriteByte(c)
285			reader.Advance(1)
286		}
287		scanAttributeDecimal(reader, &buf)
288	}
289	f, err := strconv.ParseFloat(buf.String(), 10)
290	if err != nil {
291		return 0, false
292	}
293	return float64(sign) * f, true
294}
295
296var bytesTrue = []byte("true")
297var bytesFalse = []byte("false")
298var bytesNull = []byte("null")
299
300func parseAttributeOthers(reader text.Reader) (interface{}, bool) {
301	line, _ := reader.PeekLine()
302	c := line[0]
303	if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
304		c == '_' || c == ':') {
305		return nil, false
306	}
307	i := 0
308	for ; i < len(line); i++ {
309		c := line[i]
310		if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
311			(c >= '0' && c <= '9') ||
312			c == '_' || c == ':' || c == '.' || c == '-') {
313			break
314		}
315	}
316	value := line[:i]
317	reader.Advance(i)
318	if bytes.Equal(value, bytesTrue) {
319		return true, true
320	}
321	if bytes.Equal(value, bytesFalse) {
322		return false, true
323	}
324	if bytes.Equal(value, bytesNull) {
325		return nil, true
326	}
327	return value, true
328}