html2rss 0.27.2 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +82 -25
- data/html2rss.gemspec +0 -1
- data/lib/html2rss/article.rb +1 -2
- data/lib/html2rss/auto_source/README.md +12 -9
- data/lib/html2rss/auto_source/cleanup.rb +92 -1
- data/lib/html2rss/auto_source/scraper/json_state/document_scanner.rb +3 -5
- data/lib/html2rss/auto_source/scraper/meta_oembed.rb +15 -5
- data/lib/html2rss/auto_source/scraper/microdata.rb +8 -6
- data/lib/html2rss/auto_source/scraper/native_feed.rb +93 -0
- data/lib/html2rss/auto_source/scraper/schema.rb +39 -21
- data/lib/html2rss/auto_source/scraper.rb +6 -4
- data/lib/html2rss/auto_source/segmenter/cluster.rb +2 -3
- data/lib/html2rss/auto_source/segmenter/primary_link.rb +1 -2
- data/lib/html2rss/auto_source/segmenter.rb +1 -2
- data/lib/html2rss/auto_source.rb +32 -11
- data/lib/html2rss/batch.rb +169 -0
- data/lib/html2rss/capture/README.md +15 -6
- data/lib/html2rss/capture.rb +158 -15
- data/lib/html2rss/channel.rb +1 -2
- data/lib/html2rss/cli/probe_view.rb +66 -0
- data/lib/html2rss/cli/render.rb +124 -0
- data/lib/html2rss/cli/validate.rb +91 -0
- data/lib/html2rss/cli.rb +318 -193
- data/lib/html2rss/config/auto_source_contract.rb +7 -0
- data/lib/html2rss/config/request_headers.rb +4 -14
- data/lib/html2rss/config/schema.rb +7 -5
- data/lib/html2rss/config/validator.rb +1 -0
- data/lib/html2rss/config.rb +64 -18
- data/lib/html2rss/doctor/botasaurus.rb +130 -0
- data/lib/html2rss/feed_builder/rss.rb +1 -2
- data/lib/html2rss/feed_pipeline/README.md +7 -5
- data/lib/html2rss/feed_pipeline/auto_fallback.rb +78 -19
- data/lib/html2rss/feed_pipeline/runtime_policy.rb +1 -0
- data/lib/html2rss/feed_pipeline.rb +48 -21
- data/lib/html2rss/feed_resolution/README.md +52 -0
- data/lib/html2rss/feed_resolution/candidate_generator.rb +139 -0
- data/lib/html2rss/feed_resolution/diag.rb +32 -0
- data/lib/html2rss/feed_resolution/options.rb +31 -0
- data/lib/html2rss/feed_resolution/policy.rb +47 -0
- data/lib/html2rss/feed_resolution/probe.rb +59 -0
- data/lib/html2rss/feed_resolution/scorer.rb +49 -0
- data/lib/html2rss/feed_resolution.rb +268 -0
- data/lib/html2rss/html/article_extractor/heading_extractor.rb +24 -14
- data/lib/html2rss/html/article_extractor.rb +4 -9
- data/lib/html2rss/html/feed_link.rb +4 -2
- data/lib/html2rss/html/navigator.rb +4 -39
- data/lib/html2rss/html/probe.rb +82 -0
- data/lib/html2rss/html/sst_article_extractor.rb +1 -2
- data/lib/html2rss/link_destination/noise_policy.rb +1 -2
- data/lib/html2rss/link_destination/path_classifier.rb +20 -19
- data/lib/html2rss/mcp/README.md +80 -0
- data/lib/html2rss/mcp/contract.rb +135 -13
- data/lib/html2rss/mcp/outcome/playbook.rb +122 -0
- data/lib/html2rss/mcp/outcome.rb +112 -40
- data/lib/html2rss/mcp/runtime.rb +45 -0
- data/lib/html2rss/mcp/server/tools.rb +277 -0
- data/lib/html2rss/mcp/server.rb +91 -232
- data/lib/html2rss/mcp.rb +3 -0
- data/lib/html2rss/page_recon/README.md +69 -0
- data/lib/html2rss/page_recon/diagnostics.rb +211 -0
- data/lib/html2rss/page_recon.rb +297 -0
- data/lib/html2rss/recon.rb +252 -0
- data/lib/html2rss/request_service/blocked_surface.rb +1 -0
- data/lib/html2rss/request_service/botasaurus_contract.rb +8 -1
- data/lib/html2rss/request_service/botasaurus_strategy.rb +1 -1
- data/lib/html2rss/request_service/faraday_strategy.rb +11 -7
- data/lib/html2rss/request_service/policy.rb +1 -2
- data/lib/html2rss/request_service/response.rb +27 -4
- data/lib/html2rss/request_service/strategy.rb +1 -2
- data/lib/html2rss/request_service.rb +15 -1
- data/lib/html2rss/request_session.rb +4 -2
- data/lib/html2rss/scrape_target.rb +24 -0
- data/lib/html2rss/selectors/post_processors/sanitize_html.rb +2 -4
- data/lib/html2rss/selectors/post_processors.rb +0 -1
- data/lib/html2rss/selectors.rb +33 -10
- data/lib/html2rss/sst/attrs.rb +1 -2
- data/lib/html2rss/sst/normalizer.rb +49 -11
- data/lib/html2rss/status.rb +48 -24
- data/lib/html2rss/surface_category.rb +63 -0
- data/lib/html2rss/syndication/README.md +21 -0
- data/lib/html2rss/syndication/candidate_catalog.rb +26 -0
- data/lib/html2rss/syndication/discovery.rb +215 -0
- data/lib/html2rss/syndication/parser.rb +137 -0
- data/lib/html2rss/syndication.rb +10 -0
- data/lib/html2rss/test/enhance_audit.rb +192 -0
- data/lib/html2rss/test.rb +458 -0
- data/lib/html2rss/url.rb +23 -2
- data/lib/html2rss/version.rb +1 -1
- data/lib/html2rss.rb +157 -35
- data/schema/html2rss-config.schema.json +47 -29
- metadata +34 -18
- data/lib/html2rss/mcp/inspect.rb +0 -241
- data/lib/html2rss/selectors/post_processors/html_to_markdown.rb +0 -59
data/lib/html2rss/mcp/inspect.rb
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Html2rss
|
|
4
|
-
module MCP
|
|
5
|
-
##
|
|
6
|
-
# Diagnostic inspect path (not Capture ownership). Fetches once for SST/scraper stats
|
|
7
|
-
# and recon facts (final URL, status, scheme downgrade, native feed hints).
|
|
8
|
-
module Inspect # rubocop:disable Metrics/ModuleLength -- diagnostic helpers stay co-located
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
##
|
|
12
|
-
# @param url [String]
|
|
13
|
-
# @param strategy [String, Symbol]
|
|
14
|
-
# @return [Hash]
|
|
15
|
-
def call(url:, strategy: :auto) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
|
|
16
|
-
resolved = FeedPipeline::StrategyPlan.concrete_for_diagnostic(strategy)
|
|
17
|
-
response = fetch_response(url, resolved)
|
|
18
|
-
parsed = response.parsed_body
|
|
19
|
-
|
|
20
|
-
result = recon_fields(url, response, parsed).merge(
|
|
21
|
-
strategy: resolved,
|
|
22
|
-
content_type: response.content_type,
|
|
23
|
-
html_response: response.html_response?,
|
|
24
|
-
scraper_eligibility: scraper_info(parsed),
|
|
25
|
-
sst_stats: sst_stats_from(response)
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
if response.html_response?
|
|
29
|
-
sst = sst_document(response)
|
|
30
|
-
if sst
|
|
31
|
-
result[:sst] = {
|
|
32
|
-
node_count: sst.node_count,
|
|
33
|
-
degraded: sst.degraded,
|
|
34
|
-
segment_stats: segment_stats(sst, url)
|
|
35
|
-
}
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
blocked = Html2rss::RequestService::BlockedSurface.interstitial_signature_for(response.body)
|
|
40
|
-
result[:blocked_surface] = blocked[:key].to_s if blocked
|
|
41
|
-
result[:xhr_capture] = xhr_capture_info(response) if resolved == :botasaurus
|
|
42
|
-
merge_admission_diagnostics!(result, response)
|
|
43
|
-
|
|
44
|
-
result
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
##
|
|
48
|
-
# Surfaces Cleanup admission_drops without re-running full AutoSource discovery.
|
|
49
|
-
# Uses articles already extractable from a cheap AutoSource pass only when HTML.
|
|
50
|
-
#
|
|
51
|
-
# @param result [Hash]
|
|
52
|
-
# @param response [Html2rss::RequestService::Response]
|
|
53
|
-
# @return [void]
|
|
54
|
-
def merge_admission_diagnostics!(result, response)
|
|
55
|
-
return unless response.html_response?
|
|
56
|
-
|
|
57
|
-
source = AutoSource.new(response, AutoSource::DEFAULT_CONFIG.merge(limit: 10))
|
|
58
|
-
articles = source.articles
|
|
59
|
-
result[:articles_count] = articles.size
|
|
60
|
-
drops = source.admission_drops
|
|
61
|
-
result[:admission_drops] = drops if drops.any?
|
|
62
|
-
end
|
|
63
|
-
module_function :merge_admission_diagnostics!
|
|
64
|
-
|
|
65
|
-
##
|
|
66
|
-
# @param response [Html2rss::RequestService::Response]
|
|
67
|
-
# @return [Hash] redacted XHR capture diagnostics (no query strings)
|
|
68
|
-
def xhr_capture_info(response)
|
|
69
|
-
captured = response.captured_responses
|
|
70
|
-
{
|
|
71
|
-
count: captured.size,
|
|
72
|
-
sample_endpoints: captured.first(5).filter_map { |entry| redacted_endpoint(entry) },
|
|
73
|
-
candidate_articles: captured.any? { |entry| xhr_candidate_articles?(entry) }
|
|
74
|
-
}
|
|
75
|
-
end
|
|
76
|
-
module_function :xhr_capture_info
|
|
77
|
-
|
|
78
|
-
##
|
|
79
|
-
# @param entry [Hash] captured response hash
|
|
80
|
-
# @return [String, nil] scheme+host+path only
|
|
81
|
-
def redacted_endpoint(entry)
|
|
82
|
-
raw = entry['url'] || entry[:url]
|
|
83
|
-
return unless raw
|
|
84
|
-
|
|
85
|
-
uri = URI.parse(raw.to_s)
|
|
86
|
-
return unless uri.scheme && uri.host
|
|
87
|
-
|
|
88
|
-
"#{uri.scheme}://#{uri.host}#{uri.path}"
|
|
89
|
-
rescue URI::InvalidURIError
|
|
90
|
-
nil
|
|
91
|
-
end
|
|
92
|
-
module_function :redacted_endpoint
|
|
93
|
-
|
|
94
|
-
##
|
|
95
|
-
# @param entry [Hash] captured response hash
|
|
96
|
-
# @return [Boolean]
|
|
97
|
-
def xhr_candidate_articles?(entry)
|
|
98
|
-
body = entry['body'] || entry[:body]
|
|
99
|
-
return false unless body.is_a?(String)
|
|
100
|
-
|
|
101
|
-
document = JSON.parse(body, symbolize_names: true)
|
|
102
|
-
AutoSource::Scraper::JsonState::CandidateDetector.candidate_array?(document)
|
|
103
|
-
rescue JSON::ParserError
|
|
104
|
-
false
|
|
105
|
-
end
|
|
106
|
-
module_function :xhr_candidate_articles?
|
|
107
|
-
|
|
108
|
-
##
|
|
109
|
-
# @param url [String]
|
|
110
|
-
# @param strategy [Symbol]
|
|
111
|
-
# @return [Html2rss::RequestService::Response]
|
|
112
|
-
def fetch_response(url, strategy) # rubocop:disable Metrics/MethodLength -- session construction
|
|
113
|
-
raw_config = Config.auto_source_config(
|
|
114
|
-
url:,
|
|
115
|
-
request_controls: Config::RequestControls.from_shortcut(strategy:)
|
|
116
|
-
)
|
|
117
|
-
raw_config[:strategy] = strategy
|
|
118
|
-
config = Config.from_hash(raw_config)
|
|
119
|
-
resources = FeedPipeline::RuntimePolicy.resources_for(config)
|
|
120
|
-
session = RequestSession.build(
|
|
121
|
-
config:,
|
|
122
|
-
strategy: config.strategy,
|
|
123
|
-
budget: resources.budget,
|
|
124
|
-
policy: resources.policy
|
|
125
|
-
)
|
|
126
|
-
session.fetch_initial_response
|
|
127
|
-
end
|
|
128
|
-
module_function :fetch_response
|
|
129
|
-
|
|
130
|
-
##
|
|
131
|
-
# @param parsed [Object] parsed response body
|
|
132
|
-
# @return [Array<String>, Hash]
|
|
133
|
-
def scraper_info(parsed)
|
|
134
|
-
return { error: 'Response is not HTML' } unless parsed.is_a?(Nokogiri::HTML::Document)
|
|
135
|
-
|
|
136
|
-
begin
|
|
137
|
-
Html2rss::AutoSource::Scraper.from(parsed).map(&:name)
|
|
138
|
-
rescue Html2rss::AutoSource::Scraper::NoScraperFound => error
|
|
139
|
-
{ none_found: error.category.to_s }
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
module_function :scraper_info
|
|
143
|
-
|
|
144
|
-
##
|
|
145
|
-
# @param response [Html2rss::RequestService::Response]
|
|
146
|
-
# @return [Hash, nil]
|
|
147
|
-
def sst_stats_from(response)
|
|
148
|
-
return nil unless response.html_response?
|
|
149
|
-
|
|
150
|
-
doc = sst_document(response)
|
|
151
|
-
return nil unless doc
|
|
152
|
-
|
|
153
|
-
{ node_count: doc.node_count, degraded: doc.degraded }
|
|
154
|
-
rescue StandardError
|
|
155
|
-
nil
|
|
156
|
-
end
|
|
157
|
-
module_function :sst_stats_from
|
|
158
|
-
|
|
159
|
-
##
|
|
160
|
-
# @param response [Html2rss::RequestService::Response]
|
|
161
|
-
# @return [Html2rss::SST::Document, nil]
|
|
162
|
-
def sst_document(response)
|
|
163
|
-
Html2rss::SST::Normalizer.call(response.body)
|
|
164
|
-
rescue ArgumentError
|
|
165
|
-
nil
|
|
166
|
-
end
|
|
167
|
-
module_function :sst_document
|
|
168
|
-
|
|
169
|
-
##
|
|
170
|
-
# @param sst [Html2rss::SST::Document]
|
|
171
|
-
# @param url [String]
|
|
172
|
-
# @return [Hash]
|
|
173
|
-
def segment_stats(sst, url)
|
|
174
|
-
segments = discover_segments(sst, url)
|
|
175
|
-
return { found: 0 } if segments.empty?
|
|
176
|
-
|
|
177
|
-
{
|
|
178
|
-
found: segments.size,
|
|
179
|
-
strategies: segments.map(&:strategy).uniq,
|
|
180
|
-
sample_paths: segments.first(5).map { |s| s.root_node.tag_path }
|
|
181
|
-
}
|
|
182
|
-
end
|
|
183
|
-
module_function :segment_stats
|
|
184
|
-
|
|
185
|
-
##
|
|
186
|
-
# @param sst [Html2rss::SST::Document]
|
|
187
|
-
# @param url [String]
|
|
188
|
-
# @return [Array]
|
|
189
|
-
def discover_segments(sst, url)
|
|
190
|
-
link_resolver = Scoring::LinkResolver.new(url)
|
|
191
|
-
AutoSource::Segmenter.call(
|
|
192
|
-
sst,
|
|
193
|
-
base_url: url,
|
|
194
|
-
strategy: :list,
|
|
195
|
-
link_resolver:
|
|
196
|
-
)
|
|
197
|
-
rescue StandardError
|
|
198
|
-
[]
|
|
199
|
-
end
|
|
200
|
-
module_function :discover_segments
|
|
201
|
-
|
|
202
|
-
##
|
|
203
|
-
# @param requested_url [String]
|
|
204
|
-
# @param response [Html2rss::RequestService::Response]
|
|
205
|
-
# @param parsed [Object]
|
|
206
|
-
# @return [Hash]
|
|
207
|
-
def recon_fields(requested_url, response, parsed)
|
|
208
|
-
requested = Url.from_absolute(requested_url)
|
|
209
|
-
final = response.url
|
|
210
|
-
|
|
211
|
-
{
|
|
212
|
-
requested_url: requested.to_s,
|
|
213
|
-
final_url: final.to_s,
|
|
214
|
-
status: response.status,
|
|
215
|
-
scheme_downgrade: scheme_downgrade?(requested, final),
|
|
216
|
-
alternate_feeds: alternate_feeds_from(parsed)
|
|
217
|
-
}
|
|
218
|
-
end
|
|
219
|
-
module_function :recon_fields
|
|
220
|
-
|
|
221
|
-
##
|
|
222
|
-
# @param requested [Html2rss::Url]
|
|
223
|
-
# @param final [Html2rss::Url]
|
|
224
|
-
# @return [Boolean] true when the fetch downgraded https to http
|
|
225
|
-
def scheme_downgrade?(requested, final)
|
|
226
|
-
requested.scheme == 'https' && final.scheme == 'http'
|
|
227
|
-
end
|
|
228
|
-
module_function :scheme_downgrade?
|
|
229
|
-
|
|
230
|
-
##
|
|
231
|
-
# @param parsed [Object]
|
|
232
|
-
# @return [Array<Hash{Symbol => String}>]
|
|
233
|
-
def alternate_feeds_from(parsed)
|
|
234
|
-
return [] unless parsed.is_a?(Nokogiri::HTML::Document)
|
|
235
|
-
|
|
236
|
-
Html::FeedLink.from_document(parsed).map { |link| { href: link.href, mime_type: link.mime_type } }
|
|
237
|
-
end
|
|
238
|
-
module_function :alternate_feeds_from
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
end
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'reverse_markdown'
|
|
4
|
-
|
|
5
|
-
module Html2rss
|
|
6
|
-
class Selectors
|
|
7
|
-
module PostProcessors
|
|
8
|
-
##
|
|
9
|
-
# Returns HTML code as Markdown formatted String.
|
|
10
|
-
# Before converting to markdown, the HTML is sanitized with SanitizeHtml.
|
|
11
|
-
# Imagine this HTML structure:
|
|
12
|
-
#
|
|
13
|
-
# <section>
|
|
14
|
-
# Lorem <b>ipsum</b> dolor...
|
|
15
|
-
# <iframe src="https://evil.corp/miner"></iframe>
|
|
16
|
-
# <script>alert();</script>
|
|
17
|
-
# </section>
|
|
18
|
-
#
|
|
19
|
-
# YAML usage example:
|
|
20
|
-
#
|
|
21
|
-
# selectors:
|
|
22
|
-
# description:
|
|
23
|
-
# selector: section
|
|
24
|
-
# extractor: html
|
|
25
|
-
# post_process:
|
|
26
|
-
# name: html_to_markdown
|
|
27
|
-
#
|
|
28
|
-
# Would return:
|
|
29
|
-
# 'Lorem **ipsum** dolor'
|
|
30
|
-
class HtmlToMarkdown < Base
|
|
31
|
-
# JSON Schema description exported via +schema_doc+.
|
|
32
|
-
DESCRIPTION = 'Sanitize HTML then convert it to Markdown (via ReverseMarkdown).'
|
|
33
|
-
|
|
34
|
-
# Example post-process objects for JSON Schema +examples+.
|
|
35
|
-
EXAMPLES = [
|
|
36
|
-
{ 'name' => 'html_to_markdown' }
|
|
37
|
-
].freeze
|
|
38
|
-
|
|
39
|
-
# @return [Hash{Symbol => Object}] JSON Schema fragment for this post-processor
|
|
40
|
-
def self.schema_doc = SchemaDoc.for_post_processor(name: :html_to_markdown, klass: self)
|
|
41
|
-
|
|
42
|
-
# @param value [String] extracted selector value
|
|
43
|
-
# @param context [Selectors::Context] post-processor context
|
|
44
|
-
# @return [void]
|
|
45
|
-
def self.validate_args!(value, context)
|
|
46
|
-
assert_type value, String, :value, context:
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
##
|
|
50
|
-
# @return [String] formatted in Markdown
|
|
51
|
-
def get
|
|
52
|
-
sanitized_value = SanitizeHtml.new(value, context).get
|
|
53
|
-
|
|
54
|
-
ReverseMarkdown.convert(sanitized_value)
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|