jekyll_picture_tag 1.14.0 → 2.0.3

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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +2 -0
  3. data/.github/workflows/code-checks.yml +2 -12
  4. data/.rubocop.yml +2 -0
  5. data/.ruby-version +1 -1
  6. data/docs/devs/contributing/code.md +11 -3
  7. data/docs/devs/contributing/testing.md +0 -11
  8. data/docs/devs/releases.md +38 -2
  9. data/docs/index.md +43 -18
  10. data/docs/logo.png +0 -0
  11. data/docs/logo.svg +880 -0
  12. data/docs/users/deployment.md +49 -0
  13. data/docs/users/getting_started.md +55 -0
  14. data/docs/users/installation.md +18 -38
  15. data/docs/users/liquid_tag/argument_reference/crop.md +21 -36
  16. data/docs/users/liquid_tag/examples.md +13 -25
  17. data/docs/users/liquid_tag/index.md +1 -1
  18. data/docs/users/notes/{migration.md → migration_1.md} +1 -1
  19. data/docs/users/notes/migration_2.md +99 -0
  20. data/docs/users/presets/cropping.md +21 -22
  21. data/docs/users/presets/default.md +10 -2
  22. data/docs/users/presets/examples.md +77 -45
  23. data/docs/users/presets/fallback_image.md +1 -1
  24. data/docs/users/presets/html_attributes.md +1 -1
  25. data/docs/users/presets/image_formats.md +3 -3
  26. data/docs/users/presets/image_quality.md +71 -56
  27. data/docs/users/presets/index.md +19 -45
  28. data/docs/users/presets/link_source.md +1 -1
  29. data/docs/users/presets/media_queries.md +1 -1
  30. data/docs/users/presets/nomarkdown_override.md +1 -1
  31. data/docs/users/presets/pixel_ratio_srcsets.md +1 -1
  32. data/docs/users/presets/width_height_attributes.md +1 -1
  33. data/docs/users/presets/width_srcsets.md +61 -23
  34. data/docs/users/presets/writing_presets.md +65 -0
  35. data/docs/users/tutorial.md +97 -0
  36. data/jekyll_picture_tag.gemspec +33 -23
  37. data/lib/jekyll_picture_tag.rb +8 -6
  38. data/lib/jekyll_picture_tag/cache.rb +64 -3
  39. data/lib/jekyll_picture_tag/defaults/global.rb +18 -0
  40. data/lib/jekyll_picture_tag/defaults/presets.rb +57 -0
  41. data/lib/jekyll_picture_tag/images.rb +1 -0
  42. data/lib/jekyll_picture_tag/images/generated_image.rb +25 -63
  43. data/lib/jekyll_picture_tag/images/image_file.rb +105 -0
  44. data/lib/jekyll_picture_tag/images/img_uri.rb +3 -12
  45. data/lib/jekyll_picture_tag/images/source_image.rb +44 -9
  46. data/lib/jekyll_picture_tag/instructions.rb +70 -6
  47. data/lib/jekyll_picture_tag/instructions/children/config.rb +128 -0
  48. data/lib/jekyll_picture_tag/instructions/children/context.rb +24 -0
  49. data/lib/jekyll_picture_tag/instructions/children/params.rb +90 -0
  50. data/lib/jekyll_picture_tag/instructions/children/parsers.rb +48 -0
  51. data/lib/jekyll_picture_tag/instructions/children/preset.rb +182 -0
  52. data/lib/jekyll_picture_tag/instructions/parents/conditional_instruction.rb +69 -0
  53. data/lib/jekyll_picture_tag/instructions/parents/env_instruction.rb +29 -0
  54. data/lib/jekyll_picture_tag/output_formats/basic.rb +5 -17
  55. data/lib/jekyll_picture_tag/parsers.rb +6 -0
  56. data/lib/jekyll_picture_tag/{instructions → parsers}/arg_splitter.rb +1 -1
  57. data/lib/jekyll_picture_tag/parsers/configuration.rb +28 -0
  58. data/lib/jekyll_picture_tag/{instructions → parsers}/html_attributes.rb +1 -1
  59. data/lib/jekyll_picture_tag/parsers/image_backend.rb +46 -0
  60. data/lib/jekyll_picture_tag/parsers/preset.rb +43 -0
  61. data/lib/jekyll_picture_tag/{instructions → parsers}/tag_parser.rb +15 -12
  62. data/lib/jekyll_picture_tag/router.rb +35 -93
  63. data/lib/jekyll_picture_tag/srcsets/basic.rb +4 -10
  64. data/lib/jekyll_picture_tag/utils.rb +10 -20
  65. data/lib/jekyll_picture_tag/version.rb +1 -1
  66. data/readme.md +38 -0
  67. metadata +126 -105
  68. data/Dockerfile +0 -9
  69. data/docs/users/notes/input_checking.md +0 -6
  70. data/docs/users/presets/strip_metadata.md +0 -13
  71. data/install_imagemagick.sh +0 -23
  72. data/jekyll-picture-tag.gemspec +0 -52
  73. data/lib/jekyll-picture-tag.rb +0 -25
  74. data/lib/jekyll_picture_tag/cache/base.rb +0 -61
  75. data/lib/jekyll_picture_tag/cache/generated.rb +0 -20
  76. data/lib/jekyll_picture_tag/cache/source.rb +0 -19
  77. data/lib/jekyll_picture_tag/defaults/global.yml +0 -13
  78. data/lib/jekyll_picture_tag/defaults/presets.yml +0 -12
  79. data/lib/jekyll_picture_tag/instructions/configuration.rb +0 -121
  80. data/lib/jekyll_picture_tag/instructions/preset.rb +0 -122
  81. data/lib/jekyll_picture_tag/instructions/set.rb +0 -75
@@ -0,0 +1,69 @@
1
+ module PictureTag
2
+ module Instructions
3
+ # Many inputs take a common format: a generic setting which applies all of
4
+ # the time, or more specific versions of that setting for specific
5
+ # circumstances. For example, quality can be set globally, or per image
6
+ # format. This instruction class handles those cases.
7
+ #
8
+ # To use, you must at minimum define setting_basename, setting_prefix, and
9
+ # add to the acceptable_types (or write your own validation).
10
+ class ConditionalInstruction < Instruction
11
+ def value(*args)
12
+ coerce(*args)
13
+ end
14
+
15
+ private
16
+
17
+ def setting_basename
18
+ raise NotImplementedError
19
+ end
20
+
21
+ # Special condition for setting; media, crop, etc
22
+ def setting_prefix
23
+ raise NotImplementedError
24
+ end
25
+
26
+ def acceptable_types
27
+ [NilClass]
28
+ end
29
+
30
+ def coerce(arg)
31
+ raise ArgumentError unless valid?
32
+
33
+ value_hash[arg]
34
+ end
35
+
36
+ def source
37
+ {
38
+ hash: PictureTag.preset[setting_prefix + '_' + setting_name],
39
+ default: PictureTag.preset[setting_name]
40
+ }
41
+ end
42
+
43
+ def value_hash
44
+ vals = source[:hash] || {}
45
+ vals.default = source[:default]
46
+
47
+ vals
48
+ end
49
+
50
+ def valid?
51
+ valid_hash? && valid_default?
52
+ end
53
+
54
+ def acceptable_type?(value)
55
+ acceptable_types.any? { |type| value.is_a? type }
56
+ end
57
+
58
+ def valid_hash?
59
+ source[:hash].nil? || source[:hash].values.all? do |v|
60
+ acceptable_type?(v)
61
+ end
62
+ end
63
+
64
+ def valid_default?
65
+ acceptable_type? source[:default]
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,29 @@
1
+ module PictureTag
2
+ module Instructions
3
+ # There are a few config settings which are environment dependent, and can
4
+ # either be booleans, environment names, or arrays of environment names.
5
+ # This class works it out and returns a boolean.
6
+ class EnvInstruction < Instruction
7
+ private
8
+
9
+ def coerce
10
+ get_bool(source)
11
+ end
12
+
13
+ def get_bool(value)
14
+ case value
15
+ when true, false, nil then value
16
+ when String then value == PictureTag.jekyll_env
17
+ when Array then value.include? PictureTag.jekyll_env
18
+ when Hash then get_bool(value[:setting])
19
+ else raise ArgumentError, error_message
20
+ end
21
+ end
22
+
23
+ def error_message
24
+ "JPT - #{setting_name} must be a boolean, an environment name," \
25
+ ' or an array of environment names.'
26
+ end
27
+ end
28
+ end
29
+ end
@@ -76,9 +76,7 @@ module PictureTag
76
76
  image = GeneratedImage.new(
77
77
  source_file: PictureTag.source_images.first,
78
78
  format: PictureTag.fallback_format,
79
- width: checked_fallback_width,
80
- crop: PictureTag.crop,
81
- gravity: PictureTag.gravity
79
+ width: checked_fallback_width
82
80
  )
83
81
 
84
82
  image.generate
@@ -92,9 +90,7 @@ module PictureTag
92
90
  @fallback_candidate ||= GeneratedImage.new(
93
91
  source_file: PictureTag.source_images.first,
94
92
  format: PictureTag.fallback_format,
95
- width: PictureTag.fallback_width,
96
- crop: PictureTag.crop,
97
- gravity: PictureTag.gravity
93
+ width: PictureTag.fallback_width
98
94
  )
99
95
  end
100
96
 
@@ -119,22 +115,14 @@ module PictureTag
119
115
  PictureTag.source_images.first
120
116
  end
121
117
 
122
- def source_width
123
- if PictureTag.crop
124
- fallback_candidate.source_width
125
- else
126
- source.width
127
- end
128
- end
129
-
130
118
  def checked_fallback_width
131
119
  target = PictureTag.fallback_width
132
120
 
133
- if target > source_width
121
+ if target > source.width
134
122
  Utils.warning "#{source.shortname} is smaller than the " \
135
- "requested fallback width of #{target}px. Using #{source_width}" \
123
+ "requested fallback width of #{target}px. Using #{source.width}" \
136
124
  ' px instead.'
137
- source_width
125
+ source.width
138
126
  else
139
127
  target
140
128
  end
@@ -0,0 +1,6 @@
1
+ require_relative 'parsers/arg_splitter'
2
+ require_relative 'parsers/configuration'
3
+ require_relative 'parsers/html_attributes'
4
+ require_relative 'parsers/preset'
5
+ require_relative 'parsers/tag_parser'
6
+ require_relative 'parsers/image_backend'
@@ -1,5 +1,5 @@
1
1
  module PictureTag
2
- module Instructions
2
+ module Parsers
3
3
  # This class takes in the arguments passed to the liquid tag, and splits it
4
4
  # up into 'words' (correctly handling quotes and backslash escapes.)
5
5
  #
@@ -0,0 +1,28 @@
1
+ module PictureTag
2
+ module Parsers
3
+ # Global config (big picture). loads jekyll data/config files, and the j-p-t
4
+ # defaults from included yml files.
5
+ class Configuration
6
+ # returns jekyll's configuration (picture is a subset)
7
+ def [](key)
8
+ content[key]
9
+ end
10
+
11
+ private
12
+
13
+ def content
14
+ @content ||= setting_merge(DEFAULT_CONFIG, PictureTag.site.config)
15
+ end
16
+
17
+ def setting_merge(default, jekyll)
18
+ jekyll.merge default do |_key, config_setting, default_setting|
19
+ if default_setting.respond_to? :merge
20
+ setting_merge(default_setting, config_setting)
21
+ else
22
+ config_setting
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,5 +1,5 @@
1
1
  module PictureTag
2
- module Instructions
2
+ module Parsers
3
3
  # Handles HTML attributes, sourced from configuration and the liquid tag,
4
4
  # sent to various elements.
5
5
  # Stored as a hash, with string keys.
@@ -0,0 +1,46 @@
1
+ module PictureTag
2
+ module Parsers
3
+ # Returns information regarding image handlers
4
+ class ImageBackend
5
+ def handler_for(format)
6
+ if (vips_formats & all_names(format)).any?
7
+ :vips
8
+ elsif (magick_formats & all_names(format)).any?
9
+ :magick
10
+ else
11
+ raise "No support for generating #{format} files in this environment."
12
+ end
13
+ end
14
+
15
+ # Returns array of formats that vips can save to
16
+ def vips_formats
17
+ @vips_formats ||= `vips -l filesave`
18
+ .scan(/\.[a-z]{1,5}/)
19
+ .uniq
20
+ .map { |format| format.strip.delete_prefix('.') }
21
+ end
22
+
23
+ # Returns an array of formats that imagemagick can handle.
24
+ def magick_formats
25
+ @magick_formats ||= `convert -version`
26
+ .split("\n")
27
+ .last
28
+ .delete_prefix('Delegates (built-in):')
29
+ .split
30
+ end
31
+
32
+ # Returns an array of all known names of a format, for the purposes of
33
+ # parsing supported output formats.
34
+ def all_names(format)
35
+ alts = alternates.select { |a| a.include? format }.flatten
36
+ alts.any? ? alts : [format]
37
+ end
38
+
39
+ private
40
+
41
+ def alternates
42
+ [%w[jpg jpeg], %w[avif heic heif]]
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,43 @@
1
+ module PictureTag
2
+ module Parsers
3
+ # Handles the specific tag image set to construct.
4
+ class Preset
5
+ attr_reader :name
6
+
7
+ def initialize(name)
8
+ @name = name
9
+ end
10
+
11
+ def [](key)
12
+ content[key]
13
+ end
14
+
15
+ protected
16
+
17
+ def content
18
+ @content ||= DEFAULT_PRESET.merge settings
19
+ end
20
+
21
+ private
22
+
23
+ def settings
24
+ PictureTag.site.data.dig('picture', 'presets', name) ||
25
+ STOCK_PRESETS[name] ||
26
+ no_preset
27
+ end
28
+
29
+ def no_preset
30
+ unless name == 'default'
31
+ Utils.warning(
32
+ <<~HEREDOC
33
+ Preset "#{name}" not found in #{PictureTag.config['data_dir']}/picture.yml
34
+ under 'presets' key, or in stock presets. Using default values."
35
+ HEREDOC
36
+ )
37
+ end
38
+
39
+ {}
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,5 +1,5 @@
1
1
  module PictureTag
2
- module Instructions
2
+ module Parsers
3
3
  # Tag Parsing Responsibilities:
4
4
  #
5
5
  # {% picture mypreset a.jpg 3:2 mobile: b.jpg --alt "Alt" --link "/" %}
@@ -9,17 +9,16 @@ module PictureTag
9
9
  # string), hands them to ArgSplitter (which breaks them up into an array of
10
10
  # words), extracts the preset name (if present), source image name(s),
11
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
12
+ # crop and keep. HTML attributes are handed off to its respective class
13
13
  # (as 'leftovers')
14
14
  #
15
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
16
+ # orders. Crop settings are stored in a hash, keyed by their
17
17
  # relevant media presets. Note that the base image will have a media preset
18
18
  # of nil, which is a perfectly fine hash key.
19
- #
20
19
  class TagParser
21
- attr_reader :preset_name, :source_names, :media_presets, :gravities,
22
- :geometries, :leftovers
20
+ attr_reader :preset_name, :source_names, :media_presets, :keep,
21
+ :crop, :leftovers
23
22
 
24
23
  def initialize(raw_params)
25
24
  @raw_params = raw_params
@@ -27,8 +26,8 @@ module PictureTag
27
26
 
28
27
  @media_presets = []
29
28
  @source_names = []
30
- @geometries = {}
31
- @gravities = {}
29
+ @keep = {}
30
+ @crop = {}
32
31
 
33
32
  parse_params
34
33
  end
@@ -51,14 +50,18 @@ module PictureTag
51
50
  end
52
51
 
53
52
  def parse_param(param)
53
+ # Media query, i.e. 'mobile:'
54
54
  if param.match?(/[\w\-]+:$/)
55
55
  add_media_source
56
56
 
57
- elsif Utils::GRAVITIES.include?(param.downcase)
58
- @gravities[@media_presets.last] = @params.shift
57
+ # Smartcrop interestingness setting. We label it 'keep', since it
58
+ # determines what to keep when cropping.
59
+ elsif %w[none centre center entropy attention].include?(param.downcase)
60
+ @keep[@media_presets.last] = @params.shift
59
61
 
60
- elsif param.match?(Utils::GEOMETRY_REGEX)
61
- @geometries[@media_presets.last] = @params.shift
62
+ # Aspect ratio, i.e. '16:9'
63
+ elsif param.match?(/\A\d+:\d+\z/)
64
+ @crop[@media_presets.last] = @params.shift
62
65
 
63
66
  else
64
67
  raise_error(param)
@@ -1,115 +1,57 @@
1
1
  module PictureTag
2
2
  # The rest of the application doesn't care where the instruction logic
3
- # resides. The following module 'routes' method calls to the right place, so
4
- # the rest of the application can just call 'PictureTag.(some method)'
5
-
6
- # At first I thought I'd do some sweet dynamic metaprogramming here, but it
7
- # ended up complicated and clever, rather than convenient and understandable.
8
- # This way is not strictly DRY, but it's straightforward and readable. If it
9
- # gets big, I'll refactor.
3
+ # resides. This module 'routes' method calls to the right place, so
4
+ # information consumers can just call 'PictureTag.(some method)'
5
+ #
6
+ # This is accomplished with a bit of metaprogramming, which is hopefully not
7
+ # unnecessarily clever or complicated. Missing methods are converted to class
8
+ # names, which are looked up under the Instructions module namespace.
9
+ #
10
+ # Instantiated Instructions are stored in a hash, keyed by method name.
10
11
  module Router
11
- attr_accessor :instructions, :context
12
-
13
- # Context forwarding
14
-
15
- # Global site data
16
- def site
17
- @context.registers[:site]
18
- end
19
-
20
- # Page which tag is called from
21
- def page
22
- @context.registers[:page]
23
- end
24
-
25
- # Instructions forwarding
26
-
27
- def config
28
- @instructions.config
29
- end
30
-
31
- def preset
32
- @instructions.preset
33
- end
34
-
35
- def media_presets
36
- @instructions.media_presets
37
- end
38
-
39
- def html_attributes
40
- @instructions.html_attributes
41
- end
42
-
43
- def output_class
44
- @instructions.output_class
45
- end
12
+ # These two attributes encompass everything passed in by Jekyll.
13
+ attr_accessor :raw_params, :context
46
14
 
47
- def source_images
48
- @instructions.source_images
15
+ def method_missing(method_name, *args)
16
+ if instruction_exists?(method_name)
17
+ instruction(method_name).value(*args)
18
+ else
19
+ super
20
+ end
49
21
  end
50
22
 
51
- def crop(media = nil)
52
- @instructions.crop(media)
23
+ def respond_to_missing?(method_name, *args)
24
+ instruction_exists?(method_name) || super
53
25
  end
54
26
 
55
- def gravity(media = nil)
56
- @instructions.gravity(media)
27
+ # Required at least for testing; instructions are persisted between tags
28
+ # otherwise.
29
+ def clear_instructions
30
+ instructions.clear
57
31
  end
58
32
 
59
- # Config Forwarding
60
-
61
- def source_dir
62
- config.source_dir
63
- end
64
-
65
- def dest_dir
66
- config.dest_dir
67
- end
68
-
69
- def continue_on_missing?
70
- config.continue_on_missing?
71
- end
72
-
73
- def cdn?
74
- config.cdn?
75
- end
76
-
77
- def pconfig
78
- config.pconfig
79
- end
80
-
81
- def disabled?
82
- config.disabled?
83
- end
84
-
85
- def fast_build?
86
- config.fast_build?
87
- end
88
-
89
- # Preset forwarding
90
-
91
- def widths(media)
92
- preset.widths(media)
93
- end
33
+ private
94
34
 
95
- def formats
96
- preset.formats
35
+ def instruction(method_name)
36
+ instructions[method_name] ||= instruction_class(method_name).new
97
37
  end
98
38
 
99
- def fallback_format
100
- preset.fallback_format
39
+ def instructions
40
+ @instructions ||= {}
101
41
  end
102
42
 
103
- def fallback_width
104
- preset.fallback_width
43
+ def instruction_exists?(method_name)
44
+ Object.const_defined? instruction_class_name(method_name.to_sym)
105
45
  end
106
46
 
107
- def nomarkdown?
108
- preset.nomarkdown?
47
+ # Class names can't contain question marks, so we strip them.
48
+ def instruction_class(method_name)
49
+ Object.const_get instruction_class_name(method_name)
109
50
  end
110
51
 
111
- def quality(format, width)
112
- preset.quality(format, width)
52
+ def instruction_class_name(method_name)
53
+ 'PictureTag::Instructions::' +
54
+ Utils.titleize(method_name.to_s.delete_suffix('?'))
113
55
  end
114
56
  end
115
57
  end