1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
package logic
// htmlToText turns a feed item's title or description, or the content of a
// note sent to the birb, into plain text.
//
// Every tag goes; their text stays, except that of the elements in
// textDropped, which is not text a reader sees. Text comes out unescaped:
// whoever puts it into HTML escapes it there (texts.WithVals does), and
// getUrl must see "&" in a URL, not "&". Blocks and <br> become line
// breaks, so the text of two paragraphs does not run together, nor a URL into
// the word after it. Runs of white space become one space or line break, as
// a browser would show them.
import (
"strings"
"unicode"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
)
// textDropped are left out with everything in them. These are the elements
// whose content bluemonday's StrictPolicy, used before, skipped, plus
// template and svg.
var textDropped = map[string]bool{
"script": true, "style": true, "title": true, "noscript": true,
"iframe": true, "frame": true, "frameset": true, "noframes": true,
"noembed": true, "nostyle": true, "object": true,
"template": true, "svg": true,
}
// textBlocks start and end a line.
var textBlocks = map[atom.Atom]bool{
atom.P: true, atom.Div: true, atom.Br: true, atom.Hr: true,
atom.Section: true, atom.Article: true, atom.Header: true, atom.Footer: true,
atom.Main: true, atom.Nav: true, atom.Aside: true, atom.Address: true,
atom.H1: true, atom.H2: true, atom.H3: true, atom.H4: true, atom.H5: true, atom.H6: true,
atom.Ul: true, atom.Ol: true, atom.Li: true, atom.Dl: true, atom.Dt: true, atom.Dd: true,
atom.Table: true, atom.Thead: true, atom.Tbody: true, atom.Tfoot: true, atom.Tr: true,
atom.Caption: true, atom.Blockquote: true, atom.Pre: true,
atom.Figure: true, atom.Figcaption: true,
}
// textCells are set apart by a space, so that a row's cells do not run
// together.
var textCells = map[atom.Atom]bool{atom.Td: true, atom.Th: true}
func htmlToText(s string) string {
body := &html.Node{Type: html.ElementNode, Data: "body", DataAtom: atom.Body}
// Reading from a strings.Reader cannot fail, and the parser repairs
// any markup, so there is no error to handle.
nodes, _ := html.ParseFragment(strings.NewReader(s), body)
var w textWriter
for _, n := range nodes {
w.walk(n)
}
return w.b.String()
}
// textWriter writes text with its white space collapsed. A break, a space or
// a line break, is only written before the next text, so that there is none
// at the start or the end, and a line break wins over a space.
type textWriter struct {
b strings.Builder
space bool
line bool
}
func (w *textWriter) walk(n *html.Node) {
switch n.Type {
case html.TextNode:
w.text(n.Data)
return
case html.ElementNode:
if textDropped[n.Data] {
return
}
case html.DocumentNode:
default: // comments, doctypes
return
}
block := textBlocks[n.DataAtom]
if block {
w.line = true
}
if textCells[n.DataAtom] {
w.space = true
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
w.walk(c)
}
if block {
w.line = true
}
}
func (w *textWriter) text(s string) {
for _, r := range s {
if unicode.IsSpace(r) {
w.space = true
continue
}
if w.b.Len() > 0 {
if w.line {
w.b.WriteByte('\n')
} else if w.space {
w.b.WriteByte(' ')
}
}
w.space, w.line = false, false
w.b.WriteRune(r)
}
}
|