jekyll_picture_tag 1.7.0 → 1.10.1

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.solargraph.yml +15 -0
  4. data/.travis.yml +4 -7
  5. data/Dockerfile +9 -0
  6. data/docs/Gemfile +4 -0
  7. data/docs/Gemfile.lock +249 -0
  8. data/docs/_config.yml +13 -0
  9. data/docs/_layouts/directory.html +32 -0
  10. data/docs/assets/style.css +31 -0
  11. data/{contributing.md → docs/contributing.md} +57 -15
  12. data/docs/{examples/_data/picture.yml → example_presets.md} +36 -25
  13. data/docs/global_configuration.md +61 -3
  14. data/docs/index.md +114 -0
  15. data/docs/installation.md +36 -21
  16. data/docs/migration.md +4 -0
  17. data/docs/notes.md +64 -58
  18. data/docs/output.md +63 -0
  19. data/docs/presets.md +175 -221
  20. data/docs/releases.md +64 -0
  21. data/docs/usage.md +91 -79
  22. data/jekyll_picture_tag.gemspec +3 -1
  23. data/lib/jekyll_picture_tag.rb +27 -10
  24. data/lib/jekyll_picture_tag/defaults/global.yml +2 -0
  25. data/lib/jekyll_picture_tag/defaults/presets.yml +2 -0
  26. data/lib/jekyll_picture_tag/generated_image.rb +105 -19
  27. data/lib/jekyll_picture_tag/instructions.rb +1 -0
  28. data/lib/jekyll_picture_tag/instructions/arg_splitter.rb +68 -0
  29. data/lib/jekyll_picture_tag/instructions/configuration.rb +47 -11
  30. data/lib/jekyll_picture_tag/instructions/html_attributes.rb +14 -8
  31. data/lib/jekyll_picture_tag/instructions/preset.rb +34 -14
  32. data/lib/jekyll_picture_tag/instructions/set.rb +18 -8
  33. data/lib/jekyll_picture_tag/instructions/tag_parser.rb +59 -69
  34. data/lib/jekyll_picture_tag/output_formats/basic.rb +35 -6
  35. data/lib/jekyll_picture_tag/output_formats/data_attributes.rb +4 -1
  36. data/lib/jekyll_picture_tag/router.rb +16 -0
  37. data/lib/jekyll_picture_tag/source_image.rb +6 -1
  38. data/lib/jekyll_picture_tag/srcsets/basic.rb +45 -19
  39. data/lib/jekyll_picture_tag/srcsets/pixel_ratio.rb +1 -3
  40. data/lib/jekyll_picture_tag/srcsets/width.rb +1 -1
  41. data/lib/jekyll_picture_tag/utils.rb +18 -0
  42. data/lib/jekyll_picture_tag/version.rb +1 -1
  43. data/readme.md +43 -200
  44. metadata +49 -13
  45. data/docs/examples/_config.yml +0 -10
  46. data/docs/examples/post.md +0 -46
  47. data/docs/readme.md +0 -23
@@ -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
@@ -4,22 +4,28 @@ module PictureTag
4
4
  # sent to various elements.
5
5
  # Stored as a hash, with string keys.
6
6
  class HTMLAttributeSet
7
- # Initialize with leftovers passed into the liquid tag
7
+ # Initialize with leftovers passed into the liquid tag. These leftovers
8
+ # (params) take the form of an array of strings, called words, which the
9
+ # tag parser has separated.
8
10
  def initialize(params)
9
- @content = load_preset
11
+ @attributes = load_preset
10
12
 
11
13
  parse_params(params) if params
12
14
  handle_source_url
13
15
  end
14
16
 
15
17
  def [](key)
16
- @content[key]
18
+ @attributes[key]
17
19
  end
18
20
 
19
21
  private
20
22
 
21
23
  def load_preset
22
- PictureTag.preset['attributes'].dup || {}
24
+ # Shamelessly stolen from stackoverflow. Deep cloning a hash is
25
+ # surprisingly tricky! I could pull in ActiveSupport and get
26
+ # Hash#deep_dup, but for now I don't think it's necessary.
27
+
28
+ Marshal.load(Marshal.dump(PictureTag.preset['attributes'])) || {}
23
29
  end
24
30
 
25
31
  # Syntax this function processes:
@@ -30,10 +36,10 @@ module PictureTag
30
36
  words.each do |word|
31
37
  if word.match(/^--/)
32
38
  key = word.delete_prefix('--')
33
- elsif @content[key]
34
- @content[key] << ' ' + word
39
+ elsif @attributes[key]
40
+ @attributes[key] << ' ' + word
35
41
  else
36
- @content[key] = word
42
+ @attributes[key] = word
37
43
  end
38
44
  end
39
45
  end
@@ -43,7 +49,7 @@ module PictureTag
43
49
 
44
50
  target = PictureTag.source_images.first.shortname
45
51
 
46
- @content['link'] = ImgURI.new(target, source_image: true).to_s
52
+ @attributes['link'] = ImgURI.new(target, source_image: true).to_s
47
53
  end
48
54
  end
49
55
  end
@@ -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