goldmark/parser/atx_heading.go
2019-05-04 22:25:41 +09:00

130 lines
3.2 KiB
Go

package parser
import (
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
"regexp"
)
// A HeadingConfig struct is a data structure that holds configuration of the renderers related to headings.
type HeadingConfig struct {
AutoHeadingID bool
}
// SetOption implements SetOptioner.
func (b *HeadingConfig) SetOption(name OptionName, value interface{}) {
switch name {
case AutoHeadingID:
b.AutoHeadingID = true
}
}
// A HeadingOption interface sets options for heading parsers.
type HeadingOption interface {
SetHeadingOption(*HeadingConfig)
}
// AutoHeadingID is an option name that enables auto IDs for headings.
var AutoHeadingID OptionName = "AutoHeadingID"
type withAutoHeadingID struct {
}
func (o *withAutoHeadingID) SetConfig(c *Config) {
c.Options[AutoHeadingID] = true
}
func (o *withAutoHeadingID) SetHeadingOption(p *HeadingConfig) {
p.AutoHeadingID = true
}
// WithAutoHeadingID is a functional option that enables custom heading ids and
// auto generated heading ids.
func WithAutoHeadingID() interface {
Option
HeadingOption
} {
return &withAutoHeadingID{}
}
var atxHeadingRegexp = regexp.MustCompile(`^[ ]{0,3}(#{1,6})(?:\s+(.*?)\s*([\s]#+\s*)?)?\n?$`)
type atxHeadingParser struct {
HeadingConfig
}
// NewATXHeadingParser return a new BlockParser that can parse ATX headings.
func NewATXHeadingParser(opts ...HeadingOption) BlockParser {
p := &atxHeadingParser{}
for _, o := range opts {
o.SetHeadingOption(&p.HeadingConfig)
}
return p
}
func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
line, segment := reader.PeekLine()
pos := pc.BlockOffset()
i := pos
for ; i < len(line) && line[i] == '#'; i++ {
}
level := i - pos
if i == pos || level > 6 {
return nil, NoChildren
}
l := util.TrimLeftSpaceLength(line[i:])
if l == 0 {
return nil, NoChildren
}
start := i + l
stop := len(line) - util.TrimRightSpaceLength(line)
if stop <= start { // empty headings like '##[space]'
stop = start + 1
} else {
i = stop - 1
for ; line[i] == '#' && i >= start; i-- {
}
if i != stop-1 && !util.IsSpace(line[i]) {
i = stop - 1
}
i++
stop = i
}
node := ast.NewHeading(level)
if len(util.TrimRight(line[start:stop], []byte{'#'})) != 0 { // empty heading like '### ###'
node.Lines().Append(text.NewSegment(segment.Start+start, segment.Start+stop))
}
return node, NoChildren
}
func (b *atxHeadingParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
return Close
}
func (b *atxHeadingParser) Close(node ast.Node, reader text.Reader, pc Context) {
if !b.AutoHeadingID {
return
}
generateAutoHeadingID(node.(*ast.Heading), reader, pc)
}
func (b *atxHeadingParser) CanInterruptParagraph() bool {
return true
}
func (b *atxHeadingParser) CanAcceptIndentedLine() bool {
return false
}
var attrAutoHeadingIDPrefix = []byte("heading")
var attrNameID = []byte("#")
func generateAutoHeadingID(node *ast.Heading, reader text.Reader, pc Context) {
lastIndex := node.Lines().Len() - 1
lastLine := node.Lines().At(lastIndex)
line := lastLine.Value(reader.Source())
headingID := pc.IDs().Generate(line, attrAutoHeadingIDPrefix)
node.SetAttribute(attrNameID, headingID)
}