1// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2// See LICENSE.txt for license information.
3
4package markdown
5
6import (
7	"strings"
8)
9
10type ListItem struct {
11	blockBase
12	markdown                    string
13	hasTrailingBlankLine        bool
14	hasBlankLineBetweenChildren bool
15
16	Indentation int
17	Children    []Block
18}
19
20func (b *ListItem) Continuation(indentation int, r Range) *continuation {
21	s := b.markdown[r.Position:r.End]
22	if strings.TrimSpace(s) == "" {
23		if b.Children == nil {
24			return nil
25		}
26		return &continuation{
27			Remaining: r,
28		}
29	}
30	if indentation < b.Indentation {
31		return nil
32	}
33	return &continuation{
34		Indentation: indentation - b.Indentation,
35		Remaining:   r,
36	}
37}
38
39func (b *ListItem) AddChild(openBlocks []Block) []Block {
40	b.Children = append(b.Children, openBlocks[0])
41	if b.hasTrailingBlankLine {
42		b.hasBlankLineBetweenChildren = true
43	}
44	b.hasTrailingBlankLine = false
45	return openBlocks
46}
47
48func (b *ListItem) AddLine(indentation int, r Range) bool {
49	isBlank := strings.TrimSpace(b.markdown[r.Position:r.End]) == ""
50	if isBlank {
51		b.hasTrailingBlankLine = true
52	}
53	return false
54}
55
56func (b *ListItem) HasTrailingBlankLine() bool {
57	return b.hasTrailingBlankLine || (len(b.Children) > 0 && b.Children[len(b.Children)-1].HasTrailingBlankLine())
58}
59
60func (b *ListItem) isLoose() bool {
61	if b.hasBlankLineBetweenChildren {
62		return true
63	}
64	for i, child := range b.Children {
65		if i < len(b.Children)-1 && child.HasTrailingBlankLine() {
66			return true
67		}
68	}
69	return false
70}
71
72type List struct {
73	blockBase
74	markdown                    string
75	hasTrailingBlankLine        bool
76	hasBlankLineBetweenChildren bool
77
78	IsLoose           bool
79	IsOrdered         bool
80	OrderedStart      int
81	BulletOrDelimiter byte
82	Children          []*ListItem
83}
84
85func (b *List) Continuation(indentation int, r Range) *continuation {
86	s := b.markdown[r.Position:r.End]
87	if strings.TrimSpace(s) == "" {
88		return &continuation{
89			Remaining: r,
90		}
91	}
92	return &continuation{
93		Indentation: indentation,
94		Remaining:   r,
95	}
96}
97
98func (b *List) AddChild(openBlocks []Block) []Block {
99	if item, ok := openBlocks[0].(*ListItem); ok {
100		b.Children = append(b.Children, item)
101		if b.hasTrailingBlankLine {
102			b.hasBlankLineBetweenChildren = true
103		}
104		b.hasTrailingBlankLine = false
105		return openBlocks
106	} else if list, ok := openBlocks[0].(*List); ok {
107		if len(list.Children) == 1 && list.IsOrdered == b.IsOrdered && list.BulletOrDelimiter == b.BulletOrDelimiter {
108			return b.AddChild(openBlocks[1:])
109		}
110	}
111	return nil
112}
113
114func (b *List) AddLine(indentation int, r Range) bool {
115	isBlank := strings.TrimSpace(b.markdown[r.Position:r.End]) == ""
116	if isBlank {
117		b.hasTrailingBlankLine = true
118	}
119	return false
120}
121
122func (b *List) HasTrailingBlankLine() bool {
123	return b.hasTrailingBlankLine || (len(b.Children) > 0 && b.Children[len(b.Children)-1].HasTrailingBlankLine())
124}
125
126func (b *List) isLoose() bool {
127	if b.hasBlankLineBetweenChildren {
128		return true
129	}
130	for i, child := range b.Children {
131		if child.isLoose() || (i < len(b.Children)-1 && child.HasTrailingBlankLine()) {
132			return true
133		}
134	}
135	return false
136}
137
138func (b *List) Close() {
139	b.IsLoose = b.isLoose()
140}
141
142func parseListMarker(markdown string, r Range) (success, isOrdered bool, orderedStart int, bulletOrDelimiter byte, markerWidth int, remaining Range) {
143	digits := 0
144	n := 0
145	for i := r.Position; i < r.End && markdown[i] >= '0' && markdown[i] <= '9'; i++ {
146		digits++
147		n = n*10 + int(markdown[i]-'0')
148	}
149	if digits > 0 {
150		if digits > 9 || r.Position+digits >= r.End {
151			return
152		}
153		next := markdown[r.Position+digits]
154		if next != '.' && next != ')' {
155			return
156		}
157		return true, true, n, next, digits + 1, Range{r.Position + digits + 1, r.End}
158	}
159	if r.Position >= r.End {
160		return
161	}
162	next := markdown[r.Position]
163	if next != '-' && next != '+' && next != '*' {
164		return
165	}
166	return true, false, 0, next, 1, Range{r.Position + 1, r.End}
167}
168
169func listStart(markdown string, indent int, r Range, matchedBlocks, unmatchedBlocks []Block) []Block {
170	afterList := false
171	if len(matchedBlocks) > 0 {
172		_, afterList = matchedBlocks[len(matchedBlocks)-1].(*List)
173	}
174	if !afterList && indent > 3 {
175		return nil
176	}
177
178	success, isOrdered, orderedStart, bulletOrDelimiter, markerWidth, remaining := parseListMarker(markdown, r)
179	if !success {
180		return nil
181	}
182
183	isBlank := strings.TrimSpace(markdown[remaining.Position:remaining.End]) == ""
184	if len(matchedBlocks) > 0 && len(unmatchedBlocks) == 0 {
185		if _, ok := matchedBlocks[len(matchedBlocks)-1].(*Paragraph); ok {
186			if isBlank || (isOrdered && orderedStart != 1) {
187				return nil
188			}
189		}
190	}
191
192	indentAfterMarker, indentBytesAfterMarker := countIndentation(markdown, remaining)
193	if !isBlank && indentAfterMarker < 1 {
194		return nil
195	}
196
197	remaining = Range{remaining.Position + indentBytesAfterMarker, remaining.End}
198	consumedIndentAfterMarker := indentAfterMarker
199	if isBlank || indentAfterMarker >= 5 {
200		consumedIndentAfterMarker = 1
201	}
202
203	listItem := &ListItem{
204		markdown:    markdown,
205		Indentation: indent + markerWidth + consumedIndentAfterMarker,
206	}
207	list := &List{
208		markdown:          markdown,
209		IsOrdered:         isOrdered,
210		OrderedStart:      orderedStart,
211		BulletOrDelimiter: bulletOrDelimiter,
212		Children:          []*ListItem{listItem},
213	}
214	ret := []Block{list, listItem}
215	if descendants := blockStartOrParagraph(markdown, indentAfterMarker-consumedIndentAfterMarker, remaining, nil, nil); descendants != nil {
216		listItem.Children = append(listItem.Children, descendants[0])
217		ret = append(ret, descendants...)
218	}
219	return ret
220}
221