gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

list.go (6711B)


      1 package parser
      2 
      3 import (
      4 	"strconv"
      5 
      6 	"github.com/yuin/goldmark/ast"
      7 	"github.com/yuin/goldmark/text"
      8 	"github.com/yuin/goldmark/util"
      9 )
     10 
     11 type listItemType int
     12 
     13 const (
     14 	notList listItemType = iota
     15 	bulletList
     16 	orderedList
     17 )
     18 
     19 var skipListParserKey = NewContextKey()
     20 var emptyListItemWithBlankLines = NewContextKey()
     21 var listItemFlagValue interface{} = true
     22 
     23 // Same as
     24 // `^(([ ]*)([\-\*\+]))(\s+.*)?\n?$`.FindSubmatchIndex or
     25 // `^(([ ]*)(\d{1,9}[\.\)]))(\s+.*)?\n?$`.FindSubmatchIndex
     26 func parseListItem(line []byte) ([6]int, listItemType) {
     27 	i := 0
     28 	l := len(line)
     29 	ret := [6]int{}
     30 	for ; i < l && line[i] == ' '; i++ {
     31 		c := line[i]
     32 		if c == '\t' {
     33 			return ret, notList
     34 		}
     35 	}
     36 	if i > 3 {
     37 		return ret, notList
     38 	}
     39 	ret[0] = 0
     40 	ret[1] = i
     41 	ret[2] = i
     42 	var typ listItemType
     43 	if i < l && (line[i] == '-' || line[i] == '*' || line[i] == '+') {
     44 		i++
     45 		ret[3] = i
     46 		typ = bulletList
     47 	} else if i < l {
     48 		for ; i < l && util.IsNumeric(line[i]); i++ {
     49 		}
     50 		ret[3] = i
     51 		if ret[3] == ret[2] || ret[3]-ret[2] > 9 {
     52 			return ret, notList
     53 		}
     54 		if i < l && (line[i] == '.' || line[i] == ')') {
     55 			i++
     56 			ret[3] = i
     57 		} else {
     58 			return ret, notList
     59 		}
     60 		typ = orderedList
     61 	} else {
     62 		return ret, notList
     63 	}
     64 	if i < l && line[i] != '\n' {
     65 		w, _ := util.IndentWidth(line[i:], 0)
     66 		if w == 0 {
     67 			return ret, notList
     68 		}
     69 	}
     70 	if i >= l {
     71 		ret[4] = -1
     72 		ret[5] = -1
     73 		return ret, typ
     74 	}
     75 	ret[4] = i
     76 	ret[5] = len(line)
     77 	if line[ret[5]-1] == '\n' && line[i] != '\n' {
     78 		ret[5]--
     79 	}
     80 	return ret, typ
     81 }
     82 
     83 func matchesListItem(source []byte, strict bool) ([6]int, listItemType) {
     84 	m, typ := parseListItem(source)
     85 	if typ != notList && (!strict || strict && m[1] < 4) {
     86 		return m, typ
     87 	}
     88 	return m, notList
     89 }
     90 
     91 func calcListOffset(source []byte, match [6]int) int {
     92 	offset := 0
     93 	if match[4] < 0 || util.IsBlank(source[match[4]:]) { // list item starts with a blank line
     94 		offset = 1
     95 	} else {
     96 		offset, _ = util.IndentWidth(source[match[4]:], match[4])
     97 		if offset > 4 { // offseted codeblock
     98 			offset = 1
     99 		}
    100 	}
    101 	return offset
    102 }
    103 
    104 func lastOffset(node ast.Node) int {
    105 	lastChild := node.LastChild()
    106 	if lastChild != nil {
    107 		return lastChild.(*ast.ListItem).Offset
    108 	}
    109 	return 0
    110 }
    111 
    112 type listParser struct {
    113 }
    114 
    115 var defaultListParser = &listParser{}
    116 
    117 // NewListParser returns a new BlockParser that
    118 // parses lists.
    119 // This parser must take precedence over the ListItemParser.
    120 func NewListParser() BlockParser {
    121 	return defaultListParser
    122 }
    123 
    124 func (b *listParser) Trigger() []byte {
    125 	return []byte{'-', '+', '*', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
    126 }
    127 
    128 func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
    129 	last := pc.LastOpenedBlock().Node
    130 	if _, lok := last.(*ast.List); lok || pc.Get(skipListParserKey) != nil {
    131 		pc.Set(skipListParserKey, nil)
    132 		return nil, NoChildren
    133 	}
    134 	line, _ := reader.PeekLine()
    135 	match, typ := matchesListItem(line, true)
    136 	if typ == notList {
    137 		return nil, NoChildren
    138 	}
    139 	start := -1
    140 	if typ == orderedList {
    141 		number := line[match[2] : match[3]-1]
    142 		start, _ = strconv.Atoi(string(number))
    143 	}
    144 
    145 	if ast.IsParagraph(last) && last.Parent() == parent {
    146 		// we allow only lists starting with 1 to interrupt paragraphs.
    147 		if typ == orderedList && start != 1 {
    148 			return nil, NoChildren
    149 		}
    150 		//an empty list item cannot interrupt a paragraph:
    151 		if match[4] < 0 || util.IsBlank(line[match[4]:match[5]]) {
    152 			return nil, NoChildren
    153 		}
    154 	}
    155 
    156 	marker := line[match[3]-1]
    157 	node := ast.NewList(marker)
    158 	if start > -1 {
    159 		node.Start = start
    160 	}
    161 	pc.Set(emptyListItemWithBlankLines, nil)
    162 	return node, HasChildren
    163 }
    164 
    165 func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
    166 	list := node.(*ast.List)
    167 	line, _ := reader.PeekLine()
    168 	if util.IsBlank(line) {
    169 		if node.LastChild().ChildCount() == 0 {
    170 			pc.Set(emptyListItemWithBlankLines, listItemFlagValue)
    171 		}
    172 		return Continue | HasChildren
    173 	}
    174 
    175 	// "offset" means a width that bar indicates.
    176 	//    -  aaaaaaaa
    177 	// |----|
    178 	//
    179 	// If the indent is less than the last offset like
    180 	// - a
    181 	//  - b          <--- current line
    182 	// it maybe a new child of the list.
    183 	//
    184 	// Empty list items can have multiple blanklines
    185 	//
    186 	// -             <--- 1st item is an empty thus "offset" is unknown
    187 	//
    188 	//
    189 	//   -           <--- current line
    190 	//
    191 	// -> 1 list with 2 blank items
    192 	//
    193 	// So if the last item is an empty, it maybe a new child of the list.
    194 	//
    195 	offset := lastOffset(node)
    196 	lastIsEmpty := node.LastChild().ChildCount() == 0
    197 	indent, _ := util.IndentWidth(line, reader.LineOffset())
    198 
    199 	if indent < offset || lastIsEmpty {
    200 		if indent < 4 {
    201 			match, typ := matchesListItem(line, false) // may have a leading spaces more than 3
    202 			if typ != notList && match[1]-offset < 4 {
    203 				marker := line[match[3]-1]
    204 				if !list.CanContinue(marker, typ == orderedList) {
    205 					return Close
    206 				}
    207 				// Thematic Breaks take precedence over lists
    208 				if isThematicBreak(line[match[3]-1:], 0) {
    209 					isHeading := false
    210 					last := pc.LastOpenedBlock().Node
    211 					if ast.IsParagraph(last) {
    212 						c, ok := matchesSetextHeadingBar(line[match[3]-1:])
    213 						if ok && c == '-' {
    214 							isHeading = true
    215 						}
    216 					}
    217 					if !isHeading {
    218 						return Close
    219 					}
    220 				}
    221 				return Continue | HasChildren
    222 			}
    223 		}
    224 		if !lastIsEmpty {
    225 			return Close
    226 		}
    227 	}
    228 
    229 	if lastIsEmpty && indent < offset {
    230 		return Close
    231 	}
    232 
    233 	// Non empty items can not exist next to an empty list item
    234 	// with blank lines. So we need to close the current list
    235 	//
    236 	// -
    237 	//
    238 	//   foo
    239 	//
    240 	// -> 1 list with 1 blank items and 1 paragraph
    241 	if pc.Get(emptyListItemWithBlankLines) != nil {
    242 		return Close
    243 	}
    244 	return Continue | HasChildren
    245 }
    246 
    247 func (b *listParser) Close(node ast.Node, reader text.Reader, pc Context) {
    248 	list := node.(*ast.List)
    249 
    250 	for c := node.FirstChild(); c != nil && list.IsTight; c = c.NextSibling() {
    251 		if c.FirstChild() != nil && c.FirstChild() != c.LastChild() {
    252 			for c1 := c.FirstChild().NextSibling(); c1 != nil; c1 = c1.NextSibling() {
    253 				if bl, ok := c1.(ast.Node); ok && bl.HasBlankPreviousLines() {
    254 					list.IsTight = false
    255 					break
    256 				}
    257 			}
    258 		}
    259 		if c != node.FirstChild() {
    260 			if bl, ok := c.(ast.Node); ok && bl.HasBlankPreviousLines() {
    261 				list.IsTight = false
    262 			}
    263 		}
    264 	}
    265 
    266 	if list.IsTight {
    267 		for child := node.FirstChild(); child != nil; child = child.NextSibling() {
    268 			for gc := child.FirstChild(); gc != nil; {
    269 				paragraph, ok := gc.(*ast.Paragraph)
    270 				gc = gc.NextSibling()
    271 				if ok {
    272 					textBlock := ast.NewTextBlock()
    273 					textBlock.SetLines(paragraph.Lines())
    274 					child.ReplaceChild(child, paragraph, textBlock)
    275 				}
    276 			}
    277 		}
    278 	}
    279 }
    280 
    281 func (b *listParser) CanInterruptParagraph() bool {
    282 	return true
    283 }
    284 
    285 func (b *listParser) CanAcceptIndentedLine() bool {
    286 	return false
    287 }