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}