markitzero.go (view raw)
1//
2// Copyright (c) 2019 Ted Unangst <tedu@tedunangst.com>
3//
4// Permission to use, copy, modify, and distribute this software for any
5// purpose with or without fee is hereby granted, provided that the above
6// copyright notice and this permission notice appear in all copies.
7//
8// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
16package main
17
18import (
19 "fmt"
20 "regexp"
21 "strings"
22
23 "golang.org/x/net/html"
24 "humungus.tedunangst.com/r/webs/synlight"
25)
26
27var re_bolder = regexp.MustCompile(`(^|\W)\*\*([\w\s,.!?':_-]+)\*\*($|\W)`)
28var re_italicer = regexp.MustCompile(`(^|\W)\*([\w\s,.!?':_-]+)\*($|\W)`)
29var re_bigcoder = regexp.MustCompile("```(.*)\n?((?s:.*?))\n?```\n?")
30var re_coder = regexp.MustCompile("`([^`]*)`")
31var re_quoter = regexp.MustCompile(`(?m:^> (.*)\n?)`)
32var re_link = regexp.MustCompile(`.?.?https?://[^\s"]+[\w/)!]`)
33var re_zerolink = regexp.MustCompile(`\[([^]]*)\]\(([^)]*\)?)\)`)
34
35var lighter = synlight.New(synlight.Options{Format: synlight.HTML})
36
37func markitzero(s string) string {
38 // prepare the string
39 s = strings.TrimSpace(s)
40 s = strings.Replace(s, "\r", "", -1)
41
42 // save away the code blocks so we don't mess them up further
43 var bigcodes, lilcodes []string
44 s = re_bigcoder.ReplaceAllStringFunc(s, func(code string) string {
45 bigcodes = append(bigcodes, code)
46 return "``````"
47 })
48 s = re_coder.ReplaceAllStringFunc(s, func(code string) string {
49 lilcodes = append(lilcodes, code)
50 return "`x`"
51 })
52
53 // fewer side effects than html.EscapeString
54 buf := make([]byte, 0, len(s))
55 for _, c := range []byte(s) {
56 switch c {
57 case '&':
58 buf = append(buf, []byte("&")...)
59 case '<':
60 buf = append(buf, []byte("<")...)
61 case '>':
62 buf = append(buf, []byte(">")...)
63 default:
64 buf = append(buf, c)
65 }
66 }
67 s = string(buf)
68
69 // mark it zero
70 s = re_bolder.ReplaceAllString(s, "$1<b>$2</b>$3")
71 s = re_italicer.ReplaceAllString(s, "$1<i>$2</i>$3")
72 s = re_quoter.ReplaceAllString(s, "<blockquote>$1</blockquote><p>")
73 s = re_link.ReplaceAllStringFunc(s, linkreplacer)
74 s = re_zerolink.ReplaceAllString(s, `<a class="mention u-url" href="$2">$1</a>`)
75
76 // now restore the code blocks
77 s = re_coder.ReplaceAllStringFunc(s, func(string) string {
78 code := lilcodes[0]
79 lilcodes = lilcodes[1:]
80 code = html.EscapeString(code)
81 return code
82 })
83 s = re_bigcoder.ReplaceAllStringFunc(s, func(string) string {
84 code := bigcodes[0]
85 bigcodes = bigcodes[1:]
86 m := re_bigcoder.FindStringSubmatch(code)
87 return "<pre><code>" + lighter.HighlightString(m[2], m[1]) + "</code></pre><p>"
88 })
89 s = re_coder.ReplaceAllString(s, "<code>$1</code>")
90
91 // some final fixups
92 s = strings.Replace(s, "\n", "<br>", -1)
93 s = strings.Replace(s, "<br><blockquote>", "<blockquote>", -1)
94 s = strings.Replace(s, "<br><pre>", "<pre>", -1)
95 s = strings.Replace(s, "<p><br>", "<p>", -1)
96 return s
97}
98
99func linkreplacer(url string) string {
100 if url[0:2] == "](" {
101 return url
102 }
103 prefix := ""
104 for !strings.HasPrefix(url, "http") {
105 prefix += url[0:1]
106 url = url[1:]
107 }
108 addparen := false
109 adddot := false
110 if strings.HasSuffix(url, ")") && strings.IndexByte(url, '(') == -1 {
111 url = url[:len(url)-1]
112 addparen = true
113 }
114 if strings.HasSuffix(url, ".") {
115 url = url[:len(url)-1]
116 adddot = true
117 }
118 url = fmt.Sprintf(`<a class="mention u-url" href="%s">%s</a>`, url, url)
119 if adddot {
120 url += "."
121 }
122 if addparen {
123 url += ")"
124 }
125 return prefix + url
126}