jekyll_picture_tag 1.7.1 → 1.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +4 -7
  3. data/Dockerfile +9 -0
  4. data/docs/Gemfile.lock +183 -88
  5. data/docs/contributing.md +50 -16
  6. data/docs/example_presets.md +1 -1
  7. data/docs/global_configuration.md +55 -2
  8. data/docs/index.md +26 -20
  9. data/docs/installation.md +22 -7
  10. data/docs/notes.md +11 -1
  11. data/docs/output.md +32 -21
  12. data/docs/presets.md +109 -54
  13. data/docs/releases.md +17 -1
  14. data/docs/usage.md +68 -38
  15. data/jekyll_picture_tag.gemspec +2 -1
  16. data/lib/jekyll_picture_tag.rb +27 -10
  17. data/lib/jekyll_picture_tag/defaults/global.yml +2 -0
  18. data/lib/jekyll_picture_tag/defaults/presets.yml +2 -0
  19. data/lib/jekyll_picture_tag/generated_image.rb +105 -19
  20. data/lib/jekyll_picture_tag/instructions.rb +1 -0
  21. data/lib/jekyll_picture_tag/instructions/arg_splitter.rb +68 -0
  22. data/lib/jekyll_picture_tag/instructions/configuration.rb +47 -11
  23. data/lib/jekyll_picture_tag/instructions/preset.rb +34 -14
  24. data/lib/jekyll_picture_tag/instructions/set.rb +18 -8
  25. data/lib/jekyll_picture_tag/instructions/tag_parser.rb +59 -69
  26. data/lib/jekyll_picture_tag/output_formats/basic.rb +36 -7
  27. data/lib/jekyll_picture_tag/output_formats/data_attributes.rb +4 -1
  28. data/lib/jekyll_picture_tag/router.rb +16 -0
  29. data/lib/jekyll_picture_tag/source_image.rb +6 -1
  30. data/lib/jekyll_picture_tag/srcsets/basic.rb +45 -19
  31. data/lib/jekyll_picture_tag/srcsets/pixel_ratio.rb +1 -3
  32. data/lib/jekyll_picture_tag/srcsets/width.rb +1 -1
  33. data/lib/jekyll_picture_tag/utils.rb +18 -0
  34. data/lib/jekyll_picture_tag/version.rb +1 -1
  35. data/readme.md +39 -16
  36. metadata +24 -8
@@ -3,3 +3,4 @@ require_relative './instructions/configuration'
3
3
  require_relative './instructions/html_attributes'
4
4
  require_relative './instructions/preset'
5
5
  require_relative './instructions/tag_parser'
6
+ require_relative './instructions/arg_splitter'
@@ -0,0 +1,68 @@
1
+ module PictureTag
2
+ module Instructions
3
+ # This class takes in the arguments passed to the liquid tag, and splits it
4
+ # up into 'words' (correctly handling quotes and backslash escapes.)
5
+ #
6
+ # To handle quotes and backslash escaping, we have to parse the string by
7
+ # characters to break it up correctly. I'm sure there's a library to do
8
+ # this, but it's not that much code honestly. If this starts getting big,
9
+ # we'll pull in a new dependency.
10
+ #
11
+ class ArgSplitter
12
+ attr_reader :words
13
+
14
+ def initialize(raw_params)
15
+ @words = []
16
+ @word = ''
17
+ @in_quotes = false
18
+ @escaped = false
19
+
20
+ raw_params.each_char { |c| handle_char(c) }
21
+
22
+ add_word # We have to explicitly add the last one.
23
+ end
24
+
25
+ private
26
+
27
+ def handle_char(char)
28
+ # last character was a backslash:
29
+ if @escaped
30
+ close_escape char
31
+
32
+ # char is a backslash or a quote:
33
+ elsif char.match?(/["\\]/)
34
+ handle_special char
35
+
36
+ # Character isn't whitespace, or it's inside double quotes:
37
+ elsif @in_quotes || char.match(/\S/)
38
+ @word << char
39
+
40
+ # Character is whitespace outside of double quotes:
41
+ else
42
+ add_word
43
+ end
44
+ end
45
+
46
+ def add_word
47
+ return if @word.empty?
48
+
49
+ @words << @word
50
+ @word = ''
51
+ end
52
+
53
+ def handle_special(char)
54
+ if char == '\\'
55
+ @escaped = true
56
+ elsif char == '"'
57
+ @in_quotes = !@in_quotes
58
+ @word << char
59
+ end
60
+ end
61
+
62
+ def close_escape(char)
63
+ @word << char
64
+ @escaped = false
65
+ end
66
+ end
67
+ end
68
+ end
@@ -30,7 +30,7 @@ module PictureTag
30
30
  # source_dest is the jekyll-picture-tag destination directory. (generated
31
31
  # file location setting.)
32
32
  def dest_dir
33
- File.join PictureTag.site.dest, pconfig['output']
33
+ File.join PictureTag.site.config['destination'], pconfig['output']
34
34
  end
35
35
 
36
36
  def nomarkdown?
@@ -38,28 +38,64 @@ module PictureTag
38
38
  end
39
39
 
40
40
  def continue_on_missing?
41
- setting = pconfig['ignore_missing_images']
42
-
43
- # Config setting can be a string, an array, or a boolean
44
- if setting.is_a? Array
45
- setting.include? jekyll_env
46
- elsif setting.is_a? String
47
- setting == jekyll_env
48
- else
49
- setting
50
- end
41
+ env_check pconfig['ignore_missing_images']
42
+ rescue ArgumentError
43
+ raise ArgumentError,
44
+ <<~HEREDOC
45
+ continue_on_missing setting invalid. Must be either a boolean
46
+ (true/false), an environment name, or an array of environment
47
+ names.
48
+ HEREDOC
51
49
  end
52
50
 
53
51
  def cdn?
54
52
  pconfig['cdn_url'] && pconfig['cdn_environments'].include?(jekyll_env)
55
53
  end
56
54
 
55
+ def disabled?
56
+ env_check pconfig['disabled']
57
+ rescue ArgumentError
58
+ raise ArgumentError,
59
+ <<~HEREDOC
60
+ "disabled" setting invalid. Must be either a boolean
61
+ (true/false), an environment name, or an array of environment
62
+ names.
63
+ HEREDOC
64
+ end
65
+
66
+ def fast_build?
67
+ env_check pconfig['fast_build']
68
+ rescue ArgumentError
69
+ raise ArgumentError,
70
+ <<~HEREDOC
71
+ "fast_build" setting invalid. Must be either a boolean
72
+ (true/false), an environment name, or an array of environment
73
+ names.
74
+ HEREDOC
75
+ end
76
+
57
77
  private
58
78
 
59
79
  def content
60
80
  @content ||= setting_merge(defaults, PictureTag.site.config)
61
81
  end
62
82
 
83
+ # There are a few config settings which can either be booleans,
84
+ # environment names, or arrays of environment names. This method works it
85
+ # out and returns a boolean.
86
+ def env_check(setting)
87
+ if setting.is_a? Array
88
+ setting.include? jekyll_env
89
+ elsif setting.is_a? String
90
+ setting == jekyll_env
91
+ elsif [true, false].include? setting
92
+ setting
93
+ else
94
+ raise ArgumentError,
95
+ "#{setting} must either be a string, an array, or a boolean."
96
+ end
97
+ end
98
+
63
99
  def setting_merge(default, jekyll)
64
100
  jekyll.merge default do |_key, config_setting, default_setting|
65
101
  if default_setting.respond_to? :merge
@@ -12,13 +12,6 @@ module PictureTag
12
12
  @content[key]
13
13
  end
14
14
 
15
- # Returns the set of widths to use for a given media query.
16
- def widths(media)
17
- width_hash = self['media_widths'] || {}
18
- width_hash.default = self['widths']
19
- width_hash[media]
20
- end
21
-
22
15
  def formats
23
16
  @content['formats']
24
17
  end
@@ -41,15 +34,38 @@ module PictureTag
41
34
  end
42
35
  end
43
36
 
37
+ # Image widths to generate for a given media query.
38
+ def widths(media = nil)
39
+ setting_lookup('widths', 'media', media)
40
+ end
41
+
42
+ # Image quality setting, possibly dependent on format.
44
43
  def quality(format = nil)
45
- qualities = @content['format_quality'] || {}
46
- qualities.default = @content['quality']
44
+ setting_lookup('quality', 'format', format)
45
+ end
46
+
47
+ # Gravity setting (for imagemagick cropping)
48
+ def gravity(media = nil)
49
+ setting_lookup('gravity', 'media', media)
50
+ end
47
51
 
48
- qualities[format]
52
+ # Crop value
53
+ def crop(media = nil)
54
+ setting_lookup('crop', 'media', media)
49
55
  end
50
56
 
51
57
  private
52
58
 
59
+ # Return arbitrary setting values, taking their defaults into account.
60
+ # Ex: quality can be set for all image formats, or individually per
61
+ # format. Per-format settings should override the general setting.
62
+ def setting_lookup(setting, prefix, lookup)
63
+ media_values = @content[prefix + '_' + setting] || {}
64
+ media_values.default = @content[setting]
65
+
66
+ media_values[lookup]
67
+ end
68
+
53
69
  def build_preset
54
70
  # The _data/picture.yml file is optional.
55
71
  picture_data_file = grab_data_file
@@ -70,10 +86,14 @@ module PictureTag
70
86
  end
71
87
 
72
88
  def no_preset
73
- Utils.warning(
74
- " Preset \"#{@name}\" not found in #{PictureTag.config['data_dir']}/"\
75
- + 'picture.yml under markup_presets key. Using default values.'
76
- )
89
+ unless @name == 'default'
90
+ Utils.warning(
91
+ <<~HEREDOC
92
+ Preset "#{@name}" not found in {PictureTag.config['data_dir']}/picture.yml
93
+ under markup_presets key. Using default values."
94
+ HEREDOC
95
+ )
96
+ end
77
97
 
78
98
  {}
79
99
  end
@@ -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
@@ -69,12 +69,30 @@ module PictureTag
69
69
  element.media = srcset.media_attribute if srcset.media
70
70
  end
71
71
 
72
- # File, not HTML
72
+ # GeneratedImage class, not HTML
73
73
  def build_fallback_image
74
- GeneratedImage.new(
74
+ return fallback_candidate if fallback_candidate.exists?
75
+
76
+ image = GeneratedImage.new(
77
+ source_file: PictureTag.source_images.first,
78
+ format: PictureTag.fallback_format,
79
+ width: checked_fallback_width,
80
+ crop: PictureTag.crop,
81
+ gravity: PictureTag.gravity
82
+ )
83
+
84
+ image.generate
85
+
86
+ image
87
+ end
88
+
89
+ def fallback_candidate
90
+ @fallback_candidate ||= GeneratedImage.new(
75
91
  source_file: PictureTag.source_images.first,
76
92
  format: PictureTag.fallback_format,
77
- width: checked_fallback_width
93
+ width: PictureTag.fallback_width,
94
+ crop: PictureTag.crop,
95
+ gravity: PictureTag.gravity
78
96
  )
79
97
  end
80
98
 
@@ -95,15 +113,26 @@ module PictureTag
95
113
  content.add_parent anchor
96
114
  end
97
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
+
98
128
  def checked_fallback_width
99
- source = PictureTag.source_images.first
100
129
  target = PictureTag.fallback_width
101
130
 
102
- if target > source.width
131
+ if target > source_width
103
132
  Utils.warning "#{source.shortname} is smaller than the " \
104
- "requested fallback width of #{target}px. Using #{source.width}" \
133
+ "requested fallback width of #{target}px. Using #{source_width}" \
105
134
  ' px instead.'
106
- source.width
135
+ source_width
107
136
  else
108
137
  target
109
138
  end