1package main
2
3import (
4	"strconv"
5	"strings"
6	"unicode"
7
8	"github.com/xyproto/vt100"
9)
10
11// ToggleCheckboxCurrentLine will attempt to toggle the Markdown checkbox on the current line of the editor.
12// Returns true if toggled.
13func (e *Editor) ToggleCheckboxCurrentLine() bool {
14	var checkboxPrefixes = []string{"- [ ]", "- [x]", "- [X]", "* [ ]", "* [x]", "* [X]"}
15	// Toggle Markdown checkboxes
16	if line := e.CurrentLine(); hasAnyPrefixWord(strings.TrimSpace(line), checkboxPrefixes) {
17		if strings.Contains(line, "[ ]") {
18			e.SetLine(e.DataY(), strings.Replace(line, "[ ]", "[x]", 1))
19			e.redraw = true
20		} else if strings.Contains(line, "[x]") {
21			e.SetLine(e.DataY(), strings.Replace(line, "[x]", "[ ]", 1))
22			e.redraw = true
23		} else if strings.Contains(line, "[X]") {
24			e.SetLine(e.DataY(), strings.Replace(line, "[X]", "[ ]", 1))
25			e.redraw = true
26		}
27		e.redrawCursor = e.redraw
28		return true
29	}
30	return false
31}
32
33// quotedWordReplace will replace quoted words with a highlighted version
34// line is the uncolored string
35// quote is the quote string (like "`" or "**")
36// regular is the color of the regular text
37// quoted is the color of the highlighted quoted text (including the quotes)
38func quotedWordReplace(line string, quote rune, regular, quoted vt100.AttributeColor) string {
39	// Now do backtick replacements
40	if strings.ContainsRune(line, quote) && runeCount(line, quote)%2 == 0 {
41		inQuote := false
42		s := make([]rune, 0, len(line)*2)
43		// Start by setting the color to the regular one
44		s = append(s, []rune(regular.String())...)
45		var prevR, nextR rune
46		runes := []rune(line)
47		for i, r := range runes {
48			// Look for quotes, but also handle **`asdf`** and __`asdf`__
49			if r == quote && prevR != '*' && nextR != '*' && prevR != '_' && nextR != '_' {
50				inQuote = !inQuote
51				if inQuote {
52					s = append(s, []rune(vt100.Stop())...)
53					s = append(s, []rune(quoted.String())...)
54					s = append(s, r)
55					continue
56				} else {
57					s = append(s, r)
58					s = append(s, []rune(vt100.Stop())...)
59					s = append(s, []rune(regular.String())...)
60					continue
61				}
62			}
63			s = append(s, r)
64			prevR = r                 // the previous r, for the next round
65			nextR = r                 // default value, in case the next rune can not be fetched
66			if (i + 2) < len(runes) { // + 2 since it must look 1 head for the next round
67				nextR = []rune(line)[i+2]
68			}
69		}
70		// End by turning the color off
71		s = append(s, []rune(vt100.Stop())...)
72		return string(s)
73	}
74	// Return the same line, but colored, if the quotes are not balanced
75	return regular.Get(line)
76}
77
78func style(line, marker string, textColor, styleColor vt100.AttributeColor) string {
79	n := strings.Count(line, marker)
80	if n < 2 {
81		// There must be at least two found markers
82		return line
83	}
84	if n%2 != 0 {
85		// The markers must be found in pairs
86		return line
87	}
88	// Split the line up in parts, then combine the parts, with colors
89	parts := strings.Split(line, marker)
90	lastIndex := len(parts) - 1
91	result := ""
92	for i, part := range parts {
93		switch {
94		case i == lastIndex:
95			// Last case
96			result += part
97		case i%2 == 0:
98			// Even case that is not the last case
99			if len(part) == 0 {
100				result += marker
101			} else {
102				result += part + vt100.Stop() + styleColor.String() + marker
103			}
104		default:
105			// Odd case that is not the last case
106			if len(part) == 0 {
107				result += marker
108			} else {
109				result += part + marker + vt100.Stop() + textColor.String()
110			}
111		}
112	}
113	return result
114}
115
116func emphasis(line string, textColor, italicsColor, boldColor, strikeColor vt100.AttributeColor) string {
117	result := line
118	result = style(result, "~~", textColor, strikeColor)
119	result = style(result, "**", textColor, boldColor)
120	result = style(result, "__", textColor, boldColor)
121	// For now, nested emphasis and italics are not supported, only bold and strikethrough
122	// TODO: Implement nested emphasis and italics
123	//result = style(result, "*", textColor, italicsColor)
124	//result = style(result, "_", textColor, italicsColor)
125	return result
126}
127
128// isListItem checks if the given line is likely to be a Markdown list item
129func isListItem(line string) bool {
130	trimmedLine := strings.TrimSpace(line)
131	fields := strings.Fields(trimmedLine)
132	if len(fields) == 0 {
133		return false
134	}
135	firstWord := fields[0]
136
137	// Check if this is a regular list item
138	switch firstWord {
139	case "*", "-", "+":
140		return true
141	}
142
143	// Check if this is a numbered list item
144	if strings.HasSuffix(firstWord, ".") {
145		if _, err := strconv.Atoi(firstWord[:len(firstWord)-1]); err == nil { // success
146			return true
147		}
148	}
149
150	return false
151}
152
153// markdownHighlight returns a VT100 colored line, a bool that is true if it worked out and a bool that is true if it's the start or stop of a block quote
154func (e *Editor) markdownHighlight(line string, inCodeBlock bool, listItemRecord []bool, inListItem *bool) (string, bool, bool) {
155
156	dataPos := 0
157	for i, r := range line {
158		if unicode.IsSpace(r) {
159			dataPos = i + 1
160		} else {
161			break
162		}
163	}
164
165	// First position of non-space on line is now dataPos
166	leadingSpace := line[:dataPos]
167
168	// Get the rest of the line that isn't whitespace
169	rest := line[dataPos:]
170
171	// Starting or ending a code block
172	if strings.HasPrefix(rest, "~~~") || strings.HasPrefix(rest, "```") { // TODO: fix syntax highlighting when this comment is removed `
173		return e.CodeBlockColor.Get(line), true, true
174	}
175
176	if inCodeBlock {
177		return e.CodeBlockColor.Get(line), true, false
178	}
179
180	// N is the number of lines to highlight with the same color for each numbered point or bullet point in a list
181	N := 3
182	prevNisListItem := false
183	for i := len(listItemRecord) - 1; i > (len(listItemRecord) - N); i-- {
184		if i >= 0 && listItemRecord[i] {
185			prevNisListItem = true
186		}
187	}
188
189	if leadingSpace == "    " && !strings.HasPrefix(rest, "*") && !strings.HasPrefix(rest, "-") && !prevNisListItem {
190		// Four leading spaces means a quoted line
191		// Also assume it's not a quote if it starts with "*" or "-"
192		return e.CodeColor.Get(line), true, false
193	}
194
195	// An image (or a link to a single image) on a single line
196	if (strings.HasPrefix(rest, "[!") || strings.HasPrefix(rest, "!")) && strings.HasSuffix(rest, ")") {
197		return e.ImageColor.Get(line), true, false
198	}
199
200	// A link on a single line
201	if strings.HasPrefix(rest, "[") && strings.HasSuffix(rest, ")") && strings.Count(rest, "[") == 1 {
202		return e.LinkColor.Get(line), true, false
203	}
204
205	// A header line
206	if strings.HasPrefix(rest, "---") || strings.HasPrefix(rest, "===") {
207		return e.HeaderTextColor.Get(line), true, false
208	}
209
210	// HTML comments
211	if strings.HasPrefix(rest, "<!--") || strings.HasPrefix(rest, "-->") {
212		return e.CommentColor.Get(line), true, false
213	}
214
215	// A line with just a quote mark
216	if strings.TrimSpace(rest) == ">" {
217		return e.QuoteColor.Get(line), true, false
218	}
219
220	// A quote with something that follows
221	if pos := strings.Index(rest, "> "); pos >= 0 && pos < 5 {
222		words := strings.Fields(rest)
223		if len(words) >= 2 {
224			return e.QuoteColor.Get(words[0]) + " " + e.QuoteTextColor.Get(strings.Join(words[1:], " ")), true, false
225		}
226	}
227
228	// HTML
229	if strings.HasPrefix(rest, "<") || strings.HasPrefix(rest, ">") {
230		return e.HTMLColor.Get(line), true, false
231	}
232
233	// Table
234	if strings.HasPrefix(rest, "|") || strings.HasSuffix(rest, "|") {
235		if strings.HasPrefix(line, "|-") {
236			return e.TableColor.String() + line + e.TableBackground.String(), true, false
237		}
238		return strings.Replace(line, "|", e.TableColor.String()+"|"+e.TableBackground.String(), -1), true, false
239	}
240
241	// Split the rest of the line into words
242	words := strings.Fields(rest)
243	if len(words) == 0 {
244		*inListItem = false
245		// Nothing to do here
246		return "", false, false
247	}
248
249	// Color differently depending on the leading word
250	firstWord := words[0]
251	lastWord := words[len(words)-1]
252
253	switch {
254	case consistsOf(firstWord, '#', []rune{'.', ' '}):
255		if strings.HasSuffix(lastWord, "#") && strings.Contains(rest, " ") {
256			centerLen := len(rest) - (len(firstWord) + len(lastWord))
257			if centerLen > 0 {
258				centerText := rest[len(firstWord) : len(rest)-len(lastWord)]
259				return leadingSpace + e.HeaderBulletColor.Get(firstWord) + e.HeaderTextColor.Get(centerText) + e.HeaderBulletColor.Get(lastWord), true, false
260			}
261			return leadingSpace + e.HeaderBulletColor.Get(rest), true, false
262		} else if len(words) > 1 {
263			return leadingSpace + e.HeaderBulletColor.Get(firstWord) + " " + e.HeaderTextColor.Get(emphasis(quotedWordReplace(line[dataPos+len(firstWord)+1:], '`', e.HeaderTextColor, e.CodeColor), e.HeaderTextColor, e.ItalicsColor, e.BoldColor, e.StrikeColor)), true, false // TODO: `
264		}
265		return leadingSpace + e.HeaderTextColor.Get(rest), true, false
266	case isListItem(line):
267		if strings.HasPrefix(rest, "- [ ] ") || strings.HasPrefix(rest, "- [x] ") || strings.HasPrefix(rest, "- [X] ") {
268			return leadingSpace + e.ListBulletColor.Get(rest[:1]) + " " + e.CheckboxColor.Get(rest[2:3]) + e.XColor.Get(rest[3:4]) + e.CheckboxColor.Get(rest[4:5]) + " " + emphasis(quotedWordReplace(line[dataPos+6:], '`', e.ListTextColor, e.ListCodeColor), e.ListTextColor, e.ItalicsColor, e.BoldColor, e.StrikeColor), true, false
269		}
270		if len(words) > 1 {
271			return leadingSpace + e.ListBulletColor.Get(firstWord) + " " + emphasis(quotedWordReplace(line[dataPos+len(firstWord)+1:], '`', e.ListTextColor, e.ListCodeColor), e.ListTextColor, e.ItalicsColor, e.BoldColor, e.StrikeColor), true, false
272		}
273		return leadingSpace + e.ListTextColor.Get(rest), true, false
274	}
275
276	// Leading hash without a space afterwards?
277	if strings.HasPrefix(line, "#") && !strings.HasPrefix(line, "# ") {
278		return e.MenuArrowColor.Get(line), true, false
279	}
280
281	if prevNisListItem {
282		*inListItem = true
283	}
284
285	// A completely regular line of text that is also the continuation of a list item
286	if *inListItem {
287		return emphasis(quotedWordReplace(line, '`', e.ListTextColor, e.ListCodeColor), e.ListTextColor, e.ItalicsColor, e.BoldColor, e.StrikeColor), true, false
288	}
289
290	// A completely regular line of text
291	return emphasis(quotedWordReplace(line, '`', e.MarkdownTextColor, e.CodeColor), e.MarkdownTextColor, e.ItalicsColor, e.BoldColor, e.StrikeColor), true, false
292}
293