mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2024-12-27 13:13:10 +00:00
eb08529f35
Co-authored-by: Autumn! <autumnull@posteo.net>
328 lines
6.9 KiB
Go
328 lines
6.9 KiB
Go
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
|
|
}
|