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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +82 -25
  3. data/html2rss.gemspec +0 -1
  4. data/lib/html2rss/article.rb +1 -2
  5. data/lib/html2rss/auto_source/README.md +8 -8
  6. data/lib/html2rss/auto_source/cleanup.rb +92 -1
  7. data/lib/html2rss/auto_source/scraper/json_state/document_scanner.rb +3 -5
  8. data/lib/html2rss/auto_source/scraper/meta_oembed.rb +15 -5
  9. data/lib/html2rss/auto_source/scraper/microdata.rb +8 -6
  10. data/lib/html2rss/auto_source/scraper/native_feed.rb +5 -2
  11. data/lib/html2rss/auto_source/scraper/schema.rb +39 -21
  12. data/lib/html2rss/auto_source/scraper.rb +2 -3
  13. data/lib/html2rss/auto_source/segmenter/cluster.rb +2 -3
  14. data/lib/html2rss/auto_source/segmenter/primary_link.rb +1 -2
  15. data/lib/html2rss/auto_source/segmenter.rb +1 -2
  16. data/lib/html2rss/auto_source.rb +25 -11
  17. data/lib/html2rss/batch.rb +169 -0
  18. data/lib/html2rss/capture/README.md +15 -6
  19. data/lib/html2rss/capture.rb +158 -15
  20. data/lib/html2rss/channel.rb +1 -2
  21. data/lib/html2rss/cli/probe_view.rb +66 -0
  22. data/lib/html2rss/cli/render.rb +124 -0
  23. data/lib/html2rss/cli/validate.rb +91 -0
  24. data/lib/html2rss/cli.rb +318 -193
  25. data/lib/html2rss/config/request_headers.rb +4 -14
  26. data/lib/html2rss/config/schema.rb +7 -5
  27. data/lib/html2rss/config/validator.rb +1 -0
  28. data/lib/html2rss/config.rb +59 -22
  29. data/lib/html2rss/doctor/botasaurus.rb +130 -0
  30. data/lib/html2rss/feed_builder/rss.rb +1 -2
  31. data/lib/html2rss/feed_pipeline/README.md +5 -5
  32. data/lib/html2rss/feed_pipeline/auto_fallback.rb +29 -10
  33. data/lib/html2rss/feed_pipeline.rb +27 -16
  34. data/lib/html2rss/feed_resolution/README.md +11 -11
  35. data/lib/html2rss/feed_resolution.rb +6 -12
  36. data/lib/html2rss/html/article_extractor/heading_extractor.rb +24 -14
  37. data/lib/html2rss/html/article_extractor.rb +4 -9
  38. data/lib/html2rss/html/feed_link.rb +4 -2
  39. data/lib/html2rss/html/navigator.rb +4 -39
  40. data/lib/html2rss/html/probe.rb +82 -0
  41. data/lib/html2rss/html/sst_article_extractor.rb +1 -2
  42. data/lib/html2rss/link_destination/noise_policy.rb +1 -2
  43. data/lib/html2rss/link_destination/path_classifier.rb +20 -19
  44. data/lib/html2rss/mcp/README.md +80 -0
  45. data/lib/html2rss/mcp/contract.rb +135 -13
  46. data/lib/html2rss/mcp/outcome/playbook.rb +122 -0
  47. data/lib/html2rss/mcp/outcome.rb +112 -42
  48. data/lib/html2rss/mcp/runtime.rb +45 -0
  49. data/lib/html2rss/mcp/server/tools.rb +277 -0
  50. data/lib/html2rss/mcp/server.rb +91 -232
  51. data/lib/html2rss/mcp.rb +3 -0
  52. data/lib/html2rss/page_recon/README.md +69 -0
  53. data/lib/html2rss/page_recon/diagnostics.rb +211 -0
  54. data/lib/html2rss/page_recon.rb +56 -2
  55. data/lib/html2rss/recon.rb +252 -0
  56. data/lib/html2rss/request_service/blocked_surface.rb +1 -0
  57. data/lib/html2rss/request_service/faraday_strategy.rb +11 -7
  58. data/lib/html2rss/request_service/policy.rb +1 -2
  59. data/lib/html2rss/request_service/response.rb +3 -4
  60. data/lib/html2rss/request_service/strategy.rb +1 -2
  61. data/lib/html2rss/request_service.rb +2 -0
  62. data/lib/html2rss/request_session.rb +1 -2
  63. data/lib/html2rss/scrape_target.rb +1 -1
  64. data/lib/html2rss/selectors/post_processors/sanitize_html.rb +2 -4
  65. data/lib/html2rss/selectors/post_processors.rb +0 -1
  66. data/lib/html2rss/selectors.rb +33 -10
  67. data/lib/html2rss/sst/attrs.rb +1 -2
  68. data/lib/html2rss/sst/normalizer.rb +49 -11
  69. data/lib/html2rss/status.rb +3 -6
  70. data/lib/html2rss/syndication/README.md +6 -6
  71. data/lib/html2rss/syndication/discovery.rb +2 -4
  72. data/lib/html2rss/test/enhance_audit.rb +192 -0
  73. data/lib/html2rss/test.rb +458 -0
  74. data/lib/html2rss/url.rb +1 -2
  75. data/lib/html2rss/version.rb +1 -1
  76. data/lib/html2rss.rb +157 -35
  77. data/schema/html2rss-config.schema.json +9 -29
  78. metadata +17 -18
  79. data/lib/html2rss/mcp/inspect.rb +0 -138
  80. 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 optional enhancement settings."
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.28.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.28.0
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:
@@ -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