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)\*\*((?s:.*?))\*\*($|\W)`)
28var re_italicer = regexp.MustCompile(`(^|\W)\*((?s:.*?))\*($|\W)`)
29var re_bigcoder = regexp.MustCompile("```(.*)\n?((?s:.*?))\n?```\n?")
30var re_coder = regexp.MustCompile("`([^`]*)`")
31var re_quoter = regexp.MustCompile(`(?m:^> (.*)(\n-(.*))?\n?)`)
32var re_link = regexp.MustCompile(`.?.?https?://[^\s"]+[\w/)!]`)
33var re_zerolink = regexp.MustCompile(`\[([^]]*)\]\(([^)]*\)?)\)`)
34var re_imgfix = regexp.MustCompile(`<img ([^>]*)>`)
35var re_lister = regexp.MustCompile(`((^|\n)(\+|-).*)+\n?`)
36
37var lighter = synlight.New(synlight.Options{Format: synlight.HTML})
38
39func markitzero(s string) string {
40 // prepare the string
41 s = strings.TrimSpace(s)
42 s = strings.Replace(s, "\r", "", -1)
43
44 // save away the code blocks so we don't mess them up further
45 var bigcodes, lilcodes, images []string
46 s = re_bigcoder.ReplaceAllStringFunc(s, func(code string) string {
47 bigcodes = append(bigcodes, code)
48 return "``````"
49 })
50 s = re_coder.ReplaceAllStringFunc(s, func(code string) string {
51 lilcodes = append(lilcodes, code)
52 return "`x`"
53 })
54 s = re_imgfix.ReplaceAllStringFunc(s, func(img string) string {
55 images = append(images, img)
56 return "<img x>"
57 })
58
59 // fewer side effects than html.EscapeString
60 buf := make([]byte, 0, len(s))
61 for _, c := range []byte(s) {
62 switch c {
63 case '&':
64 buf = append(buf, []byte("&")...)
65 case '<':
66 buf = append(buf, []byte("<")...)
67 case '>':
68 buf = append(buf, []byte(">")...)
69 default:
70 buf = append(buf, c)
71 }
72 }
73 s = string(buf)
74
75 // mark it zero
76 s = re_link.ReplaceAllStringFunc(s, linkreplacer)
77 s = re_zerolink.ReplaceAllString(s, `<a href="$2">$1</a>`)
78 s = re_bolder.ReplaceAllString(s, "$1<b>$2</b>$3")
79 s = re_italicer.ReplaceAllString(s, "$1<i>$2</i>$3")
80 s = re_quoter.ReplaceAllString(s, "<blockquote>$1<br><cite>$3</cite></blockquote><p>")
81 s = strings.Replace(s, "\n---\n", "<hr><p>", -1)
82
83 s = re_lister.ReplaceAllStringFunc(s, func(m string) string {
84 m = strings.Trim(m, "\n")
85 items := strings.Split(m, "\n")
86 r := "<ul>"
87 for _, item := range items {
88 r += "<li>" + strings.Trim(item[1:], " ")
89 }
90 r += "</ul><p>"
91 return r
92 })
93
94 // restore images
95 s = strings.Replace(s, "<img x>", "<img x>", -1)
96 s = re_imgfix.ReplaceAllStringFunc(s, func(string) string {
97 img := images[0]
98 images = images[1:]
99 return img
100 })
101
102 // now restore the code blocks
103 s = re_coder.ReplaceAllStringFunc(s, func(string) string {
104 code := lilcodes[0]
105 lilcodes = lilcodes[1:]
106 code = html.EscapeString(code)
107 return code
108 })
109 s = re_bigcoder.ReplaceAllStringFunc(s, func(string) string {
110 code := bigcodes[0]
111 bigcodes = bigcodes[1:]
112 m := re_bigcoder.FindStringSubmatch(code)
113 return "<pre><code>" + lighter.HighlightString(m[2], m[1]) + "</code></pre><p>"
114 })
115 s = re_coder.ReplaceAllString(s, "<code>$1</code>")
116
117 // some final fixups
118 s = strings.Replace(s, "\n", "<br>", -1)
119 s = strings.Replace(s, "<br><blockquote>", "<blockquote>", -1)
120 s = strings.Replace(s, "<br><cite></cite>", "", -1)
121 s = strings.Replace(s, "<br><pre>", "<pre>", -1)
122 s = strings.Replace(s, "<br><ul>", "<ul>", -1)
123 s = strings.Replace(s, "<p><br>", "<p>", -1)
124 return s
125}
126
127func linkreplacer(url string) string {
128 if url[0:2] == "](" {
129 return url
130 }
131 prefix := ""
132 for !strings.HasPrefix(url, "http") {
133 prefix += url[0:1]
134 url = url[1:]
135 }
136 addparen := false
137 adddot := false
138 if strings.HasSuffix(url, ")") && strings.IndexByte(url, '(') == -1 {
139 url = url[:len(url)-1]
140 addparen = true
141 }
142 if strings.HasSuffix(url, ".") {
143 url = url[:len(url)-1]
144 adddot = true
145 }
146 url = fmt.Sprintf(`<a href="%s">%s</a>`, url, url)
147 if adddot {
148 url += "."
149 }
150 if addparen {
151 url += ")"
152 }
153 return prefix + url
154}