-
Notifications
You must be signed in to change notification settings - Fork 90
/
parser-parse.go
156 lines (132 loc) · 4.38 KB
/
parser-parse.go
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package readability
import (
"fmt"
"io"
nurl "net/url"
"strings"
"time"
"github.com/araddon/dateparse"
"github.com/go-shiori/dom"
"golang.org/x/net/html"
)
// Parse parses a reader and find the main readable content.
func (ps *Parser) Parse(input io.Reader, pageURL *nurl.URL) (Article, error) {
// Parse input
doc, err := dom.Parse(input)
if err != nil {
return Article{}, fmt.Errorf("failed to parse input: %v", err)
}
return ps.ParseDocument(doc, pageURL)
}
// ParseDocument parses the specified document and find the main readable content.
func (ps *Parser) ParseDocument(doc *html.Node, pageURL *nurl.URL) (Article, error) {
// Clone document to make sure the original kept untouched
ps.doc = dom.Clone(doc, true)
// Reset parser data
ps.articleTitle = ""
ps.articleByline = ""
ps.articleDir = ""
ps.articleSiteName = ""
ps.documentURI = pageURL
ps.attempts = []parseAttempt{}
ps.flags = flags{
stripUnlikelys: true,
useWeightClasses: true,
cleanConditionally: true,
}
// Avoid parsing too large documents, as per configuration option
if ps.MaxElemsToParse > 0 {
numTags := len(dom.GetElementsByTagName(ps.doc, "*"))
if numTags > ps.MaxElemsToParse {
return Article{}, fmt.Errorf("documents too large: %d elements", numTags)
}
}
// Unwrap image from noscript
ps.unwrapNoscriptImages(ps.doc)
// Extract JSON-LD metadata before removing scripts
var jsonLd map[string]string
if !ps.DisableJSONLD {
jsonLd, _ = ps.getJSONLD()
}
// Remove script tags from the document.
ps.removeScripts(ps.doc)
// Prepares the HTML document
ps.prepDocument()
// Fetch metadata
metadata := ps.getArticleMetadata(jsonLd)
ps.articleTitle = metadata["title"]
// Try to grab article content
finalHTMLContent := ""
finalTextContent := ""
articleContent := ps.grabArticle()
var readableNode *html.Node
if articleContent != nil {
ps.postProcessContent(articleContent)
// If we haven't found an excerpt in the article's metadata,
// use the article's first paragraph as the excerpt. This is used
// for displaying a preview of the article's content.
if metadata["excerpt"] == "" {
paragraphs := dom.GetElementsByTagName(articleContent, "p")
if len(paragraphs) > 0 {
metadata["excerpt"] = strings.TrimSpace(dom.TextContent(paragraphs[0]))
}
}
readableNode = dom.FirstElementChild(articleContent)
finalHTMLContent = dom.InnerHTML(articleContent)
finalTextContent = dom.TextContent(articleContent)
finalTextContent = strings.TrimSpace(finalTextContent)
}
finalByline := metadata["byline"]
if finalByline == "" {
finalByline = ps.articleByline
}
// Excerpt is an supposed to be short and concise,
// so it shouldn't have any new line
excerpt := strings.TrimSpace(metadata["excerpt"])
excerpt = strings.Join(strings.Fields(excerpt), " ")
// go-readability special:
// Internet is dangerous and weird, and sometimes we will find
// metadata isn't encoded using a valid Utf-8, so here we check it.
var replacementTitle string
if pageURL != nil {
replacementTitle = pageURL.String()
}
validTitle := strings.ToValidUTF8(ps.articleTitle, replacementTitle)
validByline := strings.ToValidUTF8(finalByline, "")
validExcerpt := strings.ToValidUTF8(excerpt, "")
publishedTime := ps.getDate(metadata, "publishedTime")
modifiedTime := ps.getDate(metadata, "modifiedTime")
return Article{
Title: validTitle,
Byline: validByline,
Node: readableNode,
Content: finalHTMLContent,
TextContent: finalTextContent,
Length: charCount(finalTextContent),
Excerpt: validExcerpt,
SiteName: metadata["siteName"],
Image: metadata["image"],
Favicon: metadata["favicon"],
Language: ps.articleLang,
PublishedTime: publishedTime,
ModifiedTime: modifiedTime,
}, nil
}
// getDate tries to get a date from metadata, and parse it using a list of known formats.
func (ps *Parser) getDate(metadata map[string]string, fieldName string) *time.Time {
dateStr, ok := metadata[fieldName]
if ok && len(dateStr) > 0 {
return ps.getParsedDate(dateStr)
}
return nil
}
// getParsedDate tries to parse a date string using a list of known formats.
// If the date string can't be parsed, it will return nil.
func (ps *Parser) getParsedDate(dateStr string) *time.Time {
d, err := dateparse.ParseAny(dateStr)
if err != nil {
ps.logf("failed to parse date \"%s\": %v\n", dateStr, err)
return nil
}
return &d
}