jekyll_picture_tag 1.9.0 → 1.10.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.
@@ -34,6 +34,24 @@ module PictureTag
34
34
  @source_images ||= build_source_images
35
35
  end
36
36
 
37
+ def crop(media = nil)
38
+ params.geometries[media] || preset.crop(media)
39
+ end
40
+
41
+ def gravity(media = nil)
42
+ params.gravities[media] || preset.gravity(media)
43
+ end
44
+
45
+ # Returns a class constant for the selected output format, which is used
46
+ # to dynamically instantiate it.
47
+ def output_class
48
+ Object.const_get(
49
+ 'PictureTag::OutputFormats::' + Utils.titleize(preset['markup'])
50
+ )
51
+ end
52
+
53
+ private
54
+
37
55
  def build_source_images
38
56
  source_names = params.source_names
39
57
  media_presets = params.media_presets
@@ -48,14 +66,6 @@ module PictureTag
48
66
 
49
67
  sources
50
68
  end
51
-
52
- # Returns a class constant for the selected output format, which is used
53
- # to dynamically instantiate it.
54
- def output_class
55
- Object.const_get(
56
- 'PictureTag::OutputFormats::' + Utils.titleize(preset['markup'])
57
- )
58
- end
59
69
  end
60
70
  end
61
71
  end
@@ -1,105 +1,95 @@
1
1
  module PictureTag
2
2
  module Instructions
3
- # This tag takes the arguments handed to the liquid tag, and extracts the
4
- # preset name (if present), source image name(s), and associated media
5
- # queries (if present). The leftovers (html attributes) are handed off to
6
- # its respective class.
3
+ # Tag Parsing Responsibilities:
4
+ #
5
+ # {% picture mypreset a.jpg 3:2 mobile: b.jpg --alt "Alt" --link "/" %}
6
+ # | Jekyll | TagParser | HTMLAttributes |
7
+ #
8
+ # This class takes the arguments handed to the liquid tag (given as a simple
9
+ # string), hands them to ArgSplitter (which breaks them up into an array of
10
+ # words), extracts the preset name (if present), source image name(s),
11
+ # associated media queries (if present), and image-related arguments such as
12
+ # crop and gravity. HTML attributes are handed off to its respective class
13
+ # (as 'leftovers')
14
+ #
15
+ # Media presets and source names are stored as arrays in their correct
16
+ # orders. Gravities and geometries are stored in a hash, keyed by their
17
+ # relevant media presets. Note that the base image will have a media preset
18
+ # of nil, which is a perfectly fine hash key.
19
+ #
7
20
  class TagParser
8
- attr_reader :preset_name, :source_names, :media_presets
9
- def initialize(raw_params)
10
- build_params PictureTag::Utils.liquid_lookup(raw_params)
21
+ attr_reader :preset_name, :source_names, :media_presets, :gravities,
22
+ :geometries, :leftovers
11
23
 
12
- @preset_name = grab_preset_name
24
+ def initialize(raw_params)
25
+ @raw_params = raw_params
26
+ @params = split_params
13
27
 
14
- # The first param specified will be our base image, so it has no
15
- # associated media query.
16
28
  @media_presets = []
17
- @source_names = [] << strip_quotes(@params.shift)
29
+ @source_names = []
30
+ @geometries = {}
31
+ @gravities = {}
18
32
 
19
- # Detect and store arguments of the format 'media_query: img.jpg' as
20
- # keys and values.
21
- add_media_source while @params.first =~ /[\w\-]+:$/
22
- end
23
-
24
- def leftovers
25
- @params
33
+ parse_params
26
34
  end
27
35
 
28
36
  private
29
37
 
30
- def add_media_source
31
- @media_presets << @params.shift.delete_suffix(':')
32
- @source_names << strip_quotes(@params.shift)
38
+ def split_params
39
+ ArgSplitter
40
+ .new(Utils.liquid_lookup(@raw_params))
41
+ .words
33
42
  end
34
43
 
35
- # First param is the preset name, unless it's a filename.
36
- def grab_preset_name
37
- if @params.first.include? '.'
38
- 'default'
39
- else
40
- @params.shift
41
- end
42
- end
43
-
44
- # Originally separating arguments was just handled by splitting the raw
45
- # params on spaces. To handle quotes and backslash escaping, we have to
46
- # parse the string by characters to break it up correctly. I'm sure
47
- # there's a library to do this, but it's not that much code honestly. If
48
- # this starts getting big, we'll pull in a new dependency.
49
- def build_params(raw_params)
50
- @params = []
51
- @word = ''
52
- @in_quotes = false
53
- @escaped = false
44
+ def parse_params
45
+ @preset_name = determine_preset_name
46
+ @source_names << strip_quotes(@params.shift)
54
47
 
55
- raw_params.each_char { |c| handle_char(c) }
48
+ parse_param(@params.first) until stop_here? @params.first
56
49
 
57
- add_word # We have to explicitly add the last one.
50
+ @leftovers = @params
58
51
  end
59
52
 
60
- def handle_char(char)
61
- # last character was a backslash:
62
- if @escaped
63
- close_escape char
53
+ def parse_param(param)
54
+ if param.match?(/[\w\-]+:$/)
55
+ add_media_source
64
56
 
65
- # char is a backslash or a quote:
66
- elsif char.match(/["\\]/)
67
- handle_special char
57
+ elsif Utils::GRAVITIES.include?(param.downcase)
58
+ @gravities[@media_presets.last] = @params.shift
68
59
 
69
- # Character isn't whitespace, or it's inside double quotes:
70
- elsif @in_quotes || char.match(/\S/)
71
- @word << char
60
+ elsif param.match?(Utils::GEOMETRY_REGEX)
61
+ @geometries[@media_presets.last] = @params.shift
72
62
 
73
- # Character is whitespace outside of double quotes:
74
63
  else
75
- add_word
64
+ raise_error(param)
76
65
  end
77
66
  end
78
67
 
79
- def handle_special(char)
80
- if char == '\\'
81
- @escaped = true
82
- elsif char == '"'
83
- @in_quotes = !@in_quotes
84
- @word << char
85
- end
68
+ # HTML attributes are handled by its own class; once we encounter them
69
+ # we are finished here.
70
+ def stop_here?(param)
71
+ # No param Explicit HTML attribute Implicit HTML attribute
72
+ param.nil? || param.match?(/^--\S*/) || param.match?(/^\w*="/)
86
73
  end
87
74
 
88
- def add_word
89
- return if @word.empty?
90
-
91
- @params << @word
92
- @word = ''
75
+ def add_media_source
76
+ @media_presets << @params.shift.delete_suffix(':')
77
+ @source_names << strip_quotes(@params.shift)
93
78
  end
94
79
 
95
- def close_escape(char)
96
- @word << char
97
- @escaped = false
80
+ # The first param is the preset name, unless it's a filename.
81
+ def determine_preset_name
82
+ @params.first.include?('.') ? 'default' : @params.shift
98
83
  end
99
84
 
100
85
  def strip_quotes(name)
101
86
  name.delete_prefix('"').delete_suffix('"')
102
87
  end
88
+
89
+ def raise_error(param)
90
+ raise ArgumentError, "Could not parse '#{param}' in the following "\
91
+ "tag: \n {% picture #{@raw_params} %}"
92
+ end
103
93
  end
104
94
  end
105
95
  end
@@ -71,22 +71,28 @@ module PictureTag
71
71
 
72
72
  # GeneratedImage class, not HTML
73
73
  def build_fallback_image
74
- fallback = GeneratedImage.new(
74
+ return fallback_candidate if fallback_candidate.exists?
75
+
76
+ build_new_fallback_image
77
+ end
78
+
79
+ def fallback_candidate
80
+ @fallback_candidate ||= GeneratedImage.new(
75
81
  source_file: PictureTag.source_images.first,
76
82
  format: PictureTag.fallback_format,
77
- width: PictureTag.fallback_width
83
+ width: PictureTag.fallback_width,
84
+ crop: PictureTag.crop,
85
+ gravity: PictureTag.gravity
78
86
  )
79
-
80
- return fallback if fallback.exists?
81
-
82
- build_new_fallback_image
83
87
  end
84
88
 
85
89
  def build_new_fallback_image
86
90
  GeneratedImage.new(
87
91
  source_file: PictureTag.source_images.first,
88
92
  format: PictureTag.fallback_format,
89
- width: checked_fallback_width
93
+ width: checked_fallback_width,
94
+ crop: PictureTag.crop,
95
+ gravity: PictureTag.gravity
90
96
  )
91
97
  end
92
98
 
@@ -107,15 +113,26 @@ module PictureTag
107
113
  content.add_parent anchor
108
114
  end
109
115
 
116
+ def source
117
+ PictureTag.source_images.first
118
+ end
119
+
120
+ def source_width
121
+ if PictureTag.crop
122
+ fallback_candidate.cropped_source_width
123
+ else
124
+ source.width
125
+ end
126
+ end
127
+
110
128
  def checked_fallback_width
111
- source = PictureTag.source_images.first
112
129
  target = PictureTag.fallback_width
113
130
 
114
- if target > source.width
131
+ if target > source_width
115
132
  Utils.warning "#{source.shortname} is smaller than the " \
116
- "requested fallback width of #{target}px. Using #{source.width}" \
133
+ "requested fallback width of #{target}px. Using #{source_width}" \
117
134
  ' px instead.'
118
- source.width
135
+ source_width
119
136
  else
120
137
  target
121
138
  end
@@ -47,6 +47,14 @@ module PictureTag
47
47
  @instructions.source_images
48
48
  end
49
49
 
50
+ def crop(media = nil)
51
+ @instructions.crop(media)
52
+ end
53
+
54
+ def gravity(media = nil)
55
+ @instructions.gravity(media)
56
+ end
57
+
50
58
  # Config Forwarding
51
59
 
52
60
  def source_dir
@@ -65,26 +65,37 @@ module PictureTag
65
65
  end
66
66
 
67
67
  def checked_targets
68
- if target_files.any? { |f| f.width > @source_image.width }
68
+ if target_files.any? { |f| f.width > source_width }
69
69
 
70
70
  small_source_warn
71
71
 
72
- files = target_files.reject { |f| f.width >= @source_image.width }
73
- files.push(generate_file(@source_image.width))
72
+ files = target_files.reject { |f| f.width >= source_width }
73
+ files.push(generate_file(source_width))
74
74
  end
75
75
 
76
76
  files || target_files
77
77
  end
78
78
 
79
+ def source_width
80
+ @source_width ||= if PictureTag.crop(@media)
81
+ target_files.first.cropped_source_width
82
+ else
83
+ @source_image.width
84
+ end
85
+ end
86
+
79
87
  def target_files
80
88
  @target_files ||= widths.collect { |w| generate_file(w) }
81
89
  end
82
90
 
83
91
  def small_source_warn
84
92
  Utils.warning(
85
- " #{@source_image.shortname} is #{@source_image.width}px wide,"\
86
- " smaller than at least one size in the set #{widths}. Will not"\
87
- ' enlarge.'
93
+ <<~HEREDOC
94
+ #{@source_image.shortname}
95
+ is #{source_width}px wide (after cropping, if applicable),
96
+ smaller than at least one size in the set #{widths}.
97
+ Will not enlarge.
98
+ HEREDOC
88
99
  )
89
100
  end
90
101
 
@@ -92,7 +103,9 @@ module PictureTag
92
103
  GeneratedImage.new(
93
104
  source_file: @source_image,
94
105
  width: width,
95
- format: @input_format
106
+ format: @input_format,
107
+ crop: PictureTag.crop(@media),
108
+ gravity: PictureTag.gravity(@media)
96
109
  )
97
110
  end
98
111
  end
@@ -2,6 +2,24 @@ module PictureTag
2
2
  # This is a little module to hold logic that doesn't fit other places. If it
3
3
  # starts getting big, refactor.
4
4
  module Utils
5
+ # These are valid ImageMagick gravity arguments (relevant to our use
6
+ # case):
7
+ GRAVITIES =
8
+ %w[center
9
+ north
10
+ northeast
11
+ east
12
+ southeast
13
+ south
14
+ southwest
15
+ west
16
+ northwest].freeze
17
+
18
+ # This is an attempt to recognize valid imagemagick geometry arguments
19
+ # with regex. It only tries to match values which don't preserve aspect
20
+ # ratio, as they're the ones people might actually need here.
21
+ GEOMETRY_REGEX = /\A\d*%?[x:]?\d*[%!]?([+-]\d+){,2}\Z/i.freeze
22
+
5
23
  class << self
6
24
  # Configure Jekyll to keep our generated files
7
25
  def keep_files
@@ -1,3 +1,3 @@
1
1
  module PictureTag
2
- VERSION = '1.9.0'.freeze
2
+ VERSION = '1.10.0'.freeze
3
3
  end
data/readme.md CHANGED
@@ -1,21 +1,20 @@
1
1
  # Jekyll Picture Tag
2
2
 
3
- **Easy responsive images for Jekyll.**
3
+ **Responsive Images done correctly.**
4
4
 
5
- It's easy to throw an image on a webpage and call it a day. Doing justice to your users by serving
6
- it efficiently on all browsers and screen sizes is tedious and tricky. Tedious, tricky things should be
7
- automated.
5
+ It's simple to throw a photo on a page and call it a day, but doing justice to users on all
6
+ different browsers and devices is tedious and tricky. Tedious, tricky things should be automated.
8
7
 
9
- Jekyll Picture Tag adds responsive images to your [Jekyll](http://jekyllrb.com) static site. It
10
- automatically creates resized, reformatted source images, is fully configurable, implements sensible
11
- defaults, and solves both the art direction and resolution switching problems with a little YAML
12
- configuration and a simple template tag.
8
+ Jekyll Picture Tag automatically builds cropped, resized, and reformatted images, builds several
9
+ kinds of markup, offers extensive configuration while requiring none, and solves both the art
10
+ direction and resolution switching problems with a little YAML configuration and a simple template
11
+ tag.
13
12
 
14
13
  ## Why use Responsive Images?
15
14
 
16
- **Performance:** The fastest sites are static sites, but when you plonk a 2mb picture of your dog at
17
- the top of a blog post you're throwing it all away. Responsive images allow you to keep your site
18
- fast, without compromising image quality.
15
+ **Performance:** The fastest sites are static sites, but if you plonk a 2mb picture of your dog at
16
+ the top of a blog post you throw it all away. Responsive images allow you to keep your site fast,
17
+ without compromising image quality.
19
18
 
20
19
  **Design:** Your desktop image may not work well on mobile, regardless of its resolution. We often
21
20
  want to do more than just resize images for different screen sizes, we want to crop them or use a
@@ -24,15 +23,35 @@ different image entirely.
24
23
  ## Why use Jekyll Picture Tag?
25
24
 
26
25
  **Developer Sanity:** If you want to serve multiple images in multiple formats and resolutions, you
27
- have a litany of markup to write and a big pile of images to generate. Jekyll Picture Tag is your
28
- responsive images minion - give it simple instructions and it'll handle the rest.
26
+ have a litany of markup to write and a big pile of images to generate and organize. Jekyll Picture
27
+ Tag is your responsive images minion - give it simple instructions and it'll handle the rest.
29
28
 
30
29
  ## Features
31
30
 
32
- * Automatic generation and organization of resized image files in basically any format(s).
33
- * Automatic generation of complex markup in several different formats.
34
- * No configuration required, extensive configuration available.
31
+ * Generate piles of cropped, resized, and converted image files.
32
+ * Generate corresponding markup in several different formats.
33
+ * Configure it easily, or not at all.
34
+ * Make Lighthouse happy.
35
35
 
36
36
  ## Documentation:
37
37
 
38
38
  https://rbuchberger.github.io/jekyll_picture_tag/
39
+
40
+ ## Changelog:
41
+
42
+ https://rbuchberger.github.io/jekyll_picture_tag/releases
43
+
44
+ Latest version:
45
+
46
+ * 1.10.0 May 11, 2020
47
+ * **Image Cropping support!** access the power of ImageMagick's `crop` function.
48
+ * Don't issue a warning when `default` preset is not found.
49
+ * Documentation improvements
50
+
51
+ ## Help Wanted
52
+
53
+ Writing code is only part of the job; often the harder part is knowing what needs to be changed. Any
54
+ and all feedback is greatly appreciated, especially in regards to documentation. What are your pain
55
+ points? See the [contributing
56
+ guidelines](https://rbuchberger.github.io/jekyll_picture_tag/contributing), or the
57
+ [issues](https://github.com/rbuchberger/jekyll_picture_tag/issues) page for more.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll_picture_tag
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Wierzbowski
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2020-02-04 00:00:00.000000000 Z
13
+ date: 2020-05-11 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: bundler
@@ -152,6 +152,20 @@ dependencies:
152
152
  - - "~>"
153
153
  - !ruby/object:Gem::Version
154
154
  version: '2.6'
155
+ - !ruby/object:Gem::Dependency
156
+ name: base32
157
+ requirement: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - "~>"
160
+ - !ruby/object:Gem::Version
161
+ version: '0.3'
162
+ type: :runtime
163
+ prerelease: false
164
+ version_requirements: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - "~>"
167
+ - !ruby/object:Gem::Version
168
+ version: '0.3'
155
169
  - !ruby/object:Gem::Dependency
156
170
  name: mime-types
157
171
  requirement: !ruby/object:Gem::Requirement
@@ -186,14 +200,14 @@ dependencies:
186
200
  requirements:
187
201
  - - "~>"
188
202
  - !ruby/object:Gem::Version
189
- version: 1.1.1
203
+ version: 1.1.2
190
204
  type: :runtime
191
205
  prerelease: false
192
206
  version_requirements: !ruby/object:Gem::Requirement
193
207
  requirements:
194
208
  - - "~>"
195
209
  - !ruby/object:Gem::Version
196
- version: 1.1.1
210
+ version: 1.1.2
197
211
  - !ruby/object:Gem::Dependency
198
212
  name: jekyll
199
213
  requirement: !ruby/object:Gem::Requirement
@@ -225,6 +239,7 @@ files:
225
239
  - ".ruby-version"
226
240
  - ".solargraph.yml"
227
241
  - ".travis.yml"
242
+ - Dockerfile
228
243
  - Gemfile
229
244
  - LICENSE.txt
230
245
  - Rakefile
@@ -253,6 +268,7 @@ files:
253
268
  - lib/jekyll_picture_tag/generated_image.rb
254
269
  - lib/jekyll_picture_tag/img_uri.rb
255
270
  - lib/jekyll_picture_tag/instructions.rb
271
+ - lib/jekyll_picture_tag/instructions/arg_splitter.rb
256
272
  - lib/jekyll_picture_tag/instructions/configuration.rb
257
273
  - lib/jekyll_picture_tag/instructions/html_attributes.rb
258
274
  - lib/jekyll_picture_tag/instructions/preset.rb
@@ -301,7 +317,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
301
317
  - !ruby/object:Gem::Version
302
318
  version: '0'
303
319
  requirements: []
304
- rubygems_version: 3.0.4
320
+ rubygems_version: 3.1.3
305
321
  signing_key:
306
322
  specification_version: 4
307
323
  summary: Easy responsive images for Jekyll.