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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: html2rss
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.22.2
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gil Desmarais
@@ -273,63 +273,87 @@ files:
273
273
  - exe/html2rss
274
274
  - html2rss.gemspec
275
275
  - lib/html2rss.rb
276
- - lib/html2rss/articles/deduplicator.rb
276
+ - lib/html2rss/article.rb
277
+ - lib/html2rss/article/deduplicator.rb
278
+ - lib/html2rss/article/enclosure.rb
277
279
  - lib/html2rss/auto_source.rb
278
280
  - lib/html2rss/auto_source/cleanup.rb
281
+ - lib/html2rss/auto_source/discovery.rb
282
+ - lib/html2rss/auto_source/discovery/dom_clustering.rb
283
+ - lib/html2rss/auto_source/discovery/dom_clustering/group_scorer.rb
284
+ - lib/html2rss/auto_source/discovery/dom_clustering/overlap_resolver.rb
285
+ - lib/html2rss/auto_source/discovery/list_candidates.rb
286
+ - lib/html2rss/auto_source/discovery/semantic_anchor_candidates.rb
287
+ - lib/html2rss/auto_source/discovery/semantic_containers.rb
288
+ - lib/html2rss/auto_source/discovery/sitemap.rb
289
+ - lib/html2rss/auto_source/link_heuristics.rb
290
+ - lib/html2rss/auto_source/link_heuristics/anchor_signals.rb
291
+ - lib/html2rss/auto_source/link_heuristics/container_assessor.rb
292
+ - lib/html2rss/auto_source/link_heuristics/container_signals.rb
293
+ - lib/html2rss/auto_source/link_heuristics/destination_facts.rb
294
+ - lib/html2rss/auto_source/link_heuristics/href_extractor.rb
295
+ - lib/html2rss/auto_source/link_heuristics/path_classifier.rb
296
+ - lib/html2rss/auto_source/link_heuristics/text_classifier.rb
279
297
  - lib/html2rss/auto_source/scraper.rb
280
298
  - lib/html2rss/auto_source/scraper/html.rb
281
- - lib/html2rss/auto_source/scraper/html/class_clustering.rb
282
299
  - lib/html2rss/auto_source/scraper/json_state.rb
283
- - lib/html2rss/auto_source/scraper/link_heuristics.rb
300
+ - lib/html2rss/auto_source/scraper/meta_oembed.rb
284
301
  - lib/html2rss/auto_source/scraper/microdata.rb
302
+ - lib/html2rss/auto_source/scraper/microformats2.rb
285
303
  - lib/html2rss/auto_source/scraper/schema.rb
286
304
  - lib/html2rss/auto_source/scraper/schema/category_extractor.rb
287
305
  - lib/html2rss/auto_source/scraper/schema/item_list.rb
288
306
  - lib/html2rss/auto_source/scraper/schema/list_item.rb
289
307
  - lib/html2rss/auto_source/scraper/schema/thing.rb
290
308
  - lib/html2rss/auto_source/scraper/semantic_html.rb
291
- - lib/html2rss/auto_source/scraper/semantic_html/anchor_selector.rb
292
- - lib/html2rss/auto_source/scraper/semantic_html/deduplicator.rb
309
+ - lib/html2rss/auto_source/scraper/semantic_html/entry_deduplicator.rb
310
+ - lib/html2rss/auto_source/scraper/sitemap.rb
293
311
  - lib/html2rss/auto_source/scraper/wordpress_api.rb
294
312
  - lib/html2rss/auto_source/scraper/wordpress_api/page_scope.rb
313
+ - lib/html2rss/auto_source/scraper/wordpress_api/page_scope/date_archive_range.rb
295
314
  - lib/html2rss/auto_source/scraper/wordpress_api/posts_endpoint.rb
296
- - lib/html2rss/blocked_surface.rb
297
- - lib/html2rss/category_extractor.rb
315
+ - lib/html2rss/channel.rb
298
316
  - lib/html2rss/cli.rb
299
317
  - lib/html2rss/config.rb
300
- - lib/html2rss/config/class_methods.rb
318
+ - lib/html2rss/config/auto_source_contract.rb
301
319
  - lib/html2rss/config/dynamic_params.rb
302
320
  - lib/html2rss/config/multiple_feeds_config.rb
321
+ - lib/html2rss/config/request_controls.rb
303
322
  - lib/html2rss/config/request_headers.rb
304
323
  - lib/html2rss/config/schema.rb
324
+ - lib/html2rss/config/selectors_validator.rb
305
325
  - lib/html2rss/config/validator.rb
306
- - lib/html2rss/configuration.rb
326
+ - lib/html2rss/defaults.rb
307
327
  - lib/html2rss/error.rb
328
+ - lib/html2rss/feed_builder.rb
329
+ - lib/html2rss/feed_builder/json_feed.rb
330
+ - lib/html2rss/feed_builder/json_feed/item.rb
331
+ - lib/html2rss/feed_builder/rss.rb
332
+ - lib/html2rss/feed_builder/rss/stylesheet.rb
308
333
  - lib/html2rss/feed_pipeline.rb
309
334
  - lib/html2rss/feed_pipeline/auto_fallback.rb
335
+ - lib/html2rss/feed_pipeline/runtime_policy.rb
336
+ - lib/html2rss/feed_pipeline/strategy_plan.rb
310
337
  - lib/html2rss/hash_util.rb
311
- - lib/html2rss/html_extractor.rb
312
- - lib/html2rss/html_extractor/date_extractor.rb
313
- - lib/html2rss/html_extractor/enclosure_extractor.rb
314
- - lib/html2rss/html_extractor/heading_extractor.rb
315
- - lib/html2rss/html_extractor/id_generator.rb
316
- - lib/html2rss/html_extractor/image_extractor.rb
317
- - lib/html2rss/html_extractor/list_candidates.rb
318
- - lib/html2rss/html_extractor/semantic_anchor_candidates.rb
319
- - lib/html2rss/html_extractor/semantic_containers.rb
320
- - lib/html2rss/html_extractor/text_extractor.rb
321
- - lib/html2rss/html_navigator.rb
322
- - lib/html2rss/json_feed_builder.rb
323
- - lib/html2rss/json_feed_builder/item.rb
324
- - lib/html2rss/rendering.rb
325
- - lib/html2rss/rendering/audio_renderer.rb
326
- - lib/html2rss/rendering/description_builder.rb
327
- - lib/html2rss/rendering/image_renderer.rb
328
- - lib/html2rss/rendering/media_renderer.rb
329
- - lib/html2rss/rendering/pdf_renderer.rb
330
- - lib/html2rss/rendering/video_renderer.rb
331
- - lib/html2rss/request_controls.rb
338
+ - lib/html2rss/html.rb
339
+ - lib/html2rss/html/article_extractor.rb
340
+ - lib/html2rss/html/article_extractor/category_extractor.rb
341
+ - lib/html2rss/html/article_extractor/date_extractor.rb
342
+ - lib/html2rss/html/article_extractor/enclosure_extractor.rb
343
+ - lib/html2rss/html/article_extractor/heading_extractor.rb
344
+ - lib/html2rss/html/article_extractor/id_generator.rb
345
+ - lib/html2rss/html/article_extractor/image_extractor.rb
346
+ - lib/html2rss/html/navigator.rb
347
+ - lib/html2rss/html/navigator/text_extractor.rb
348
+ - lib/html2rss/html/rendering.rb
349
+ - lib/html2rss/html/rendering/audio_renderer.rb
350
+ - lib/html2rss/html/rendering/description_builder.rb
351
+ - lib/html2rss/html/rendering/image_renderer.rb
352
+ - lib/html2rss/html/rendering/media_renderer.rb
353
+ - lib/html2rss/html/rendering/pdf_renderer.rb
354
+ - lib/html2rss/html/rendering/video_renderer.rb
332
355
  - lib/html2rss/request_service.rb
356
+ - lib/html2rss/request_service/blocked_surface.rb
333
357
  - lib/html2rss/request_service/botasaurus_contract.rb
334
358
  - lib/html2rss/request_service/botasaurus_strategy.rb
335
359
  - lib/html2rss/request_service/browserless_strategy.rb
@@ -337,28 +361,30 @@ files:
337
361
  - lib/html2rss/request_service/context.rb
338
362
  - lib/html2rss/request_service/faraday_strategy.rb
339
363
  - lib/html2rss/request_service/local_file_strategy.rb
364
+ - lib/html2rss/request_service/network_guard.rb
340
365
  - lib/html2rss/request_service/policy.rb
341
366
  - lib/html2rss/request_service/puppet_commander.rb
367
+ - lib/html2rss/request_service/puppet_commander/navigation_guards.rb
368
+ - lib/html2rss/request_service/puppet_commander/preload_runner.rb
342
369
  - lib/html2rss/request_service/response.rb
343
370
  - lib/html2rss/request_service/response_guard.rb
344
371
  - lib/html2rss/request_service/strategy.rb
345
372
  - lib/html2rss/request_session.rb
346
- - lib/html2rss/request_session/rel_next_pager.rb
347
- - lib/html2rss/request_session/runtime_input.rb
348
- - lib/html2rss/request_session/runtime_policy.rb
349
- - lib/html2rss/rss_builder.rb
350
- - lib/html2rss/rss_builder/article.rb
351
- - lib/html2rss/rss_builder/channel.rb
352
- - lib/html2rss/rss_builder/enclosure.rb
353
- - lib/html2rss/rss_builder/stylesheet.rb
373
+ - lib/html2rss/request_session/pager.rb
374
+ - lib/html2rss/request_session/pager/base.rb
375
+ - lib/html2rss/request_session/pager/custom_selector.rb
376
+ - lib/html2rss/request_session/pager/json_cursor.rb
377
+ - lib/html2rss/request_session/pager/offset.rb
378
+ - lib/html2rss/request_session/pager/rel_next.rb
379
+ - lib/html2rss/request_session/pager/url_template.rb
354
380
  - lib/html2rss/selectors.rb
355
- - lib/html2rss/selectors/config.rb
356
381
  - lib/html2rss/selectors/extractors.rb
357
382
  - lib/html2rss/selectors/extractors/attribute.rb
358
383
  - lib/html2rss/selectors/extractors/href.rb
359
384
  - lib/html2rss/selectors/extractors/html.rb
360
385
  - lib/html2rss/selectors/extractors/static.rb
361
386
  - lib/html2rss/selectors/extractors/text.rb
387
+ - lib/html2rss/selectors/item_scope.rb
362
388
  - lib/html2rss/selectors/object_to_xml_converter.rb
363
389
  - lib/html2rss/selectors/post_processors.rb
364
390
  - lib/html2rss/selectors/post_processors/base.rb
@@ -381,7 +407,7 @@ licenses:
381
407
  - MIT
382
408
  metadata:
383
409
  allowed_push_host: https://rubygems.org
384
- changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.22.2
410
+ changelog_uri: https://github.com/html2rss/html2rss/releases/tag/v0.23.0
385
411
  rubygems_mfa_required: 'true'
386
412
  rdoc_options: []
387
413
  require_paths:
@@ -397,7 +423,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
397
423
  - !ruby/object:Gem::Version
398
424
  version: '0'
399
425
  requirements: []
400
- rubygems_version: 4.0.10
426
+ rubygems_version: 4.0.16
401
427
  specification_version: 4
402
428
  summary: Generates RSS feeds from websites by scraping a URL and using CSS selectors
403
429
  to extract item.
@@ -1,196 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Html2rss
4
- class AutoSource
5
- module Scraper
6
- class Html
7
- ##
8
- # ClassClustering clusters DOM elements on anchorless pages by class lists and scores
9
- # candidate groups to find the best list of content cards/articles.
10
- # rubocop:disable Metrics/ClassLength
11
- class ClassClustering
12
- # Node tags considered layout containers
13
- LAYOUT_TAG_NAMES = Set['div', 'section', 'article', 'li', 'ul', 'ol'].freeze
14
- # HTML/layout tags excluded from candidate nodes
15
- EXCLUDED_TAGS = Set['html', 'body', 'nav', 'footer', 'header', 'svg', 'script', 'style'].freeze
16
-
17
- class << self
18
- ##
19
- # Clusters elements in parsed_body and returns the best set of content card nodes.
20
- #
21
- # @param parsed_body [Nokogiri::HTML::Document] parsed HTML document
22
- # @param minimum_selector_frequency [Integer] minimum frequency for class groups
23
- # @return [Array<Nokogiri::XML::Node>] candidate nodes of the top-scoring class group
24
- def call(parsed_body, minimum_selector_frequency:)
25
- new(parsed_body, minimum_selector_frequency:).call
26
- end
27
- end
28
-
29
- # @param parsed_body [Nokogiri::HTML::Document]
30
- # @param minimum_selector_frequency [Integer]
31
- def initialize(parsed_body, minimum_selector_frequency:)
32
- @parsed_body = parsed_body
33
- @minimum_frequency = minimum_selector_frequency
34
- @text_words = {}.compare_by_identity
35
- @has_date = {}.compare_by_identity
36
- end
37
-
38
- # @return [Array<Nokogiri::XML::Node>]
39
- def call
40
- candidate_groups = collect_candidate_groups
41
- return [] if candidate_groups.empty?
42
-
43
- non_containers = filter_containers(candidate_groups)
44
- final_groups = filter_1_to_1_overlap(non_containers)
45
-
46
- select_best_group(final_groups)
47
- end
48
-
49
- private
50
-
51
- def collect_candidate_groups
52
- class_groups = Hash.new { |h, k| h[k] = [] }
53
- cache = {}.compare_by_identity
54
-
55
- @parsed_body.css('[class]').each { |node| add_node_to_groups(node, class_groups, cache) }
56
-
57
- class_groups.select { |_, nodes| nodes.size >= @minimum_frequency }
58
- end
59
-
60
- def add_node_to_groups(node, class_groups, cache)
61
- return if EXCLUDED_TAGS.include?(node.name) || HtmlExtractor.ignored_container_path?(node, cache)
62
-
63
- cls = normalize_class(node['class'])
64
- class_groups[cls] << node unless cls.empty?
65
- end
66
-
67
- def normalize_class(class_attr)
68
- class_str = class_attr.to_s.strip
69
- return '' if class_str.empty?
70
-
71
- # Bypass split/sort/join allocation for single-class lists
72
- if class_str.include?(' ')
73
- class_str.split(/\s+/).sort.join(' ')
74
- else
75
- class_str
76
- end
77
- end
78
-
79
- # Discard group A if any node of A contains > 1 node of another group B
80
- def filter_containers(groups)
81
- groups.reject do |cls_a, nodes_a|
82
- groups.any? { |cls_b, nodes_b| cls_a != cls_b && container_of?(nodes_a, nodes_b) }
83
- end
84
- end
85
-
86
- # rubocop:disable Metrics/MethodLength
87
- def container_of?(nodes_a, nodes_b)
88
- return false unless LAYOUT_TAG_NAMES.include?(nodes_b.first.name)
89
-
90
- nodes_a.any? do |node_a|
91
- count = 0
92
- nodes_b.each do |node_b|
93
- next if node_a == node_b
94
-
95
- if HtmlNavigator.descendant_of?(node_b, node_a)
96
- count += 1
97
- break if count > 1
98
- end
99
- end
100
- count > 1
101
- end
102
- end
103
- # rubocop:enable Metrics/MethodLength
104
-
105
- # If group A contains group B, and they have the same size:
106
- # - If B (the descendant) contains >= 80% of A's words, AND B's tag is div/section/article,
107
- # B is the actual content card. Discard A.
108
- # - Otherwise, B is a sub-element (header, metadata line, button). Discard B.
109
- def filter_1_to_1_overlap(groups)
110
- discarded = Set.new
111
- groups.each_key do |cls_a|
112
- groups.each_key do |cls_b|
113
- next if cls_a == cls_b || discarded.include?(cls_a) || discarded.include?(cls_b)
114
-
115
- resolve_1_to_1_overlap(cls_a, cls_b, groups, discarded)
116
- end
117
- end
118
-
119
- groups.except(*discarded)
120
- end
121
-
122
- def resolve_1_to_1_overlap(cls_a, cls_b, groups, discarded)
123
- nodes_a = groups[cls_a]
124
- nodes_b = groups[cls_b]
125
- return if nodes_a.size != nodes_b.size
126
- return unless nodes_a.zip(nodes_b).all? { |a, b| a != b && HtmlNavigator.descendant_of?(b, a) }
127
-
128
- discarded << (keep_descendant?(nodes_a, nodes_b) ? cls_a : cls_b)
129
- end
130
-
131
- def keep_descendant?(nodes_a, nodes_b)
132
- avg_words(nodes_b) >= 0.8 * avg_words(nodes_a) &&
133
- LAYOUT_TAG_NAMES.include?(nodes_b.first.name)
134
- end
135
-
136
- def select_best_group(groups)
137
- best_nodes = []
138
- best_score = -1
139
-
140
- groups.each_value do |nodes|
141
- score = score_group(nodes)
142
- next if score.negative?
143
-
144
- (best_nodes = nodes) && (best_score = score) if score > best_score
145
- end
146
-
147
- best_nodes
148
- end
149
-
150
- def score_group(nodes)
151
- avg_w = avg_words(nodes)
152
- return -1 if avg_w < 5
153
-
154
- score = nodes.size + (avg_w / 5.0)
155
- score += 20 if nodes_heading?(nodes)
156
- score += 20 if nodes_time?(nodes)
157
- score += 40 if nodes_date?(nodes)
158
- score
159
- end
160
-
161
- def nodes_heading?(nodes)
162
- nodes.any? do |n|
163
- n.at_css(HtmlExtractor::HEADING_TAGS.join(',')) ||
164
- n.at_css('.font-bold, .font-semibold')
165
- end
166
- end
167
-
168
- def nodes_time?(nodes)
169
- nodes.any? { |n| n.at_css('time, [datetime]') }
170
- end
171
-
172
- def nodes_date?(nodes)
173
- nodes.any? { |n| date?(n) }
174
- end
175
-
176
- def avg_words(nodes)
177
- nodes.sum { |n| text_words(n) } / nodes.size.to_f
178
- end
179
-
180
- def text_words(node)
181
- @text_words[node] ||= HtmlExtractor.extract_visible_text(node).to_s.scan(/\p{Alnum}+/).size
182
- end
183
-
184
- def date?(node)
185
- @has_date[node] ||= begin
186
- text = HtmlExtractor.extract_visible_text(node).to_s
187
- text.match?(%r{\b\d{4}[-/]\d{2}[-/]\d{2}\b}) ||
188
- text.match?(/\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\b/i)
189
- end
190
- end
191
- # rubocop:enable Metrics/ClassLength
192
- end
193
- end
194
- end
195
- end
196
- end