html2rss 0.22.2 → 0.23.0

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 (138) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -108
  3. data/lib/html2rss/{articles → article}/deduplicator.rb +3 -4
  4. data/lib/html2rss/{rss_builder → article}/enclosure.rb +6 -22
  5. data/lib/html2rss/article.rb +206 -0
  6. data/lib/html2rss/auto_source/discovery/dom_clustering/group_scorer.rb +80 -0
  7. data/lib/html2rss/auto_source/discovery/dom_clustering/overlap_resolver.rb +86 -0
  8. data/lib/html2rss/auto_source/discovery/dom_clustering.rb +119 -0
  9. data/lib/html2rss/auto_source/discovery/list_candidates.rb +94 -0
  10. data/lib/html2rss/auto_source/discovery/semantic_anchor_candidates.rb +219 -0
  11. data/lib/html2rss/auto_source/discovery/semantic_containers.rb +71 -0
  12. data/lib/html2rss/auto_source/discovery/sitemap.rb +159 -0
  13. data/lib/html2rss/auto_source/discovery.rb +14 -0
  14. data/lib/html2rss/auto_source/link_heuristics/anchor_signals.rb +28 -0
  15. data/lib/html2rss/auto_source/link_heuristics/container_assessor.rb +106 -0
  16. data/lib/html2rss/auto_source/link_heuristics/container_signals.rb +80 -0
  17. data/lib/html2rss/auto_source/link_heuristics/destination_facts.rb +42 -0
  18. data/lib/html2rss/auto_source/link_heuristics/href_extractor.rb +38 -0
  19. data/lib/html2rss/auto_source/link_heuristics/path_classifier.rb +221 -0
  20. data/lib/html2rss/auto_source/link_heuristics/text_classifier.rb +66 -0
  21. data/lib/html2rss/auto_source/link_heuristics.rb +139 -0
  22. data/lib/html2rss/auto_source/scraper/html.rb +22 -33
  23. data/lib/html2rss/auto_source/scraper/json_state.rb +1 -1
  24. data/lib/html2rss/auto_source/scraper/meta_oembed.rb +163 -0
  25. data/lib/html2rss/auto_source/scraper/microformats2.rb +146 -0
  26. data/lib/html2rss/auto_source/scraper/schema/thing.rb +21 -2
  27. data/lib/html2rss/auto_source/scraper/semantic_html/{deduplicator.rb → entry_deduplicator.rb} +3 -3
  28. data/lib/html2rss/auto_source/scraper/semantic_html.rb +23 -171
  29. data/lib/html2rss/auto_source/scraper/sitemap.rb +153 -0
  30. data/lib/html2rss/auto_source/scraper/wordpress_api/page_scope/date_archive_range.rb +110 -0
  31. data/lib/html2rss/auto_source/scraper/wordpress_api/page_scope.rb +2 -78
  32. data/lib/html2rss/auto_source/scraper/wordpress_api.rb +9 -0
  33. data/lib/html2rss/auto_source/scraper.rb +10 -3
  34. data/lib/html2rss/auto_source.rb +30 -35
  35. data/lib/html2rss/channel.rb +118 -0
  36. data/lib/html2rss/cli.rb +5 -6
  37. data/lib/html2rss/config/auto_source_contract.rb +51 -0
  38. data/lib/html2rss/config/request_controls.rb +136 -0
  39. data/lib/html2rss/config/request_headers.rb +1 -1
  40. data/lib/html2rss/config/schema.rb +65 -114
  41. data/lib/html2rss/{selectors/config.rb → config/selectors_validator.rb} +56 -7
  42. data/lib/html2rss/config/validator.rb +6 -6
  43. data/lib/html2rss/config.rb +176 -19
  44. data/lib/html2rss/{configuration.rb → defaults.rb} +12 -10
  45. data/lib/html2rss/feed_builder/json_feed/item.rb +96 -0
  46. data/lib/html2rss/feed_builder/json_feed.rb +61 -0
  47. data/lib/html2rss/feed_builder/rss/stylesheet.rb +65 -0
  48. data/lib/html2rss/feed_builder/rss.rb +116 -0
  49. data/lib/html2rss/feed_builder.rb +23 -0
  50. data/lib/html2rss/feed_pipeline/auto_fallback.rb +59 -42
  51. data/lib/html2rss/feed_pipeline/runtime_policy.rb +108 -0
  52. data/lib/html2rss/feed_pipeline/strategy_plan.rb +64 -0
  53. data/lib/html2rss/feed_pipeline.rb +52 -65
  54. data/lib/html2rss/html/article_extractor/category_extractor.rb +123 -0
  55. data/lib/html2rss/html/article_extractor/date_extractor.rb +22 -0
  56. data/lib/html2rss/html/article_extractor/enclosure_extractor.rb +95 -0
  57. data/lib/html2rss/html/article_extractor/heading_extractor.rb +54 -0
  58. data/lib/html2rss/html/article_extractor/id_generator.rb +69 -0
  59. data/lib/html2rss/html/article_extractor/image_extractor.rb +60 -0
  60. data/lib/html2rss/html/article_extractor.rb +134 -0
  61. data/lib/html2rss/html/navigator/text_extractor.rb +79 -0
  62. data/lib/html2rss/html/navigator.rb +146 -0
  63. data/lib/html2rss/html/rendering/audio_renderer.rb +38 -0
  64. data/lib/html2rss/html/rendering/description_builder.rb +89 -0
  65. data/lib/html2rss/html/rendering/image_renderer.rb +43 -0
  66. data/lib/html2rss/html/rendering/media_renderer.rb +39 -0
  67. data/lib/html2rss/html/rendering/pdf_renderer.rb +36 -0
  68. data/lib/html2rss/html/rendering/video_renderer.rb +38 -0
  69. data/lib/html2rss/html/rendering.rb +25 -0
  70. data/lib/html2rss/html.rb +8 -0
  71. data/lib/html2rss/request_service/blocked_surface.rb +67 -0
  72. data/lib/html2rss/request_service/botasaurus_contract.rb +0 -4
  73. data/lib/html2rss/request_service/botasaurus_strategy.rb +9 -27
  74. data/lib/html2rss/request_service/browserless_strategy.rb +2 -23
  75. data/lib/html2rss/request_service/budget.rb +90 -16
  76. data/lib/html2rss/request_service/context.rb +1 -3
  77. data/lib/html2rss/request_service/faraday_strategy.rb +12 -26
  78. data/lib/html2rss/request_service/local_file_strategy.rb +12 -4
  79. data/lib/html2rss/request_service/network_guard.rb +125 -0
  80. data/lib/html2rss/request_service/policy.rb +10 -82
  81. data/lib/html2rss/request_service/puppet_commander/navigation_guards.rb +148 -0
  82. data/lib/html2rss/request_service/puppet_commander/preload_runner.rb +86 -0
  83. data/lib/html2rss/request_service/puppet_commander.rb +22 -191
  84. data/lib/html2rss/request_service/response_guard.rb +1 -1
  85. data/lib/html2rss/request_service/strategy.rb +102 -6
  86. data/lib/html2rss/request_service.rb +7 -2
  87. data/lib/html2rss/request_session/pager/base.rb +83 -0
  88. data/lib/html2rss/request_session/pager/custom_selector.rb +39 -0
  89. data/lib/html2rss/request_session/pager/json_cursor.rb +78 -0
  90. data/lib/html2rss/request_session/pager/offset.rb +48 -0
  91. data/lib/html2rss/request_session/pager/rel_next.rb +23 -0
  92. data/lib/html2rss/request_session/pager/url_template.rb +48 -0
  93. data/lib/html2rss/request_session/pager.rb +75 -0
  94. data/lib/html2rss/request_session.rb +25 -16
  95. data/lib/html2rss/selectors/item_scope.rb +30 -0
  96. data/lib/html2rss/selectors/post_processors/base.rb +1 -1
  97. data/lib/html2rss/selectors/post_processors/markdown_to_html.rb +0 -1
  98. data/lib/html2rss/selectors/post_processors/sanitize_html.rb +0 -2
  99. data/lib/html2rss/selectors/post_processors/template.rb +7 -3
  100. data/lib/html2rss/selectors.rb +63 -65
  101. data/lib/html2rss/version.rb +1 -3
  102. data/lib/html2rss.rb +19 -19
  103. data/schema/html2rss-config.schema.json +109 -9
  104. metadata +68 -42
  105. data/lib/html2rss/auto_source/scraper/html/class_clustering.rb +0 -196
  106. data/lib/html2rss/auto_source/scraper/link_heuristics.rb +0 -401
  107. data/lib/html2rss/auto_source/scraper/semantic_html/anchor_selector.rb +0 -49
  108. data/lib/html2rss/blocked_surface.rb +0 -65
  109. data/lib/html2rss/category_extractor.rb +0 -116
  110. data/lib/html2rss/config/class_methods.rb +0 -183
  111. data/lib/html2rss/html_extractor/date_extractor.rb +0 -20
  112. data/lib/html2rss/html_extractor/enclosure_extractor.rb +0 -91
  113. data/lib/html2rss/html_extractor/heading_extractor.rb +0 -50
  114. data/lib/html2rss/html_extractor/id_generator.rb +0 -67
  115. data/lib/html2rss/html_extractor/image_extractor.rb +0 -58
  116. data/lib/html2rss/html_extractor/list_candidates.rb +0 -88
  117. data/lib/html2rss/html_extractor/semantic_anchor_candidates.rb +0 -260
  118. data/lib/html2rss/html_extractor/semantic_containers.rb +0 -69
  119. data/lib/html2rss/html_extractor/text_extractor.rb +0 -77
  120. data/lib/html2rss/html_extractor.rb +0 -190
  121. data/lib/html2rss/html_navigator.rb +0 -71
  122. data/lib/html2rss/json_feed_builder/item.rb +0 -94
  123. data/lib/html2rss/json_feed_builder.rb +0 -59
  124. data/lib/html2rss/rendering/audio_renderer.rb +0 -36
  125. data/lib/html2rss/rendering/description_builder.rb +0 -87
  126. data/lib/html2rss/rendering/image_renderer.rb +0 -41
  127. data/lib/html2rss/rendering/media_renderer.rb +0 -37
  128. data/lib/html2rss/rendering/pdf_renderer.rb +0 -34
  129. data/lib/html2rss/rendering/video_renderer.rb +0 -36
  130. data/lib/html2rss/rendering.rb +0 -23
  131. data/lib/html2rss/request_controls.rb +0 -133
  132. data/lib/html2rss/request_session/rel_next_pager.rb +0 -70
  133. data/lib/html2rss/request_session/runtime_input.rb +0 -71
  134. data/lib/html2rss/request_session/runtime_policy.rb +0 -84
  135. data/lib/html2rss/rss_builder/article.rb +0 -208
  136. data/lib/html2rss/rss_builder/channel.rb +0 -118
  137. data/lib/html2rss/rss_builder/stylesheet.rb +0 -63
  138. data/lib/html2rss/rss_builder.rb +0 -101
@@ -22,9 +22,12 @@ module Html2rss
22
22
  # Ordered scraper classes considered during auto-source extraction.
23
23
  SCRAPERS = [
24
24
  WordpressApi,
25
+ Sitemap,
25
26
  Schema,
26
27
  Microdata,
28
+ Microformats2,
27
29
  JsonState,
30
+ MetaOembed,
28
31
  SemanticHtml,
29
32
  Html
30
33
  ].freeze
@@ -67,12 +70,14 @@ module Html2rss
67
70
 
68
71
  ##
69
72
  # Returns an array of scraper classes that claim to find articles in the parsed body.
70
- # @param parsed_body [Nokogiri::HTML::Document] The parsed HTML body.
73
+ # @param parsed_body [Nokogiri::HTML::Document] The parsed HTML document.
71
74
  # @param opts [Hash] The options hash.
72
75
  # @option opts [Hash] :wordpress_api scraper toggle and configuration
73
76
  # @option opts [Hash] :schema scraper toggle and configuration
74
77
  # @option opts [Hash] :microdata scraper toggle and configuration
78
+ # @option opts [Hash] :microformats2 scraper toggle and configuration
75
79
  # @option opts [Hash] :json_state scraper toggle and configuration
80
+ # @option opts [Hash] :meta_oembed scraper toggle and configuration
76
81
  # @option opts [Hash] :semantic_html scraper toggle and configuration
77
82
  # @option opts [Hash] :html scraper toggle and configuration
78
83
  # @return [Array<Class>] An array of scraper classes that can handle the parsed body.
@@ -86,14 +91,16 @@ module Html2rss
86
91
  end
87
92
 
88
93
  # Returns scraper instances ready for extraction.
89
- # @param parsed_body [Nokogiri::HTML::Document] The parsed HTML body.
94
+ # @param parsed_body [Nokogiri::HTML::Document] The parsed HTML document.
90
95
  # @param url [String, Html2rss::Url] The page url.
91
96
  # @param request_session [Html2rss::RequestSession, nil] Shared follow-up session.
92
97
  # @param opts [Hash] The options hash.
93
98
  # @option opts [Hash] :wordpress_api scraper toggle and configuration
94
99
  # @option opts [Hash] :schema scraper toggle and configuration
95
100
  # @option opts [Hash] :microdata scraper toggle and configuration
101
+ # @option opts [Hash] :microformats2 scraper toggle and configuration
96
102
  # @option opts [Hash] :json_state scraper toggle and configuration
103
+ # @option opts [Hash] :meta_oembed scraper toggle and configuration
97
104
  # @option opts [Hash] :semantic_html scraper toggle and configuration
98
105
  # @option opts [Hash] :html scraper toggle and configuration
99
106
  # @return [Array<Object>] An array of scraper instances that can handle the parsed body.
@@ -139,7 +146,7 @@ module Html2rss
139
146
  private_class_method :classify_no_scraper_surface
140
147
 
141
148
  def self.blocked_surface?(parsed_body)
142
- Html2rss::BlockedSurface.interstitial?(parsed_body.to_html)
149
+ Html2rss::RequestService::BlockedSurface.interstitial?(parsed_body.to_html)
143
150
  end
144
151
  private_class_method :blocked_surface?
145
152
 
@@ -22,15 +22,26 @@ module Html2rss
22
22
  wordpress_api: {
23
23
  enabled: true
24
24
  },
25
+ sitemap: {
26
+ enabled: true,
27
+ min_priority: Discovery::Sitemap::DEFAULT_MIN_PRIORITY,
28
+ max_age_days: Discovery::Sitemap::DEFAULT_MAX_AGE_DAYS
29
+ },
25
30
  schema: {
26
31
  enabled: true
27
32
  },
28
33
  microdata: {
29
34
  enabled: true
30
35
  },
36
+ microformats2: {
37
+ enabled: true
38
+ },
31
39
  json_state: {
32
40
  enabled: true
33
41
  },
42
+ meta_oembed: {
43
+ enabled: true
44
+ },
34
45
  semantic_html: {
35
46
  enabled: true,
36
47
  fallback_anchorless: true
@@ -45,39 +56,23 @@ module Html2rss
45
56
  cleanup: Cleanup::DEFAULT_CONFIG
46
57
  }.freeze
47
58
 
48
- SCRAPER_CONFIG = proc do
49
- optional(:wordpress_api).hash do
50
- optional(:enabled).filled(:bool)
51
- end
52
- optional(:schema).hash do
53
- optional(:enabled).filled(:bool)
54
- end
55
- optional(:microdata).hash do
56
- optional(:enabled).filled(:bool)
57
- end
58
- optional(:json_state).hash do
59
- optional(:enabled).filled(:bool)
60
- end
61
- optional(:semantic_html).hash do
62
- optional(:enabled).filled(:bool)
63
- optional(:fallback_anchorless).filled(:bool)
64
- end
65
- optional(:html).hash do
66
- optional(:enabled).filled(:bool)
67
- optional(:minimum_selector_frequency).filled(:integer, gt?: 0)
68
- optional(:use_top_selectors).filled(:integer, gt?: 0)
69
- optional(:fallback_anchorless).filled(:bool)
70
- end
71
- end.freeze
72
- private_constant :SCRAPER_CONFIG
73
-
74
- # Runtime schema used to validate auto-source config values.
75
- Config = Dry::Schema.Params do
76
- optional(:scraper).hash(&SCRAPER_CONFIG)
59
+ class << self
60
+ ##
61
+ # Returns the sum of required request slots for all enabled scrapers in the config.
62
+ #
63
+ # @param config [Hash, nil] auto_source configuration hash
64
+ # @return [Integer] total request slots required by scrapers
65
+ def request_slots_for(config)
66
+ return 0 unless config
77
67
 
78
- optional(:cleanup).hash do
79
- optional(:keep_different_domain).filled(:bool)
80
- optional(:min_words_title).filled(:integer, gt?: 0)
68
+ Scraper::SCRAPERS.sum do |scraper|
69
+ if config.dig(:scraper, scraper.options_key, :enabled)
70
+ opts = config.dig(:scraper, scraper.options_key)
71
+ scraper.respond_to?(:request_slots) ? scraper.request_slots(opts) : 0
72
+ else
73
+ 0
74
+ end
75
+ end
81
76
  end
82
77
  end
83
78
 
@@ -98,7 +93,7 @@ module Html2rss
98
93
  ##
99
94
  # Extracts article candidates by selecting every scraper that can explain the
100
95
  # page shape, running those scrapers, and normalizing the resulting hashes
101
- # into `RssBuilder::Article` objects.
96
+ # into `Article` objects.
102
97
  #
103
98
  # The contributor-facing flow is:
104
99
  # 1. choose scraper instances that match the page
@@ -108,7 +103,7 @@ module Html2rss
108
103
  # Scrapers with expensive precomputation, such as `SemanticHtml`, keep that
109
104
  # state on the instance so detection and extraction can reuse the same work.
110
105
  #
111
- # @return [Array<Html2rss::RssBuilder::Article>] extracted articles
106
+ # @return [Array<Html2rss::Article>] extracted articles
112
107
  def articles
113
108
  @articles ||= extract_articles
114
109
  rescue Html2rss::AutoSource::Scraper::NoScraperFound => error
@@ -133,7 +128,7 @@ module Html2rss
133
128
 
134
129
  def run_scraper(instance)
135
130
  instance.each.map do |article_hash|
136
- RssBuilder::Article.new(**article_hash, scraper: instance.class)
131
+ Article.new(**article_hash, scraper: instance.class)
137
132
  end
138
133
  end
139
134
 
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Html2rss
4
+ ##
5
+ # Channel metadata for feed builders, extracted from the HTML document head
6
+ # and HTTP response (with optional config overrides).
7
+ #
8
+ # Format adapters ({FeedBuilder::Rss}, {FeedBuilder::JsonFeed}) consume this type; they do
9
+ # not own channel extraction.
10
+ class Channel
11
+ # Fallback RSS ttl (in minutes) when no cache directives are present.
12
+ DEFAULT_TTL_IN_MINUTES = 360
13
+ # Description template used when no explicit or discovered description exists.
14
+ DEFAULT_DESCRIPTION_TEMPLATE = 'Latest items from %<url>s'
15
+
16
+ ##
17
+ # @param response [Html2rss::RequestService::Response]
18
+ # @param overrides [Hash{Symbol => String}] optional overrides for channel attributes
19
+ def initialize(response, overrides: {})
20
+ @response = response
21
+ @overrides = overrides
22
+ end
23
+
24
+ # @return [String] channel title derived from overrides, document title, or URL
25
+ def title
26
+ @title ||= fetch_title
27
+ end
28
+
29
+ # @return [Html2rss::Url] canonical channel URL
30
+ def url = @url ||= Html2rss::Url.from_absolute(@response.url)
31
+
32
+ # @return [String] channel description text
33
+ def description
34
+ return overrides[:description] unless overrides[:description].to_s.empty?
35
+
36
+ description = parsed_body.at_css('meta[name="description"]')&.[]('content') if html_response?
37
+
38
+ return format(DEFAULT_DESCRIPTION_TEMPLATE, url:) if description.to_s.empty?
39
+
40
+ description
41
+ end
42
+
43
+ # @return [Integer] cache time-to-live in minutes
44
+ def ttl
45
+ calculated = if overrides[:ttl]
46
+ overrides[:ttl].to_i
47
+ elsif (max_age = headers['cache-control']&.match(/max-age=(\d+)/)&.[](1))
48
+ max_age.to_i.fdiv(60).ceil
49
+ else
50
+ DEFAULT_TTL_IN_MINUTES
51
+ end
52
+
53
+ min_ttl = Html2rss.defaults.min_ttl
54
+ min_ttl ? [calculated, min_ttl].max : calculated
55
+ end
56
+
57
+ # @return [String, nil] ISO-like language code when available
58
+ def language
59
+ return overrides[:language] if overrides[:language]
60
+
61
+ if (language_code = headers['content-language']&.match(/^([a-z]{2})/))
62
+ return language_code[0]
63
+ end
64
+
65
+ return unless html_response?
66
+
67
+ parsed_body['lang'] || parsed_body.at_css('[lang]')&.[]('lang')
68
+ end
69
+
70
+ # @return [String, nil] channel author metadata
71
+ def author
72
+ return overrides[:author] if overrides[:author]
73
+
74
+ return unless html_response?
75
+
76
+ parsed_body.at_css('meta[name="author"]')&.[]('content')
77
+ end
78
+
79
+ # @return [String, Time] source last-modified timestamp or current time fallback
80
+ def last_build_date = headers['last-modified'] || Time.now
81
+
82
+ # @return [Html2rss::Url, nil] channel image URL
83
+ def image
84
+ return overrides[:image] if overrides[:image]
85
+
86
+ return unless html_response?
87
+
88
+ if (image_url = parsed_body.at_css('meta[property="og:image"]')&.[]('content'))
89
+ Url.sanitize(image_url)
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ attr_reader :overrides
96
+
97
+ def parsed_body = @parsed_body ||= @response.parsed_body
98
+ def headers = @headers ||= @response.headers
99
+ def html_response? = @html_response ||= @response.html_response?
100
+
101
+ def fetch_title
102
+ override_title = overrides[:title]
103
+ return override_title if override_title
104
+ return parsed_title if parsed_title
105
+
106
+ url.channel_titleized
107
+ end
108
+
109
+ def parsed_title
110
+ return unless html_response?
111
+
112
+ title = parsed_body.at_css('head > title')&.text.to_s
113
+ return if title.empty?
114
+
115
+ title.gsub(/\s+/, ' ').strip
116
+ end
117
+ end
118
+ end
data/lib/html2rss/cli.rb CHANGED
@@ -4,21 +4,20 @@ require 'fileutils'
4
4
  require 'json'
5
5
  require 'thor'
6
6
 
7
- ##
8
- # The Html2rss namespace / command line interface.
9
7
  module Html2rss
10
8
  ##
11
9
  # The Html2rss command line interface.
12
10
  class CLI < Thor # rubocop:disable Metrics/ClassLength
13
11
  check_unknown_options!
14
- # Ordered fallback chain attempted by auto strategy.
12
+ # Ordered fallback chain attempted by the feed-level :auto plan.
15
13
  #
16
14
  # @return [Array<Symbol>]
17
15
  AUTO_FALLBACK_CHAIN = Html2rss::FeedPipeline::AutoFallback::CHAIN.freeze
18
- # Supported CLI strategy option values.
16
+ # Supported CLI strategy plan option values (:auto plus concrete strategies).
19
17
  #
20
18
  # @return [Array<String>]
21
- STRATEGY_OPTION_ENUM = (['auto'] + Html2rss::RequestService.strategy_names).uniq.freeze
19
+ STRATEGY_OPTION_ENUM = Html2rss::FeedPipeline::StrategyPlan.accepted_names.map(&:to_s).freeze
20
+
22
21
  # User-facing strategy help text that reflects the current fallback chain.
23
22
  #
24
23
  # @return [String]
@@ -188,7 +187,7 @@ module Html2rss
188
187
  end
189
188
 
190
189
  def request_controls
191
- Html2rss::RequestControls.new(
190
+ Html2rss::Config::RequestControls.new(
192
191
  strategy: options[:strategy]&.to_sym,
193
192
  max_redirects: options[:max_redirects],
194
193
  max_requests: options[:max_requests],
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-validation'
4
+
5
+ module Html2rss
6
+ class Config
7
+ # Runtime source of truth for validating auto-source config values.
8
+ AutoSourceContract = Dry::Schema.Params do # rubocop:disable Metrics/BlockLength
9
+ optional(:scraper).hash do # rubocop:disable Metrics/BlockLength
10
+ optional(:wordpress_api).hash do
11
+ optional(:enabled).filled(:bool)
12
+ end
13
+ optional(:sitemap).hash do
14
+ optional(:enabled).filled(:bool)
15
+ optional(:min_priority).filled(:float)
16
+ optional(:max_age_days).filled(:integer, gt?: 0)
17
+ end
18
+ optional(:schema).hash do
19
+ optional(:enabled).filled(:bool)
20
+ end
21
+ optional(:microdata).hash do
22
+ optional(:enabled).filled(:bool)
23
+ end
24
+ optional(:microformats2).hash do
25
+ optional(:enabled).filled(:bool)
26
+ end
27
+ optional(:json_state).hash do
28
+ optional(:enabled).filled(:bool)
29
+ end
30
+ optional(:meta_oembed).hash do
31
+ optional(:enabled).filled(:bool)
32
+ end
33
+ optional(:semantic_html).hash do
34
+ optional(:enabled).filled(:bool)
35
+ optional(:fallback_anchorless).filled(:bool)
36
+ end
37
+ optional(:html).hash do
38
+ optional(:enabled).filled(:bool)
39
+ optional(:minimum_selector_frequency).filled(:integer, gt?: 0)
40
+ optional(:use_top_selectors).filled(:integer, gt?: 0)
41
+ optional(:fallback_anchorless).filled(:bool)
42
+ end
43
+ end
44
+
45
+ optional(:cleanup).hash do
46
+ optional(:keep_different_domain).filled(:bool)
47
+ optional(:min_words_title).filled(:integer, gt?: 0)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Html2rss
4
+ class Config
5
+ ##
6
+ # Wire provenance for request control values (explicit vs default).
7
+ class RequestControls
8
+ # Request-control keys accepted at the top level of feed config.
9
+ TOP_LEVEL_KEYS = %i[strategy].freeze
10
+ # Request-control keys accepted under the nested `request` config.
11
+ REQUEST_KEYS = %i[max_redirects max_requests total_timeout_seconds].freeze
12
+
13
+ ##
14
+ # @param config [Hash{Symbol => Object}] raw config input
15
+ # @return [RequestControls] request controls extracted from the config hash
16
+ def self.from_config(config)
17
+ HashUtil.assert_symbol_keys!(config, context: 'config', deep: false)
18
+ HashUtil.assert_symbol_keys!(config[:request], context: 'config[:request]') if config[:request].is_a?(Hash)
19
+
20
+ new(
21
+ strategy: config[:strategy],
22
+ max_redirects: request_value_for(config, :max_redirects),
23
+ max_requests: request_value_for(config, :max_requests),
24
+ total_timeout_seconds: request_value_for(config, :total_timeout_seconds),
25
+ explicit_keys: explicit_keys_for(config)
26
+ )
27
+ end
28
+
29
+ def self.explicit_keys_for(config)
30
+ TOP_LEVEL_KEYS.filter { config.key?(_1) } +
31
+ REQUEST_KEYS.filter { request_key?(config, _1) }
32
+ end
33
+
34
+ def self.request_value_for(config, key)
35
+ request_config = config[:request]
36
+ return nil unless request_config.is_a?(Hash)
37
+
38
+ request_config[key]
39
+ end
40
+
41
+ def self.request_key?(config, key)
42
+ request_config = config[:request]
43
+ request_config.is_a?(Hash) && request_config.key?(key)
44
+ end
45
+
46
+ private_class_method :explicit_keys_for, :request_value_for, :request_key?
47
+
48
+ ##
49
+ # @param strategy [Symbol, nil] effective request strategy
50
+ # @param max_redirects [Integer, nil] effective redirect limit
51
+ # @param max_requests [Integer, nil] effective request budget
52
+ # @param total_timeout_seconds [Integer, nil] effective request timeout
53
+ # @param explicit_keys [Array<Symbol>] controls explicitly supplied by the caller
54
+ def initialize(strategy: nil, max_redirects: nil, max_requests: nil,
55
+ total_timeout_seconds: nil, explicit_keys: [])
56
+ @strategy = strategy
57
+ @max_redirects = max_redirects
58
+ @max_requests = max_requests
59
+ @total_timeout_seconds = total_timeout_seconds
60
+ @explicit_keys = explicit_keys.map(&:to_sym).uniq.freeze
61
+ freeze
62
+ end
63
+
64
+ ##
65
+ # @return [Symbol, nil] effective request strategy
66
+ attr_reader :strategy
67
+
68
+ ##
69
+ # @return [Integer, nil] effective redirect limit
70
+ attr_reader :max_redirects
71
+
72
+ ##
73
+ # @return [Integer, nil] effective request budget
74
+ attr_reader :max_requests
75
+
76
+ ##
77
+ # @return [Integer, nil] effective request timeout
78
+ attr_reader :total_timeout_seconds
79
+
80
+ ##
81
+ # @param name [Symbol, String] request control name
82
+ # @return [Boolean] whether the control was explicitly supplied
83
+ def explicit?(name)
84
+ explicit_keys.include?(name.to_sym)
85
+ end
86
+
87
+ ##
88
+ # @param strategy [Symbol, nil] validated request strategy
89
+ # @param max_redirects [Integer, nil] validated redirect limit
90
+ # @param max_requests [Integer, nil] validated request budget
91
+ # @param total_timeout_seconds [Integer, nil] validated request timeout
92
+ # @return [RequestControls] controls updated with validated effective values
93
+ def with_effective_values(strategy:, max_redirects:, max_requests:, total_timeout_seconds:)
94
+ self.class.new(
95
+ strategy:,
96
+ max_redirects:,
97
+ max_requests:,
98
+ total_timeout_seconds:,
99
+ explicit_keys:
100
+ )
101
+ end
102
+
103
+ ##
104
+ # Applies only explicitly set controls to the provided config hash.
105
+ #
106
+ # @param config [Hash{Symbol => Object}] mutable config hash
107
+ # @return [Hash{Symbol => Object}] the same hash with explicit controls written
108
+ def apply_to(config)
109
+ config[:strategy] = strategy if explicit?(:strategy)
110
+ apply_request_value(config, :max_redirects, max_redirects)
111
+ apply_request_value(config, :max_requests, max_requests)
112
+ apply_request_value(config, :total_timeout_seconds, total_timeout_seconds)
113
+ config
114
+ end
115
+
116
+ private
117
+
118
+ attr_reader :explicit_keys
119
+
120
+ def apply_request_value(config, key, value)
121
+ return unless explicit?(key)
122
+
123
+ ensure_request_config!(config)
124
+ config[:request][key] = value
125
+ end
126
+
127
+ def ensure_request_config!(config)
128
+ request_config = config[:request]
129
+ return config[:request] = {} if request_config.nil?
130
+ return if request_config.is_a?(Hash)
131
+
132
+ raise ArgumentError, 'request config must be a hash'
133
+ end
134
+ end
135
+ end
136
+ end
@@ -41,7 +41,7 @@ module Html2rss
41
41
  # @return [Hash{String => String}] the default header set merged with global defaults
42
42
  def browser_defaults
43
43
  defaults = DEFAULT_HEADERS.dup
44
- global_headers = Html2rss.configuration.headers
44
+ global_headers = Html2rss.defaults.headers
45
45
  global_headers = global_headers.call if global_headers.respond_to?(:call)
46
46
 
47
47
  if global_headers.is_a?(Hash)