github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/extend/plugin_markdown.go (about) 1 package extend 2 3 import ( 4 "strings" 5 6 c "github.com/Azareal/Gosora/common" 7 ) 8 9 var markdownMaxDepth = 25 // How deep the parser will go when parsing Markdown strings 10 var markdownUnclosedElement []byte 11 12 var markdownBoldTagOpen []byte 13 var markdownBoldTagClose []byte 14 var markdownItalicTagOpen []byte 15 var markdownItalicTagClose []byte 16 var markdownUnderlineTagOpen []byte 17 var markdownUnderlineTagClose []byte 18 var markdownStrikeTagOpen []byte 19 var markdownStrikeTagClose []byte 20 var markdownQuoteTagOpen []byte 21 var markdownQuoteTagClose []byte 22 var markdownSpoilerTagOpen []byte 23 var markdownSpoilerTagClose []byte 24 var markdownH1TagOpen []byte 25 var markdownH1TagClose []byte 26 27 func init() { 28 c.Plugins.Add(&c.Plugin{UName: "markdown", Name: "Markdown", Author: "Azareal", URL: "https://github.com/Azareal", Init: InitMarkdown, Deactivate: deactivateMarkdown}) 29 } 30 31 func InitMarkdown(pl *c.Plugin) error { 32 markdownUnclosedElement = []byte("<red>[Unclosed Element]</red>") 33 34 markdownBoldTagOpen = []byte("<b>") 35 markdownBoldTagClose = []byte("</b>") 36 markdownItalicTagOpen = []byte("<i>") 37 markdownItalicTagClose = []byte("</i>") 38 markdownUnderlineTagOpen = []byte("<u>") 39 markdownUnderlineTagClose = []byte("</u>") 40 markdownStrikeTagOpen = []byte("<s>") 41 markdownStrikeTagClose = []byte("</s>") 42 markdownQuoteTagOpen = []byte("<blockquote>") 43 markdownQuoteTagClose = []byte("</blockquote>") 44 markdownSpoilerTagOpen = []byte("<spoiler>") 45 markdownSpoilerTagClose = []byte("</spoiler>") 46 markdownH1TagOpen = []byte("<h2>") 47 markdownH1TagClose = []byte("</h2>") 48 49 pl.AddHook("parse_assign", MarkdownParse) 50 return nil 51 } 52 53 func deactivateMarkdown(pl *c.Plugin) { 54 pl.RemoveHook("parse_assign", MarkdownParse) 55 } 56 57 // An adapter for the parser, so that the parser can call itself recursively. 58 // This is less for the simple Markdown elements like bold and italics and more for the really complicated ones I plan on adding at some point. 59 func MarkdownParse(msg string) string { 60 msg = _markdownParse(msg+" ", 0) 61 if msg[len(msg)-1] == ' ' { 62 msg = msg[:len(msg)-1] 63 } 64 return msg 65 } 66 67 // Under Construction! 68 func _markdownParse(msg string, n int) string { 69 if n > markdownMaxDepth { 70 return "<red>[Markdown Error: Overflowed the max depth of 20]</red>" 71 } 72 73 var outbytes []byte 74 var lastElement int 75 breaking := false 76 //c.DebugLogf("Initial Msg: %+v\n", strings.Replace(msg, "\r", "\\r", -1)) 77 78 for index := 0; index < len(msg); index++ { 79 simpleMatch := func(char byte, o []byte, c []byte) { 80 startIndex := index 81 if (index + 1) >= len(msg) { 82 breaking = true 83 return 84 } 85 86 index++ 87 index = markdownSkipUntilChar(msg, index, char) 88 if (index-(startIndex+1)) < 1 || index >= len(msg) { 89 breaking = true 90 return 91 } 92 93 sIndex := startIndex + 1 94 lIndex := index 95 index++ 96 97 outbytes = append(outbytes, msg[lastElement:startIndex]...) 98 outbytes = append(outbytes, o...) 99 // TODO: Implement this without as many type conversions 100 outbytes = append(outbytes, []byte(_markdownParse(msg[sIndex:lIndex], n+1))...) 101 outbytes = append(outbytes, c...) 102 103 lastElement = index 104 index-- 105 } 106 107 startLine := func() { 108 startIndex := index 109 if (index + 1) >= len(msg) /*|| (index + 2) >= len(msg)*/ { 110 breaking = true 111 return 112 } 113 index++ 114 115 index = markdownSkipUntilNotChar(msg, index, 32) 116 if (index + 1) >= len(msg) { 117 breaking = true 118 return 119 } 120 //index++ 121 122 index = markdownSkipUntilStrongSpace(msg, index) 123 sIndex := startIndex + 1 124 lIndex := index 125 index++ 126 127 outbytes = append(outbytes, msg[lastElement:startIndex]...) 128 outbytes = append(outbytes, markdownH1TagOpen...) 129 // TODO: Implement this without as many type conversions 130 //fmt.Println("msg[sIndex:lIndex]:", string(msg[sIndex:lIndex])) 131 // TODO: Quick hack to eliminate trailing spaces... 132 outbytes = append(outbytes, []byte(strings.TrimSpace(_markdownParse(msg[sIndex:lIndex], n+1)))...) 133 outbytes = append(outbytes, markdownH1TagClose...) 134 135 lastElement = index 136 index-- 137 } 138 139 uniqueWord := func(i int) bool { 140 if i == 0 { 141 return true 142 } 143 return msg[i-1] <= 32 144 } 145 146 switch msg[index] { 147 // TODO: Do something slightly less hacky for skipping URLs 148 case '/': 149 if len(msg) > (index+2) && msg[index+1] == '/' { 150 for ; index < len(msg) && msg[index] != ' '; index++ { 151 } 152 index-- 153 continue 154 } 155 case '_': 156 if !uniqueWord(index) { 157 break 158 } 159 simpleMatch('_', markdownUnderlineTagOpen, markdownUnderlineTagClose) 160 if breaking { 161 break 162 } 163 case '~': 164 simpleMatch('~', markdownStrikeTagOpen, markdownStrikeTagClose) 165 if breaking { 166 break 167 } 168 case '*': 169 startIndex := index 170 italic := true 171 bold := false 172 if (index + 2) < len(msg) { 173 if msg[index+1] == '*' { 174 bold = true 175 index++ 176 if msg[index+1] != '*' { 177 italic = false 178 } else { 179 index++ 180 } 181 } 182 } 183 184 // Does the string terminate abruptly? 185 if (index + 1) >= len(msg) { 186 break 187 } 188 index++ 189 190 index = markdownSkipUntilAsterisk(msg, index) 191 if index >= len(msg) { 192 break 193 } 194 195 preBreak := func() { 196 outbytes = append(outbytes, msg[lastElement:startIndex]...) 197 lastElement = startIndex 198 } 199 200 sIndex := startIndex 201 lIndex := index 202 if bold && italic { 203 if (index + 3) >= len(msg) { 204 preBreak() 205 break 206 } 207 index += 3 208 sIndex += 3 209 } else if bold { 210 if (index + 2) >= len(msg) { 211 preBreak() 212 break 213 } 214 index += 2 215 sIndex += 2 216 } else { 217 if (index + 1) >= len(msg) { 218 preBreak() 219 break 220 } 221 index++ 222 sIndex++ 223 } 224 225 if lIndex <= sIndex { 226 preBreak() 227 break 228 } 229 if sIndex < 0 || lIndex < 0 { 230 preBreak() 231 break 232 } 233 outbytes = append(outbytes, msg[lastElement:startIndex]...) 234 235 if bold { 236 outbytes = append(outbytes, markdownBoldTagOpen...) 237 } 238 if italic { 239 outbytes = append(outbytes, markdownItalicTagOpen...) 240 } 241 242 // TODO: Implement this without as many type conversions 243 outbytes = append(outbytes, []byte(_markdownParse(msg[sIndex:lIndex], n+1))...) 244 245 if italic { 246 outbytes = append(outbytes, markdownItalicTagClose...) 247 } 248 if bold { 249 outbytes = append(outbytes, markdownBoldTagClose...) 250 } 251 252 lastElement = index 253 index-- 254 case '\\': 255 if (index + 1) < len(msg) { 256 if isMarkdownStartChar(msg[index+1]) && msg[index+1] != '\\' { 257 outbytes = append(outbytes, msg[lastElement:index]...) 258 index++ 259 lastElement = index 260 } 261 } 262 // TODO: Add a inline quote variant 263 case '`': 264 simpleMatch('`', markdownQuoteTagOpen, markdownQuoteTagClose) 265 if breaking { 266 break 267 } 268 // TODO: Might need to be double pipe 269 case '|': 270 simpleMatch('|', markdownSpoilerTagOpen, markdownSpoilerTagClose) 271 if breaking { 272 break 273 } 274 case 10: // newline 275 if (index + 1) >= len(msg) { 276 break 277 } 278 index++ 279 280 if msg[index] != '#' { 281 continue 282 } 283 startLine() 284 if breaking { 285 break 286 } 287 case '#': 288 if index != 0 { 289 continue 290 } 291 startLine() 292 if breaking { 293 break 294 } 295 } 296 } 297 298 if len(outbytes) == 0 { 299 return msg 300 } else if lastElement < (len(msg) - 1) { 301 msg = string(outbytes) + msg[lastElement:] 302 return msg 303 } 304 return string(outbytes) 305 } 306 307 func isMarkdownStartChar(ch byte) bool { // char 308 return ch == '\\' || ch == '~' || ch == '_' || ch == 10 || ch == '`' || ch == '*' || ch == '|' 309 } 310 311 func markdownFindChar(data string, index int, char byte) bool { 312 for ; index < len(data); index++ { 313 item := data[index] 314 if item > 32 { 315 return (item == char) 316 } 317 } 318 return false 319 } 320 321 func markdownSkipUntilChar(data string, index int, char byte) int { 322 for ; index < len(data); index++ { 323 if data[index] == char { 324 break 325 } 326 } 327 return index 328 } 329 330 func markdownSkipUntilNotChar(data string, index int, char byte) int { 331 for ; index < len(data); index++ { 332 if data[index] != char { 333 break 334 } 335 } 336 return index 337 } 338 339 func markdownSkipUntilStrongSpace(data string, index int) int { 340 inSpace := false 341 for ; index < len(data); index++ { 342 if inSpace && data[index] == 32 { 343 index-- 344 break 345 } else if data[index] == 32 { 346 inSpace = true 347 } else if data[index] < 32 { 348 break 349 } else { 350 inSpace = false 351 } 352 } 353 return index 354 } 355 356 func markdownSkipUntilAsterisk(data string, index int) int { 357 SwitchLoop: 358 for ; index < len(data); index++ { 359 switch data[index] { 360 case 10: 361 if ((index + 1) < len(data)) && markdownFindChar(data, index, '*') { 362 index = markdownSkipList(data, index) 363 } 364 case '*': 365 break SwitchLoop 366 } 367 } 368 return index 369 } 370 371 // plugin_markdown doesn't support lists yet, but I want it to be easy to have nested lists when we do have them 372 func markdownSkipList(data string, index int) int { 373 var lastNewline int 374 datalen := len(data) 375 for ; index < datalen; index++ { 376 SkipListInnerLoop: 377 if data[index] == 10 { 378 lastNewline = index 379 for ; index < datalen; index++ { 380 if data[index] > 32 { 381 break 382 } else if data[index] == 10 { 383 goto SkipListInnerLoop 384 } 385 } 386 if index >= datalen { 387 if data[index] != '*' && data[index] != '-' { 388 if (lastNewline + 1) < datalen { 389 return lastNewline + 1 390 } 391 return lastNewline 392 } 393 } 394 } 395 } 396 397 return index 398 }