list.go

  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}