1package parser
2
3import (
4 "strconv"
5
6 "github.com/yuin/goldmark/ast"
7 "github.com/yuin/goldmark/text"
8 "github.com/yuin/goldmark/util"
9)
10
11type listItemType int
12
13const (
14 notList listItemType = iota
15 bulletList
16 orderedList
17)
18
19var skipListParserKey = NewContextKey()
20var emptyListItemWithBlankLines = NewContextKey()
21var listItemFlagValue interface{} = true
22
23// Same as
24// `^(([ ]*)([\-\*\+]))(\s+.*)?\n?$`.FindSubmatchIndex or
25// `^(([ ]*)(\d{1,9}[\.\)]))(\s+.*)?\n?$`.FindSubmatchIndex.
26func 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
83func 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
91func calcListOffset(source []byte, match [6]int) int {
92 var offset int
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
104func 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
112type listParser struct {
113}
114
115var defaultListParser = &listParser{}
116
117// NewListParser returns a new BlockParser that
118// parses lists.
119// This parser must take precedence over the ListItemParser.
120func NewListParser() BlockParser {
121 return defaultListParser
122}
123
124func (b *listParser) Trigger() []byte {
125 return []byte{'-', '+', '*', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
126}
127
128func (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
165func (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
247func (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 c1.HasBlankPreviousLines() {
254 list.IsTight = false
255 break
256 }
257 }
258 }
259 if c != node.FirstChild() {
260 if c.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
281func (b *listParser) CanInterruptParagraph() bool {
282 return true
283}
284
285func (b *listParser) CanAcceptIndentedLine() bool {
286 return false
287}