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