mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-27 14:16:39 +00:00
127 lines
3 KiB
Go
127 lines
3 KiB
Go
|
package parser
|
||
|
|
||
|
import (
|
||
|
"github.com/yuin/goldmark/ast"
|
||
|
"github.com/yuin/goldmark/text"
|
||
|
"github.com/yuin/goldmark/util"
|
||
|
)
|
||
|
|
||
|
var temporaryParagraphKey = NewContextKey()
|
||
|
|
||
|
type setextHeadingParser struct {
|
||
|
HeadingConfig
|
||
|
}
|
||
|
|
||
|
func matchesSetextHeadingBar(line []byte) (byte, bool) {
|
||
|
start := 0
|
||
|
end := len(line)
|
||
|
space := util.TrimLeftLength(line, []byte{' '})
|
||
|
if space > 3 {
|
||
|
return 0, false
|
||
|
}
|
||
|
start += space
|
||
|
level1 := util.TrimLeftLength(line[start:end], []byte{'='})
|
||
|
c := byte('=')
|
||
|
var level2 int
|
||
|
if level1 == 0 {
|
||
|
level2 = util.TrimLeftLength(line[start:end], []byte{'-'})
|
||
|
c = '-'
|
||
|
}
|
||
|
if util.IsSpace(line[end-1]) {
|
||
|
end -= util.TrimRightSpaceLength(line[start:end])
|
||
|
}
|
||
|
if !((level1 > 0 && start+level1 == end) || (level2 > 0 && start+level2 == end)) {
|
||
|
return 0, false
|
||
|
}
|
||
|
return c, true
|
||
|
}
|
||
|
|
||
|
// NewSetextHeadingParser return a new BlockParser that can parse Setext headings.
|
||
|
func NewSetextHeadingParser(opts ...HeadingOption) BlockParser {
|
||
|
p := &setextHeadingParser{}
|
||
|
for _, o := range opts {
|
||
|
o.SetHeadingOption(&p.HeadingConfig)
|
||
|
}
|
||
|
return p
|
||
|
}
|
||
|
|
||
|
func (b *setextHeadingParser) Trigger() []byte {
|
||
|
return []byte{'-', '='}
|
||
|
}
|
||
|
|
||
|
func (b *setextHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
|
||
|
last := pc.LastOpenedBlock().Node
|
||
|
if last == nil {
|
||
|
return nil, NoChildren
|
||
|
}
|
||
|
paragraph, ok := last.(*ast.Paragraph)
|
||
|
if !ok || paragraph.Parent() != parent {
|
||
|
return nil, NoChildren
|
||
|
}
|
||
|
line, segment := reader.PeekLine()
|
||
|
c, ok := matchesSetextHeadingBar(line)
|
||
|
if !ok {
|
||
|
return nil, NoChildren
|
||
|
}
|
||
|
level := 1
|
||
|
if c == '-' {
|
||
|
level = 2
|
||
|
}
|
||
|
node := ast.NewHeading(level)
|
||
|
node.Lines().Append(segment)
|
||
|
pc.Set(temporaryParagraphKey, last)
|
||
|
return node, NoChildren | RequireParagraph
|
||
|
}
|
||
|
|
||
|
func (b *setextHeadingParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
|
||
|
return Close
|
||
|
}
|
||
|
|
||
|
func (b *setextHeadingParser) Close(node ast.Node, reader text.Reader, pc Context) {
|
||
|
heading := node.(*ast.Heading)
|
||
|
segment := node.Lines().At(0)
|
||
|
heading.Lines().Clear()
|
||
|
tmp := pc.Get(temporaryParagraphKey).(*ast.Paragraph)
|
||
|
pc.Set(temporaryParagraphKey, nil)
|
||
|
if tmp.Lines().Len() == 0 {
|
||
|
next := heading.NextSibling()
|
||
|
segment = segment.TrimLeftSpace(reader.Source())
|
||
|
if next == nil || !ast.IsParagraph(next) {
|
||
|
para := ast.NewParagraph()
|
||
|
para.Lines().Append(segment)
|
||
|
heading.Parent().InsertAfter(heading.Parent(), heading, para)
|
||
|
} else {
|
||
|
next.(ast.Node).Lines().Unshift(segment)
|
||
|
}
|
||
|
heading.Parent().RemoveChild(heading.Parent(), heading)
|
||
|
} else {
|
||
|
heading.SetLines(tmp.Lines())
|
||
|
heading.SetBlankPreviousLines(tmp.HasBlankPreviousLines())
|
||
|
tp := tmp.Parent()
|
||
|
if tp != nil {
|
||
|
tp.RemoveChild(tp, tmp)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if b.Attribute {
|
||
|
parseLastLineAttributes(node, reader, pc)
|
||
|
}
|
||
|
|
||
|
if b.AutoHeadingID {
|
||
|
id, ok := node.AttributeString("id")
|
||
|
if !ok {
|
||
|
generateAutoHeadingID(heading, reader, pc)
|
||
|
} else {
|
||
|
pc.IDs().Put(id.([]byte))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (b *setextHeadingParser) CanInterruptParagraph() bool {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (b *setextHeadingParser) CanAcceptIndentedLine() bool {
|
||
|
return false
|
||
|
}
|