html2rss 0.20.1 → 0.22.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/html2rss.gemspec +1 -2
- data/lib/html2rss/auto_source/scraper/html/class_clustering.rb +185 -0
- data/lib/html2rss/auto_source/scraper/html.rb +73 -16
- data/lib/html2rss/auto_source/scraper/json_state.rb +41 -28
- data/lib/html2rss/auto_source/scraper/link_heuristics.rb +85 -131
- data/lib/html2rss/auto_source/scraper/microdata.rb +2 -2
- data/lib/html2rss/auto_source/scraper/schema/category_extractor.rb +74 -28
- data/lib/html2rss/auto_source/scraper/schema/list_item.rb +3 -2
- data/lib/html2rss/auto_source/scraper/schema/thing.rb +31 -60
- data/lib/html2rss/auto_source/scraper/schema.rb +8 -2
- data/lib/html2rss/auto_source/scraper/semantic_html/deduplicator.rb +4 -18
- data/lib/html2rss/auto_source/scraper/semantic_html.rb +55 -11
- data/lib/html2rss/auto_source/scraper.rb +0 -3
- data/lib/html2rss/auto_source.rb +5 -12
- data/lib/html2rss/category_extractor.rb +54 -20
- data/lib/html2rss/cli.rb +61 -10
- data/lib/html2rss/config/class_methods.rb +3 -3
- data/lib/html2rss/config/validator.rb +1 -0
- data/lib/html2rss/config.rb +2 -2
- data/lib/html2rss/html_extractor/enclosure_extractor.rb +60 -89
- data/lib/html2rss/html_extractor/heading_extractor.rb +50 -0
- data/lib/html2rss/html_extractor/id_generator.rb +67 -0
- data/lib/html2rss/html_extractor/list_candidates.rb +2 -8
- data/lib/html2rss/html_extractor/semantic_anchor_candidates.rb +29 -12
- data/lib/html2rss/html_extractor/semantic_containers.rb +9 -35
- data/lib/html2rss/html_extractor/text_extractor.rb +77 -0
- data/lib/html2rss/html_extractor.rb +80 -61
- data/lib/html2rss/rendering/description_builder.rb +3 -3
- data/lib/html2rss/rendering.rb +2 -2
- data/lib/html2rss/request_service/local_file_strategy.rb +29 -0
- data/lib/html2rss/request_service/puppet_commander.rb +4 -0
- data/lib/html2rss/request_service.rb +2 -1
- data/lib/html2rss/rss_builder/article.rb +44 -23
- data/lib/html2rss/rss_builder/enclosure.rb +4 -2
- data/lib/html2rss/selectors/extractors/attribute.rb +1 -1
- data/lib/html2rss/selectors/extractors/href.rb +1 -1
- data/lib/html2rss/selectors/extractors/html.rb +1 -1
- data/lib/html2rss/selectors/extractors/static.rb +1 -1
- data/lib/html2rss/selectors/extractors/text.rb +1 -1
- data/lib/html2rss/selectors/post_processors/sanitize_html.rb +32 -36
- data/lib/html2rss/selectors/post_processors/substring.rb +11 -18
- data/lib/html2rss/selectors/post_processors/template.rb +3 -2
- data/lib/html2rss/selectors.rb +19 -5
- data/lib/html2rss/url.rb +29 -15
- data/lib/html2rss/version.rb +1 -1
- data/lib/html2rss.rb +28 -6
- data/schema/html2rss-config.schema.json +12 -1
- metadata +8 -17
|
@@ -24,19 +24,30 @@ module Html2rss
|
|
|
24
24
|
) do
|
|
25
25
|
# @param url [Html2rss::Url] normalized destination URL
|
|
26
26
|
# @return [DestinationFacts] route facts for downstream link scoring
|
|
27
|
-
def self.build(url)
|
|
27
|
+
def self.build(url) # rubocop:disable Metrics/MethodLength
|
|
28
28
|
classifier = PathClassifier.new(url.path_segments)
|
|
29
29
|
|
|
30
30
|
new(
|
|
31
31
|
url:,
|
|
32
32
|
destination: url.to_s,
|
|
33
|
-
|
|
33
|
+
segments: classifier.segments,
|
|
34
|
+
strong_post_suffix: classifier.strong_post_suffix?,
|
|
35
|
+
content_path: classifier.content_path?,
|
|
36
|
+
utility_path: classifier.utility_path?,
|
|
37
|
+
taxonomy_path: classifier.taxonomy_path?,
|
|
38
|
+
vanity_path: classifier.vanity_path?,
|
|
39
|
+
shallow: classifier.shallow?,
|
|
40
|
+
high_confidence_junk_path: classifier.junk_path?,
|
|
41
|
+
high_confidence_utility_destination: classifier.utility_destination?
|
|
34
42
|
)
|
|
35
43
|
end
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
# Extracts a normalized href from a Nokogiri anchor or raw href value.
|
|
39
47
|
class HrefExtractor
|
|
48
|
+
# Regexp to capture everything before the first '#'
|
|
49
|
+
HREF_BASE_PATTERN = /\A([^#]*)/
|
|
50
|
+
|
|
40
51
|
# @param anchor_or_href [Nokogiri::XML::Element, String, #to_s] anchor element or href-like value
|
|
41
52
|
# @return [String, nil] href without fragment, or nil when blank
|
|
42
53
|
def self.call(anchor_or_href) = new(anchor_or_href).call
|
|
@@ -48,20 +59,18 @@ module Html2rss
|
|
|
48
59
|
|
|
49
60
|
# @return [String, nil] href without fragment, or nil when blank
|
|
50
61
|
def call
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
href = case @anchor_or_href
|
|
63
|
+
when Nokogiri::XML::Node
|
|
64
|
+
@anchor_or_href['href']
|
|
65
|
+
else
|
|
66
|
+
@anchor_or_href
|
|
67
|
+
end
|
|
55
68
|
|
|
56
|
-
|
|
69
|
+
return unless href
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
@anchor_or_href['href']
|
|
62
|
-
else
|
|
63
|
-
@anchor_or_href
|
|
64
|
-
end
|
|
71
|
+
# Extract base part before # and strip whitespace
|
|
72
|
+
base = href.to_s[HREF_BASE_PATTERN, 1].strip
|
|
73
|
+
base unless base.empty?
|
|
65
74
|
end
|
|
66
75
|
end
|
|
67
76
|
|
|
@@ -125,8 +134,7 @@ module Html2rss
|
|
|
125
134
|
end
|
|
126
135
|
|
|
127
136
|
# Classifies normalized destination path segments for scoring.
|
|
128
|
-
# rubocop:disable Metrics/ClassLength
|
|
129
|
-
class PathClassifier
|
|
137
|
+
class PathClassifier # rubocop:disable Metrics/ClassLength
|
|
130
138
|
attr_reader :segments
|
|
131
139
|
|
|
132
140
|
# Segment groups used to classify article, taxonomy, utility, and vanity routes.
|
|
@@ -206,48 +214,25 @@ module Html2rss
|
|
|
206
214
|
@segments = segments
|
|
207
215
|
end
|
|
208
216
|
|
|
209
|
-
# @return [Hash] destination attributes consumed by DestinationFacts
|
|
210
|
-
def destination_attributes
|
|
211
|
-
route_attributes.merge(confidence_attributes)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# @return [Hash] baseline path classification attributes
|
|
215
|
-
def route_attributes
|
|
216
|
-
{
|
|
217
|
-
segments:,
|
|
218
|
-
content_path: content_path?,
|
|
219
|
-
utility_path: utility_path?,
|
|
220
|
-
taxonomy_path: taxonomy_path?,
|
|
221
|
-
vanity_path: vanity_path?,
|
|
222
|
-
shallow: shallow?,
|
|
223
|
-
strong_post_suffix: strong_post_suffix?
|
|
224
|
-
}
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
# @return [Hash] high-confidence noise classification attributes
|
|
228
|
-
def confidence_attributes
|
|
229
|
-
ConfidenceClassifier.new(self).attributes
|
|
230
|
-
end
|
|
231
|
-
|
|
232
217
|
# @return [Boolean] true when the route has article-like path evidence
|
|
233
218
|
def content_path?
|
|
234
|
-
@content_path ||= SEGMENT_SETS
|
|
219
|
+
@content_path ||= segments.any? { |s| SEGMENT_SETS[:content].include?(s) } ||
|
|
235
220
|
yearish_content_context?
|
|
236
221
|
end
|
|
237
222
|
|
|
238
223
|
# @return [Boolean] true when the route includes utility/navigation evidence
|
|
239
224
|
def utility_path?
|
|
240
|
-
@utility_path ||= SEGMENT_SETS
|
|
225
|
+
@utility_path ||= segments.any? { |s| SEGMENT_SETS[:utility].include?(s) }
|
|
241
226
|
end
|
|
242
227
|
|
|
243
228
|
# @return [Boolean] true when the route points at conversion or account chrome
|
|
244
229
|
def vanity_path?
|
|
245
|
-
@vanity_path ||= SEGMENT_SETS
|
|
230
|
+
@vanity_path ||= segments.any? { |s| SEGMENT_SETS[:vanity].include?(s) }
|
|
246
231
|
end
|
|
247
232
|
|
|
248
233
|
# @return [Boolean] true when the route points at taxonomy/listing chrome
|
|
249
234
|
def taxonomy_path?
|
|
250
|
-
@taxonomy_path ||= SEGMENT_SETS
|
|
235
|
+
@taxonomy_path ||= segments.any? { |s| SEGMENT_SETS[:taxonomy].include?(s) }
|
|
251
236
|
end
|
|
252
237
|
|
|
253
238
|
# @return [Boolean] true when the route is too shallow to strongly indicate an article
|
|
@@ -260,7 +245,9 @@ module Html2rss
|
|
|
260
245
|
|
|
261
246
|
# @return [Boolean] true when the final path segment looks like a post slug
|
|
262
247
|
def strong_post_suffix?
|
|
263
|
-
|
|
248
|
+
@strong_post_suffix ||= segments.any? &&
|
|
249
|
+
included_last_segment? &&
|
|
250
|
+
trusted_post_context?(segments.size - 1)
|
|
264
251
|
end
|
|
265
252
|
|
|
266
253
|
# @return [Boolean] true when every path segment is utility chrome
|
|
@@ -282,131 +269,81 @@ module Html2rss
|
|
|
282
269
|
|
|
283
270
|
# @return [Boolean] true when the leading segments are all utility chrome
|
|
284
271
|
def deep_utility_context_route?
|
|
285
|
-
|
|
272
|
+
all_junk?(segments.size - 1)
|
|
286
273
|
end
|
|
287
274
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def yearish_content_context?
|
|
291
|
-
segments.any? { |segment| segment.match?(YEARISH_SEGMENT) } &&
|
|
292
|
-
(strong_post_suffix? || LeadingSegments.new(segments).trusted_post_context?)
|
|
293
|
-
end
|
|
294
|
-
end
|
|
295
|
-
# rubocop:enable Metrics/ClassLength
|
|
296
|
-
|
|
297
|
-
# Classifies high-confidence junk and utility routes from path facts.
|
|
298
|
-
class ConfidenceClassifier
|
|
299
|
-
# @param path [PathClassifier] classified destination path
|
|
300
|
-
def initialize(path)
|
|
301
|
-
@path = path
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
# @return [Hash] high-confidence route classification attributes
|
|
305
|
-
def attributes
|
|
306
|
-
{
|
|
307
|
-
high_confidence_junk_path: junk_path?,
|
|
308
|
-
high_confidence_utility_destination: utility_destination?
|
|
309
|
-
}
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
private
|
|
313
|
-
|
|
275
|
+
# @return [Boolean] true when the route is shallow and contains high-confidence noise
|
|
314
276
|
def junk_path?
|
|
315
277
|
return false if excluded_content_route?
|
|
316
278
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
279
|
+
taxonomy_path? ||
|
|
280
|
+
utility_only_route? ||
|
|
281
|
+
deep_utility_context_route? ||
|
|
282
|
+
shallow_high_confidence_route?
|
|
321
283
|
end
|
|
322
284
|
|
|
285
|
+
# @return [Boolean] true when the route points at conversion or account chrome
|
|
323
286
|
def utility_destination?
|
|
324
287
|
return false if excluded_content_route?
|
|
325
288
|
|
|
326
|
-
|
|
289
|
+
vanity_path? || utility_route?
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
private
|
|
293
|
+
|
|
294
|
+
def yearish_content_context?
|
|
295
|
+
segments.any? { |segment| segment.match?(YEARISH_SEGMENT) } &&
|
|
296
|
+
(strong_post_suffix? || trusted_post_context?(segments.size - 1))
|
|
327
297
|
end
|
|
328
298
|
|
|
329
299
|
def excluded_content_route?
|
|
330
|
-
|
|
300
|
+
segments.empty? || content_path? || strong_post_suffix?
|
|
331
301
|
end
|
|
332
302
|
|
|
333
303
|
def utility_route?
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
304
|
+
taxonomy_path? ||
|
|
305
|
+
utility_only_route? ||
|
|
306
|
+
deep_utility_context_route? ||
|
|
337
307
|
shallow_utility_route?
|
|
338
308
|
end
|
|
339
309
|
|
|
340
310
|
def shallow_utility_route?
|
|
341
|
-
|
|
342
|
-
end
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
# Classifies route context before the final segment.
|
|
346
|
-
class LeadingSegments
|
|
347
|
-
# @param segments [Array<String>] normalized URL path segments
|
|
348
|
-
def initialize(segments)
|
|
349
|
-
@segments = segments[0...-1]
|
|
311
|
+
shallow? && utility_path?
|
|
350
312
|
end
|
|
351
313
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
junk_segments = PathClassifier::SEGMENT_SETS.fetch(:high_confidence_junk)
|
|
314
|
+
def all_junk?(limit)
|
|
315
|
+
return false if limit <= 0
|
|
355
316
|
|
|
356
|
-
|
|
317
|
+
junk_segments = SEGMENT_SETS.fetch(:high_confidence_junk)
|
|
318
|
+
(0...limit).all? { |i| junk_segments.include?(segments[i]) }
|
|
357
319
|
end
|
|
358
320
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
321
|
+
def trusted_post_context?(limit)
|
|
322
|
+
return false if limit <= 0
|
|
323
|
+
|
|
324
|
+
content_segments = SEGMENT_SETS.fetch(:content)
|
|
325
|
+
context_segments = SEGMENT_SETS.fetch(:deep_post_context)
|
|
363
326
|
|
|
364
|
-
|
|
327
|
+
(0...limit).any? do |i|
|
|
328
|
+
segment = segments[i]
|
|
365
329
|
content_segments.include?(segment) ||
|
|
366
330
|
segment.match?(PathClassifier::YEARISH_SEGMENT) ||
|
|
367
331
|
context_segments.include?(segment)
|
|
368
332
|
end
|
|
369
333
|
end
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
# Classifies whether the final segment is a strong post-like suffix.
|
|
373
|
-
class PostSuffixClassifier
|
|
374
|
-
# @param segments [Array<String>] normalized URL path segments
|
|
375
|
-
def initialize(segments)
|
|
376
|
-
@segments = segments
|
|
377
|
-
end
|
|
378
|
-
|
|
379
|
-
# @return [Boolean] true when the final path segment looks like a post slug
|
|
380
|
-
def strong?
|
|
381
|
-
@segments.any? &&
|
|
382
|
-
included_last_segment? &&
|
|
383
|
-
LeadingSegments.new(@segments).trusted_post_context?
|
|
384
|
-
end
|
|
385
|
-
|
|
386
|
-
private
|
|
387
334
|
|
|
388
335
|
def included_last_segment?
|
|
389
336
|
!excluded_last_segment? && slug_last_segment?
|
|
390
337
|
end
|
|
391
338
|
|
|
392
339
|
def excluded_last_segment?
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
def excluded_segments
|
|
397
|
-
[
|
|
398
|
-
PathClassifier::SEGMENT_SETS.fetch(:high_confidence_junk),
|
|
399
|
-
PathClassifier::SEGMENT_SETS.fetch(:vanity)
|
|
400
|
-
]
|
|
340
|
+
last = segments.last
|
|
341
|
+
[SEGMENT_SETS[:high_confidence_junk], SEGMENT_SETS[:vanity]].any? { |set| set.include?(last) }
|
|
401
342
|
end
|
|
402
343
|
|
|
403
344
|
def slug_last_segment?
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
def last_segment
|
|
409
|
-
@segments.last
|
|
345
|
+
last = segments.last
|
|
346
|
+
last.match?(YEARISH_SEGMENT) || last.match?(POST_SLUG_SEGMENT)
|
|
410
347
|
end
|
|
411
348
|
end
|
|
412
349
|
|
|
@@ -421,11 +358,15 @@ module Html2rss
|
|
|
421
358
|
# @param anchor_or_href [Nokogiri::XML::Element, String, #to_s] anchor element or href-like value
|
|
422
359
|
# @return [DestinationFacts, nil] normalized destination facts, or nil for blank/invalid URLs
|
|
423
360
|
def destination_facts(anchor_or_href)
|
|
361
|
+
return node_facts[anchor_or_href] if node_facts.key?(anchor_or_href)
|
|
362
|
+
|
|
424
363
|
href = HrefExtractor.call(anchor_or_href)
|
|
425
364
|
return unless href
|
|
426
365
|
|
|
427
|
-
|
|
428
|
-
|
|
366
|
+
res = memoized_destination_facts(href)
|
|
367
|
+
|
|
368
|
+
node_facts[anchor_or_href] = res if anchor_or_href.is_a?(Nokogiri::XML::Node)
|
|
369
|
+
res
|
|
429
370
|
rescue ArgumentError
|
|
430
371
|
nil
|
|
431
372
|
end
|
|
@@ -441,6 +382,19 @@ module Html2rss
|
|
|
441
382
|
# @param text [String, #to_s] visible anchor text
|
|
442
383
|
# @return [Boolean] true when text identifies recommendation chrome
|
|
443
384
|
def recommended_text?(text) = @text_classifier.recommended?(text)
|
|
385
|
+
|
|
386
|
+
private
|
|
387
|
+
|
|
388
|
+
def node_facts
|
|
389
|
+
@node_facts ||= {}.compare_by_identity
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def memoized_destination_facts(href)
|
|
393
|
+
(@destination_facts ||= {})[href] ||= begin
|
|
394
|
+
url = Html2rss::Url.from_relative(href, @base_url)
|
|
395
|
+
DestinationFacts.build(url)
|
|
396
|
+
end
|
|
397
|
+
end
|
|
444
398
|
end
|
|
445
399
|
end
|
|
446
400
|
end
|
|
@@ -92,7 +92,7 @@ module Html2rss
|
|
|
92
92
|
attr_reader :parsed_body, :url
|
|
93
93
|
|
|
94
94
|
# @param root [Nokogiri::XML::Element] supported Microdata root node
|
|
95
|
-
# @return [Hash{Symbol => Object
|
|
95
|
+
# @return [Hash{Symbol => Object, nil}] normalized article hash
|
|
96
96
|
def article_from(root)
|
|
97
97
|
schema_object = SchemaObjectBuilder.call(root)
|
|
98
98
|
return unless schema_object
|
|
@@ -378,7 +378,7 @@ module Html2rss
|
|
|
378
378
|
extend ValueNormalizer
|
|
379
379
|
|
|
380
380
|
# @param root [Nokogiri::XML::Element] supported microdata root node
|
|
381
|
-
# @return [Hash{Symbol => Object
|
|
381
|
+
# @return [Hash{Symbol => Object, nil}] compact schema-like object
|
|
382
382
|
def call(root)
|
|
383
383
|
type = Microdata.supported_type_name(root)
|
|
384
384
|
return unless type
|
|
@@ -13,11 +13,10 @@ module Html2rss
|
|
|
13
13
|
# @param schema_object [Hash] The schema object
|
|
14
14
|
# @return [Array<String>] Array of category strings
|
|
15
15
|
def self.call(schema_object)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
(field_categories | about_categories).to_a
|
|
16
|
+
Set.new.tap do |categories|
|
|
17
|
+
extract_field_categories!(categories, schema_object)
|
|
18
|
+
extract_about_categories!(categories, schema_object)
|
|
19
|
+
end.to_a
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
##
|
|
@@ -26,10 +25,18 @@ module Html2rss
|
|
|
26
25
|
# @param schema_object [Hash] The schema object
|
|
27
26
|
# @return [Set<String>] Set of category strings
|
|
28
27
|
def self.extract_field_categories(schema_object)
|
|
29
|
-
Set.new.tap
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
Set.new.tap { |categories| extract_field_categories!(categories, schema_object) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# Extracts categories from keywords, categories, and tags fields.
|
|
33
|
+
#
|
|
34
|
+
# @param categories [Set<String>] Accumulator set
|
|
35
|
+
# @param schema_object [Hash] The schema object
|
|
36
|
+
# @return [void]
|
|
37
|
+
def self.extract_field_categories!(categories, schema_object)
|
|
38
|
+
%i[keywords categories tags].each do |field|
|
|
39
|
+
extract_field_value!(categories, schema_object[field])
|
|
33
40
|
end
|
|
34
41
|
end
|
|
35
42
|
|
|
@@ -39,15 +46,23 @@ module Html2rss
|
|
|
39
46
|
# @param schema_object [Hash] The schema object
|
|
40
47
|
# @return [Set<String>] Set of category strings
|
|
41
48
|
def self.extract_about_categories(schema_object)
|
|
49
|
+
Set.new.tap { |categories| extract_about_categories!(categories, schema_object) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
##
|
|
53
|
+
# Extracts categories from the about field.
|
|
54
|
+
#
|
|
55
|
+
# @param categories [Set<String>] Accumulator set
|
|
56
|
+
# @param schema_object [Hash] The schema object
|
|
57
|
+
# @return [void]
|
|
58
|
+
def self.extract_about_categories!(categories, schema_object)
|
|
42
59
|
about = schema_object[:about]
|
|
43
|
-
return
|
|
60
|
+
return unless about
|
|
44
61
|
|
|
45
62
|
if about.is_a?(Array)
|
|
46
|
-
extract_about_array(about)
|
|
63
|
+
extract_about_array!(categories, about)
|
|
47
64
|
elsif about.is_a?(String)
|
|
48
|
-
extract_string_categories(about)
|
|
49
|
-
else
|
|
50
|
-
Set.new
|
|
65
|
+
extract_string_categories!(categories, about)
|
|
51
66
|
end
|
|
52
67
|
end
|
|
53
68
|
|
|
@@ -58,15 +73,25 @@ module Html2rss
|
|
|
58
73
|
# @param field [String] The field name
|
|
59
74
|
# @return [Set<String>] Set of category strings
|
|
60
75
|
def self.extract_field_value(schema_object, field)
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
Set.new.tap { |categories| extract_field_value!(categories, schema_object[field.to_sym]) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# Extracts categories from a single field value.
|
|
81
|
+
#
|
|
82
|
+
# @param categories [Set<String>] Accumulator set
|
|
83
|
+
# @param value [Object] The field value
|
|
84
|
+
# @return [void]
|
|
85
|
+
def self.extract_field_value!(categories, value)
|
|
86
|
+
return unless value
|
|
63
87
|
|
|
64
88
|
if value.is_a?(Array)
|
|
65
|
-
|
|
89
|
+
value.each do |item|
|
|
90
|
+
s = item.to_s
|
|
91
|
+
categories.add(s) unless s.empty?
|
|
92
|
+
end
|
|
66
93
|
elsif value.is_a?(String)
|
|
67
|
-
extract_string_categories(value)
|
|
68
|
-
else
|
|
69
|
-
Set.new
|
|
94
|
+
extract_string_categories!(categories, value)
|
|
70
95
|
end
|
|
71
96
|
end
|
|
72
97
|
|
|
@@ -76,13 +101,21 @@ module Html2rss
|
|
|
76
101
|
# @param about [Array] The about array
|
|
77
102
|
# @return [Set<String>] Set of category strings
|
|
78
103
|
def self.extract_about_array(about)
|
|
79
|
-
Set.new.tap
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
104
|
+
Set.new.tap { |categories| extract_about_array!(categories, about) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
# Extracts categories from an about array.
|
|
109
|
+
#
|
|
110
|
+
# @param categories [Set<String>] Accumulator set
|
|
111
|
+
# @param about [Array] The about array
|
|
112
|
+
# @return [void]
|
|
113
|
+
def self.extract_about_array!(categories, about)
|
|
114
|
+
about.each do |item|
|
|
115
|
+
if item.is_a?(Hash) && item[:name]
|
|
116
|
+
categories.add(item[:name].to_s)
|
|
117
|
+
elsif item.is_a?(String)
|
|
118
|
+
categories.add(item)
|
|
86
119
|
end
|
|
87
120
|
end
|
|
88
121
|
end
|
|
@@ -93,7 +126,20 @@ module Html2rss
|
|
|
93
126
|
# @param string [String] source string that may contain category delimiters
|
|
94
127
|
# @return [Set<String>] Set of category strings
|
|
95
128
|
def self.extract_string_categories(string)
|
|
96
|
-
Set.new(string
|
|
129
|
+
Set.new.tap { |categories| extract_string_categories!(categories, string) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
##
|
|
133
|
+
# Extracts categories from a string by splitting on separators.
|
|
134
|
+
#
|
|
135
|
+
# @param categories [Set<String>] Accumulator set
|
|
136
|
+
# @param string [String] source string that may contain category delimiters
|
|
137
|
+
# @return [void]
|
|
138
|
+
def self.extract_string_categories!(categories, string)
|
|
139
|
+
string.split(/[,;|]/).each do |part|
|
|
140
|
+
s = part.strip
|
|
141
|
+
categories.add(s) unless s.empty?
|
|
142
|
+
end
|
|
97
143
|
end
|
|
98
144
|
end
|
|
99
145
|
end
|
|
@@ -16,9 +16,10 @@ module Html2rss
|
|
|
16
16
|
|
|
17
17
|
# @return [Html2rss::Url, nil]
|
|
18
18
|
def url
|
|
19
|
-
url
|
|
19
|
+
return @url if defined?(@url)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
item_url = schema_object.dig(:item, :url)
|
|
22
|
+
@url = item_url ? Url.from_relative(item_url, base_url || item_url) : super
|
|
22
23
|
end
|
|
23
24
|
end
|
|
24
25
|
end
|