html.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 "html/template"
21 "io"
22 "log"
23 "net/url"
24 "regexp"
25 "sort"
26 "strings"
27
28 "golang.org/x/net/html"
29)
30
31var permittedtags = []string{"div", "h1", "h2", "h3", "h4", "h5", "h6",
32 "table", "thead", "tbody", "th", "tr", "td",
33 "p", "br", "pre", "code", "blockquote", "strong", "em", "b", "i", "s", "sup",
34 "ol", "ul", "li"}
35var permittedattr = []string{"colspan", "rowspan"}
36var bannedtags = []string{"script", "style"}
37
38func init() {
39 sort.Strings(permittedtags)
40 sort.Strings(permittedattr)
41 sort.Strings(bannedtags)
42}
43
44func contains(array []string, tag string) bool {
45 idx := sort.SearchStrings(array, tag)
46 return idx < len(array) && array[idx] == tag
47}
48
49func getattr(node *html.Node, attr string) string {
50 for _, a := range node.Attr {
51 if a.Key == attr {
52 return a.Val
53 }
54 }
55 return ""
56}
57
58func hasclass(node *html.Node, class string) bool {
59 return strings.Contains(" "+getattr(node, "class")+" ", " "+class+" ")
60}
61
62func writetag(w io.Writer, node *html.Node) {
63 io.WriteString(w, "<")
64 io.WriteString(w, node.Data)
65 for _, attr := range node.Attr {
66 if contains(permittedattr, attr.Key) {
67 fmt.Fprintf(w, ` %s="%s"`, attr.Key, html.EscapeString(attr.Val))
68 }
69 }
70 io.WriteString(w, ">")
71}
72
73func render(w io.Writer, node *html.Node) {
74 switch node.Type {
75 case html.ElementNode:
76 tag := node.Data
77 switch {
78 case tag == "a":
79 href := getattr(node, "href")
80 hrefurl, err := url.Parse(href)
81 if err != nil {
82 href = "#BROKEN-" + href
83 } else {
84 href = hrefurl.String()
85 }
86 fmt.Fprintf(w, `<a href="%s" rel=noreferrer>`, html.EscapeString(href))
87 case tag == "img":
88 div := replaceimg(node)
89 if div != "skip" {
90 io.WriteString(w, div)
91 }
92 case tag == "span":
93 case tag == "iframe":
94 src := html.EscapeString(getattr(node, "src"))
95 fmt.Fprintf(w, `<iframe src="<a href="%s">%s</a>">`, src, src)
96 case contains(permittedtags, tag):
97 writetag(w, node)
98 case contains(bannedtags, tag):
99 return
100 }
101 case html.TextNode:
102 io.WriteString(w, html.EscapeString(node.Data))
103 }
104 for c := node.FirstChild; c != nil; c = c.NextSibling {
105 render(w, c)
106 }
107 if node.Type == html.ElementNode {
108 tag := node.Data
109 if tag == "a" || (contains(permittedtags, tag) && tag != "br") {
110 fmt.Fprintf(w, "</%s>", tag)
111 }
112 if tag == "p" || tag == "div" {
113 io.WriteString(w, "\n")
114 }
115 }
116}
117
118func replaceimg(node *html.Node) string {
119 src := getattr(node, "src")
120 alt := getattr(node, "alt")
121 //title := getattr(node, "title")
122 if hasclass(node, "Emoji") && alt != "" {
123 return html.EscapeString(alt)
124 }
125 return html.EscapeString(fmt.Sprintf(`<img src="%s">`, src))
126}
127
128func cleannode(node *html.Node) template.HTML {
129 var buf strings.Builder
130 render(&buf, node)
131 return template.HTML(buf.String())
132}
133
134func cleanstring(shtml string) template.HTML {
135 reader := strings.NewReader(shtml)
136 body, err := html.Parse(reader)
137 if err != nil {
138 log.Printf("error parsing html: %s", err)
139 return ""
140 }
141 return cleannode(body)
142}
143
144func textonly(w io.Writer, node *html.Node) {
145 switch node.Type {
146 case html.ElementNode:
147 tag := node.Data
148 switch {
149 case tag == "a":
150 href := getattr(node, "href")
151 fmt.Fprintf(w, `<a href="%s">`, href)
152 case tag == "img":
153 io.WriteString(w, "<img>")
154 case contains(bannedtags, tag):
155 return
156 }
157 case html.TextNode:
158 io.WriteString(w, node.Data)
159 }
160 for c := node.FirstChild; c != nil; c = c.NextSibling {
161 textonly(w, c)
162 }
163 if node.Type == html.ElementNode {
164 tag := node.Data
165 if tag == "a" {
166 fmt.Fprintf(w, "</%s>", tag)
167 }
168 if tag == "p" || tag == "div" {
169 io.WriteString(w, "\n")
170 }
171 }
172}
173
174var re_whitespaceeater = regexp.MustCompile("[ \t\r]*\n[ \t\r]*")
175var re_blanklineeater = regexp.MustCompile("\n\n+")
176var re_tabeater = regexp.MustCompile("[ \t]+")
177
178func htmltotext(shtml template.HTML) string {
179 reader := strings.NewReader(string(shtml))
180 body, _ := html.Parse(reader)
181 var buf strings.Builder
182 textonly(&buf, body)
183 rv := buf.String()
184 rv = re_whitespaceeater.ReplaceAllLiteralString(rv, "\n")
185 rv = re_blanklineeater.ReplaceAllLiteralString(rv, "\n\n")
186 rv = re_tabeater.ReplaceAllLiteralString(rv, " ")
187 for len(rv) > 0 && rv[0] == '\n' {
188 rv = rv[1:]
189 }
190 return rv
191}