all repos — honk @ 647c846225285633b42eb5af0f24ebf921e2780c

my fork of honk

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, `&lt;iframe src="<a href="%s">%s</a>"&gt;`, 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}