metainspector 5.18.0 → 5.19.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c74f59026aa7587f6f96d99231af70edd75c801c37d4896d2a23c48485fbaef1
4
- data.tar.gz: 72ffabbe924db985aca5214ccfcbb656f0abbe694f46c333f95f101f5ff17ded
3
+ metadata.gz: 5bc16dfdcdd22661ba7bb196d4db8750a5fa4d48d054197982768fa7fac44835
4
+ data.tar.gz: ce356b1f0f2511069ff96329e775e3581282d68d395c4dada93a9275e5dd4aa6
5
5
  SHA512:
6
- metadata.gz: 9a8433d032d7cbc0fc7666f323bb3414f1bac39002c6c85fac25ed0b1993d3051a3fcf268a21c0662b6812d18b38438e77be881b59418be5c1af09c792938ce1
7
- data.tar.gz: accccae27d24bb1b06ab43bf333152c60b4a7a090de3caa0491d42bc0958d03841ea8409baef5705f912ad10ff29b6466330f87be434819a9587fcd5b3a192aa
6
+ metadata.gz: 6a669057baeb8107c8ae6940f4323f60664982fa3c8c74ce4f029b434ebfe227436c743a6b6041acbcddfdc2ea7fb38704a6126d3204b18479a8ca81b2b63a0e
7
+ data.tar.gz: 123f1cbb41fd5492307d96e316b17c91fc4f17d523a12f9fd865be49a3de31d8b932ceae6365bc7e1f6f49fd9721f8626eb9a8b5bbcbf9d86b2e972aedaa1544
data/.circleci/config.yml CHANGED
@@ -35,6 +35,20 @@ jobs:
35
35
  name: Run tests
36
36
  command: bundle exec rake
37
37
 
38
+ # Same as test_4_0, but ignores Gemfile.lock and uses the newest versions the
39
+ # gemspec allows, which is what people who install the gem get.
40
+ test_4_0_latest_deps:
41
+ docker:
42
+ - image: cimg/ruby:4.0
43
+ steps:
44
+ - checkout
45
+ - run:
46
+ name: Update dependencies to the newest allowed versions
47
+ command: bundle update
48
+ - run:
49
+ name: Run tests
50
+ command: bundle exec rake
51
+
38
52
  workflows:
39
53
  version: 2
40
54
  deploy:
@@ -42,3 +56,4 @@ workflows:
42
56
  - test_3_3
43
57
  - test_3_4
44
58
  - test_4_0
59
+ - test_4_0_latest_deps
@@ -4,4 +4,4 @@ updates:
4
4
  - package-ecosystem: "bundler"
5
5
  directory: "/"
6
6
  schedule:
7
- interval: "daily"
7
+ interval: "weekly"
data/.gitignore CHANGED
@@ -9,4 +9,4 @@ pkg/*
9
9
  .rubocop_todo.yml
10
10
  .rubocop.yml
11
11
  .tool-versions
12
- Gemfile.lock
12
+ docs/
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # MetaInspector Changelog
2
2
 
3
+ ## [Changes in 5.19.0](https://github.com/jaimeiniesta/metainspector/compare/v5.18.1...v5.19.0)
4
+
5
+ * Security: stop modifying the `faraday_options` hash passed to MetaInspector. When the same hash was reused, its `redirect` options were deleted after the first request, so a redirect `callback` used to block private hosts only ran once and later requests followed any redirect ([GHSA-mm2p-6q2v-7ph5](https://github.com/jaimeiniesta/metainspector/security/advisories/GHSA-mm2p-6q2v-7ph5)). Frozen option hashes no longer raise `FrozenError`.
6
+ * Add `language` to expose the root HTML `lang` attribute, trimming surrounding whitespace and returning `nil` for missing or blank values. Include it in `to_hash` (#121).
7
+ * Add opt-in `max_image_downloads` to limit image size lookup attempts while preserving dimensions already available in HTML (#242).
8
+ * Add `parsely-title`, `parsely-author`, and `parsely-image-url` fallbacks to `best_title`, `best_author`, and `images.owner_suggested` / `images.best`, preserving existing preferred metadata sources (#214).
9
+ * Resolve relative base hrefs against the full document URL, preserving nested paths and non-default ports (#240).
10
+ * Skip invalid image URLs in `images.with_size`, preventing `images.best` from failing when FastImage receives a nil URL ([#521](https://github.com/jaimeiniesta/metainspector/issues/521), follow-up to [#359](https://github.com/jaimeiniesta/metainspector/issues/359)).
11
+ * Add opt-in `head_first: true` to check content type with HEAD and reject non-HTML files before downloading them (#221).
12
+
13
+ ## [Changes in 5.18.1](https://github.com/jaimeiniesta/metainspector/compare/v5.18.0...v5.18.1)
14
+
15
+ * Require nokogiri 1.19.5, which fixes three low-severity security advisories.
16
+ * Commit Gemfile.lock to the repository, and run CI against the newest allowed dependencies as well.
17
+
3
18
  ## [Changes in 5.18.0](https://github.com/jaimeiniesta/metainspector/compare/v5.17.4...v5.18.0)
4
19
 
5
20
  * Remove support for Ruby 3.2, which reached end of life on 2026-04-01.
data/README.md CHANGED
@@ -84,6 +84,7 @@ page.feeds # Get rss or atom links in meta data fields as array of
84
84
  ### Texts
85
85
 
86
86
  ```ruby
87
+ page.language # declared language from the root <html lang="..."> attribute
87
88
  page.title # title of the page from the head section, as string
88
89
  page.best_title # best title of the page, from a selection of candidates
89
90
  page.author # author of the page from the meta author tag
@@ -98,6 +99,20 @@ page.h5 # returns h5 text array
98
99
  page.h6 # returns h6 text array
99
100
  ```
100
101
 
102
+ `language` trims surrounding whitespace and preserves the declared value, including
103
+ capitalization and regional tags such as `en-US`. It returns `nil` when the `lang`
104
+ attribute is missing, empty, or whitespace-only. It does not infer language from
105
+ the page content or other attributes. The value is also included in `to_hash`
106
+ under the `"language"` key.
107
+
108
+ `best_title` selects the first non-empty value from the standard `title` meta tag,
109
+ `og:title`, the head `<title>`, a body `<title>`, the first `<h1>`, and finally
110
+ `parsely-title`.
111
+
112
+ `best_author` selects the first non-empty value from the standard `author` meta tag,
113
+ the first link with `rel="author"`, the first `<address>`, `twitter:creator`, and
114
+ finally `parsely-author`. Both methods trim and collapse whitespace in their results.
115
+
101
116
  ### Links
102
117
 
103
118
  ```ruby
@@ -114,10 +129,15 @@ page.links.external # every external link found on the page as an absolute
114
129
  ```ruby
115
130
  page.images # enumerable collection, with every img found on the page as an absolute URL
116
131
  page.images.with_size # a sorted array (by descending area) of [image_url, width, height]
117
- page.images.best # Most relevant image, if defined with the og:image or twitter:image metatags. Fallback to the first page.images array element
132
+ page.images.best # Most relevant image from og:image, twitter:image, or parsely-image-url; falls back to page.images.largest
118
133
  page.images.favicon # absolute URL to the favicon
119
134
  ```
120
135
 
136
+ `images.owner_suggested` tries `og:image`, `twitter:image`, and `parsely-image-url`
137
+ in that order, skipping empty or whitespace-only values. Relative image URLs are
138
+ resolved against the document's base URL. `images.best` uses this suggested image
139
+ before falling back to the largest suitable image on the page.
140
+
121
141
  ### Meta tags
122
142
 
123
143
  When it comes to meta tags, you have several options:
@@ -385,6 +405,28 @@ page.content_type # "image/png"
385
405
  page.description # will return a garbled string
386
406
  ```
387
407
 
408
+ ### Check content type before downloading
409
+
410
+ To avoid downloading large non-HTML files, enable `head_first`:
411
+
412
+ ```ruby
413
+ page = MetaInspector.new('http://example.com/page', head_first: true)
414
+ ```
415
+
416
+ This sends an HTTP HEAD request before GET. If a successful HEAD response declares
417
+ a content type other than `text/html`, MetaInspector raises `MetaInspector::NonHtmlError`
418
+ without downloading the body. Content-type parameters such as `charset` are ignored.
419
+ With `allow_non_html_content: true`, the download proceeds regardless of content type.
420
+
421
+ If HEAD omits the content type or returns an unsuccessful status (including 405 or
422
+ 501 for servers that do not support HEAD), MetaInspector continues with GET and its
423
+ usual content-type check. HEAD timeouts and connection errors use the same exception
424
+ handling as GET. The final page data, response headers and status come from GET.
425
+
426
+ This option is off by default because it adds a request and not every server
427
+ supports HEAD. Both requests use the configured headers, timeouts, retries and
428
+ redirect settings. Passing a `document` directly makes no HTTP requests.
429
+
388
430
  ### URL Normalization
389
431
 
390
432
  By default, URLs are normalized using the Addressable gem. For example:
@@ -413,6 +455,19 @@ If you want to disable this, you can specify it like this:
413
455
  page = MetaInspector.new('http://example.com', download_images: false)
414
456
  ```
415
457
 
458
+ To limit how many images are downloaded to determine their dimensions, use `max_image_downloads`:
459
+
460
+ ```ruby
461
+ page = MetaInspector.new('http://example.com', max_image_downloads: 10)
462
+ page.images.with_size
463
+ ```
464
+
465
+ The default is `nil` (unlimited). The option accepts a non-negative integer or `nil`; `0` disables image downloads. `download_images: false` always disables downloads, regardless of the limit.
466
+
467
+ The limit counts image size lookup attempts in HTML order, after excluding unparseable URLs and duplicates. Images with both HTML dimensions already present do not consume the limit, and those dimensions are preserved even after the limit is reached. Failed lookups count toward the limit. Images with incomplete dimensions that are not downloaded remain in `images.with_size` with dimensions `[0, 0]`, matching the behavior when downloads are disabled.
468
+
469
+ The limit leaves `images.to_a` unchanged, but can affect `images.largest` and the largest-image fallback in `images.best`, since images with unknown dimensions cannot be ranked by size. Size lookup results are cached per document, so repeated calls reuse the same results. This limits size lookup attempts, not elapsed time or HTTP requests made while following image redirects.
470
+
416
471
  ### Caching responses
417
472
 
418
473
  MetaInspector can be configured to use [Faraday::HttpCache](https://github.com/plataformatec/faraday-http-cache) to cache page responses. For that you should pass the `faraday_http_cache` option with at least the `:store` key, for example:
@@ -9,12 +9,20 @@ module MetaInspector
9
9
  # * read_timeout: defaults to 20 seconds
10
10
  # * retries: defaults to 3 times
11
11
  # * allow_redirections: when true, follow HTTP redirects. Defaults to true
12
+ # * head_first: check content type with HEAD before GET. Defaults to false
12
13
  # * document: the html of the url as a string
13
14
  # * headers: object containing custom headers for the request
14
15
  # * normalize_url: true by default
16
+ # * download_images: fetch missing image dimensions. Defaults to true
17
+ # * max_image_downloads: maximum image size lookups, or nil for unlimited
15
18
  # * faraday_options: an optional hash of options to pass to Faraday on the request
16
19
  def initialize(initial_url, options = {})
17
20
  options = defaults.merge(options)
21
+ @max_image_downloads = options[:max_image_downloads]
22
+ unless @max_image_downloads.nil? || (@max_image_downloads.is_a?(Integer) && @max_image_downloads >= 0)
23
+ raise ArgumentError, 'max_image_downloads must be a non-negative Integer or nil'
24
+ end
25
+
18
26
  @connection_timeout = options[:connection_timeout]
19
27
  @read_timeout = options[:read_timeout]
20
28
  @retries = options[:retries]
@@ -31,6 +39,8 @@ module MetaInspector
31
39
  @faraday_http_cache = options[:faraday_http_cache]
32
40
  @url = MetaInspector::URL.new(initial_url, normalize: @normalize_url)
33
41
  @request = MetaInspector::Request.new(@url, allow_redirections: @allow_redirections,
42
+ head_first: options[:head_first],
43
+ allow_non_html_content: @allow_non_html_content,
34
44
  connection_timeout: @connection_timeout,
35
45
  read_timeout: @read_timeout,
36
46
  retries: @retries,
@@ -38,7 +48,8 @@ module MetaInspector
38
48
  headers: @headers,
39
49
  faraday_options: @faraday_options,
40
50
  faraday_http_cache: @faraday_http_cache) unless @document
41
- @parser = MetaInspector::Parser.new(self, download_images: @download_images)
51
+ @parser = MetaInspector::Parser.new(self, download_images: @download_images,
52
+ max_image_downloads: @max_image_downloads)
42
53
  end
43
54
 
44
55
  extend Forwardable
@@ -47,7 +58,7 @@ module MetaInspector
47
58
 
48
59
  delegate [:content_type, :response] => :@request
49
60
 
50
- delegate [:parsed, :title, :best_title, :author, :best_author,
61
+ delegate [:parsed, :language, :title, :best_title, :author, :best_author,
51
62
  :h1, :h2, :h3, :h4, :h5, :h6, :description, :best_description, :links,
52
63
  :images, :feeds, :feed, :charset, :meta_tags,
53
64
  :meta_tag, :meta, :favicon,
@@ -60,6 +71,7 @@ module MetaInspector
60
71
  'scheme' => scheme,
61
72
  'host' => host,
62
73
  'root_url' => root_url,
74
+ 'language' => language,
63
75
  'title' => title,
64
76
  'best_title' => best_title,
65
77
  'author' => author,
@@ -101,8 +113,10 @@ module MetaInspector
101
113
  },
102
114
  :allow_redirections => true,
103
115
  :allow_non_html_content => false,
116
+ :head_first => false,
104
117
  :normalize_url => true,
105
- :download_images => true }
118
+ :download_images => true,
119
+ :max_image_downloads => nil }
106
120
  end
107
121
 
108
122
  def default_user_agent
@@ -14,7 +14,8 @@ module MetaInspector
14
14
  @meta_tag_parser = MetaInspector::Parsers::MetaTagsParser.new(self)
15
15
  @links_parser = MetaInspector::Parsers::LinksParser.new(self)
16
16
  @download_images = options[:download_images]
17
- @images_parser = MetaInspector::Parsers::ImagesParser.new(self, download_images: @download_images)
17
+ @images_parser = MetaInspector::Parsers::ImagesParser.new(self, download_images: @download_images,
18
+ max_image_downloads: options[:max_image_downloads])
18
19
  @texts_parser = MetaInspector::Parsers::TextsParser.new(self)
19
20
 
20
21
  parsed # parse early so we can fail early
@@ -26,7 +27,7 @@ module MetaInspector
26
27
  delegate [:head_links, :stylesheets, :canonicals, :feeds, :feed] => :@head_links_parser
27
28
  delegate [:links, :base_url] => :@links_parser
28
29
  delegate :images => :@images_parser
29
- delegate [:title, :best_title, :author, :best_author, :description, :best_description,
30
+ delegate [:language, :title, :best_title, :author, :best_author, :description, :best_description,
30
31
  :h1, :h2, :h3, :h4, :h5, :h6] => :@texts_parser
31
32
 
32
33
  # Returns the whole parsed document
@@ -10,6 +10,7 @@ module MetaInspector
10
10
 
11
11
  def initialize(main_parser, options = {})
12
12
  @download_images = options[:download_images]
13
+ @max_image_downloads = options[:max_image_downloads]
13
14
  super(main_parser)
14
15
  end
15
16
 
@@ -17,7 +18,7 @@ module MetaInspector
17
18
  self
18
19
  end
19
20
 
20
- # Returns either the Facebook Open Graph image, twitter suggested image or
21
+ # Returns the Facebook Open Graph image, Twitter image, Parsely image or
21
22
  # the largest image in the image collection
22
23
  def best
23
24
  owner_suggested || largest
@@ -26,9 +27,10 @@ module MetaInspector
26
27
  # Returns the parsed image from Facebook's open graph property tags
27
28
  # Most major websites now define this property and is usually relevant
28
29
  # See doc at http://developers.facebook.com/docs/opengraph/
29
- # If none found, tries with Twitter image
30
+ # If none found, tries with Twitter image, then Parsely image
30
31
  def owner_suggested
31
- suggested_img = content_of(meta['og:image']) || content_of(meta['twitter:image'])
32
+ suggested_img = content_of(meta['og:image']) || content_of(meta['twitter:image']) ||
33
+ content_of(meta['parsely-image-url'])
32
34
  URL.absolutify(suggested_img, base_url, normalize: false) if suggested_img
33
35
  end
34
36
 
@@ -39,17 +41,19 @@ module MetaInspector
39
41
  imgs_with_size = img_nodes.map do |img_node|
40
42
  [URL.absolutify(img_node['src'], base_url, normalize: false), img_node['width'], img_node['height']]
41
43
  end
44
+ imgs_with_size.reject! { |url, width, height| url.nil? || url.empty? }
42
45
  imgs_with_size.uniq! { |url, width, height| url }
43
- if @download_images
44
- imgs_with_size.map! do |url, width, height|
45
- width, height = FastImage.size(url) if width.nil? || height.nil?
46
- [url, width.to_i, height.to_i]
47
- end
48
- else
49
- imgs_with_size.map! do |url, width, height|
50
- width, height = [0, 0] if width.nil? || height.nil?
51
- [url, width.to_i, height.to_i]
46
+ download_attempts = 0
47
+ imgs_with_size.map! do |url, width, height|
48
+ if width.nil? || height.nil?
49
+ if @download_images && (@max_image_downloads.nil? || download_attempts < @max_image_downloads)
50
+ download_attempts += 1
51
+ width, height = FastImage.size(url)
52
+ else
53
+ width, height = [0, 0]
54
+ end
52
55
  end
56
+ [url, width.to_i, height.to_i]
53
57
  end
54
58
  imgs_with_size.sort_by { |url, width, height| -(width.to_i * height.to_i) }
55
59
  end
@@ -93,7 +97,7 @@ module MetaInspector
93
97
  end
94
98
 
95
99
  def content_of(content)
96
- return nil if content.nil? || content.empty?
100
+ return nil if content.nil? || content.strip.empty?
97
101
  content
98
102
  end
99
103
  end
@@ -46,8 +46,9 @@ module MetaInspector
46
46
  # Returns the base url to absolutify relative links.
47
47
  # This can be the one set on a <base> tag,
48
48
  # or the url of the document if no <base> tag was found.
49
+ # Relative base hrefs are resolved against the document URL.
49
50
  def base_url
50
- current_base_href = base_href.to_s.strip.empty? ? nil : URL.absolutify(base_href, URL.new(url).root_url)
51
+ current_base_href = base_href.to_s.strip.empty? ? nil : URL.absolutify(base_href, url)
51
52
  current_base_href || url
52
53
  end
53
54
 
@@ -3,6 +3,12 @@ module MetaInspector
3
3
  class TextsParser < Base
4
4
  delegate [:parsed, :meta] => :@main_parser
5
5
 
6
+ # Returns the declared HTML language, or nil when missing or blank
7
+ def language
8
+ value = parsed.at_css('html')&.[]('lang')&.strip
9
+ value unless value.to_s.empty?
10
+ end
11
+
6
12
  # Returns the parsed document title, from the content of the <title> tag
7
13
  # within the <head> section.
8
14
  def title
@@ -42,12 +48,13 @@ module MetaInspector
42
48
  @author ||= meta['author']
43
49
  end
44
50
 
45
- # An author getter that returns the first non-nil description
51
+ # An author getter that returns the first non-empty author
46
52
  # from the following candidates:
47
- # - the standard meta description
53
+ # - the standard meta author
48
54
  # - a link with the relational attribute "author"
49
55
  # - address tag which may contain the author
50
56
  # - the twitter:creator meta tag for the username
57
+ # - the parsely-author meta tag
51
58
  def best_author
52
59
  @best_author ||= find_best_author
53
60
  end
@@ -80,7 +87,8 @@ module MetaInspector
80
87
  meta['og:title'],
81
88
  parsed.css('head title'),
82
89
  parsed.css('body title'),
83
- parsed.css('h1').first
90
+ parsed.css('h1').first,
91
+ meta['parsely-title']
84
92
  ]
85
93
  candidates.flatten!
86
94
  candidates.compact!
@@ -94,7 +102,8 @@ module MetaInspector
94
102
  meta['author'],
95
103
  parsed.css('a[rel="author"]').first,
96
104
  parsed.css('address').first,
97
- meta['twitter:creator']
105
+ meta['twitter:creator'],
106
+ meta['parsely-author']
98
107
  ]
99
108
  candidates.flatten!
100
109
  candidates.compact!
@@ -18,6 +18,8 @@ module MetaInspector
18
18
  fail MetaInspector::RequestError.new('URL must be HTTP') unless @url.url =~ /http[s]?:\/\//i
19
19
 
20
20
  @allow_redirections = options[:allow_redirections]
21
+ @head_first = options[:head_first]
22
+ @allow_non_html_content = options[:allow_non_html_content]
21
23
  @connection_timeout = options[:connection_timeout]
22
24
  @read_timeout = options[:read_timeout]
23
25
  @retries = options[:retries]
@@ -42,8 +44,7 @@ module MetaInspector
42
44
  end
43
45
 
44
46
  def content_type
45
- return nil if response.headers['content-type'].nil?
46
- response.headers['content-type'].split(';')[0] if response
47
+ response_content_type(response)
47
48
  end
48
49
 
49
50
  def response
@@ -58,10 +59,12 @@ module MetaInspector
58
59
 
59
60
  def fetch
60
61
  Timeout::timeout(fatal_timeout) do
61
- @faraday_options.merge!(:url => url)
62
- follow_redirects_options = @faraday_options.delete(:redirect) || {}
62
+ # Work on copies: the caller may reuse or freeze the hashes it passed in, and
63
+ # losing :redirect would drop a callback that blocks redirects to private hosts
64
+ faraday_options = @faraday_options.merge(:url => url)
65
+ follow_redirects_options = (faraday_options.delete(:redirect) || {}).dup
63
66
 
64
- session = Faraday.new(@faraday_options) do |faraday|
67
+ session = Faraday.new(faraday_options) do |faraday|
65
68
  faraday.request :retry, max: @retries
66
69
 
67
70
  faraday.request :gzip
@@ -73,8 +76,9 @@ module MetaInspector
73
76
  end
74
77
 
75
78
  if @faraday_http_cache.is_a?(Hash)
76
- @faraday_http_cache[:serializer] ||= Marshal
77
- faraday.use Faraday::HttpCache, **@faraday_http_cache
79
+ cache_options = @faraday_http_cache.dup
80
+ cache_options[:serializer] ||= Marshal
81
+ faraday.use Faraday::HttpCache, **cache_options
78
82
  end
79
83
 
80
84
  faraday.headers.merge!(@headers || {})
@@ -82,10 +86,8 @@ module MetaInspector
82
86
  faraday.adapter :net_http
83
87
  end
84
88
 
85
- response = session.get do |req|
86
- req.options.timeout = @connection_timeout
87
- req.options.open_timeout = @read_timeout
88
- end
89
+ check_head_content_type(session) if @head_first
90
+ response = perform_request(session, :get)
89
91
 
90
92
  if @allow_redirections
91
93
  @url.url = response.env.url.to_s
@@ -97,6 +99,27 @@ module MetaInspector
97
99
  raise MetaInspector::TimeoutError.new(e)
98
100
  end
99
101
 
102
+ def check_head_content_type(session)
103
+ head_response = perform_request(session, :head)
104
+ return unless head_response.success?
105
+
106
+ type = response_content_type(head_response)
107
+ if !@allow_non_html_content && !type.nil? && type != 'text/html'
108
+ fail MetaInspector::NonHtmlError.new "The url provided contains #{type} content instead of text/html content"
109
+ end
110
+ end
111
+
112
+ def response_content_type(response)
113
+ response.headers['content-type']&.split(';')&.first
114
+ end
115
+
116
+ def perform_request(session, method)
117
+ session.public_send(method) do |req|
118
+ req.options.timeout = @connection_timeout
119
+ req.options.open_timeout = @read_timeout
120
+ end
121
+ end
122
+
100
123
  # Timeouts when connecting / reading a request are handled by Faraday, but in the
101
124
  # case of URLs that respond with streaming, Faraday will never return. In that case,
102
125
  # we'll resort to our own timeout
@@ -1,3 +1,3 @@
1
1
  module MetaInspector
2
- VERSION = '5.18.0'
2
+ VERSION = '5.19.0'
3
3
  end
@@ -8,14 +8,14 @@ Gem::Specification.new do |gem|
8
8
  gem.homepage = "https://github.com/jaimeiniesta/metainspector"
9
9
  gem.license = "MIT"
10
10
 
11
- gem.files = `git ls-files`.split("\n")
11
+ gem.files = `git ls-files`.split("\n") - ["Gemfile.lock"]
12
12
  gem.test_files = `git ls-files -- {spec}/*`.split("\n")
13
13
  gem.name = "metainspector"
14
14
  gem.require_paths = ["lib"]
15
15
  gem.version = MetaInspector::VERSION
16
16
  gem.required_ruby_version = '>= 3.3'
17
17
 
18
- gem.add_dependency 'nokogiri', '~> 1.19.4'
18
+ gem.add_dependency 'nokogiri', '~> 1.19.5'
19
19
  gem.add_dependency 'faraday', '~> 2.14', '>= 2.14.3'
20
20
  gem.add_dependency 'faraday-cookie_jar', '~> 0.0'
21
21
  gem.add_dependency 'faraday-encoding', '~> 0.0'
@@ -29,11 +29,10 @@ Gem::Specification.new do |gem|
29
29
 
30
30
  gem.add_development_dependency 'rspec', '~> 3.11'
31
31
  gem.add_development_dependency 'webmock', '~> 3.17'
32
- gem.add_development_dependency 'awesome_print', '~> 1.9'
33
32
  gem.add_development_dependency 'rake', '~> 13.0'
34
33
  gem.add_development_dependency 'pry', '~> 0.14'
35
- gem.add_development_dependency 'puma', '~> 8.0.0'
34
+ gem.add_development_dependency 'puma', '~> 8.0', '>= 8.0.2'
36
35
  gem.add_development_dependency 'rubocop', '~> 1.34'
37
- gem.add_development_dependency 'resolv', '~> 0.8.0'
38
- gem.add_development_dependency 'sinatra', '~> 4.2.0'
36
+ gem.add_development_dependency 'resolv', '~> 0.8'
37
+ gem.add_development_dependency 'sinatra', '~> 4.2'
39
38
  end
@@ -1,6 +1,18 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe MetaInspector::Document do
4
+ describe 'max_image_downloads option' do
5
+ [-1, 1.5, '1', true, false].each do |limit|
6
+ it "should reject #{limit.inspect} before fetching the document" do
7
+ expect do
8
+ MetaInspector::Document.new('http://example.com/', max_image_downloads: limit)
9
+ end.to raise_error(ArgumentError, 'max_image_downloads must be a non-negative Integer or nil')
10
+
11
+ expect(WebMock).not_to have_requested(:get, 'http://example.com/')
12
+ end
13
+ end
14
+ end
15
+
4
16
  describe 'passing the contents of the document as html' do
5
17
  let(:doc) { MetaInspector::Document.new('http://cnn.com/', :document => "<html><head><title>Hello From Passed Html</title><a href='/hello'>Hello link</a></head><body></body></html>") }
6
18
 
@@ -17,6 +29,13 @@ describe MetaInspector::Document do
17
29
  expect(MetaInspector::Document.new('http://pagerankalert.com').to_s.class).to eq(String)
18
30
  end
19
31
 
32
+ it 'should include a nil language in the hash when the document has no declared language' do
33
+ stub_request(:get, 'http://example.com/').to_return(body: '<html><body></body></html>', headers: { 'Content-Type' => 'text/html' })
34
+ doc = MetaInspector::Document.new('http://example.com/')
35
+
36
+ expect(doc.to_hash).to include('language' => nil)
37
+ end
38
+
20
39
  it "should return a Hash with all the values set" do
21
40
  doc = MetaInspector::Document.new('http://pagerankalert.com')
22
41
  expect(doc.to_hash).to eq({
@@ -24,6 +43,7 @@ describe MetaInspector::Document do
24
43
  "scheme" => "http",
25
44
  "host" => "pagerankalert.com",
26
45
  "root_url" => "http://pagerankalert.com/",
46
+ "language" => "en",
27
47
  "title" => "PageRankAlert.com :: Track your PageRank changes & receive alerts",
28
48
  "best_title" => "PageRankAlert.com :: Track your PageRank changes & receive alerts",
29
49
  "author" => nil,