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
@@ -42,6 +42,10 @@ module Html2rss
42
42
 
43
43
  string = context[:options]&.dig(:string).to_s
44
44
  raise InvalidType, 'The `string` template is absent.' if string.empty?
45
+
46
+ return if context.item_scope
47
+
48
+ raise MissingOption, 'The post-processor context is missing `item_scope`.', [], cause: nil
45
49
  end
46
50
 
47
51
  ##
@@ -51,8 +55,6 @@ module Html2rss
51
55
  super
52
56
 
53
57
  @options = context[:options] || {}
54
- @scraper = context[:scraper]
55
- @item = context[:item]
56
58
  @string = @options[:string].to_s
57
59
  @getter = ->(key) { item_value(key) }
58
60
  end
@@ -69,7 +71,9 @@ module Html2rss
69
71
  # @return [String]
70
72
  def item_value(key)
71
73
  key = key.to_sym
72
- key == :self ? value : @scraper.select(key, @item)
74
+ return value if key == :self
75
+
76
+ @context.item_scope.select(key)
73
77
  end
74
78
  end
75
79
  end
@@ -18,7 +18,9 @@ module Html2rss
18
18
  include Enumerable
19
19
 
20
20
  # A context instance passed to item extractors and post-processors.
21
- Context = Struct.new('Context', :options, :item, :config, :scraper, keyword_init: true)
21
+ # When built via {ItemScope#context_for}, +item_scope+ carries the per-item
22
+ # extraction base_url for nested selects (e.g. Template).
23
+ Context = Struct.new('Context', :options, :item, :config, :scraper, :item_scope, keyword_init: true)
22
24
 
23
25
  # Default selectors options merged into user configuration.
24
26
  DEFAULT_CONFIG = { items: { enhance: true } }.freeze
@@ -30,9 +32,6 @@ module Html2rss
30
32
  # Item attributes that require dedicated extraction logic.
31
33
  SPECIAL_ATTRIBUTES = Set[:guid, :enclosure, :categories].freeze
32
34
 
33
- # Mapping of new attribute names to their legacy names for backward compatibility.
34
- RENAMED_ATTRIBUTES = { published_at: %i[updated pubDate] }.freeze
35
-
36
35
  ##
37
36
  # Initializes a new Selectors instance.
38
37
  #
@@ -46,14 +45,14 @@ module Html2rss
46
45
  @time_zone = time_zone
47
46
 
48
47
  prepare_selectors!
49
- @rss_item_attributes = @selectors.keys & Html2rss::RssBuilder::Article::PROVIDED_KEYS
48
+ @rss_item_attributes = @selectors.keys & Html2rss::Article::PROVIDED_KEYS
50
49
  end
51
50
 
52
51
  ##
53
52
  # Returns articles extracted from the response.
54
53
  # Reverses order if config specifies reverse ordering.
55
54
  #
56
- # @return [Array<Html2rss::RssBuilder::Article>]
55
+ # @return [Array<Html2rss::Article>]
57
56
  def articles
58
57
  @articles ||= @selectors.dig(ITEMS_SELECTOR_KEY, :order) == 'reverse' ? to_a.tap(&:reverse!) : to_a
59
58
  end
@@ -61,7 +60,7 @@ module Html2rss
61
60
  ##
62
61
  # Iterates over each scraped article.
63
62
  #
64
- # @yield [article] Gives each article as an Html2rss::RssBuilder::Article.
63
+ # @yield [article] Gives each article as an Html2rss::Article.
65
64
  # @return [Enumerator] An enumerator if no block is given.
66
65
  def each(&)
67
66
  return enum_for(:each) unless block_given?
@@ -73,7 +72,7 @@ module Html2rss
73
72
 
74
73
  enhance_article_hash(article_hash, item, response.url) if enhance
75
74
 
76
- yield Html2rss::RssBuilder::Article.new(**article_hash, scraper: self.class)
75
+ yield Html2rss::Article.new(**article_hash, scraper: self.class)
77
76
  end
78
77
  end
79
78
 
@@ -92,7 +91,8 @@ module Html2rss
92
91
  # @param page_response [RequestService::Response] response used for selector extraction context
93
92
  # @return [Hash] Hash of attributes for the article.
94
93
  def extract_article(item, page_response = response)
95
- @rss_item_attributes.to_h { |key| [key, select(key, item, base_url: page_response.url)] }.compact
94
+ scope = item_scope_for(item, page_response.url)
95
+ @rss_item_attributes.to_h { |key| [key, scope.select(key)] }.compact
96
96
  end
97
97
 
98
98
  ##
@@ -105,13 +105,13 @@ module Html2rss
105
105
  # @return [Hash] The enhanced article hash.
106
106
  # rubocop:disable Metrics/MethodLength
107
107
  def enhance_article_hash(article_hash, article_tag, base_url = @url)
108
- selected_anchor = HtmlExtractor.main_anchor_for(article_tag)
109
- extracted = HtmlExtractor.new(
108
+ selected_anchor = Html2rss::Html::Navigator.main_anchor_for(article_tag)
109
+ extracted = Html2rss::Html::ArticleExtractor.call(
110
110
  article_tag,
111
111
  base_url:,
112
112
  selected_anchor:,
113
113
  fallback_anchorless: true
114
- ).call
114
+ )
115
115
  return article_hash unless extracted
116
116
 
117
117
  extracted.each_with_object(article_hash) do |(key, value), hash|
@@ -131,6 +131,18 @@ module Html2rss
131
131
  # @return [Object, Array<Object>] The selected value(s).
132
132
  # @raise [InvalidSelectorName] If the attribute name is invalid or not defined.
133
133
  def select(name, item, base_url: @url)
134
+ select_in_scope(name, item_scope_for(item, base_url))
135
+ end
136
+
137
+ ##
138
+ # Selects the value for a given attribute within an existing {ItemScope}.
139
+ # Used by {ItemScope#select} so nested selects reuse one scope per extraction pass.
140
+ #
141
+ # @param name [Symbol, String] Name of the attribute.
142
+ # @param scope [ItemScope] Per-item extraction scope.
143
+ # @return [Object, Array<Object>] The selected value(s).
144
+ # @raise [InvalidSelectorName] If the attribute name is invalid or not defined.
145
+ def select_in_scope(name, scope)
134
146
  name = name.to_sym
135
147
 
136
148
  raise InvalidSelectorName, "Attribute selector '#{name}' is reserved for items." if name == ITEMS_SELECTOR_KEY
@@ -138,9 +150,9 @@ module Html2rss
138
150
  selector_key, config = selector_config_for(name)
139
151
 
140
152
  if SPECIAL_ATTRIBUTES.member?(selector_key)
141
- select_special(selector_key, item:, config:, base_url:)
153
+ select_special(selector_key, scope:, config:)
142
154
  else
143
- select_regular(selector_key, item:, config:, base_url:)
155
+ select_regular(selector_key, scope:, config:)
144
156
  end
145
157
  end
146
158
 
@@ -148,10 +160,18 @@ module Html2rss
148
160
 
149
161
  attr_reader :response
150
162
 
163
+ def item_scope_for(item, base_url)
164
+ ItemScope.new(
165
+ item:,
166
+ base_url:,
167
+ scraper: self,
168
+ channel: channel_context(base_url)
169
+ )
170
+ end
171
+
151
172
  def prepare_selectors!
152
173
  validate_url_and_link_exclusivity!
153
174
  fix_url_and_link!
154
- handle_renamed_attributes!
155
175
  end
156
176
 
157
177
  def validate_url_and_link_exclusivity!
@@ -167,17 +187,6 @@ module Html2rss
167
187
  @selectors[:url] = @selectors[:link]
168
188
  end
169
189
 
170
- def handle_renamed_attributes!
171
- RENAMED_ATTRIBUTES.each_pair do |new_name, old_names|
172
- old_names.each do |old_name|
173
- next unless @selectors.key?(old_name)
174
-
175
- Html2rss::Log.warn("Selector '#{old_name}' is deprecated. Please rename to '#{new_name}'.")
176
- @selectors[new_name] ||= @selectors.delete(old_name)
177
- end
178
- end
179
- end
180
-
181
190
  def parsed_body
182
191
  parsed_body_for(response)
183
192
  end
@@ -192,64 +201,58 @@ module Html2rss
192
201
  end
193
202
  end
194
203
 
195
- def select_special(name, item:, config:, base_url:)
204
+ def select_special(name, scope:, config:)
196
205
  case name
197
206
  when :enclosure
198
- enclosure(item:, config:, base_url:)
207
+ enclosure(scope:, config:)
199
208
  when :guid
200
- Array(config).map { |selector_name| select(selector_name, item, base_url:) }
209
+ Array(config).map { |selector_name| scope.select(selector_name) }
201
210
  when :categories
202
- select_categories(category_selectors: config, item:, base_url:)
211
+ select_categories(category_selectors: config, scope:)
203
212
  end
204
213
  end
205
214
 
206
- def select_regular(_name, item:, config:, base_url:)
215
+ def select_regular(_name, scope:, config:)
207
216
  @merged_configs ||= {}
208
- merged_config = @merged_configs[[config.object_id, base_url]] ||=
209
- config.merge(channel: channel_context(base_url)).freeze
210
- value = Extractors.get(merged_config, item)
217
+ merged_config = @merged_configs[[config.object_id, scope.base_url]] ||=
218
+ config.merge(channel: scope.channel).freeze
219
+ value = Extractors.get(merged_config, scope.item)
211
220
 
212
221
  if value && (post_process_steps = config[:post_process])
213
222
  steps = post_process_steps.is_a?(Array) ? post_process_steps : [post_process_steps]
214
- value = post_process(item, value, steps, base_url:)
223
+ value = post_process(scope, value, steps)
215
224
  end
216
225
 
217
226
  value
218
227
  end
219
228
 
220
- def post_process(item, value, post_process_steps, base_url:)
221
- pp_context = channel_post_process_context(base_url)
229
+ def post_process(scope, value, post_process_steps)
222
230
  post_process_steps.each do |options|
223
- context = Context.new(config: pp_context,
224
- item:, scraper: self, options:)
225
-
226
- value = PostProcessors.get(options[:name], value, context)
231
+ value = PostProcessors.get(options[:name], value, scope.context_for(options:))
227
232
  end
228
233
 
229
234
  value
230
235
  end
231
236
 
232
- def select_categories(category_selectors:, item:, base_url:)
237
+ def select_categories(category_selectors:, scope:)
233
238
  Array(category_selectors).flat_map do |selector_name|
234
- extract_category_values(selector_name, item:, base_url:)
239
+ extract_category_values(selector_name, scope:)
235
240
  end
236
241
  end
237
242
 
238
- def extract_category_values(selector_name, item:, base_url:)
243
+ def extract_category_values(selector_name, scope:)
239
244
  selector_key, config = selector_config_for(selector_name, allow_nil: true)
240
245
  return [] unless config
241
246
 
242
- nodes = extract_nodes(item:, config:)
243
- unless node_set_with_multiple_elements?(nodes)
244
- return Array(select_regular(selector_key, item:, config:, base_url:))
245
- end
247
+ nodes = extract_nodes(item: scope.item, config:)
248
+ return Array(select_regular(selector_key, scope:, config:)) unless node_set_with_multiple_elements?(nodes)
246
249
 
247
- Array(nodes).flat_map { |node| extract_categories_from_node(node, item:, config:, base_url:) }
250
+ Array(nodes).flat_map { |node| extract_categories_from_node(node, scope:, config:) }
248
251
  end
249
252
 
250
- def extract_categories_from_node(node, item:, config:, base_url:)
251
- values = Extractors.get(category_node_options(config, base_url:), node)
252
- values = apply_post_process_steps(item:, value: values, post_process_steps: config[:post_process], base_url:)
253
+ def extract_categories_from_node(node, scope:, config:)
254
+ values = Extractors.get(category_node_options(config, scope:), node)
255
+ values = apply_post_process_steps(scope:, value: values, post_process_steps: config[:post_process])
253
256
 
254
257
  Array(values).filter_map { |category| extract_category_text(category) }
255
258
  end
@@ -257,7 +260,7 @@ module Html2rss
257
260
  def extract_category_text(category)
258
261
  text = case category
259
262
  when Nokogiri::XML::Node, Nokogiri::XML::NodeSet
260
- HtmlExtractor.extract_visible_text(category)
263
+ Html2rss::Html::Navigator.extract_visible_text(category)
261
264
  else
262
265
  category&.to_s
263
266
  end
@@ -270,19 +273,19 @@ module Html2rss
270
273
  nodes.is_a?(Nokogiri::XML::NodeSet) && nodes.length > 1
271
274
  end
272
275
 
273
- def category_node_options(selector_config, base_url:)
276
+ def category_node_options(selector_config, scope:)
274
277
  @category_node_configs ||= {}
275
- @category_node_configs[[selector_config.object_id, base_url]] ||= selector_config.merge(
276
- channel: channel_context(base_url),
278
+ @category_node_configs[[selector_config.object_id, scope.base_url]] ||= selector_config.merge(
279
+ channel: scope.channel,
277
280
  selector: nil
278
281
  ).freeze
279
282
  end
280
283
 
281
- def apply_post_process_steps(item:, value:, post_process_steps:, base_url:)
284
+ def apply_post_process_steps(scope:, value:, post_process_steps:)
282
285
  return value unless value && post_process_steps
283
286
 
284
287
  steps = post_process_steps.is_a?(Array) ? post_process_steps : [post_process_steps]
285
- post_process(item, value, steps, base_url:)
288
+ post_process(scope, value, steps)
286
289
  end
287
290
 
288
291
  def selector_config_for(name, allow_nil: false)
@@ -305,14 +308,9 @@ module Html2rss
305
308
  @channel_contexts[base_url] ||= { url: base_url, time_zone: @time_zone }.freeze
306
309
  end
307
310
 
308
- def channel_post_process_context(base_url)
309
- @channel_pp_contexts ||= {}
310
- @channel_pp_contexts[base_url] ||= { channel: channel_context(base_url) }.freeze
311
- end
312
-
313
311
  # @return [Hash] enclosure details.
314
- def enclosure(item:, config:, base_url:)
315
- url = Url.from_relative(select_regular(:enclosure, item:, config:, base_url:), base_url)
312
+ def enclosure(scope:, config:)
313
+ url = Url.from_relative(select_regular(:enclosure, scope:, config:), scope.base_url)
316
314
 
317
315
  { url:, type: config[:content_type] }
318
316
  end
@@ -1,9 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- ##
4
- # The Html2rss namespace.
5
3
  module Html2rss
6
4
  # Current application version.
7
- VERSION = '0.22.2'
5
+ VERSION = '0.23.0'
8
6
  public_constant :VERSION
9
7
  end
data/lib/html2rss.rb CHANGED
@@ -8,7 +8,7 @@ loader.setup
8
8
 
9
9
  require 'logger'
10
10
  require 'forwardable'
11
- require 'html2rss/configuration'
11
+ require 'html2rss/defaults'
12
12
 
13
13
  ##
14
14
  # The Html2rss namespace.
@@ -61,14 +61,14 @@ module Html2rss
61
61
  # @param strategy [Symbol] request strategy to use
62
62
  # @param items_selector [String, nil] optional selector hint for item extraction
63
63
  # @param max_redirects [Integer, nil] optional redirect limit override
64
- # @param max_requests [Integer, nil] optional request budget override
64
+ # @param max_requests [Integer] optional request budget override (default: 4 for sitemap sub-fetches)
65
65
  # @param local_file_path [String, nil] optional local HTML file path
66
66
  # @return [RSS::Rss] generated RSS feed
67
67
  def self.auto_source(url,
68
68
  strategy: :auto,
69
69
  items_selector: nil,
70
70
  max_redirects: nil,
71
- max_requests: nil,
71
+ max_requests: 4,
72
72
  local_file_path: nil)
73
73
  feed(build_auto_source_config(url:, strategy:, items_selector:, max_redirects:, max_requests:, local_file_path:))
74
74
  end
@@ -80,14 +80,14 @@ module Html2rss
80
80
  # @param strategy [Symbol] request strategy to use
81
81
  # @param items_selector [String, nil] optional selector hint for item extraction
82
82
  # @param max_redirects [Integer, nil] optional redirect limit override
83
- # @param max_requests [Integer, nil] optional request budget override
83
+ # @param max_requests [Integer] optional request budget override (default: 4 for sitemap sub-fetches)
84
84
  # @param local_file_path [String, nil] optional local HTML file path
85
85
  # @return [Hash] JSONFeed-compliant hash
86
86
  def self.auto_json_feed(url,
87
87
  strategy: :auto,
88
88
  items_selector: nil,
89
89
  max_redirects: nil,
90
- max_requests: nil,
90
+ max_requests: 4,
91
91
  local_file_path: nil)
92
92
  json_feed(build_auto_source_config(url:, strategy:, items_selector:, max_redirects:, max_requests:,
93
93
  local_file_path:))
@@ -98,26 +98,26 @@ module Html2rss
98
98
  # rubocop:disable ThreadSafety/ClassInstanceVariable
99
99
  class << self
100
100
  ##
101
- # @return [Html2rss::Configuration] the global configuration instance
102
- def configuration
103
- @configuration ||= Configuration.new.freeze
101
+ # @return [Html2rss::Defaults] the global defaults instance
102
+ def defaults
103
+ @defaults ||= Defaults.new.freeze
104
104
  end
105
105
 
106
106
  ##
107
107
  # Configures global library defaults.
108
108
  #
109
- # @yieldparam config [Html2rss::Configuration]
110
- # @return [Html2rss::Configuration] the frozen configuration
109
+ # @yieldparam config [Html2rss::Defaults]
110
+ # @return [Html2rss::Defaults] the frozen defaults
111
111
  def configure
112
- config = configuration.dup
112
+ config = defaults.dup
113
113
  yield config
114
- @configuration = config.freeze
114
+ @defaults = config.freeze
115
115
  end
116
116
 
117
117
  ##
118
118
  # @return [Object] the logger
119
119
  def logger
120
- configuration.logger
120
+ defaults.logger
121
121
  end
122
122
 
123
123
  ##
@@ -129,12 +129,12 @@ module Html2rss
129
129
  private
130
130
 
131
131
  ##
132
- # Resets the global configuration to defaults (mainly for testing).
132
+ # Resets the global defaults (mainly for testing).
133
133
  #
134
134
  # @return [void]
135
- def reset_configuration!
136
- @configuration = nil
137
- logger.level = configuration.log_level if logger.respond_to?(:level=)
135
+ def reset_defaults!
136
+ @defaults = nil
137
+ logger.level = defaults.log_level if logger.respond_to?(:level=)
138
138
  end
139
139
  end
140
140
  # rubocop:enable ThreadSafety/ClassInstanceVariable
@@ -156,7 +156,7 @@ module Html2rss
156
156
  end
157
157
 
158
158
  def shortcut_request_controls(strategy:, max_redirects:, max_requests:)
159
- RequestControls.new(
159
+ Config::RequestControls.new(
160
160
  strategy:,
161
161
  max_redirects:,
162
162
  max_requests:,
@@ -173,7 +173,7 @@ module Html2rss
173
173
  end
174
174
  end
175
175
 
176
- logger.level = configuration.log_level if logger.respond_to?(:level=)
176
+ logger.level = defaults.log_level if logger.respond_to?(:level=)
177
177
  end
178
178
 
179
179
  loader.eager_load
@@ -109,6 +109,31 @@
109
109
  },
110
110
  "required": []
111
111
  },
112
+ "sitemap": {
113
+ "type": "object",
114
+ "properties": {
115
+ "enabled": {
116
+ "type": "boolean",
117
+ "not": {
118
+ "type": "null"
119
+ }
120
+ },
121
+ "min_priority": {
122
+ "type": "number",
123
+ "not": {
124
+ "type": "null"
125
+ }
126
+ },
127
+ "max_age_days": {
128
+ "type": "integer",
129
+ "not": {
130
+ "type": "null"
131
+ },
132
+ "exclusiveMinimum": 0
133
+ }
134
+ },
135
+ "required": []
136
+ },
112
137
  "schema": {
113
138
  "type": "object",
114
139
  "properties": {
@@ -133,6 +158,18 @@
133
158
  },
134
159
  "required": []
135
160
  },
161
+ "microformats2": {
162
+ "type": "object",
163
+ "properties": {
164
+ "enabled": {
165
+ "type": "boolean",
166
+ "not": {
167
+ "type": "null"
168
+ }
169
+ }
170
+ },
171
+ "required": []
172
+ },
136
173
  "json_state": {
137
174
  "type": "object",
138
175
  "properties": {
@@ -145,6 +182,18 @@
145
182
  },
146
183
  "required": []
147
184
  },
185
+ "meta_oembed": {
186
+ "type": "object",
187
+ "properties": {
188
+ "enabled": {
189
+ "type": "boolean",
190
+ "not": {
191
+ "type": "null"
192
+ }
193
+ }
194
+ },
195
+ "required": []
196
+ },
148
197
  "semantic_html": {
149
198
  "type": "object",
150
199
  "properties": {
@@ -224,15 +273,26 @@
224
273
  "wordpress_api": {
225
274
  "enabled": true
226
275
  },
276
+ "sitemap": {
277
+ "enabled": true,
278
+ "min_priority": 0.3,
279
+ "max_age_days": 30
280
+ },
227
281
  "schema": {
228
282
  "enabled": true
229
283
  },
230
284
  "microdata": {
231
285
  "enabled": true
232
286
  },
287
+ "microformats2": {
288
+ "enabled": true
289
+ },
233
290
  "json_state": {
234
291
  "enabled": true
235
292
  },
293
+ "meta_oembed": {
294
+ "enabled": true
295
+ },
236
296
  "semantic_html": {
237
297
  "enabled": true,
238
298
  "fallback_anchorless": true
@@ -277,18 +337,58 @@
277
337
  }
278
338
  },
279
339
  "pagination": {
280
- "type": "object",
281
- "properties": {
282
- "max_pages": {
340
+ "description": "Pagination configuration or maximum page count integer.",
341
+ "oneOf": [
342
+ {
283
343
  "type": "integer",
284
- "not": {
285
- "type": "null"
286
- },
287
344
  "exclusiveMinimum": 0
345
+ },
346
+ {
347
+ "type": "object",
348
+ "properties": {
349
+ "strategy": {
350
+ "type": "string",
351
+ "enum": [
352
+ "rel_next",
353
+ "custom_selector",
354
+ "url_template",
355
+ "offset",
356
+ "json_cursor"
357
+ ]
358
+ },
359
+ "max_pages": {
360
+ "type": "integer",
361
+ "exclusiveMinimum": 0
362
+ },
363
+ "selector": {
364
+ "type": "string"
365
+ },
366
+ "param": {
367
+ "type": "string"
368
+ },
369
+ "start_page": {
370
+ "type": "integer"
371
+ },
372
+ "step": {
373
+ "type": "integer",
374
+ "exclusiveMinimum": 0
375
+ },
376
+ "start_offset": {
377
+ "type": "integer"
378
+ },
379
+ "increment": {
380
+ "type": "integer",
381
+ "exclusiveMinimum": 0
382
+ },
383
+ "cursor_path": {
384
+ "type": "string"
385
+ },
386
+ "next_url_path": {
387
+ "type": "string"
388
+ }
389
+ },
390
+ "additionalProperties": true
288
391
  }
289
- },
290
- "required": [
291
- "max_pages"
292
392
  ]
293
393
  }
294
394
  },