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.
- checksums.yaml +4 -4
- data/README.md +11 -108
- data/lib/html2rss/{articles → article}/deduplicator.rb +3 -4
- data/lib/html2rss/{rss_builder → article}/enclosure.rb +6 -22
- data/lib/html2rss/article.rb +206 -0
- data/lib/html2rss/auto_source/discovery/dom_clustering/group_scorer.rb +80 -0
- data/lib/html2rss/auto_source/discovery/dom_clustering/overlap_resolver.rb +86 -0
- data/lib/html2rss/auto_source/discovery/dom_clustering.rb +119 -0
- data/lib/html2rss/auto_source/discovery/list_candidates.rb +94 -0
- data/lib/html2rss/auto_source/discovery/semantic_anchor_candidates.rb +219 -0
- data/lib/html2rss/auto_source/discovery/semantic_containers.rb +71 -0
- data/lib/html2rss/auto_source/discovery/sitemap.rb +159 -0
- data/lib/html2rss/auto_source/discovery.rb +14 -0
- data/lib/html2rss/auto_source/link_heuristics/anchor_signals.rb +28 -0
- data/lib/html2rss/auto_source/link_heuristics/container_assessor.rb +106 -0
- data/lib/html2rss/auto_source/link_heuristics/container_signals.rb +80 -0
- data/lib/html2rss/auto_source/link_heuristics/destination_facts.rb +42 -0
- data/lib/html2rss/auto_source/link_heuristics/href_extractor.rb +38 -0
- data/lib/html2rss/auto_source/link_heuristics/path_classifier.rb +221 -0
- data/lib/html2rss/auto_source/link_heuristics/text_classifier.rb +66 -0
- data/lib/html2rss/auto_source/link_heuristics.rb +139 -0
- data/lib/html2rss/auto_source/scraper/html.rb +22 -33
- data/lib/html2rss/auto_source/scraper/json_state.rb +1 -1
- data/lib/html2rss/auto_source/scraper/meta_oembed.rb +163 -0
- data/lib/html2rss/auto_source/scraper/microformats2.rb +146 -0
- data/lib/html2rss/auto_source/scraper/schema/thing.rb +21 -2
- data/lib/html2rss/auto_source/scraper/semantic_html/{deduplicator.rb → entry_deduplicator.rb} +3 -3
- data/lib/html2rss/auto_source/scraper/semantic_html.rb +23 -171
- data/lib/html2rss/auto_source/scraper/sitemap.rb +153 -0
- data/lib/html2rss/auto_source/scraper/wordpress_api/page_scope/date_archive_range.rb +110 -0
- data/lib/html2rss/auto_source/scraper/wordpress_api/page_scope.rb +2 -78
- data/lib/html2rss/auto_source/scraper/wordpress_api.rb +9 -0
- data/lib/html2rss/auto_source/scraper.rb +10 -3
- data/lib/html2rss/auto_source.rb +30 -35
- data/lib/html2rss/channel.rb +118 -0
- data/lib/html2rss/cli.rb +5 -6
- data/lib/html2rss/config/auto_source_contract.rb +51 -0
- data/lib/html2rss/config/request_controls.rb +136 -0
- data/lib/html2rss/config/request_headers.rb +1 -1
- data/lib/html2rss/config/schema.rb +65 -114
- data/lib/html2rss/{selectors/config.rb → config/selectors_validator.rb} +56 -7
- data/lib/html2rss/config/validator.rb +6 -6
- data/lib/html2rss/config.rb +176 -19
- data/lib/html2rss/{configuration.rb → defaults.rb} +12 -10
- data/lib/html2rss/feed_builder/json_feed/item.rb +96 -0
- data/lib/html2rss/feed_builder/json_feed.rb +61 -0
- data/lib/html2rss/feed_builder/rss/stylesheet.rb +65 -0
- data/lib/html2rss/feed_builder/rss.rb +116 -0
- data/lib/html2rss/feed_builder.rb +23 -0
- data/lib/html2rss/feed_pipeline/auto_fallback.rb +59 -42
- data/lib/html2rss/feed_pipeline/runtime_policy.rb +108 -0
- data/lib/html2rss/feed_pipeline/strategy_plan.rb +64 -0
- data/lib/html2rss/feed_pipeline.rb +52 -65
- data/lib/html2rss/html/article_extractor/category_extractor.rb +123 -0
- data/lib/html2rss/html/article_extractor/date_extractor.rb +22 -0
- data/lib/html2rss/html/article_extractor/enclosure_extractor.rb +95 -0
- data/lib/html2rss/html/article_extractor/heading_extractor.rb +54 -0
- data/lib/html2rss/html/article_extractor/id_generator.rb +69 -0
- data/lib/html2rss/html/article_extractor/image_extractor.rb +60 -0
- data/lib/html2rss/html/article_extractor.rb +134 -0
- data/lib/html2rss/html/navigator/text_extractor.rb +79 -0
- data/lib/html2rss/html/navigator.rb +146 -0
- data/lib/html2rss/html/rendering/audio_renderer.rb +38 -0
- data/lib/html2rss/html/rendering/description_builder.rb +89 -0
- data/lib/html2rss/html/rendering/image_renderer.rb +43 -0
- data/lib/html2rss/html/rendering/media_renderer.rb +39 -0
- data/lib/html2rss/html/rendering/pdf_renderer.rb +36 -0
- data/lib/html2rss/html/rendering/video_renderer.rb +38 -0
- data/lib/html2rss/html/rendering.rb +25 -0
- data/lib/html2rss/html.rb +8 -0
- data/lib/html2rss/request_service/blocked_surface.rb +67 -0
- data/lib/html2rss/request_service/botasaurus_contract.rb +0 -4
- data/lib/html2rss/request_service/botasaurus_strategy.rb +9 -27
- data/lib/html2rss/request_service/browserless_strategy.rb +2 -23
- data/lib/html2rss/request_service/budget.rb +90 -16
- data/lib/html2rss/request_service/context.rb +1 -3
- data/lib/html2rss/request_service/faraday_strategy.rb +12 -26
- data/lib/html2rss/request_service/local_file_strategy.rb +12 -4
- data/lib/html2rss/request_service/network_guard.rb +125 -0
- data/lib/html2rss/request_service/policy.rb +10 -82
- data/lib/html2rss/request_service/puppet_commander/navigation_guards.rb +148 -0
- data/lib/html2rss/request_service/puppet_commander/preload_runner.rb +86 -0
- data/lib/html2rss/request_service/puppet_commander.rb +22 -191
- data/lib/html2rss/request_service/response_guard.rb +1 -1
- data/lib/html2rss/request_service/strategy.rb +102 -6
- data/lib/html2rss/request_service.rb +7 -2
- data/lib/html2rss/request_session/pager/base.rb +83 -0
- data/lib/html2rss/request_session/pager/custom_selector.rb +39 -0
- data/lib/html2rss/request_session/pager/json_cursor.rb +78 -0
- data/lib/html2rss/request_session/pager/offset.rb +48 -0
- data/lib/html2rss/request_session/pager/rel_next.rb +23 -0
- data/lib/html2rss/request_session/pager/url_template.rb +48 -0
- data/lib/html2rss/request_session/pager.rb +75 -0
- data/lib/html2rss/request_session.rb +25 -16
- data/lib/html2rss/selectors/item_scope.rb +30 -0
- data/lib/html2rss/selectors/post_processors/base.rb +1 -1
- data/lib/html2rss/selectors/post_processors/markdown_to_html.rb +0 -1
- data/lib/html2rss/selectors/post_processors/sanitize_html.rb +0 -2
- data/lib/html2rss/selectors/post_processors/template.rb +7 -3
- data/lib/html2rss/selectors.rb +63 -65
- data/lib/html2rss/version.rb +1 -3
- data/lib/html2rss.rb +19 -19
- data/schema/html2rss-config.schema.json +109 -9
- metadata +68 -42
- data/lib/html2rss/auto_source/scraper/html/class_clustering.rb +0 -196
- data/lib/html2rss/auto_source/scraper/link_heuristics.rb +0 -401
- data/lib/html2rss/auto_source/scraper/semantic_html/anchor_selector.rb +0 -49
- data/lib/html2rss/blocked_surface.rb +0 -65
- data/lib/html2rss/category_extractor.rb +0 -116
- data/lib/html2rss/config/class_methods.rb +0 -183
- data/lib/html2rss/html_extractor/date_extractor.rb +0 -20
- data/lib/html2rss/html_extractor/enclosure_extractor.rb +0 -91
- data/lib/html2rss/html_extractor/heading_extractor.rb +0 -50
- data/lib/html2rss/html_extractor/id_generator.rb +0 -67
- data/lib/html2rss/html_extractor/image_extractor.rb +0 -58
- data/lib/html2rss/html_extractor/list_candidates.rb +0 -88
- data/lib/html2rss/html_extractor/semantic_anchor_candidates.rb +0 -260
- data/lib/html2rss/html_extractor/semantic_containers.rb +0 -69
- data/lib/html2rss/html_extractor/text_extractor.rb +0 -77
- data/lib/html2rss/html_extractor.rb +0 -190
- data/lib/html2rss/html_navigator.rb +0 -71
- data/lib/html2rss/json_feed_builder/item.rb +0 -94
- data/lib/html2rss/json_feed_builder.rb +0 -59
- data/lib/html2rss/rendering/audio_renderer.rb +0 -36
- data/lib/html2rss/rendering/description_builder.rb +0 -87
- data/lib/html2rss/rendering/image_renderer.rb +0 -41
- data/lib/html2rss/rendering/media_renderer.rb +0 -37
- data/lib/html2rss/rendering/pdf_renderer.rb +0 -34
- data/lib/html2rss/rendering/video_renderer.rb +0 -36
- data/lib/html2rss/rendering.rb +0 -23
- data/lib/html2rss/request_controls.rb +0 -133
- data/lib/html2rss/request_session/rel_next_pager.rb +0 -70
- data/lib/html2rss/request_session/runtime_input.rb +0 -71
- data/lib/html2rss/request_session/runtime_policy.rb +0 -84
- data/lib/html2rss/rss_builder/article.rb +0 -208
- data/lib/html2rss/rss_builder/channel.rb +0 -118
- data/lib/html2rss/rss_builder/stylesheet.rb +0 -63
- 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
|
|
74
|
+
return value if key == :self
|
|
75
|
+
|
|
76
|
+
@context.item_scope.select(key)
|
|
73
77
|
end
|
|
74
78
|
end
|
|
75
79
|
end
|
data/lib/html2rss/selectors.rb
CHANGED
|
@@ -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
|
-
|
|
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::
|
|
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::
|
|
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::
|
|
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::
|
|
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
|
-
|
|
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 =
|
|
109
|
-
extracted =
|
|
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
|
-
)
|
|
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,
|
|
153
|
+
select_special(selector_key, scope:, config:)
|
|
142
154
|
else
|
|
143
|
-
select_regular(selector_key,
|
|
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,
|
|
204
|
+
def select_special(name, scope:, config:)
|
|
196
205
|
case name
|
|
197
206
|
when :enclosure
|
|
198
|
-
enclosure(
|
|
207
|
+
enclosure(scope:, config:)
|
|
199
208
|
when :guid
|
|
200
|
-
Array(config).map { |selector_name| select(selector_name
|
|
209
|
+
Array(config).map { |selector_name| scope.select(selector_name) }
|
|
201
210
|
when :categories
|
|
202
|
-
select_categories(category_selectors: config,
|
|
211
|
+
select_categories(category_selectors: config, scope:)
|
|
203
212
|
end
|
|
204
213
|
end
|
|
205
214
|
|
|
206
|
-
def select_regular(_name,
|
|
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:
|
|
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(
|
|
223
|
+
value = post_process(scope, value, steps)
|
|
215
224
|
end
|
|
216
225
|
|
|
217
226
|
value
|
|
218
227
|
end
|
|
219
228
|
|
|
220
|
-
def post_process(
|
|
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
|
-
|
|
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:,
|
|
237
|
+
def select_categories(category_selectors:, scope:)
|
|
233
238
|
Array(category_selectors).flat_map do |selector_name|
|
|
234
|
-
extract_category_values(selector_name,
|
|
239
|
+
extract_category_values(selector_name, scope:)
|
|
235
240
|
end
|
|
236
241
|
end
|
|
237
242
|
|
|
238
|
-
def extract_category_values(selector_name,
|
|
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
|
|
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,
|
|
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,
|
|
251
|
-
values = Extractors.get(category_node_options(config,
|
|
252
|
-
values = apply_post_process_steps(
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
315
|
-
url = Url.from_relative(select_regular(:enclosure,
|
|
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
|
data/lib/html2rss/version.rb
CHANGED
data/lib/html2rss.rb
CHANGED
|
@@ -8,7 +8,7 @@ loader.setup
|
|
|
8
8
|
|
|
9
9
|
require 'logger'
|
|
10
10
|
require 'forwardable'
|
|
11
|
-
require 'html2rss/
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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::
|
|
102
|
-
def
|
|
103
|
-
@
|
|
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::
|
|
110
|
-
# @return [Html2rss::
|
|
109
|
+
# @yieldparam config [Html2rss::Defaults]
|
|
110
|
+
# @return [Html2rss::Defaults] the frozen defaults
|
|
111
111
|
def configure
|
|
112
|
-
config =
|
|
112
|
+
config = defaults.dup
|
|
113
113
|
yield config
|
|
114
|
-
@
|
|
114
|
+
@defaults = config.freeze
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
##
|
|
118
118
|
# @return [Object] the logger
|
|
119
119
|
def logger
|
|
120
|
-
|
|
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
|
|
132
|
+
# Resets the global defaults (mainly for testing).
|
|
133
133
|
#
|
|
134
134
|
# @return [void]
|
|
135
|
-
def
|
|
136
|
-
@
|
|
137
|
-
logger.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 =
|
|
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
|
-
"
|
|
281
|
-
"
|
|
282
|
-
|
|
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
|
},
|