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 any = 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 calcListOffset(source []byte, match [6]int) int {
84 var offset int
85 if match[4] < 0 || util.IsBlank(source[match[4]:]) { // list item starts with a blank line
86 offset = 1
87 } else {
88 offset, _ = util.IndentWidth(source[match[4]:], match[4])
89 if offset > 4 { // offseted codeblock
90 offset = 1
91 }
92 }
93 return offset
94}
95
96func lastOffset(node ast.Node) int {
97 lastChild := node.LastChild()
98 if lastChild != nil {
99 return lastChild.(*ast.ListItem).Offset
100 }
101 return 0
102}
103
104type listParser struct {
105}
106
107var defaultListParser = &listParser{}
108
109// NewListParser returns a new BlockParser that
110// parses lists.
111// This parser must take precedence over the ListItemParser.
112func NewListParser() BlockParser {
113 return defaultListParser
114}
115
116func (b *listParser) Trigger() []byte {
117 return []byte{'-', '+', '*', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}
118}
119
120func (b *listParser) Open(parent ast.Node, reader text.Reader, pc Context) (ast.Node, State) {
121 last := pc.LastOpenedBlock().Node
122 if _, lok := last.(*ast.List); lok || pc.Get(skipListParserKey) != nil {
123 pc.Set(skipListParserKey, nil)
124 return nil, NoChildren
125 }
126 line, _ := reader.PeekLine()
127 match, typ := parseListItem(line)
128 if typ == notList {
129 return nil, NoChildren
130 }
131 start := -1
132 if typ == orderedList {
133 number := line[match[2] : match[3]-1]
134 start, _ = strconv.Atoi(string(number))
135 }
136
137 if ast.IsParagraph(last) && last.Parent() == parent {
138 // we allow only lists starting with 1 to interrupt paragraphs.
139 if typ == orderedList && start != 1 {
140 return nil, NoChildren
141 }
142 //an empty list item cannot interrupt a paragraph:
143 if match[4] < 0 || util.IsBlank(line[match[4]:match[5]]) {
144 return nil, NoChildren
145 }
146 }
147
148 marker := line[match[3]-1]
149 node := ast.NewList(marker)
150 if start > -1 {
151 node.Start = start
152 }
153 pc.Set(emptyListItemWithBlankLines, nil)
154 return node, HasChildren
155}
156
157func (b *listParser) Continue(node ast.Node, reader text.Reader, pc Context) State {
158 list := node.(*ast.List)
159 line, _ := reader.PeekLine()
160 if util.IsBlank(line) {
161 if node.LastChild().ChildCount() == 0 {
162 pc.Set(emptyListItemWithBlankLines, listItemFlagValue)
163 }
164 return Continue | HasChildren
165 }
166
167 // "offset" means a width that bar indicates.
168 // - aaaaaaaa
169 // |----|
170 //
171 // If the indent is less than the last offset like
172 // - a
173 // - b <--- current line
174 // it maybe a new child of the list.
175 //
176 // Empty list items can have multiple blanklines
177 //
178 // - <--- 1st item is an empty thus "offset" is unknown
179 //
180 //
181 // - <--- current line
182 //
183 // -> 1 list with 2 blank items
184 //
185 // So if the last item is an empty, it maybe a new child of the list.
186 //
187 offset := lastOffset(node)
188 lastIsEmpty := node.LastChild().ChildCount() == 0
189 indent, _ := util.IndentWidth(line, reader.LineOffset())
190
191 if indent < offset || lastIsEmpty {
192 if indent < 4 {
193 match, typ := parseListItem(line)
194 if typ != notList && match[1]-offset < 4 {
195 marker := line[match[3]-1]
196 if !list.CanContinue(marker, typ == orderedList) {
197 return Close
198 }
199 // Thematic Breaks take precedence over lists
200 if isThematicBreak(line[match[3]-1:], 0) {
201 isHeading := false
202 last := pc.LastOpenedBlock().Node
203 if ast.IsParagraph(last) {
204 c, ok := matchesSetextHeadingBar(line[match[3]-1:])
205 if ok && c == '-' {
206 isHeading = true
207 }
208 }
209 if !isHeading {
210 return Close
211 }
212 }
213 return Continue | HasChildren
214 }
215 }
216 if !lastIsEmpty {
217 return Close
218 }
219 }
220
221 if lastIsEmpty && indent < offset {
222 return Close
223 }
224
225 // Non empty items can not exist next to an empty list item
226 // with blank lines. So we need to close the current list
227 //
228 // -
229 //
230 // foo
231 //
232 // -> 1 list with 1 blank items and 1 paragraph
233 if pc.Get(emptyListItemWithBlankLines) != nil {
234 return Close
235 }
236 return Continue | HasChildren
237}
238
239func (b *listParser) Close(node ast.Node, reader text.Reader, pc Context) {
240 list := node.(*ast.List)
241
242 for c := node.FirstChild(); c != nil && list.IsTight; c = c.NextSibling() {
243 if c.FirstChild() != nil && c.FirstChild() != c.LastChild() {
244 for c1 := c.FirstChild().NextSibling(); c1 != nil; c1 = c1.NextSibling() {
245 if c1.HasBlankPreviousLines() {
246 list.IsTight = false
247 break
248 }
249 }
250 }
251 if c != node.FirstChild() {
252 if c.HasBlankPreviousLines() {
253 list.IsTight = false
254 }
255 }
256 }
257
258 if list.IsTight {
259 for child := node.FirstChild(); child != nil; child = child.NextSibling() {
260 for gc := child.FirstChild(); gc != nil; {
261 paragraph, ok := gc.(*ast.Paragraph)
262 gc = gc.NextSibling()
263 if ok {
264 textBlock := ast.NewTextBlock()
265 textBlock.SetLines(paragraph.Lines())
266 child.ReplaceChild(child, paragraph, textBlock)
267 }
268 }
269 }
270 }
271}
272
273func (b *listParser) CanInterruptParagraph() bool {
274 return true
275}
276
277func (b *listParser) CanAcceptIndentedLine() bool {
278 return false
279}