jekyll_picture_tag 1.7.1 → 1.10.2

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.
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