html2rss 0.28.0 → 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 +8 -8
- 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 +5 -2
- data/lib/html2rss/auto_source/scraper/schema.rb +39 -21
- data/lib/html2rss/auto_source/scraper.rb +2 -3
- 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 +25 -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/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 +59 -22
- data/lib/html2rss/doctor/botasaurus.rb +130 -0
- data/lib/html2rss/feed_builder/rss.rb +1 -2
- data/lib/html2rss/feed_pipeline/README.md +5 -5
- data/lib/html2rss/feed_pipeline/auto_fallback.rb +29 -10
- data/lib/html2rss/feed_pipeline.rb +27 -16
- data/lib/html2rss/feed_resolution/README.md +11 -11
- data/lib/html2rss/feed_resolution.rb +6 -12
- 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 -42
- 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 +56 -2
- data/lib/html2rss/recon.rb +252 -0
- data/lib/html2rss/request_service/blocked_surface.rb +1 -0
- 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 +3 -4
- data/lib/html2rss/request_service/strategy.rb +1 -2
- data/lib/html2rss/request_service.rb +2 -0
- data/lib/html2rss/request_session.rb +1 -2
- data/lib/html2rss/scrape_target.rb +1 -1
- 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 +3 -6
- data/lib/html2rss/syndication/README.md +6 -6
- data/lib/html2rss/syndication/discovery.rb +2 -4
- data/lib/html2rss/test/enhance_audit.rb +192 -0
- data/lib/html2rss/test.rb +458 -0
- data/lib/html2rss/url.rb +1 -2
- data/lib/html2rss/version.rb +1 -1
- data/lib/html2rss.rb +157 -35
- data/schema/html2rss-config.schema.json +9 -29
- metadata +17 -18
- data/lib/html2rss/mcp/inspect.rb +0 -138
- data/lib/html2rss/selectors/post_processors/html_to_markdown.rb +0 -59
data/lib/html2rss.rb
CHANGED
|
@@ -16,7 +16,7 @@ require 'html2rss/defaults'
|
|
|
16
16
|
|
|
17
17
|
##
|
|
18
18
|
# The Html2rss namespace.
|
|
19
|
-
module Html2rss
|
|
19
|
+
module Html2rss # rubocop:disable Metrics/ModuleLength
|
|
20
20
|
##
|
|
21
21
|
# The logger instance.
|
|
22
22
|
module Log
|
|
@@ -28,6 +28,160 @@ module Html2rss
|
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
##
|
|
32
|
+
# Cheap diagnostics for a URL (final URL, status, alternates, surface).
|
|
33
|
+
# Golden path step 1 (optional); use {.recon} for verdict and native_feed.
|
|
34
|
+
#
|
|
35
|
+
# @param url [String] source page URL
|
|
36
|
+
# @param strategy [Symbol] request strategy (:auto, :faraday, :botasaurus)
|
|
37
|
+
# @param deep [Boolean] when true and strategy is :auto, one Botasaurus hop if configured
|
|
38
|
+
# @return [Html2rss::PageRecon::Diagnostics::Report]
|
|
39
|
+
def self.inspect(url, strategy: :auto, deep: false, **)
|
|
40
|
+
PageRecon::Diagnostics.call(url:, strategy:, deep:, **)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# Curation verdict and native_feed preference for a URL.
|
|
45
|
+
# Golden path step 2 (optional); adds verdict beyond {.inspect}.
|
|
46
|
+
#
|
|
47
|
+
# @param url [String, Html2rss::Url] source page URL
|
|
48
|
+
# @param strategy [Symbol] request strategy (:auto, :faraday, :botasaurus)
|
|
49
|
+
# @return [Html2rss::Recon::Result]
|
|
50
|
+
def self.recon(url, strategy: :auto, **)
|
|
51
|
+
Recon.call(url, strategy:, **)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
##
|
|
55
|
+
# Derives a reusable YAML-ready feed config from a URL.
|
|
56
|
+
# Golden path step 3.
|
|
57
|
+
#
|
|
58
|
+
# @param url [String] source page URL
|
|
59
|
+
# @param strategy [Symbol] request strategy (+:auto+, +:faraday+, +:botasaurus+)
|
|
60
|
+
# @option options [String, nil] :items_selector optional CSS selector hint for items
|
|
61
|
+
# @option options [Array<String>, nil] :topics optional directory topics override
|
|
62
|
+
# @option options [String, nil] :title optional title override
|
|
63
|
+
# @option options [String, nil] :summary optional summary override
|
|
64
|
+
# @option options [Boolean] :force whether to ignore native feed detection
|
|
65
|
+
# @option options [Boolean, nil] :enhance whether to force enhance: true
|
|
66
|
+
# @option options [Integer, nil] :max_redirects optional redirect limit override
|
|
67
|
+
# @option options [Integer, nil] :max_requests optional request budget override
|
|
68
|
+
# @option options [Integer, nil] :limit max articles to keep
|
|
69
|
+
# @option options [String, nil] :local_file_path optional local HTML file path
|
|
70
|
+
# @return [Html2rss::Capture::CaptureResult]
|
|
71
|
+
def self.capture(url, strategy: :auto, **)
|
|
72
|
+
Capture.build(url, strategy:, **)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# Validates a config hash, YAML string, or file path against the schema.
|
|
77
|
+
# Side door: schema-only check without live extraction.
|
|
78
|
+
#
|
|
79
|
+
# @param config_input [Hash, String]
|
|
80
|
+
# @param feed_name [String, nil]
|
|
81
|
+
# @param params [Hash]
|
|
82
|
+
# @return [Dry::Validation::Result, Html2rss::Config::ValidationResult]
|
|
83
|
+
def self.validate(config_input, feed_name = nil, params: {})
|
|
84
|
+
_raw, validation = Config.resolve_and_validate(config_input, feed_name:, params:)
|
|
85
|
+
validation
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
# Validates schema and asserts live item extraction.
|
|
90
|
+
# Golden path step 4.
|
|
91
|
+
#
|
|
92
|
+
# @param config_input [Hash, String] config hash, YAML string, or file path
|
|
93
|
+
# @param feed_name [String, nil] optional feed name in multi-feed file
|
|
94
|
+
# @param min_items [Integer] minimum required items (default: 1)
|
|
95
|
+
# @param params [Hash] dynamic feed params
|
|
96
|
+
# @param strategy [Symbol, nil] optional strategy override
|
|
97
|
+
# @param strict_quality [Boolean] when true, fail on ship-quality audit thresholds
|
|
98
|
+
# @param compare_enhance [Boolean] diagnostic enhance off vs on comparison on cached HTML
|
|
99
|
+
# @return [Html2rss::Test::Result]
|
|
100
|
+
def self.test(config_input, feed_name = nil, min_items: 1, params: {}, strategy: nil, strict_quality: false, # rubocop:disable Metrics/ParameterLists
|
|
101
|
+
compare_enhance: false)
|
|
102
|
+
Test.call(config_input, feed_name, min_items:, params:, strategy:, strict_quality:, compare_enhance:)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# Ships RSS from a validated config (user-facing verb for {.feed_result}).
|
|
107
|
+
# Golden path step 5.
|
|
108
|
+
#
|
|
109
|
+
# @param raw_config [Hash{Symbol => Object}] feed configuration
|
|
110
|
+
# @return [Html2rss::FeedResult]
|
|
111
|
+
def self.apply(raw_config)
|
|
112
|
+
feed_result(raw_config)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# rubocop:disable Metrics/ParameterLists
|
|
116
|
+
|
|
117
|
+
##
|
|
118
|
+
# One-shot auto-source scrape from a URL (user-facing verb for {.auto_feed_result}).
|
|
119
|
+
#
|
|
120
|
+
# @param url [String] source page URL
|
|
121
|
+
# @param strategy [Symbol] request strategy to use
|
|
122
|
+
# @param items_selector [String, nil] optional selector hint for item extraction
|
|
123
|
+
# @param max_redirects [Integer, nil] optional redirect limit override
|
|
124
|
+
# @param max_requests [Integer] optional request budget override (default: 4 for sitemap sub-fetches)
|
|
125
|
+
# @param local_file_path [String, nil] optional local HTML file path
|
|
126
|
+
# @param limit [Integer, nil] max articles to keep (default: {AutoSource::DEFAULT_LIMIT})
|
|
127
|
+
# @return [Html2rss::FeedResult]
|
|
128
|
+
def self.scrape(url,
|
|
129
|
+
strategy: :auto,
|
|
130
|
+
items_selector: nil,
|
|
131
|
+
max_redirects: nil,
|
|
132
|
+
max_requests: 4,
|
|
133
|
+
local_file_path: nil,
|
|
134
|
+
limit: nil)
|
|
135
|
+
auto_feed_result(url, strategy:, items_selector:, max_redirects:, max_requests:,
|
|
136
|
+
local_file_path:, limit:)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
##
|
|
140
|
+
# Scrapes multiple URLs in parallel using auto-source article discovery.
|
|
141
|
+
#
|
|
142
|
+
# @param urls [Enumerable<String>] list of URLs to scrape
|
|
143
|
+
# @param strategy [Symbol] request strategy (:auto, :faraday, :botasaurus)
|
|
144
|
+
# @param limit [Integer] max articles to keep per URL (default: 10)
|
|
145
|
+
# @param concurrency [Integer] max worker threads (default: 5)
|
|
146
|
+
# @return [Html2rss::Batch::BatchResult]
|
|
147
|
+
def self.batch_scrape(urls, strategy: :auto, limit: 10, concurrency: Batch::DEFAULT_CONCURRENCY)
|
|
148
|
+
Batch.batch_scrape(urls:, strategy:, limit:, concurrency:)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
##
|
|
152
|
+
# Inspects multiple URLs in parallel with per-URL error isolation.
|
|
153
|
+
#
|
|
154
|
+
# @param urls [Enumerable<String>] list of URLs to inspect
|
|
155
|
+
# @param strategy [Symbol] request strategy (:auto, :faraday, :botasaurus)
|
|
156
|
+
# @param concurrency [Integer] max worker threads (default: 5)
|
|
157
|
+
# @return [Html2rss::Batch::BatchResult]
|
|
158
|
+
def self.batch_inspect(urls, strategy: :auto, concurrency: Batch::DEFAULT_CONCURRENCY)
|
|
159
|
+
Batch.batch_inspect(urls:, strategy:, concurrency:)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
##
|
|
163
|
+
# Runs recon across multiple URLs in parallel with per-URL error isolation.
|
|
164
|
+
#
|
|
165
|
+
# @param urls [Enumerable<String>] list of URLs to recon
|
|
166
|
+
# @param strategy [Symbol] request strategy (:auto, :faraday, :botasaurus)
|
|
167
|
+
# @param concurrency [Integer] max worker threads (default: 5)
|
|
168
|
+
# @option options [String, nil] :cache_dir optional HTML cache directory
|
|
169
|
+
# @return [Html2rss::Batch::BatchResult]
|
|
170
|
+
def self.batch_recon(urls, strategy: :auto, concurrency: Batch::DEFAULT_CONCURRENCY, **)
|
|
171
|
+
Batch.batch_recon(urls:, strategy:, concurrency:, **)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# rubocop:enable Metrics/ParameterLists
|
|
175
|
+
|
|
176
|
+
##
|
|
177
|
+
# Exports the configuration JSON Schema as JSON string.
|
|
178
|
+
#
|
|
179
|
+
# @param pretty [Boolean] whether to pretty-print JSON
|
|
180
|
+
# @return [String]
|
|
181
|
+
def self.schema_json(pretty: true)
|
|
182
|
+
Config.json_schema_json(pretty:)
|
|
183
|
+
end
|
|
184
|
+
|
|
31
185
|
##
|
|
32
186
|
# Loads a feed configuration from YAML.
|
|
33
187
|
#
|
|
@@ -143,39 +297,9 @@ module Html2rss
|
|
|
143
297
|
local_file_path:, limit:).to_json_feed
|
|
144
298
|
end
|
|
145
299
|
|
|
146
|
-
##
|
|
147
|
-
# Analyzes a URL and produces a reusable YAML-ready feed config hash.
|
|
148
|
-
#
|
|
149
|
-
# Uses auto-source discovery to extract articles, then derives CSS selectors
|
|
150
|
-
# from the structural analysis.
|
|
151
|
-
#
|
|
152
|
-
# @param url [String] source page URL
|
|
153
|
-
# @param strategy [Symbol] request strategy (+:auto+, +:faraday+, +:botasaurus+)
|
|
154
|
-
# @param items_selector [String, nil] optional CSS selector hint for items
|
|
155
|
-
# @param max_redirects [Integer, nil] optional redirect limit override
|
|
156
|
-
# @param max_requests [Integer, nil] optional request budget override
|
|
157
|
-
# @param limit [Integer, nil] max articles to keep
|
|
158
|
-
# @param local_file_path [String, nil] optional local HTML file path
|
|
159
|
-
# @return [Hash] feed config hash with +:channel+ and +:selectors+
|
|
160
|
-
def self.capture(url,
|
|
161
|
-
strategy: :auto,
|
|
162
|
-
items_selector: nil,
|
|
163
|
-
max_redirects: nil,
|
|
164
|
-
max_requests: nil,
|
|
165
|
-
limit: nil,
|
|
166
|
-
local_file_path: nil)
|
|
167
|
-
Capture.build(url,
|
|
168
|
-
strategy:,
|
|
169
|
-
items_selector:,
|
|
170
|
-
max_redirects:,
|
|
171
|
-
max_requests:,
|
|
172
|
-
limit:,
|
|
173
|
-
local_file_path:).config
|
|
174
|
-
end
|
|
175
|
-
|
|
176
300
|
# rubocop:enable Metrics/ParameterLists
|
|
177
301
|
|
|
178
|
-
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
302
|
+
# rubocop:disable-next ThreadSafety/ClassInstanceVariable
|
|
179
303
|
class << self
|
|
180
304
|
##
|
|
181
305
|
# @return [Html2rss::Defaults] the global defaults instance
|
|
@@ -217,12 +341,11 @@ module Html2rss
|
|
|
217
341
|
logger.level = defaults.log_level if logger.respond_to?(:level=)
|
|
218
342
|
end
|
|
219
343
|
end
|
|
220
|
-
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
221
344
|
|
|
222
345
|
class << self
|
|
223
346
|
private
|
|
224
347
|
|
|
225
|
-
# rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
348
|
+
# rubocop:disable-next Metrics/ParameterLists, Metrics/MethodLength
|
|
226
349
|
def build_auto_source_config(url:, strategy:, items_selector:, max_redirects:, max_requests:,
|
|
227
350
|
local_file_path: nil, limit: nil)
|
|
228
351
|
config = Config.auto_source_config(
|
|
@@ -237,7 +360,6 @@ module Html2rss
|
|
|
237
360
|
end
|
|
238
361
|
config
|
|
239
362
|
end
|
|
240
|
-
# rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
|
|
241
363
|
end
|
|
242
364
|
|
|
243
365
|
logger.level = defaults.log_level if logger.respond_to?(:level=)
|
|
@@ -97,7 +97,12 @@
|
|
|
97
97
|
"consumer",
|
|
98
98
|
"civic",
|
|
99
99
|
"product",
|
|
100
|
-
"research"
|
|
100
|
+
"research",
|
|
101
|
+
"health",
|
|
102
|
+
"culture",
|
|
103
|
+
"education",
|
|
104
|
+
"rights",
|
|
105
|
+
"transport"
|
|
101
106
|
]
|
|
102
107
|
},
|
|
103
108
|
"minItems": 1
|
|
@@ -443,7 +448,8 @@
|
|
|
443
448
|
"type": "boolean",
|
|
444
449
|
"not": {
|
|
445
450
|
"type": "null"
|
|
446
|
-
}
|
|
451
|
+
},
|
|
452
|
+
"description": "List-card enrichment: run Html::ArticleExtractor on each matched item node to fill missing fields from the card HTML."
|
|
447
453
|
},
|
|
448
454
|
"pagination": {
|
|
449
455
|
"description": "Pagination configuration or maximum page count integer.",
|
|
@@ -504,7 +510,7 @@
|
|
|
504
510
|
"required": [
|
|
505
511
|
"selector"
|
|
506
512
|
],
|
|
507
|
-
"description": "Defines the items selector and
|
|
513
|
+
"description": "Defines the items selector and list-card enhance settings."
|
|
508
514
|
},
|
|
509
515
|
"enclosure": {
|
|
510
516
|
"$schema": "http://json-schema.org/draft-06/schema#",
|
|
@@ -546,9 +552,6 @@
|
|
|
546
552
|
{
|
|
547
553
|
"$ref": "#/$defs/post_processors/gsub"
|
|
548
554
|
},
|
|
549
|
-
{
|
|
550
|
-
"$ref": "#/$defs/post_processors/html_to_markdown"
|
|
551
|
-
},
|
|
552
555
|
{
|
|
553
556
|
"$ref": "#/$defs/post_processors/markdown_to_html"
|
|
554
557
|
},
|
|
@@ -638,9 +641,6 @@
|
|
|
638
641
|
{
|
|
639
642
|
"$ref": "#/$defs/post_processors/gsub"
|
|
640
643
|
},
|
|
641
|
-
{
|
|
642
|
-
"$ref": "#/$defs/post_processors/html_to_markdown"
|
|
643
|
-
},
|
|
644
644
|
{
|
|
645
645
|
"$ref": "#/$defs/post_processors/markdown_to_html"
|
|
646
646
|
},
|
|
@@ -875,26 +875,6 @@
|
|
|
875
875
|
],
|
|
876
876
|
"additionalProperties": true
|
|
877
877
|
},
|
|
878
|
-
"html_to_markdown": {
|
|
879
|
-
"type": "object",
|
|
880
|
-
"title": "html_to_markdown",
|
|
881
|
-
"description": "Sanitize HTML then convert it to Markdown (via ReverseMarkdown).",
|
|
882
|
-
"examples": [
|
|
883
|
-
{
|
|
884
|
-
"name": "html_to_markdown"
|
|
885
|
-
}
|
|
886
|
-
],
|
|
887
|
-
"properties": {
|
|
888
|
-
"name": {
|
|
889
|
-
"type": "string",
|
|
890
|
-
"const": "html_to_markdown"
|
|
891
|
-
}
|
|
892
|
-
},
|
|
893
|
-
"required": [
|
|
894
|
-
"name"
|
|
895
|
-
],
|
|
896
|
-
"additionalProperties": true
|
|
897
|
-
},
|
|
898
878
|
"markdown_to_html": {
|
|
899
879
|
"type": "object",
|
|
900
880
|
"title": "markdown_to_html",
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: html2rss
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.29.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gil Desmarais
|
|
@@ -203,20 +203,6 @@ dependencies:
|
|
|
203
203
|
- - ">="
|
|
204
204
|
- !ruby/object:Gem::Version
|
|
205
205
|
version: '0'
|
|
206
|
-
- !ruby/object:Gem::Dependency
|
|
207
|
-
name: reverse_markdown
|
|
208
|
-
requirement: !ruby/object:Gem::Requirement
|
|
209
|
-
requirements:
|
|
210
|
-
- - "~>"
|
|
211
|
-
- !ruby/object:Gem::Version
|
|
212
|
-
version: '3.0'
|
|
213
|
-
type: :runtime
|
|
214
|
-
prerelease: false
|
|
215
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
216
|
-
requirements:
|
|
217
|
-
- - "~>"
|
|
218
|
-
- !ruby/object:Gem::Version
|
|
219
|
-
version: '3.0'
|
|
220
206
|
- !ruby/object:Gem::Dependency
|
|
221
207
|
name: rss
|
|
222
208
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -351,10 +337,14 @@ files:
|
|
|
351
337
|
- lib/html2rss/auto_source/segmenter/list.rb
|
|
352
338
|
- lib/html2rss/auto_source/segmenter/primary_link.rb
|
|
353
339
|
- lib/html2rss/auto_source/segmenter/semantic.rb
|
|
340
|
+
- lib/html2rss/batch.rb
|
|
354
341
|
- lib/html2rss/capture.rb
|
|
355
342
|
- lib/html2rss/capture/README.md
|
|
356
343
|
- lib/html2rss/channel.rb
|
|
357
344
|
- lib/html2rss/cli.rb
|
|
345
|
+
- lib/html2rss/cli/probe_view.rb
|
|
346
|
+
- lib/html2rss/cli/render.rb
|
|
347
|
+
- lib/html2rss/cli/validate.rb
|
|
358
348
|
- lib/html2rss/config.rb
|
|
359
349
|
- lib/html2rss/config/auto_source_contract.rb
|
|
360
350
|
- lib/html2rss/config/dynamic_params.rb
|
|
@@ -365,6 +355,7 @@ files:
|
|
|
365
355
|
- lib/html2rss/config/selectors_validator.rb
|
|
366
356
|
- lib/html2rss/config/validator.rb
|
|
367
357
|
- lib/html2rss/defaults.rb
|
|
358
|
+
- lib/html2rss/doctor/botasaurus.rb
|
|
368
359
|
- lib/html2rss/error.rb
|
|
369
360
|
- lib/html2rss/feed_builder.rb
|
|
370
361
|
- lib/html2rss/feed_builder/item_presentation.rb
|
|
@@ -405,6 +396,7 @@ files:
|
|
|
405
396
|
- lib/html2rss/html/feed_link.rb
|
|
406
397
|
- lib/html2rss/html/navigator.rb
|
|
407
398
|
- lib/html2rss/html/navigator/text_extractor.rb
|
|
399
|
+
- lib/html2rss/html/probe.rb
|
|
408
400
|
- lib/html2rss/html/rendering.rb
|
|
409
401
|
- lib/html2rss/html/rendering/audio_renderer.rb
|
|
410
402
|
- lib/html2rss/html/rendering/description_builder.rb
|
|
@@ -420,12 +412,18 @@ files:
|
|
|
420
412
|
- lib/html2rss/link_destination/path_classifier.rb
|
|
421
413
|
- lib/html2rss/link_destination/text_classifier.rb
|
|
422
414
|
- lib/html2rss/mcp.rb
|
|
415
|
+
- lib/html2rss/mcp/README.md
|
|
423
416
|
- lib/html2rss/mcp/config_argument.rb
|
|
424
417
|
- lib/html2rss/mcp/contract.rb
|
|
425
|
-
- lib/html2rss/mcp/inspect.rb
|
|
426
418
|
- lib/html2rss/mcp/outcome.rb
|
|
419
|
+
- lib/html2rss/mcp/outcome/playbook.rb
|
|
420
|
+
- lib/html2rss/mcp/runtime.rb
|
|
427
421
|
- lib/html2rss/mcp/server.rb
|
|
422
|
+
- lib/html2rss/mcp/server/tools.rb
|
|
428
423
|
- lib/html2rss/page_recon.rb
|
|
424
|
+
- lib/html2rss/page_recon/README.md
|
|
425
|
+
- lib/html2rss/page_recon/diagnostics.rb
|
|
426
|
+
- lib/html2rss/recon.rb
|
|
429
427
|
- lib/html2rss/request_service.rb
|
|
430
428
|
- lib/html2rss/request_service/blocked_surface.rb
|
|
431
429
|
- lib/html2rss/request_service/botasaurus_contract.rb
|
|
@@ -469,7 +467,6 @@ files:
|
|
|
469
467
|
- lib/html2rss/selectors/post_processors.rb
|
|
470
468
|
- lib/html2rss/selectors/post_processors/base.rb
|
|
471
469
|
- lib/html2rss/selectors/post_processors/gsub.rb
|
|
472
|
-
- lib/html2rss/selectors/post_processors/html_to_markdown.rb
|
|
473
470
|
- lib/html2rss/selectors/post_processors/html_transformers/transform_urls_to_absolute_ones.rb
|
|
474
471
|
- lib/html2rss/selectors/post_processors/html_transformers/wrap_img_in_a.rb
|
|
475
472
|
- lib/html2rss/selectors/post_processors/markdown_to_html.rb
|
|
@@ -494,6 +491,8 @@ files:
|
|
|
494
491
|
- lib/html2rss/syndication/candidate_catalog.rb
|
|
495
492
|
- lib/html2rss/syndication/discovery.rb
|
|
496
493
|
- lib/html2rss/syndication/parser.rb
|
|
494
|
+
- lib/html2rss/test.rb
|
|
495
|
+
- lib/html2rss/test/enhance_audit.rb
|
|
497
496
|
- lib/html2rss/url.rb
|
|
498
497
|
- lib/html2rss/version.rb
|
|
499
498
|
- lib/tasks/config_schema.rake
|
|
@@ -503,7 +502,7 @@ licenses:
|
|
|
503
502
|
- MIT
|
|
504
503
|
metadata:
|
|
505
504
|
allowed_push_host: https://rubygems.org
|
|
506
|
-
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.
|
|
505
|
+
changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.29.1
|
|
507
506
|
rubygems_mfa_required: 'true'
|
|
508
507
|
rdoc_options: []
|
|
509
508
|
require_paths:
|
data/lib/html2rss/mcp/inspect.rb
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Html2rss
|
|
4
|
-
module MCP
|
|
5
|
-
##
|
|
6
|
-
# Diagnostic inspect path (not Capture ownership). Fetches once, then delegates
|
|
7
|
-
# shared recon to {Html2rss::PageRecon}; adds MCP-only scraper/XHR diagnostics.
|
|
8
|
-
module Inspect
|
|
9
|
-
module_function
|
|
10
|
-
|
|
11
|
-
##
|
|
12
|
-
# @param url [String]
|
|
13
|
-
# @param strategy [String, Symbol]
|
|
14
|
-
# @return [Hash]
|
|
15
|
-
def call(url:, strategy: :auto)
|
|
16
|
-
resolved = FeedPipeline::StrategyPlan.concrete_for_diagnostic(strategy)
|
|
17
|
-
response = fetch_response(url, resolved)
|
|
18
|
-
recon = PageRecon.call(response:, url:, strategy: resolved)
|
|
19
|
-
|
|
20
|
-
result = recon.to_h.merge(
|
|
21
|
-
strategy: resolved,
|
|
22
|
-
scraper_eligibility: scraper_info(safe_parsed_body(response))
|
|
23
|
-
)
|
|
24
|
-
result[:xhr_capture] = xhr_capture_info(response) if resolved == :botasaurus
|
|
25
|
-
result
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
##
|
|
29
|
-
# @param response [Html2rss::RequestService::Response]
|
|
30
|
-
# @return [Hash] redacted XHR capture diagnostics (no query strings)
|
|
31
|
-
def xhr_capture_info(response)
|
|
32
|
-
captured = response.captured_responses
|
|
33
|
-
{
|
|
34
|
-
count: captured.size,
|
|
35
|
-
sample_endpoints: captured.first(5).filter_map { |entry| redacted_endpoint(entry) },
|
|
36
|
-
candidate_articles: captured.any? { |entry| xhr_candidate_articles?(entry) }
|
|
37
|
-
}
|
|
38
|
-
end
|
|
39
|
-
module_function :xhr_capture_info
|
|
40
|
-
|
|
41
|
-
##
|
|
42
|
-
# @param entry [Hash] captured response hash
|
|
43
|
-
# @return [String, nil] scheme+host+path only
|
|
44
|
-
def redacted_endpoint(entry)
|
|
45
|
-
raw = entry['url'] || entry[:url]
|
|
46
|
-
return unless raw
|
|
47
|
-
|
|
48
|
-
uri = URI.parse(raw.to_s)
|
|
49
|
-
return unless uri.scheme && uri.host
|
|
50
|
-
|
|
51
|
-
"#{uri.scheme}://#{uri.host}#{uri.path}"
|
|
52
|
-
rescue URI::InvalidURIError
|
|
53
|
-
nil
|
|
54
|
-
end
|
|
55
|
-
module_function :redacted_endpoint
|
|
56
|
-
|
|
57
|
-
##
|
|
58
|
-
# @param entry [Hash] captured response hash
|
|
59
|
-
# @return [Boolean]
|
|
60
|
-
def xhr_candidate_articles?(entry)
|
|
61
|
-
body = entry['body'] || entry[:body]
|
|
62
|
-
return false unless body.is_a?(String)
|
|
63
|
-
|
|
64
|
-
document = JSON.parse(body, symbolize_names: true)
|
|
65
|
-
AutoSource::Scraper::JsonState::CandidateDetector.candidate_array?(document)
|
|
66
|
-
rescue JSON::ParserError
|
|
67
|
-
false
|
|
68
|
-
end
|
|
69
|
-
module_function :xhr_candidate_articles?
|
|
70
|
-
|
|
71
|
-
##
|
|
72
|
-
# @param url [String]
|
|
73
|
-
# @param strategy [Symbol]
|
|
74
|
-
# @return [Html2rss::RequestService::Response]
|
|
75
|
-
def fetch_response(url, strategy) # rubocop:disable Metrics/MethodLength -- session construction
|
|
76
|
-
raw_config = Config.auto_source_config(
|
|
77
|
-
url:,
|
|
78
|
-
request_controls: Config::RequestControls.from_shortcut(strategy:)
|
|
79
|
-
)
|
|
80
|
-
raw_config[:strategy] = strategy
|
|
81
|
-
config = Config.from_hash(raw_config)
|
|
82
|
-
resources = FeedPipeline::RuntimePolicy.resources_for(config)
|
|
83
|
-
session = RequestSession.build(
|
|
84
|
-
config:,
|
|
85
|
-
strategy: config.strategy,
|
|
86
|
-
budget: resources.budget,
|
|
87
|
-
policy: resources.policy
|
|
88
|
-
)
|
|
89
|
-
session.fetch_initial_response
|
|
90
|
-
end
|
|
91
|
-
module_function :fetch_response
|
|
92
|
-
|
|
93
|
-
##
|
|
94
|
-
# @param parsed [Object] parsed response body
|
|
95
|
-
# @return [Array<String>, Hash]
|
|
96
|
-
def scraper_info(parsed)
|
|
97
|
-
return { error: 'Response is not HTML' } unless parsed.is_a?(Nokogiri::HTML::Document)
|
|
98
|
-
|
|
99
|
-
begin
|
|
100
|
-
Html2rss::AutoSource::Scraper.from(parsed).map(&:name)
|
|
101
|
-
rescue Html2rss::AutoSource::Scraper::NoScraperFound => error
|
|
102
|
-
{ none_found: error.category.to_s }
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
module_function :scraper_info
|
|
106
|
-
|
|
107
|
-
##
|
|
108
|
-
# @param response [Html2rss::RequestService::Response]
|
|
109
|
-
# @return [Hash, nil]
|
|
110
|
-
def sst_stats_from(response)
|
|
111
|
-
return nil unless response.html_response?
|
|
112
|
-
|
|
113
|
-
recon = PageRecon.call(response:, url: response.url)
|
|
114
|
-
return nil unless recon.sst
|
|
115
|
-
|
|
116
|
-
{ node_count: recon.sst[:node_count], degraded: recon.sst[:degraded] }
|
|
117
|
-
end
|
|
118
|
-
module_function :sst_stats_from
|
|
119
|
-
|
|
120
|
-
##
|
|
121
|
-
# @param sst [Html2rss::SST::Document]
|
|
122
|
-
# @param url [String]
|
|
123
|
-
# @return [Array]
|
|
124
|
-
def discover_segments(sst, url) = PageRecon.discover_segments(sst, url)
|
|
125
|
-
module_function :discover_segments
|
|
126
|
-
|
|
127
|
-
def safe_parsed_body(response)
|
|
128
|
-
return unless response.html_response?
|
|
129
|
-
|
|
130
|
-
response.parsed_body
|
|
131
|
-
rescue RequestService::UnsupportedResponseContentType
|
|
132
|
-
nil
|
|
133
|
-
end
|
|
134
|
-
module_function :safe_parsed_body
|
|
135
|
-
private_class_method :safe_parsed_body
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
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
|