theme-check 0.5.0 → 0.7.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/theme-check.yml +10 -3
- data/.rubocop.yml +6 -3
- data/CHANGELOG.md +35 -0
- data/Gemfile +5 -3
- data/LICENSE.md +2 -0
- data/README.md +3 -0
- data/RELEASING.md +10 -3
- data/Rakefile +6 -0
- data/config/default.yml +11 -1
- data/data/shopify_translation_keys.yml +850 -0
- data/docs/checks/asset_size_css.md +52 -0
- data/docs/checks/img_width_and_height.md +79 -0
- data/docs/checks/parser_blocking_javascript.md +3 -3
- data/docs/checks/remote_asset.md +82 -0
- data/exe/theme-check +1 -1
- data/lib/theme_check.rb +1 -0
- data/lib/theme_check/check.rb +1 -1
- data/lib/theme_check/checks/asset_size_css.rb +89 -0
- data/lib/theme_check/checks/asset_size_javascript.rb +2 -8
- data/lib/theme_check/checks/img_width_and_height.rb +74 -0
- data/lib/theme_check/checks/matching_translations.rb +1 -1
- data/lib/theme_check/checks/parser_blocking_javascript.rb +6 -14
- data/lib/theme_check/checks/remote_asset.rb +99 -0
- data/lib/theme_check/checks/translation_key_exists.rb +13 -1
- data/lib/theme_check/checks/undefined_object.rb +1 -1
- data/lib/theme_check/checks/valid_html_translation.rb +1 -1
- data/lib/theme_check/cli.rb +106 -51
- data/lib/theme_check/config.rb +3 -0
- data/lib/theme_check/disabled_checks.rb +2 -2
- data/lib/theme_check/in_memory_storage.rb +13 -8
- data/lib/theme_check/language_server.rb +2 -0
- data/lib/theme_check/language_server/completion_engine.rb +3 -3
- data/lib/theme_check/language_server/completion_provider.rb +4 -0
- data/lib/theme_check/language_server/completion_providers/filter_completion_provider.rb +6 -2
- data/lib/theme_check/language_server/completion_providers/object_completion_provider.rb +1 -1
- data/lib/theme_check/language_server/completion_providers/render_snippet_completion_provider.rb +43 -0
- data/lib/theme_check/language_server/completion_providers/tag_completion_provider.rb +2 -2
- data/lib/theme_check/language_server/constants.rb +10 -0
- data/lib/theme_check/language_server/document_link_engine.rb +48 -0
- data/lib/theme_check/language_server/handler.rb +56 -17
- data/lib/theme_check/language_server/server.rb +4 -4
- data/lib/theme_check/liquid_check.rb +11 -0
- data/lib/theme_check/node.rb +1 -2
- data/lib/theme_check/offense.rb +3 -1
- data/lib/theme_check/packager.rb +1 -1
- data/lib/theme_check/releaser.rb +39 -0
- data/lib/theme_check/remote_asset_file.rb +1 -1
- data/lib/theme_check/shopify_liquid/deprecated_filter.rb +10 -8
- data/lib/theme_check/shopify_liquid/filter.rb +3 -5
- data/lib/theme_check/shopify_liquid/object.rb +2 -6
- data/lib/theme_check/shopify_liquid/tag.rb +1 -3
- data/lib/theme_check/storage.rb +3 -3
- data/lib/theme_check/string_helpers.rb +47 -0
- data/lib/theme_check/tags.rb +1 -2
- data/lib/theme_check/theme.rb +1 -1
- data/lib/theme_check/version.rb +1 -1
- data/packaging/homebrew/theme_check.base.rb +1 -1
- data/theme-check.gemspec +1 -2
- metadata +16 -18
@@ -0,0 +1,52 @@
|
|
1
|
+
# Prevent Large CSS bundles (`AssetSizeCSS`)
|
2
|
+
|
3
|
+
This rule exists to prevent large CSS bundles (for speed).
|
4
|
+
|
5
|
+
## Check Details
|
6
|
+
|
7
|
+
This rule disallows the use of too much CSS in themes, as configured by `threshold_in_bytes`.
|
8
|
+
|
9
|
+
:-1: Examples of **incorrect** code for this check:
|
10
|
+
```liquid
|
11
|
+
<!-- Here, assets/theme.css is **greater** than `threshold_in_bytes` compressed. -->
|
12
|
+
{{ 'theme.css' | asset_url | stylesheet_tag }}
|
13
|
+
```
|
14
|
+
|
15
|
+
:+1: Example of **correct** code for this check:
|
16
|
+
```liquid
|
17
|
+
<!-- Here, assets/theme.css is **less** than `threshold_in_bytes` compressed. -->
|
18
|
+
{{ 'theme.css' | asset_url | stylesheet_tag }}
|
19
|
+
```
|
20
|
+
|
21
|
+
## Check Options
|
22
|
+
|
23
|
+
The default configuration is the following.
|
24
|
+
|
25
|
+
```yaml
|
26
|
+
AssetSizeCSS:
|
27
|
+
enabled: false
|
28
|
+
threshold_in_bytes: 100_000
|
29
|
+
```
|
30
|
+
|
31
|
+
### `threshold_in_bytes`
|
32
|
+
|
33
|
+
The `threshold_in_bytes` option (default: `100_000`) determines the maximum allowed compressed size in bytes that a single CSS file can take.
|
34
|
+
|
35
|
+
This includes theme and remote stylesheets.
|
36
|
+
|
37
|
+
## When Not To Use It
|
38
|
+
|
39
|
+
This rule is safe to disable.
|
40
|
+
|
41
|
+
## Version
|
42
|
+
|
43
|
+
This check has been introduced in Theme Check 0.6.0.
|
44
|
+
|
45
|
+
## Resources
|
46
|
+
|
47
|
+
- [The Performance Inequality Gap](https://infrequently.org/2021/03/the-performance-inequality-gap/)
|
48
|
+
- [Rule Source][codesource]
|
49
|
+
- [Documentation Source][docsource]
|
50
|
+
|
51
|
+
[codesource]: /lib/theme_check/checks/asset_size_css.rb
|
52
|
+
[docsource]: /docs/checks/asset_size_css.md
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# Width and height attributes on image tags (`ImgWidthAndHeight`)
|
2
|
+
|
3
|
+
This check exists to prevent [cumulative layout shift][cls] (CLS) in themes.
|
4
|
+
|
5
|
+
The absence of `width` and `height` attributes on an `img` tag prevents the browser from knowing the aspect ratio of the image before it is downloaded. Unless another technique is used to allocate space, the browser will consider the image to be of height 0 until it is loaded.
|
6
|
+
|
7
|
+
This has numerous nefarious implications:
|
8
|
+
|
9
|
+
1. [This causes layout shift as images start appearing one after the other.][codepenshift] Text starts flying down the page as the image pushes it down.
|
10
|
+
2. [This breaks lazy loading.][codepenlazy] When all images have a height of 0px, every image is inside the viewport. And when everything is in the viewport, everything gets loaded. There's nothing lazy about it!
|
11
|
+
|
12
|
+
The fix is easy. Make sure the `width` and `height` attribute are set on the `img` tag and that the CSS width of the image is set.
|
13
|
+
|
14
|
+
Note: The width and height attributes of an image do not have units.
|
15
|
+
|
16
|
+
## Check Details
|
17
|
+
|
18
|
+
This check is aimed at eliminating content layout shift in themes by enforcing the use of the `width` and `height` attributes on `img` tags.
|
19
|
+
|
20
|
+
:-1: Examples of **incorrect** code for this check:
|
21
|
+
|
22
|
+
```liquid
|
23
|
+
<img alt="cat" src="cat.jpg">
|
24
|
+
<img alt="cat" src="cat.jpg" width="100px" height="100px">
|
25
|
+
<img alt="{{ image.alt }}" src="{{ image.src }}">
|
26
|
+
```
|
27
|
+
|
28
|
+
:+1: Examples of **correct** code for this check:
|
29
|
+
|
30
|
+
```liquid
|
31
|
+
<img alt="cat" src="cat.jpg" width="100" height="200">
|
32
|
+
<img
|
33
|
+
alt="{{ image.alt }}"
|
34
|
+
src="{{ image.src }}"
|
35
|
+
width="{{ image.width }}"
|
36
|
+
height="{{ image.height }}"
|
37
|
+
>
|
38
|
+
```
|
39
|
+
|
40
|
+
**NOTE:** The CSS `width` of the `img` should _also_ be set for the image to be responsive.
|
41
|
+
|
42
|
+
## Check Options
|
43
|
+
|
44
|
+
The default configuration for this check is the following:
|
45
|
+
|
46
|
+
```yaml
|
47
|
+
ImgWidthAndHeight:
|
48
|
+
enabled: true
|
49
|
+
```
|
50
|
+
|
51
|
+
## When Not To Use It
|
52
|
+
|
53
|
+
There are some cases where you can avoid content-layout shift without needing the width and height attributes:
|
54
|
+
|
55
|
+
- When the aspect-ratio of the displayed image should be independent of the uploaded image. In those cases, the solution is still the padding-top hack with an `overflow: hidden container`.
|
56
|
+
- When you are happy with the padding-top hack.
|
57
|
+
|
58
|
+
In those cases, it is fine to disable this check with the comment.
|
59
|
+
|
60
|
+
It is otherwise unwise to disable this check, since it would negatively impact the mobile search ranking of the merchants using your theme.
|
61
|
+
|
62
|
+
## Version
|
63
|
+
|
64
|
+
This check has been introduced in Theme Check 0.6.0.
|
65
|
+
|
66
|
+
## Resources
|
67
|
+
|
68
|
+
- [Cumulative Layout Shift Reference][cls]
|
69
|
+
- [Codepen illustrating the impact of width and height on layout shift][codepenshift]
|
70
|
+
- [Codepen illustrating the impact of width and height on lazy loading][codepenlazy]
|
71
|
+
- [Rule Source][codesource]
|
72
|
+
- [Documentation Source][docsource]
|
73
|
+
|
74
|
+
[cls]: https://web.dev/cls/
|
75
|
+
[codepenshift]: https://codepen.io/charlespwd/pen/YzpxPEp?editors=1100
|
76
|
+
[codepenlazy]: https://codepen.io/charlespwd/pen/abZmqXJ?editors=0111
|
77
|
+
[aspect-ratio]: https://caniuse.com/mdn-css_properties_aspect-ratio
|
78
|
+
[codesource]: /lib/theme_check/checks/img_aspect_ratio.rb
|
79
|
+
[docsource]: /docs/checks/img_aspect_ratio.md
|
@@ -31,13 +31,13 @@ This check is aimed at eliminating parser-blocking JavaScript on themes.
|
|
31
31
|
|
32
32
|
```liquid
|
33
33
|
<!-- Good. Using the asset_url filter + defer -->
|
34
|
-
<script src="{{ 'theme.js' | asset_url }}" defer
|
34
|
+
<script src="{{ 'theme.js' | asset_url }}" defer></script>
|
35
35
|
|
36
36
|
<!-- Also good. Using the asset_url filter + async -->
|
37
|
-
<script src="{{ 'theme.js' | asset_url }}" async
|
37
|
+
<script src="{{ 'theme.js' | asset_url }}" async></script>
|
38
38
|
|
39
39
|
<!-- Better than synchronous jQuery -->
|
40
|
-
<script src="https://code.jquery.com/jquery-3.6.0.min.js" defer
|
40
|
+
<script src="https://code.jquery.com/jquery-3.6.0.min.js" defer></script>
|
41
41
|
...
|
42
42
|
<button id="thing">Click me!</button>
|
43
43
|
<script>
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# Discourage use of third party domains for hosting assets (`RemoteAsset`)
|
2
|
+
|
3
|
+
Years ago, loading jQuery from a common CDN was good for performance because the browser cache could be reused across website. This is no longer true because browsers now include the domain from which the request was made in the cache key.
|
4
|
+
|
5
|
+
Therefore, this technique now makes things worse. Here's why:
|
6
|
+
|
7
|
+
* **The benefits of HTTP/2 prioritization are lost.** HTTP/2 prioritization is a mechanism used by servers. If different servers are used to deliver assets, there's no way to prioritize.
|
8
|
+
* **A new connection dance (DNS, TCP, TLS) must be done to start downloading the resource.** With HTTPS, this takes 5 round trips to achieve. The farther away the buyer is from that domain, the longer it takes.
|
9
|
+
* **The [slow start][slowstart] part of the Internet's TCP congestion control strategy must happen on every connection.** This means that the download "acceleration" we commonly observe must be repeated many times over.
|
10
|
+
|
11
|
+
The fix? Deliver as much as you can from a small number of connections. In a Shopify context, this is done by leveraging the `assets/` folder and the [URL filters][url_filters].
|
12
|
+
|
13
|
+
## Check Details
|
14
|
+
|
15
|
+
This check is aimed at eliminating unnecessary HTTP connections.
|
16
|
+
|
17
|
+
:-1: Examples of **incorrect** code for this check:
|
18
|
+
|
19
|
+
```liquid
|
20
|
+
<!-- Using multiple CDNs -->
|
21
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" defer></script>
|
22
|
+
{{ "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" | stylesheet_tag }}
|
23
|
+
<img src="https://example.com/heart.png" ...>
|
24
|
+
|
25
|
+
<!-- Missing img_url filter -->
|
26
|
+
<img src="{{ image }}" ...>
|
27
|
+
```
|
28
|
+
|
29
|
+
In the examples above, multiple connections are competing for resources, are accelerating download independently and are improperly prioritized.
|
30
|
+
|
31
|
+
:+1: Examples of **correct** code for this check:
|
32
|
+
|
33
|
+
```liquid
|
34
|
+
<!-- Good -->
|
35
|
+
<script src="{{ 'jquery.min.js' | asset_url }}" defer></script>
|
36
|
+
{{ 'bootstrap.min.css' | asset_url | stylesheet_tag }}
|
37
|
+
|
38
|
+
<!-- Better -->
|
39
|
+
<script src="{{ 'theme.js' | asset_url }}" defer></script>
|
40
|
+
{{ 'theme.css' | asset_url | stylesheet_tag }}
|
41
|
+
|
42
|
+
<!-- Images -->
|
43
|
+
<img src="{{ image | img_url }}" ...>
|
44
|
+
```
|
45
|
+
|
46
|
+
In the above, the JavaScript, CSS and images are all loading from the same connection. Making it so the browser and CDN can properly prioritize which assets are downloaded first while maintaining a "hot" connection that downloads fast.
|
47
|
+
|
48
|
+
This can be done by downloading the files from those CDNs directly into your theme's `assets/` folder.
|
49
|
+
|
50
|
+
Use the [`img_url` filter][img_url] for images.
|
51
|
+
|
52
|
+
## Check Options
|
53
|
+
|
54
|
+
The default configuration for this check is the following:
|
55
|
+
|
56
|
+
```yaml
|
57
|
+
RemoteAsset:
|
58
|
+
enabled: true
|
59
|
+
```
|
60
|
+
|
61
|
+
## When Not To Use It
|
62
|
+
|
63
|
+
When the remote content is highly dynamic.
|
64
|
+
|
65
|
+
## Version
|
66
|
+
|
67
|
+
This check has been introduced in Theme Check 0.7.0.
|
68
|
+
|
69
|
+
## Resources
|
70
|
+
|
71
|
+
- [Announcement by Google][googleprivacy]
|
72
|
+
- [HTTP Cache Partioning Explainer](https://github.com/shivanigithub/http-cache-partitioning)
|
73
|
+
- [Slow Start][slowstart]
|
74
|
+
- [Rule Source][codesource]
|
75
|
+
- [Documentation Source][docsource]
|
76
|
+
|
77
|
+
[googleprivacy]: https://developers.google.com/web/updates/2020/10/http-cache-partitioning#resources
|
78
|
+
[codesource]: /lib/theme_check/checks/remote_asset.rb
|
79
|
+
[docsource]: /docs/checks/remote_asset.md
|
80
|
+
[slowstart]: https://en.wikipedia.org/wiki/TCP_congestion_control#Slow_start
|
81
|
+
[url_filters]: https://shopify.dev/docs/themes/liquid/reference/filters/url-filters
|
82
|
+
[img_url]: https://shopify.dev/docs/themes/liquid/reference/filters/url-filters#img_url
|
data/exe/theme-check
CHANGED
data/lib/theme_check.rb
CHANGED
@@ -22,6 +22,7 @@ require_relative "theme_check/offense"
|
|
22
22
|
require_relative "theme_check/printer"
|
23
23
|
require_relative "theme_check/shopify_liquid"
|
24
24
|
require_relative "theme_check/storage"
|
25
|
+
require_relative "theme_check/string_helpers"
|
25
26
|
require_relative "theme_check/file_system_storage"
|
26
27
|
require_relative "theme_check/in_memory_storage"
|
27
28
|
require_relative "theme_check/tags"
|
data/lib/theme_check/check.rb
CHANGED
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
class AssetSizeCSS < LiquidCheck
|
4
|
+
include RegexHelpers
|
5
|
+
severity :error
|
6
|
+
category :performance
|
7
|
+
doc docs_url(__FILE__)
|
8
|
+
|
9
|
+
Link = Struct.new(:href, :index)
|
10
|
+
|
11
|
+
LINK_TAG_HREF = %r{
|
12
|
+
<link
|
13
|
+
(?=[^>]+?rel=['"]?stylesheet['"]?) # Make sure rel=stylesheet is in the link with lookahead
|
14
|
+
[^>]+ # any non closing tag character
|
15
|
+
href= # href attribute start
|
16
|
+
(?<href>#{QUOTED_LIQUID_ATTRIBUTE}) # href attribute value (may contain liquid)
|
17
|
+
[^>]* # any non closing character till the end
|
18
|
+
>
|
19
|
+
}omix
|
20
|
+
STYLESHEET_TAG = %r{
|
21
|
+
#{Liquid::VariableStart} # VariableStart
|
22
|
+
(?:(?!#{Liquid::VariableEnd}).)*? # anything that isn't followed by a VariableEnd
|
23
|
+
\|\s*asset_url\s* # | asset_url
|
24
|
+
\|\s*stylesheet_tag\s* # | stylesheet_tag
|
25
|
+
#{Liquid::VariableEnd} # VariableEnd
|
26
|
+
}omix
|
27
|
+
|
28
|
+
attr_reader :threshold_in_bytes
|
29
|
+
|
30
|
+
def initialize(threshold_in_bytes: 100_000)
|
31
|
+
@threshold_in_bytes = threshold_in_bytes
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_document(node)
|
35
|
+
@node = node
|
36
|
+
@source = node.template.source
|
37
|
+
record_offenses
|
38
|
+
end
|
39
|
+
|
40
|
+
def record_offenses
|
41
|
+
stylesheets(@source).each do |stylesheet|
|
42
|
+
file_size = href_to_file_size(stylesheet.href)
|
43
|
+
next if file_size.nil?
|
44
|
+
next if file_size <= threshold_in_bytes
|
45
|
+
add_offense(
|
46
|
+
"CSS on every page load exceding compressed size threshold (#{threshold_in_bytes} Bytes).",
|
47
|
+
node: @node,
|
48
|
+
markup: stylesheet.href,
|
49
|
+
line_number: @source[0...stylesheet.index].count("\n") + 1
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def stylesheets(source)
|
55
|
+
stylesheet_links = matches(source, LINK_TAG_HREF)
|
56
|
+
.map do |m|
|
57
|
+
Link.new(
|
58
|
+
m[:href].gsub(START_OR_END_QUOTE, ""),
|
59
|
+
m.begin(:href),
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
stylesheet_tags = matches(source, STYLESHEET_TAG)
|
64
|
+
.map do |m|
|
65
|
+
Link.new(
|
66
|
+
m[0],
|
67
|
+
m.begin(0),
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
stylesheet_links + stylesheet_tags
|
72
|
+
end
|
73
|
+
|
74
|
+
def href_to_file_size(href)
|
75
|
+
# asset_url (+ optional stylesheet_tag) variables
|
76
|
+
if href =~ /^#{VARIABLE}$/o && href =~ /asset_url/ && href =~ Liquid::QuotedString
|
77
|
+
asset_id = Regexp.last_match(0).gsub(START_OR_END_QUOTE, "")
|
78
|
+
asset = @theme.assets.find { |a| a.name.end_with?("/" + asset_id) }
|
79
|
+
return if asset.nil?
|
80
|
+
asset.gzipped_size
|
81
|
+
|
82
|
+
# remote URLs
|
83
|
+
elsif href =~ %r{^(https?:)?//}
|
84
|
+
asset = RemoteAssetFile.from_src(href)
|
85
|
+
asset.gzipped_size
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -11,17 +11,11 @@ module ThemeCheck
|
|
11
11
|
|
12
12
|
Script = Struct.new(:src, :match)
|
13
13
|
|
14
|
-
TAG = /#{Liquid::TagStart}.*?#{Liquid::TagEnd}/om
|
15
|
-
VARIABLE = /#{Liquid::VariableStart}.*?#{Liquid::VariableEnd}/om
|
16
|
-
START_OR_END_QUOTE = /(^['"])|(['"]$)/
|
17
14
|
SCRIPT_TAG_SRC = %r{
|
18
15
|
<script
|
19
16
|
[^>]+ # any non closing tag character
|
20
17
|
src= # src attribute start
|
21
|
-
(?<src
|
22
|
-
'(?:#{TAG}|#{VARIABLE}|[^']+)*'| # any combination of tag/variable or non straight quote inside straight quotes
|
23
|
-
"(?:#{TAG}|#{VARIABLE}|[^"]+)*" # any combination of tag/variable or non double quotes inside double quotes
|
24
|
-
)
|
18
|
+
(?<src>#{QUOTED_LIQUID_ATTRIBUTE}) # src attribute value (may contain liquid)
|
25
19
|
[^>]* # any non closing character till the end
|
26
20
|
>
|
27
21
|
}omix
|
@@ -62,7 +56,7 @@ module ThemeCheck
|
|
62
56
|
# More complicated liquid statements are not in scope.
|
63
57
|
if src =~ /^#{VARIABLE}$/o && src =~ /asset_url/ && src =~ Liquid::QuotedString
|
64
58
|
asset_id = Regexp.last_match(0).gsub(START_OR_END_QUOTE, "")
|
65
|
-
asset = @theme.assets.find { |a| a.name.
|
59
|
+
asset = @theme.assets.find { |a| a.name.end_with?("/" + asset_id) }
|
66
60
|
return if asset.nil?
|
67
61
|
asset.gzipped_size
|
68
62
|
elsif src =~ %r{^(https?:)?//}
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module ThemeCheck
|
3
|
+
# Reports errors when trying to use parser-blocking script tags
|
4
|
+
class ImgWidthAndHeight < LiquidCheck
|
5
|
+
include RegexHelpers
|
6
|
+
severity :error
|
7
|
+
categories :liquid, :performance
|
8
|
+
doc docs_url(__FILE__)
|
9
|
+
|
10
|
+
# Not implemented with lookbehinds and lookaheads because performance was shit!
|
11
|
+
IMG_TAG = %r{<img#{HTML_ATTRIBUTES}/?>}oxim
|
12
|
+
SRC_ATTRIBUTE = /\s(src)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
|
13
|
+
WIDTH_ATTRIBUTE = /\s(width)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
|
14
|
+
HEIGHT_ATTRIBUTE = /\s(height)=(#{QUOTED_LIQUID_ATTRIBUTE})/oxim
|
15
|
+
|
16
|
+
FIELDS = [WIDTH_ATTRIBUTE, HEIGHT_ATTRIBUTE]
|
17
|
+
ENDS_IN_CSS_UNIT = /(cm|mm|in|px|pt|pc|em|ex|ch|rem|vw|vh|vmin|vmax|%)$/i
|
18
|
+
|
19
|
+
def on_document(node)
|
20
|
+
@source = node.template.source
|
21
|
+
@node = node
|
22
|
+
record_offenses
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def record_offenses
|
28
|
+
matches(@source, IMG_TAG).each do |img_match|
|
29
|
+
next unless img_match[0] =~ SRC_ATTRIBUTE
|
30
|
+
record_missing_field_offenses(img_match)
|
31
|
+
record_units_in_field_offenses(img_match)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def record_missing_field_offenses(img_match)
|
36
|
+
width = WIDTH_ATTRIBUTE.match(img_match[0])
|
37
|
+
height = HEIGHT_ATTRIBUTE.match(img_match[0])
|
38
|
+
return if width && height
|
39
|
+
missing_width = width.nil?
|
40
|
+
missing_height = height.nil?
|
41
|
+
error_message = if missing_width && missing_height
|
42
|
+
"Missing width and height attributes"
|
43
|
+
elsif missing_width
|
44
|
+
"Missing width attribute"
|
45
|
+
elsif missing_height
|
46
|
+
"Missing height attribute"
|
47
|
+
end
|
48
|
+
|
49
|
+
add_offense(
|
50
|
+
error_message,
|
51
|
+
node: @node,
|
52
|
+
markup: img_match[0],
|
53
|
+
line_number: @source[0...img_match.begin(0)].count("\n") + 1
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def record_units_in_field_offenses(img_match)
|
58
|
+
FIELDS.each do |field|
|
59
|
+
field_match = field.match(img_match[0])
|
60
|
+
next if field_match.nil?
|
61
|
+
value = field_match[2].gsub(START_OR_END_QUOTE, '')
|
62
|
+
next unless value =~ ENDS_IN_CSS_UNIT
|
63
|
+
value_without_units = value.gsub(ENDS_IN_CSS_UNIT, '')
|
64
|
+
start = img_match.begin(0) + field_match.begin(2)
|
65
|
+
add_offense(
|
66
|
+
"The #{field_match[1]} attribute does not take units. Replace with \"#{value_without_units}\".",
|
67
|
+
node: @node,
|
68
|
+
markup: value,
|
69
|
+
line_number: @source[0...start].count("\n") + 1
|
70
|
+
)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|