package parser
import (
"strconv"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
type listItemType int
const (
notList listItemType = iota
bulletList
orderedList
)
var skipListParserKey = NewContextKey ()
var emptyListItemWithBlankLines = NewContextKey ()
var listItemFlagValue interface {} = true
func parseListItem(line []byte ) ([6 ]int , listItemType ) {
i := 0
l := len (line )
ret := [6 ]int {}
for ; i < l && line [i ] == ' ' ; i ++ {
c := line [i ]
if c == '\t' {
return ret , notList
}
}
if i > 3 {
return ret , notList
}
ret [0 ] = 0
ret [1 ] = i
ret [2 ] = i
var typ listItemType
if i < l && (line [i ] == '-' || line [i ] == '*' || line [i ] == '+' ) {
i ++
ret [3 ] = i
typ = bulletList
} else if i < l {
for ; i < l && util .IsNumeric (line [i ]); i ++ {
}
ret [3 ] = i
if ret [3 ] == ret [2 ] || ret [3 ]-ret [2 ] > 9 {
return ret , notList
}
if i < l && (line [i ] == '.' || line [i ] == ')' ) {
i ++
ret [3 ] = i
} else {
return ret , notList
}
typ = orderedList
} else {
return ret , notList
}
if i < l && line [i ] != '\n' {
w , _ := util .IndentWidth (line [i :], 0 )
if w == 0 {
return ret , notList
}
}
if i >= l {
ret [4 ] = -1
ret [5 ] = -1
return ret , typ
}
ret [4 ] = i
ret [5 ] = len (line )
if line [ret [5 ]-1 ] == '\n' && line [i ] != '\n' {
ret [5 ]--
}
return ret , typ
}
func matchesListItem(source []byte , strict bool ) ([6 ]int , listItemType ) {
m , typ := parseListItem (source )
if typ != notList && (!strict || strict && m [1 ] < 4 ) {
return m , typ
}
return m , notList
}
func calcListOffset(source []byte , match [6 ]int ) int {
var offset int
if match [4 ] < 0 || util .IsBlank (source [match [4 ]:]) {
offset = 1
} else {
offset , _ = util .IndentWidth (source [match [4 ]:], match [4 ])
if offset > 4 {
offset = 1
}
}
return offset
}
func lastOffset(node ast .Node ) int {
lastChild := node .LastChild ()
if lastChild != nil {
return lastChild .(*ast .ListItem ).Offset
}
return 0
}
type listParser struct {
}
var defaultListParser = &listParser {}
func NewListParser () BlockParser {
return defaultListParser
}
func (b *listParser ) Trigger () []byte {
return []byte {'-' , '+' , '*' , '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' }
}
func (b *listParser ) Open (parent ast .Node , reader text .Reader , pc Context ) (ast .Node , State ) {
last := pc .LastOpenedBlock ().Node
if _ , lok := last .(*ast .List ); lok || pc .Get (skipListParserKey ) != nil {
pc .Set (skipListParserKey , nil )
return nil , NoChildren
}
line , _ := reader .PeekLine ()
match , typ := matchesListItem (line , true )
if typ == notList {
return nil , NoChildren
}
start := -1
if typ == orderedList {
number := line [match [2 ] : match [3 ]-1 ]
start , _ = strconv .Atoi (string (number ))
}
if ast .IsParagraph (last ) && last .Parent () == parent {
if typ == orderedList && start != 1 {
return nil , NoChildren
}
if match [4 ] < 0 || util .IsBlank (line [match [4 ]:match [5 ]]) {
return nil , NoChildren
}
}
marker := line [match [3 ]-1 ]
node := ast .NewList (marker )
if start > -1 {
node .Start = start
}
pc .Set (emptyListItemWithBlankLines , nil )
return node , HasChildren
}
func (b *listParser ) Continue (node ast .Node , reader text .Reader , pc Context ) State {
list := node .(*ast .List )
line , _ := reader .PeekLine ()
if util .IsBlank (line ) {
if node .LastChild ().ChildCount () == 0 {
pc .Set (emptyListItemWithBlankLines , listItemFlagValue )
}
return Continue | HasChildren
}
offset := lastOffset (node )
lastIsEmpty := node .LastChild ().ChildCount () == 0
indent , _ := util .IndentWidth (line , reader .LineOffset ())
if indent < offset || lastIsEmpty {
if indent < 4 {
match , typ := matchesListItem (line , false )
if typ != notList && match [1 ]-offset < 4 {
marker := line [match [3 ]-1 ]
if !list .CanContinue (marker , typ == orderedList ) {
return Close
}
if isThematicBreak (line [match [3 ]-1 :], 0 ) {
isHeading := false
last := pc .LastOpenedBlock ().Node
if ast .IsParagraph (last ) {
c , ok := matchesSetextHeadingBar (line [match [3 ]-1 :])
if ok && c == '-' {
isHeading = true
}
}
if !isHeading {
return Close
}
}
return Continue | HasChildren
}
}
if !lastIsEmpty {
return Close
}
}
if lastIsEmpty && indent < offset {
return Close
}
if pc .Get (emptyListItemWithBlankLines ) != nil {
return Close
}
return Continue | HasChildren
}
func (b *listParser ) Close (node ast .Node , reader text .Reader , pc Context ) {
list := node .(*ast .List )
for c := node .FirstChild (); c != nil && list .IsTight ; c = c .NextSibling () {
if c .FirstChild () != nil && c .FirstChild () != c .LastChild () {
for c1 := c .FirstChild ().NextSibling (); c1 != nil ; c1 = c1 .NextSibling () {
if c1 .HasBlankPreviousLines () {
list .IsTight = false
break
}
}
}
if c != node .FirstChild () {
if c .HasBlankPreviousLines () {
list .IsTight = false
}
}
}
if list .IsTight {
for child := node .FirstChild (); child != nil ; child = child .NextSibling () {
for gc := child .FirstChild (); gc != nil ; {
paragraph , ok := gc .(*ast .Paragraph )
gc = gc .NextSibling ()
if ok {
textBlock := ast .NewTextBlock ()
textBlock .SetLines (paragraph .Lines ())
child .ReplaceChild (child , paragraph , textBlock )
}
}
}
}
}
func (b *listParser ) CanInterruptParagraph () bool {
return true
}
func (b *listParser ) CanAcceptIndentedLine () bool {
return false
}
The pages are generated with Golds v0.8.2 . (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu .
PR and bug reports are welcome and can be submitted to the issue list .
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds .