11package main
22
33import (
4- "regexp"
54 "strings"
65
76 "golang.org/x/net/html"
@@ -17,8 +16,43 @@ var killTags = []string{
1716// landmarkTags are structural tags that should be removed as noise.
1817var landmarkTags = []string {"nav" , "footer" , "aside" }
1918
20- // noisePattern matches class/id values that indicate non-content elements.
21- var noisePattern = regexp .MustCompile (`(?i)(cookie|consent|gdpr|banner|popup|modal|overlay|newsletter|subscribe|social|share|sidebar|advertisement|ad-|promo|related-posts)` )
19+ // Pre-built sets for fast tag lookup.
20+ var killSet = func () map [string ]bool {
21+ m := make (map [string ]bool , len (killTags ))
22+ for _ , t := range killTags {
23+ m [t ] = true
24+ }
25+ return m
26+ }()
27+
28+ var landmarkSet = func () map [string ]bool {
29+ m := make (map [string ]bool , len (landmarkTags ))
30+ for _ , t := range landmarkTags {
31+ m [t ] = true
32+ }
33+ return m
34+ }()
35+
36+ // noiseKeywords are substrings in class/id that indicate non-content elements.
37+ var noiseKeywords = []string {
38+ "cookie" , "consent" , "gdpr" , "banner" , "popup" , "modal" , "overlay" ,
39+ "newsletter" , "subscribe" , "social" , "share" , "sidebar" ,
40+ "advertisement" , "ad-" , "promo" , "related-posts" ,
41+ }
42+
43+ // isNoisy checks if a string contains any noise keyword (case-insensitive).
44+ func isNoisy (s string ) bool {
45+ if s == "" {
46+ return false
47+ }
48+ lower := strings .ToLower (s )
49+ for _ , kw := range noiseKeywords {
50+ if strings .Contains (lower , kw ) {
51+ return true
52+ }
53+ }
54+ return false
55+ }
2256
2357// removeNode detaches a node from its parent.
2458func removeNode (n * html.Node ) {
@@ -28,117 +62,71 @@ func removeNode(n *html.Node) {
2862}
2963
3064// stripNoise removes non-content elements from the document in-place.
65+ // Single-pass tree walk — collects all removable nodes, then detaches them.
3166func stripNoise (doc * html.Node ) {
32- // Build a set for fast lookup of kill tags
33- killSet := make (map [string ]bool , len (killTags ))
34- for _ , tag := range killTags {
35- killSet [tag ] = true
36- }
37- landmarkSet := make (map [string ]bool , len (landmarkTags ))
38- for _ , tag := range landmarkTags {
39- landmarkSet [tag ] = true
40- }
41-
42- // 1 & 2. Kill tags and landmark tags — collect then remove
4367 var toRemove []* html.Node
44- var collectKill func (* html.Node )
45- collectKill = func (n * html.Node ) {
68+
69+ var walk func (* html.Node )
70+ walk = func (n * html.Node ) {
4671 for c := n .FirstChild ; c != nil ; c = c .NextSibling {
47- if c .Type == html .ElementNode {
48- if killSet [c .Data ] || landmarkSet [c .Data ] {
49- toRemove = append (toRemove , c )
50- continue // don't recurse into removed subtrees
51- }
72+ if c .Type == html .CommentNode {
73+ toRemove = append (toRemove , c )
74+ continue
75+ }
76+ if c .Type != html .ElementNode {
77+ walk (c )
78+ continue
5279 }
53- collectKill (c )
54- }
55- }
56- collectKill (doc )
57- for _ , n := range toRemove {
58- removeNode (n )
59- }
6080
61- // 3. Kill elements with noisy class/id
62- toRemove = toRemove [:0 ]
63- var collectNoisy func (* html.Node )
64- collectNoisy = func (n * html.Node ) {
65- for c := n .FirstChild ; c != nil ; c = c .NextSibling {
66- if c .Type == html .ElementNode {
67- class := getAttr (c , "class" )
68- id := getAttr (c , "id" )
69- if noisePattern .MatchString (class ) || noisePattern .MatchString (id ) {
70- toRemove = append (toRemove , c )
71- continue
72- }
81+ tag := c .Data
82+
83+ // Kill tags + landmark tags
84+ if killSet [tag ] || landmarkSet [tag ] {
85+ toRemove = append (toRemove , c )
86+ continue
7387 }
74- collectNoisy (c )
75- }
76- }
77- collectNoisy (doc )
78- for _ , n := range toRemove {
79- removeNode (n )
80- }
8188
82- // 4. Kill header elements that do NOT contain an h1
83- toRemove = toRemove [:0 ]
84- var collectHeaders func (* html.Node )
85- collectHeaders = func (n * html.Node ) {
86- for c := n .FirstChild ; c != nil ; c = c .NextSibling {
87- if c .Type == html .ElementNode && c .Data == "header" {
88- if findFirst (c , "h1" ) == nil {
89- toRemove = append (toRemove , c )
90- continue
91- }
89+ // Header without h1
90+ if tag == "header" && findFirst (c , "h1" ) == nil {
91+ toRemove = append (toRemove , c )
92+ continue
9293 }
93- collectHeaders (c )
94- }
95- }
96- collectHeaders (doc )
97- for _ , n := range toRemove {
98- removeNode (n )
99- }
10094
101- // 5. Kill hidden elements (aria-hidden=true and display:none)
102- toRemove = toRemove [:0 ]
103- var collectHidden func (* html.Node )
104- collectHidden = func (n * html.Node ) {
105- for c := n .FirstChild ; c != nil ; c = c .NextSibling {
106- if c .Type == html .ElementNode {
107- if hasAttr (c , "aria-hidden" , "true" ) {
108- toRemove = append (toRemove , c )
109- continue
95+ // Hidden elements
96+ if hasAttr (c , "aria-hidden" , "true" ) {
97+ toRemove = append (toRemove , c )
98+ continue
99+ }
100+
101+ // Noisy class/id
102+ remove := false
103+ for _ , a := range c .Attr {
104+ if a .Key == "class" || a .Key == "id" {
105+ if isNoisy (a .Val ) {
106+ remove = true
107+ break
108+ }
110109 }
111- style := getAttr (c , "style" )
112- if style != "" {
113- normalized := strings .ReplaceAll (style , " " , "" )
114- if strings .Contains (normalized , "display:none" ) {
115- toRemove = append (toRemove , c )
116- continue
110+ if a .Key == "style" {
111+ if strings .Contains (strings .ReplaceAll (a .Val , " " , "" ), "display:none" ) {
112+ remove = true
113+ break
117114 }
118115 }
119116 }
120- collectHidden (c )
117+ if remove {
118+ toRemove = append (toRemove , c )
119+ continue
120+ }
121+
122+ walk (c )
121123 }
122124 }
123- collectHidden (doc )
125+ walk (doc )
126+
124127 for _ , n := range toRemove {
125128 removeNode (n )
126129 }
127-
128- // 6. Remove HTML comments
129- var removeComments func (* html.Node )
130- removeComments = func (n * html.Node ) {
131- var next * html.Node
132- for c := n .FirstChild ; c != nil ; c = next {
133- next = c .NextSibling
134- if c .Type == html .CommentNode {
135- n .RemoveChild (c )
136- } else {
137- removeComments (c )
138- }
139- }
140- }
141- removeComments (doc )
142130}
143131
144132// extractContent picks the main content node from the document.
@@ -193,7 +181,7 @@ func extractContent(doc *html.Node, domain string) *html.Node {
193181func readabilityScore (n * html.Node ) int {
194182 class := getAttr (n , "class" )
195183 id := getAttr (n , "id" )
196- if noisePattern . MatchString (class ) || noisePattern . MatchString (id ) {
184+ if isNoisy (class ) || isNoisy (id ) {
197185 return 0
198186 }
199187
0 commit comments