-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdefuddle.go
More file actions
149 lines (132 loc) · 4.02 KB
/
Copy pathdefuddle.go
File metadata and controls
149 lines (132 loc) · 4.02 KB
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
// Package defuddle provides web content extraction and demuddling capabilities.
package defuddle
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/dotcommander/defuddle/internal/debug"
)
// headingTags lists the HTML heading tag names used for heading detection.
var headingTags = []string{"h1", "h2", "h3", "h4", "h5", "h6"}
var errInvalidRequestURL = errors.New("URL must be absolute HTTP(S)")
// headingSelector is a CSS selector string derived from headingTags.
var headingSelector = strings.Join(headingTags, ", ")
// Defuddle represents a document parser instance
type Defuddle struct {
rawHTML string // stored for re-parsing on retry (goquery has no clone)
doc *goquery.Document
options *Options
debug bool
debugger *debug.Debugger
}
// NewDefuddle creates a new Defuddle instance from HTML content
// JavaScript original code:
//
// constructor(document: Document, options: DefuddleOptions = {}) {
// this.doc = document;
// this.options = options;
// }
func NewDefuddle(html string, options *Options) (*Defuddle, error) {
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
if err != nil {
return nil, fmt.Errorf("failed to parse HTML: %w", err)
}
debugEnabled := false
if options != nil {
debugEnabled = options.Debug
}
debugger := debug.NewDebugger(debugEnabled)
return &Defuddle{
rawHTML: html,
doc: doc,
options: options,
debug: debugEnabled,
debugger: debugger,
}, nil
}
// Parse extracts the main content from the document
// JavaScript original code:
//
// parse(): DefuddleResponse {
// const result = this.parseInternal();
// if (result.wordCount < 200) {
// const retryResult = this.parseInternal({ removePartialSelectors: false });
// if (retryResult.wordCount > result.wordCount) {
// return retryResult;
// }
// }
// return result;
// }
//
// retryStep describes one retry pass in the Parse retry ladder.
type retryStep struct {
name string
trigger int // retry when result.WordCount < trigger
mutate func(*Options) // mutations to apply to a copy of options
accept func(prev, next int) bool // accept next result if true
}
// retryLadder defines the ordered retry passes for Parse.
// Predicates are transcribed verbatim from the original logic.
var retryLadder = []retryStep{
{
name: "partial-selectors",
trigger: 200,
mutate: func(o *Options) { o.RemovePartialSelectors = new(bool) },
accept: func(prev, next int) bool { return next > prev },
},
{
name: "hidden-elements",
trigger: 50,
mutate: func(o *Options) { o.RemoveHiddenElements = new(bool) },
accept: func(prev, next int) bool { return next > prev*2 },
},
{
name: "index-page",
trigger: 50,
mutate: func(o *Options) {
o.RemoveLowScoring = new(bool)
o.RemovePartialSelectors = new(bool)
o.RemoveContentPatterns = new(bool)
},
accept: func(prev, next int) bool { return next > prev },
},
}
// Parse parses the document and returns the extracted content.
func (d *Defuddle) Parse(ctx context.Context) (*Result, error) {
// Try first with default settings
result, err := d.parseInternal(ctx, nil)
if err != nil {
return nil, err
}
for _, step := range retryLadder {
if result.WordCount >= step.trigger {
continue
}
if d.debug {
slog.Debug("Parse: trying retry", "step", step.name, "wordCount", result.WordCount, "trigger", step.trigger)
}
retryOpts := &Options{}
if d.options != nil {
*retryOpts = *d.options
}
step.mutate(retryOpts)
retryResult, retryErr := d.parseInternal(ctx, retryOpts)
if retryErr != nil {
// First retry propagates error; subsequent retries are best-effort.
if step.trigger == 200 {
return result, retryErr
}
continue
}
if step.accept(result.WordCount, retryResult.WordCount) {
if d.debug {
slog.Debug("Parse: retry accepted", "step", step.name, "originalWordCount", result.WordCount, "retryWordCount", retryResult.WordCount)
}
result = retryResult
}
}
return result, nil
}