html2rss 0.32.3 → 0.33.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/lib/html2rss/article/enclosure.rb +2 -6
- data/lib/html2rss/auto_source/cleanup.rb +1 -3
- data/lib/html2rss/auto_source/scraper/html.rb +2 -8
- data/lib/html2rss/auto_source/scraper/json_document.rb +2 -3
- data/lib/html2rss/auto_source/scraper/json_state/article_normalizer.rb +128 -128
- data/lib/html2rss/auto_source/scraper/json_state/candidate_detector.rb +37 -37
- data/lib/html2rss/auto_source/scraper/json_state/document_scanner.rb +123 -123
- data/lib/html2rss/auto_source/scraper/json_state/value_finder.rb +34 -34
- data/lib/html2rss/auto_source/scraper/json_state.rb +2 -6
- data/lib/html2rss/auto_source/scraper/meta_oembed.rb +32 -2
- data/lib/html2rss/auto_source/scraper/microdata.rb +228 -245
- data/lib/html2rss/auto_source/scraper/microformats2.rb +9 -1
- data/lib/html2rss/auto_source/scraper/native_feed.rb +1 -1
- data/lib/html2rss/auto_source/scraper/schema/category_extractor.rb +7 -2
- data/lib/html2rss/auto_source/scraper/schema/item_list.rb +2 -2
- data/lib/html2rss/auto_source/scraper/schema/thing.rb +98 -21
- data/lib/html2rss/auto_source/scraper/schema.rb +49 -2
- data/lib/html2rss/auto_source/scraper/semantic_html.rb +12 -16
- data/lib/html2rss/auto_source/scraper/sitemap/parser.rb +1 -4
- data/lib/html2rss/auto_source/scraper/sitemap.rb +3 -11
- data/lib/html2rss/auto_source/scraper/wordpress_api/page_scope.rb +2 -2
- data/lib/html2rss/auto_source/scraper/wordpress_api/posts_endpoint.rb +1 -3
- data/lib/html2rss/auto_source/scraper/wordpress_api.rb +4 -4
- data/lib/html2rss/auto_source/scraper/xhr_articles.rb +1 -1
- data/lib/html2rss/auto_source/scraper.rb +0 -11
- data/lib/html2rss/auto_source/segmenter/cluster.rb +5 -11
- data/lib/html2rss/auto_source/segmenter/list.rb +25 -57
- data/lib/html2rss/auto_source/segmenter/semantic.rb +6 -13
- data/lib/html2rss/auto_source.rb +1 -1
- data/lib/html2rss/batch.rb +22 -32
- data/lib/html2rss/capture/README.md +1 -1
- data/lib/html2rss/capture.rb +7 -27
- data/lib/html2rss/cli/render.rb +97 -96
- data/lib/html2rss/cli/validate.rb +2 -4
- data/lib/html2rss/cli.rb +33 -27
- data/lib/html2rss/config/request_controls.rb +10 -2
- data/lib/html2rss/config/schema.rb +8 -10
- data/lib/html2rss/config/selectors_validator.rb +1 -0
- data/lib/html2rss/config/validator.rb +3 -3
- data/lib/html2rss/config.rb +1 -1
- data/lib/html2rss/defaults.rb +5 -16
- data/lib/html2rss/doctor/botasaurus.rb +11 -19
- data/lib/html2rss/error.rb +54 -1
- data/lib/html2rss/feed_builder/rss.rb +1 -1
- data/lib/html2rss/feed_pipeline/README.md +1 -1
- data/lib/html2rss/feed_pipeline/auto_fallback.rb +58 -55
- data/lib/html2rss/feed_resolution/candidate_generator.rb +3 -4
- data/lib/html2rss/feed_resolution/options.rb +3 -7
- data/lib/html2rss/feed_resolution/policy.rb +29 -33
- data/lib/html2rss/feed_resolution/probe.rb +3 -3
- data/lib/html2rss/feed_resolution/scorer.rb +29 -29
- data/lib/html2rss/feed_resolution.rb +5 -9
- data/lib/html2rss/feed_result.rb +4 -3
- data/lib/html2rss/hash_util.rb +91 -90
- data/lib/html2rss/html/article_extractor/category_extractor.rb +6 -9
- data/lib/html2rss/html/article_extractor/date_extractor.rb +1 -1
- data/lib/html2rss/html/article_extractor/enclosure_extractor.rb +1 -3
- data/lib/html2rss/html/article_extractor/id_generator.rb +1 -1
- data/lib/html2rss/html/probe.rb +51 -51
- data/lib/html2rss/link_destination/path_classifier.rb +11 -33
- data/lib/html2rss/mcp/config_argument.rb +9 -2
- data/lib/html2rss/mcp/contract.rb +7 -5
- data/lib/html2rss/mcp/outcome.rb +4 -5
- data/lib/html2rss/mcp/runtime.rb +17 -24
- data/lib/html2rss/mcp/server/tools.rb +9 -9
- data/lib/html2rss/option.rb +48 -0
- data/lib/html2rss/page_recon/diagnostics.rb +7 -9
- data/lib/html2rss/page_recon.rb +0 -4
- data/lib/html2rss/recon.rb +10 -12
- data/lib/html2rss/request_service/blocked_surface.rb +3 -7
- data/lib/html2rss/request_service/botasaurus_strategy.rb +2 -2
- data/lib/html2rss/request_service/budget.rb +3 -24
- data/lib/html2rss/request_service/compressed_body.rb +36 -48
- data/lib/html2rss/request_service/context.rb +1 -3
- data/lib/html2rss/request_service/local_file_strategy.rb +3 -1
- data/lib/html2rss/request_service/network_guard.rb +3 -2
- data/lib/html2rss/request_service/policy.rb +8 -26
- data/lib/html2rss/request_service/response.rb +3 -6
- data/lib/html2rss/request_service/strategy.rb +2 -2
- data/lib/html2rss/request_service.rb +26 -2
- data/lib/html2rss/request_session/pager/base.rb +3 -3
- data/lib/html2rss/request_session/pager/custom_selector.rb +4 -4
- data/lib/html2rss/request_session/pager/offset.rb +1 -7
- data/lib/html2rss/request_session/pager/rel_next.rb +1 -1
- data/lib/html2rss/request_session/pager/url_template.rb +1 -7
- data/lib/html2rss/scoring/engine.rb +4 -32
- data/lib/html2rss/scoring/link_resolver.rb +2 -7
- data/lib/html2rss/selectors/extractors/href.rb +2 -4
- data/lib/html2rss/selectors/json_xml.rb +33 -34
- data/lib/html2rss/selectors/post_processors/gsub.rb +2 -3
- data/lib/html2rss/selectors/post_processors/parse_time.rb +4 -5
- data/lib/html2rss/selectors/schema_export.rb +60 -60
- data/lib/html2rss/selectors.rb +14 -18
- data/lib/html2rss/sst/node.rb +9 -3
- data/lib/html2rss/sst/normalizer.rb +25 -24
- data/lib/html2rss/sst/text.rb +53 -57
- data/lib/html2rss/status.rb +23 -30
- data/lib/html2rss/syndication/alternate_links.rb +39 -0
- data/lib/html2rss/syndication/discovery.rb +117 -187
- data/lib/html2rss/syndication/parser.rb +92 -115
- data/lib/html2rss/test/enhance_audit.rb +13 -15
- data/lib/html2rss/test/policy.rb +9 -11
- data/lib/html2rss/test.rb +29 -48
- data/lib/html2rss/url.rb +53 -53
- data/lib/html2rss/version.rb +1 -1
- data/lib/html2rss.rb +2 -1
- metadata +4 -2
|
@@ -47,13 +47,13 @@ module Html2rss
|
|
|
47
47
|
allow_private_networks: DEFAULTS[:allow_private_networks],
|
|
48
48
|
allow_cross_origin_followups: DEFAULTS[:allow_cross_origin_followups],
|
|
49
49
|
resolver: Socket)
|
|
50
|
-
@connect_timeout_seconds =
|
|
51
|
-
@read_timeout_seconds =
|
|
52
|
-
@total_timeout_seconds =
|
|
53
|
-
@max_redirects =
|
|
54
|
-
@max_response_bytes =
|
|
55
|
-
@max_decompressed_bytes =
|
|
56
|
-
@max_requests = [
|
|
50
|
+
@connect_timeout_seconds = Option.integer(:connect_timeout_seconds, connect_timeout_seconds, min: 1)
|
|
51
|
+
@read_timeout_seconds = Option.integer(:read_timeout_seconds, read_timeout_seconds, min: 1)
|
|
52
|
+
@total_timeout_seconds = Option.integer(:total_timeout_seconds, total_timeout_seconds, min: 1)
|
|
53
|
+
@max_redirects = Option.integer(:max_redirects, max_redirects, min: 0)
|
|
54
|
+
@max_response_bytes = Option.integer(:max_response_bytes, max_response_bytes, min: 1)
|
|
55
|
+
@max_decompressed_bytes = Option.integer(:max_decompressed_bytes, max_decompressed_bytes, min: 1)
|
|
56
|
+
@max_requests = [Option.integer(:max_requests, max_requests, min: 1), MAX_REQUESTS_CEILING].min
|
|
57
57
|
@allow_private_networks = allow_private_networks ? true : false
|
|
58
58
|
@allow_cross_origin_followups = allow_cross_origin_followups ? true : false
|
|
59
59
|
@network_guard = NetworkGuard.new(allow_private_networks: @allow_private_networks, resolver:)
|
|
@@ -135,18 +135,6 @@ module Html2rss
|
|
|
135
135
|
|
|
136
136
|
attr_reader :network_guard
|
|
137
137
|
|
|
138
|
-
def validate_positive_integer!(name, value)
|
|
139
|
-
raise ArgumentError, "#{name} must be positive" unless value.is_a?(Integer) && value.positive?
|
|
140
|
-
|
|
141
|
-
value
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def validate_non_negative_integer!(name, value)
|
|
145
|
-
raise ArgumentError, "#{name} must be non-negative" unless value.is_a?(Integer) && !value.negative?
|
|
146
|
-
|
|
147
|
-
value
|
|
148
|
-
end
|
|
149
|
-
|
|
150
138
|
def enforce_same_origin!(url, origin_url, relation)
|
|
151
139
|
return if relation == :initial || allow_cross_origin_followups?
|
|
152
140
|
|
|
@@ -163,13 +151,7 @@ module Html2rss
|
|
|
163
151
|
end
|
|
164
152
|
|
|
165
153
|
def comparable_origin(url)
|
|
166
|
-
[url.host,
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def normalized_port(url)
|
|
170
|
-
return url.port if url.port
|
|
171
|
-
|
|
172
|
-
url.scheme == 'https' ? 443 : 80
|
|
154
|
+
[url.scheme, url.host, url.port]
|
|
173
155
|
end
|
|
174
156
|
end
|
|
175
157
|
|
|
@@ -31,15 +31,12 @@ module Html2rss
|
|
|
31
31
|
# @param status [Integer, nil] the HTTP status code when available
|
|
32
32
|
# @param transport_meta [Hash] allowlisted upstream telemetry (frozen when present)
|
|
33
33
|
# @param captured_responses [Array<Hash>] JSON XHR/fetch bodies captured during browser scrapes
|
|
34
|
-
# rubocop:disable-next Metrics/
|
|
34
|
+
# rubocop:disable-next Metrics/ParameterLists -- transport + capture fields stay co-located
|
|
35
35
|
def initialize(body:, url:, headers: {}, status: nil, transport_meta: EMPTY_TRANSPORT_META,
|
|
36
36
|
captured_responses: EMPTY_CAPTURED_RESPONSES)
|
|
37
37
|
@body = body
|
|
38
38
|
|
|
39
|
-
headers = (headers || {}).transform_keys(&:to_s)
|
|
40
|
-
HashUtil.assert_string_keys!(headers, context: 'response headers', deep: false)
|
|
41
|
-
|
|
42
|
-
@headers = headers
|
|
39
|
+
@headers = (headers || {}).transform_keys(&:to_s)
|
|
43
40
|
@status = status
|
|
44
41
|
@url = url
|
|
45
42
|
@transport_meta = transport_meta.nil? || transport_meta.empty? ? EMPTY_TRANSPORT_META : transport_meta.freeze
|
|
@@ -165,7 +162,7 @@ module Html2rss
|
|
|
165
162
|
end
|
|
166
163
|
|
|
167
164
|
def feed_looking_body?
|
|
168
|
-
snippet = body.to_s[0, 800].downcase
|
|
165
|
+
snippet = body.to_s.b[0, 800].downcase
|
|
169
166
|
FEED_BODY_MARKERS.any? { |marker| snippet.include?(marker) }
|
|
170
167
|
end
|
|
171
168
|
end
|
|
@@ -133,9 +133,9 @@ module Html2rss
|
|
|
133
133
|
|
|
134
134
|
# @param error [StandardError]
|
|
135
135
|
# @return [void]
|
|
136
|
-
# @raise [
|
|
136
|
+
# @raise [ConnectionFailed]
|
|
137
137
|
def translate_connection_error(error)
|
|
138
|
-
raise error
|
|
138
|
+
raise ConnectionFailed, error.message
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
# @param error [StandardError]
|
|
@@ -28,6 +28,8 @@ module Html2rss
|
|
|
28
28
|
class RedirectLimitReached < Html2rss::Error; end
|
|
29
29
|
# Raised when blocked content surfaces are detected.
|
|
30
30
|
class BlockedSurfaceDetected < Html2rss::Error; end
|
|
31
|
+
# Raised when the target connection fails before a response document exists.
|
|
32
|
+
class ConnectionFailed < Html2rss::Error; end
|
|
31
33
|
|
|
32
34
|
# Raised when a request times out.
|
|
33
35
|
class RequestTimedOut < Html2rss::Error
|
|
@@ -76,6 +78,12 @@ module Html2rss
|
|
|
76
78
|
# @return [Symbol]
|
|
77
79
|
DEFAULT_STRATEGY_NAME = :default
|
|
78
80
|
|
|
81
|
+
# Maximum distinct [strategy, host] deprecation warnings tracked per process.
|
|
82
|
+
MAX_DEPRECATION_WARNINGS = 256
|
|
83
|
+
# Mutex guarding process-local deprecation warning deduplication.
|
|
84
|
+
DEPRECATION_MUTEX = Mutex.new
|
|
85
|
+
private_constant :DEPRECATION_MUTEX
|
|
86
|
+
|
|
79
87
|
class << self
|
|
80
88
|
# @return [Symbol] the default strategy name
|
|
81
89
|
def default_strategy_name = DEFAULT_STRATEGY_NAME
|
|
@@ -107,6 +115,13 @@ module Html2rss
|
|
|
107
115
|
strategy_class.new(ctx).execute
|
|
108
116
|
end
|
|
109
117
|
|
|
118
|
+
##
|
|
119
|
+
# Clears process-local deprecation warning state (used by specs).
|
|
120
|
+
# @return [void]
|
|
121
|
+
def reset_deprecation_warnings!
|
|
122
|
+
DEPRECATION_MUTEX.synchronize { @warned_deprecations = Set.new }
|
|
123
|
+
end
|
|
124
|
+
|
|
110
125
|
private
|
|
111
126
|
|
|
112
127
|
##
|
|
@@ -125,14 +140,15 @@ module Html2rss
|
|
|
125
140
|
end
|
|
126
141
|
|
|
127
142
|
##
|
|
128
|
-
# Emits a structured +strategy_deprecated+ warning once per
|
|
143
|
+
# Emits a structured +strategy_deprecated+ warning once per +(strategy, host)+ per process
|
|
144
|
+
# on initial session requests (+relation == :initial+). Multi-worker servers may emit once per worker.
|
|
129
145
|
# @param strategy [Symbol] deprecated strategy alias
|
|
130
146
|
# @param replacement [Symbol] preferred strategy name
|
|
131
147
|
# @param relation [Symbol] request relation
|
|
132
148
|
# @param host [String, nil] request host
|
|
133
149
|
# @return [void]
|
|
134
150
|
def warn_migration_strategy(strategy, replacement, relation:, host:)
|
|
135
|
-
return unless relation == :initial
|
|
151
|
+
return unless relation == :initial && claim_deprecation_warning?(strategy, host)
|
|
136
152
|
|
|
137
153
|
Log.warn({
|
|
138
154
|
event_name: 'strategy_deprecated',
|
|
@@ -142,6 +158,14 @@ module Html2rss
|
|
|
142
158
|
message: "#{name}: strategy=#{strategy.inspect} is deprecated; using #{replacement.inspect} instead."
|
|
143
159
|
}.to_json)
|
|
144
160
|
end
|
|
161
|
+
|
|
162
|
+
def claim_deprecation_warning?(strategy, host)
|
|
163
|
+
DEPRECATION_MUTEX.synchronize do
|
|
164
|
+
@warned_deprecations ||= Set.new
|
|
165
|
+
@warned_deprecations.clear if @warned_deprecations.size >= MAX_DEPRECATION_WARNINGS
|
|
166
|
+
!@warned_deprecations.add?([strategy, host]).nil?
|
|
167
|
+
end
|
|
168
|
+
end
|
|
145
169
|
end
|
|
146
170
|
end
|
|
147
171
|
end
|
|
@@ -26,7 +26,7 @@ module Html2rss
|
|
|
26
26
|
# @yield [RequestService::Response] each page response
|
|
27
27
|
# @return [Enumerator] enumerator when no block is given
|
|
28
28
|
def each
|
|
29
|
-
return enum_for
|
|
29
|
+
return enum_for unless block_given?
|
|
30
30
|
|
|
31
31
|
yield initial_response
|
|
32
32
|
|
|
@@ -48,14 +48,14 @@ module Html2rss
|
|
|
48
48
|
|
|
49
49
|
# @return [Integer] configured maximum pages (always a positive Integer)
|
|
50
50
|
def max_pages
|
|
51
|
-
value = config
|
|
51
|
+
value = config[:max_pages]
|
|
52
52
|
value.is_a?(Integer) && value.positive? ? value : DEFAULT_MAX_PAGES
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
# @param next_url [Html2rss::Url, String, nil]
|
|
56
56
|
# @return [Boolean]
|
|
57
57
|
def follow_up_allowed?(next_url)
|
|
58
|
-
!next_url.
|
|
58
|
+
!next_url.to_s.empty? && !session.visited?(next_url)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# @param next_url [Html2rss::Url, String]
|
|
@@ -11,7 +11,7 @@ module Html2rss
|
|
|
11
11
|
# @param page_response [RequestService::Response]
|
|
12
12
|
# @param page_number [Integer]
|
|
13
13
|
# @return [String, nil]
|
|
14
|
-
def next_page_url(page_response, page_number:
|
|
14
|
+
def next_page_url(page_response, page_number:) # rubocop:disable Lint/UnusedMethodArgument
|
|
15
15
|
selector = config[:selector]
|
|
16
16
|
return nil if selector.nil? || selector.empty?
|
|
17
17
|
|
|
@@ -19,16 +19,16 @@ module Html2rss
|
|
|
19
19
|
return nil unless node
|
|
20
20
|
|
|
21
21
|
href = node['href'] || node.text
|
|
22
|
-
return nil if href.
|
|
22
|
+
return nil if href.strip.empty?
|
|
23
23
|
|
|
24
|
-
Html2rss::Url.from_relative(href
|
|
24
|
+
Html2rss::Url.from_relative(href, page_response.url)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
# @param parsed_body [Nokogiri::HTML::Document]
|
|
28
28
|
# @param selector [String]
|
|
29
29
|
# @return [Nokogiri::XML::Node, nil]
|
|
30
30
|
def extract_node(parsed_body, selector)
|
|
31
|
-
parsed_body.at_css(selector)
|
|
31
|
+
parsed_body.at_css(selector)
|
|
32
32
|
rescue Nokogiri::CSS::SyntaxError, Nokogiri::XML::XPath::SyntaxError
|
|
33
33
|
# Pure XPath like `descendant::a` fails LOOKS_LIKE_XPATH / at_css; fall back explicitly.
|
|
34
34
|
parsed_body.at_xpath(selector)
|
|
@@ -25,14 +25,8 @@ module Html2rss
|
|
|
25
25
|
# @return [Html2rss::Url]
|
|
26
26
|
def next_page_url(page_response, page_number:)
|
|
27
27
|
param = config.fetch(:param, DEFAULT_PARAM)
|
|
28
|
-
target_offset_val = target_offset_for(page_number)
|
|
29
28
|
url = Html2rss::Url.from_absolute(page_response.url)
|
|
30
|
-
|
|
31
|
-
if url.to_s.include?('{offset}')
|
|
32
|
-
Html2rss::Url.from_absolute(url.to_s.gsub('{offset}', target_offset_val.to_s))
|
|
33
|
-
else
|
|
34
|
-
url.with_query_values(url.query_values.merge(param.to_s => target_offset_val.to_s))
|
|
35
|
-
end
|
|
29
|
+
url.with_query_values(url.query_values.merge(param.to_s => target_offset_for(page_number).to_s))
|
|
36
30
|
end
|
|
37
31
|
|
|
38
32
|
# @param page_number [Integer]
|
|
@@ -11,7 +11,7 @@ module Html2rss
|
|
|
11
11
|
# @param page_response [RequestService::Response]
|
|
12
12
|
# @param page_number [Integer]
|
|
13
13
|
# @return [String, nil]
|
|
14
|
-
def next_page_url(page_response, page_number:
|
|
14
|
+
def next_page_url(page_response, page_number:) # rubocop:disable Lint/UnusedMethodArgument
|
|
15
15
|
href = page_response.parsed_body.at_css('link[rel~="next"][href], a[rel~="next"][href]')&.[]('href')
|
|
16
16
|
return nil if href.nil? || href.empty?
|
|
17
17
|
|
|
@@ -25,14 +25,8 @@ module Html2rss
|
|
|
25
25
|
# @return [Html2rss::Url]
|
|
26
26
|
def next_page_url(page_response, page_number:)
|
|
27
27
|
param = config.fetch(:param, DEFAULT_PARAM)
|
|
28
|
-
target_page_val = target_page_for(page_number)
|
|
29
28
|
url = Html2rss::Url.from_absolute(page_response.url)
|
|
30
|
-
|
|
31
|
-
if url.to_s.include?('{page}')
|
|
32
|
-
Html2rss::Url.from_absolute(url.to_s.gsub('{page}', target_page_val.to_s))
|
|
33
|
-
else
|
|
34
|
-
url.with_query_values(url.query_values.merge(param.to_s => target_page_val.to_s))
|
|
35
|
-
end
|
|
29
|
+
url.with_query_values(url.query_values.merge(param.to_s => target_page_for(page_number).to_s))
|
|
36
30
|
end
|
|
37
31
|
|
|
38
32
|
# @param page_number [Integer]
|
|
@@ -91,21 +91,17 @@ module Html2rss
|
|
|
91
91
|
|
|
92
92
|
private
|
|
93
93
|
|
|
94
|
-
def rank_one(segment, hard_junk_mode: :strict) # rubocop:disable Metrics/
|
|
94
|
+
def rank_one(segment, hard_junk_mode: :strict) # rubocop:disable Metrics/AbcSize
|
|
95
95
|
facts = segment.primary_link ? @link_resolver.destination_facts(segment.primary_link) : nil
|
|
96
96
|
obs = @assessor.call(segment.root_node, segment.primary_link, destination_facts: facts)
|
|
97
97
|
return if obs.hard_junk?(mode: hard_junk_mode)
|
|
98
98
|
|
|
99
99
|
quality, quality_parts = apply_rules(QUALITY_RULES, obs)
|
|
100
100
|
junk, junk_parts = apply_rules(JUNK_RULES, obs)
|
|
101
|
-
|
|
101
|
+
RankedSegment.new(
|
|
102
102
|
segment:,
|
|
103
|
-
score:
|
|
104
|
-
|
|
105
|
-
quality:,
|
|
106
|
-
junk:,
|
|
107
|
-
breakdown: quality_parts.merge(junk_parts)
|
|
108
|
-
)
|
|
103
|
+
score: Score.new(composite: (quality - junk).to_f, quality: quality.to_f, junk: junk.to_f,
|
|
104
|
+
breakdown: quality_parts.merge(junk_parts).freeze)
|
|
109
105
|
)
|
|
110
106
|
end
|
|
111
107
|
|
|
@@ -116,30 +112,6 @@ module Html2rss
|
|
|
116
112
|
end
|
|
117
113
|
[parts.values.sum, parts]
|
|
118
114
|
end
|
|
119
|
-
|
|
120
|
-
def build_score(composite:, quality: nil, junk: nil, breakdown: nil)
|
|
121
|
-
raise ArgumentError, 'composite must be Numeric' unless composite.is_a?(Numeric)
|
|
122
|
-
|
|
123
|
-
Score.new(
|
|
124
|
-
composite: composite.to_f,
|
|
125
|
-
quality: (quality.nil? ? composite : quality).to_f,
|
|
126
|
-
junk: (junk || 0).to_f,
|
|
127
|
-
breakdown: normalize_breakdown(breakdown)
|
|
128
|
-
)
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
def normalize_breakdown(breakdown)
|
|
132
|
-
return Score::EMPTY_BREAKDOWN if breakdown.nil?
|
|
133
|
-
|
|
134
|
-
breakdown.transform_keys { |id| self.class.assert_feature_id!(id) }.freeze
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def ranked(segment:, score:)
|
|
138
|
-
raise ArgumentError, 'segment must be AutoSource::Segment' unless segment.is_a?(Html2rss::AutoSource::Segment)
|
|
139
|
-
raise ArgumentError, 'score must be Scoring::Score' unless score.is_a?(Score)
|
|
140
|
-
|
|
141
|
-
RankedSegment.new(segment:, score:)
|
|
142
|
-
end
|
|
143
115
|
end
|
|
144
116
|
end
|
|
145
117
|
end
|
|
@@ -24,7 +24,6 @@ module Html2rss
|
|
|
24
24
|
def initialize(base_url)
|
|
25
25
|
@base_url = base_url
|
|
26
26
|
@by_href = {}
|
|
27
|
-
@by_node = {}.compare_by_identity
|
|
28
27
|
@text_classifier = LinkDestination::TextClassifier.new
|
|
29
28
|
end
|
|
30
29
|
|
|
@@ -35,15 +34,11 @@ module Html2rss
|
|
|
35
34
|
# @param node_or_href [SST::Node, String, #to_s]
|
|
36
35
|
# @return [LinkDestination::DestinationFacts, nil]
|
|
37
36
|
def destination_facts(node_or_href)
|
|
38
|
-
return @by_node[node_or_href] if node_or_href.is_a?(SST::Node) && @by_node.key?(node_or_href)
|
|
39
|
-
|
|
40
37
|
href = extract_href(node_or_href)
|
|
41
38
|
return unless href
|
|
42
39
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
facts
|
|
46
|
-
rescue ArgumentError
|
|
40
|
+
@by_href[href] ||= build_facts(href)
|
|
41
|
+
rescue Html2rss::Url::InvalidUrlError
|
|
47
42
|
nil
|
|
48
43
|
end
|
|
49
44
|
|
|
@@ -54,11 +54,9 @@ module Html2rss
|
|
|
54
54
|
##
|
|
55
55
|
# Retrieves and returns the normalized absolute URL.
|
|
56
56
|
#
|
|
57
|
-
# @return [Html2rss::Url, nil] The absolute URL.
|
|
57
|
+
# @return [Html2rss::Url, nil] The absolute URL, or nil when nothing matched or the +href+ is blank.
|
|
58
58
|
def call
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Url.from_relative(@href, @base_url)
|
|
59
|
+
Url.from_relative(@href, @base_url) unless @href.strip.empty?
|
|
62
60
|
end
|
|
63
61
|
end
|
|
64
62
|
end
|
|
@@ -14,48 +14,47 @@ module Html2rss
|
|
|
14
14
|
array: ['<array>', '</array>']
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
class << self
|
|
18
|
+
##
|
|
19
|
+
# Converts the object to XML format.
|
|
20
|
+
#
|
|
21
|
+
# @param object [Object] any Ruby object (Hash, Array, String, Symbol, etc.)
|
|
22
|
+
# @return [String] representing the object in XML
|
|
23
|
+
def call(object)
|
|
24
|
+
object_to_xml(object).tap do |converted|
|
|
25
|
+
Html2rss::Log.debug("#{self}: converted object to XML (#{converted.bytesize} bytes)")
|
|
26
|
+
end
|
|
27
27
|
end
|
|
28
|
-
end
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def object_to_xml(object)
|
|
32
|
+
case object
|
|
33
|
+
when Hash
|
|
34
|
+
hash_to_xml(object)
|
|
35
|
+
when Array
|
|
36
|
+
array_to_xml(object)
|
|
37
|
+
else
|
|
38
|
+
CGI.escapeHTML(object.to_s)
|
|
39
|
+
end
|
|
38
40
|
end
|
|
39
|
-
end
|
|
40
|
-
private_class_method :object_to_xml
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
def hash_to_xml(object)
|
|
43
|
+
prefix, suffix = OBJECT_TO_XML_TAGS[:hash]
|
|
44
|
+
inner_xml = object.each_with_object(+'') do |(key, value), str|
|
|
45
|
+
str << "<#{key}>#{object_to_xml(value)}</#{key}>"
|
|
46
|
+
end
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
private_class_method :hash_to_xml
|
|
48
|
+
"#{prefix}#{inner_xml}#{suffix}"
|
|
49
|
+
end
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
def array_to_xml(object)
|
|
52
|
+
prefix, suffix = OBJECT_TO_XML_TAGS[:array]
|
|
53
|
+
inner_xml = object.each_with_object(+'') { |value, str| str << object_to_xml(value) }
|
|
55
54
|
|
|
56
|
-
|
|
55
|
+
"#{prefix}#{inner_xml}#{suffix}"
|
|
56
|
+
end
|
|
57
57
|
end
|
|
58
|
-
private_class_method :array_to_xml
|
|
59
58
|
end
|
|
60
59
|
end
|
|
61
60
|
end
|
|
@@ -101,7 +101,7 @@ module Html2rss
|
|
|
101
101
|
|
|
102
102
|
source = regexp_source(string)
|
|
103
103
|
|
|
104
|
-
expression = Regexp::Parser.parse(source
|
|
104
|
+
expression = Regexp::Parser.parse(source)
|
|
105
105
|
raise ArgumentError, 'pattern contains nested quantifiers' if nested_quantifiers?(expression)
|
|
106
106
|
|
|
107
107
|
expression.to_re
|
|
@@ -161,8 +161,7 @@ module Html2rss
|
|
|
161
161
|
##
|
|
162
162
|
# @return [Regexp]
|
|
163
163
|
def pattern
|
|
164
|
-
|
|
165
|
-
raw.is_a?(String) ? self.class.compiled_pattern(raw) : raw
|
|
164
|
+
self.class.compiled_pattern(@pattern)
|
|
166
165
|
end
|
|
167
166
|
end
|
|
168
167
|
end
|
|
@@ -67,11 +67,10 @@ module Html2rss
|
|
|
67
67
|
def self.validate_args!(_value, context)
|
|
68
68
|
time_zone_value = context.time_zone
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
raise ArgumentError, 'time_zone cannot be nil or empty', [], cause: nil
|
|
72
|
-
end
|
|
70
|
+
raise ArgumentError, 'time_zone cannot be nil or empty', [], cause: nil if time_zone_value.nil?
|
|
73
71
|
|
|
74
72
|
assert_type(time_zone_value, String, :time_zone, context:)
|
|
73
|
+
raise ArgumentError, 'time_zone cannot be nil or empty', [], cause: nil if time_zone_value.empty?
|
|
75
74
|
end
|
|
76
75
|
|
|
77
76
|
##
|
|
@@ -89,7 +88,7 @@ module Html2rss
|
|
|
89
88
|
private
|
|
90
89
|
|
|
91
90
|
def parse_time(string, time_zone)
|
|
92
|
-
components = Date._parse(string
|
|
91
|
+
components = Date._parse(string)
|
|
93
92
|
if components[:zone] || components.key?(:offset)
|
|
94
93
|
Time.parse(string)
|
|
95
94
|
else
|
|
@@ -104,7 +103,7 @@ module Html2rss
|
|
|
104
103
|
components[:mday],
|
|
105
104
|
components[:hour] || 0,
|
|
106
105
|
components[:min] || 0,
|
|
107
|
-
|
|
106
|
+
components[:sec] || 0
|
|
108
107
|
)
|
|
109
108
|
end
|
|
110
109
|
end
|
|
@@ -8,72 +8,72 @@ module Html2rss
|
|
|
8
8
|
# Each registry class owns +DESCRIPTION+, +EXAMPLES+, and optional +OPTIONS+;
|
|
9
9
|
# this module is the export adapter only.
|
|
10
10
|
module SchemaExport
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
end
|
|
11
|
+
class << self
|
|
12
|
+
##
|
|
13
|
+
# @param name [Symbol, String] registry key
|
|
14
|
+
# @param klass [Class] post-processor class
|
|
15
|
+
# @return [Hash{Symbol => Object}] JSON Schema object for one post-processor
|
|
16
|
+
def for_post_processor(name:, klass:)
|
|
17
|
+
name = name.to_s
|
|
18
|
+
{
|
|
19
|
+
type: 'object',
|
|
20
|
+
title: name,
|
|
21
|
+
description: klass::DESCRIPTION,
|
|
22
|
+
examples: klass::EXAMPLES,
|
|
23
|
+
properties: post_processor_properties(name, klass),
|
|
24
|
+
required: post_processor_required(klass),
|
|
25
|
+
additionalProperties: true
|
|
26
|
+
}
|
|
27
|
+
end
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
29
|
+
##
|
|
30
|
+
# @param name [Symbol, String] registry key
|
|
31
|
+
# @param klass [Class] extractor class
|
|
32
|
+
# @return [Hash{Symbol => Object}] JSON Schema for one extractor name
|
|
33
|
+
def for_extractor(name:, klass:)
|
|
34
|
+
name = name.to_s
|
|
35
|
+
{
|
|
36
|
+
type: 'string',
|
|
37
|
+
const: name,
|
|
38
|
+
title: name,
|
|
39
|
+
description: klass::DESCRIPTION,
|
|
40
|
+
examples: klass::EXAMPLES
|
|
41
|
+
}
|
|
42
|
+
end
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
##
|
|
45
|
+
# @param name [String] registry key
|
|
46
|
+
# @param klass [Class]
|
|
47
|
+
# @return [Hash{Symbol => Hash}]
|
|
48
|
+
def post_processor_properties(name, klass)
|
|
49
|
+
properties = { name: { type: 'string', const: name } }
|
|
50
|
+
OptionSpec.for(klass).each do |spec|
|
|
51
|
+
properties[spec.name] = property_schema_for(spec.type)
|
|
52
|
+
end
|
|
53
|
+
properties
|
|
53
54
|
end
|
|
54
|
-
properties
|
|
55
|
-
end
|
|
56
55
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
##
|
|
57
|
+
# @param klass [Class]
|
|
58
|
+
# @return [Array<String>]
|
|
59
|
+
def post_processor_required(klass)
|
|
60
|
+
required = ['name']
|
|
61
|
+
required + OptionSpec.for(klass).select(&:required).map { |spec| spec.name.to_s }
|
|
62
|
+
end
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
##
|
|
65
|
+
# @param ruby_type [Class, Array<Class>]
|
|
66
|
+
# @return [Hash{Symbol => String, Array<String>}]
|
|
67
|
+
def property_schema_for(ruby_type)
|
|
68
|
+
{ type: json_type_for(ruby_type) }
|
|
69
|
+
end
|
|
71
70
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
##
|
|
72
|
+
# @param ruby_type [Class, Array<Class>]
|
|
73
|
+
# @return [String, Array<String>]
|
|
74
|
+
def json_type_for(ruby_type)
|
|
75
|
+
OptionSpec.json_type_for(ruby_type)
|
|
76
|
+
end
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
end
|