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  }