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 }