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.
- checksums.yaml +4 -4
- data/.travis.yml +4 -7
- data/Dockerfile +9 -0
- data/docs/Gemfile.lock +183 -88
- data/docs/contributing.md +50 -16
- data/docs/example_presets.md +1 -1
- data/docs/global_configuration.md +55 -2
- data/docs/index.md +26 -20
- data/docs/installation.md +22 -7
- data/docs/notes.md +11 -1
- data/docs/output.md +32 -21
- data/docs/presets.md +109 -54
- data/docs/releases.md +17 -1
- data/docs/usage.md +68 -38
- data/jekyll_picture_tag.gemspec +2 -1
- data/lib/jekyll_picture_tag.rb +27 -10
- data/lib/jekyll_picture_tag/defaults/global.yml +2 -0
- data/lib/jekyll_picture_tag/defaults/presets.yml +2 -0
- data/lib/jekyll_picture_tag/generated_image.rb +105 -19
- data/lib/jekyll_picture_tag/instructions.rb +1 -0
- data/lib/jekyll_picture_tag/instructions/arg_splitter.rb +68 -0
- data/lib/jekyll_picture_tag/instructions/configuration.rb +47 -11
- data/lib/jekyll_picture_tag/instructions/preset.rb +34 -14
- data/lib/jekyll_picture_tag/instructions/set.rb +18 -8
- data/lib/jekyll_picture_tag/instructions/tag_parser.rb +59 -69
- data/lib/jekyll_picture_tag/output_formats/basic.rb +36 -7
- data/lib/jekyll_picture_tag/output_formats/data_attributes.rb +4 -1
- data/lib/jekyll_picture_tag/router.rb +16 -0
- data/lib/jekyll_picture_tag/source_image.rb +6 -1
- data/lib/jekyll_picture_tag/srcsets/basic.rb +45 -19
- data/lib/jekyll_picture_tag/srcsets/pixel_ratio.rb +1 -3
- data/lib/jekyll_picture_tag/srcsets/width.rb +1 -1
- data/lib/jekyll_picture_tag/utils.rb +18 -0
- data/lib/jekyll_picture_tag/version.rb +1 -1
- data/readme.md +39 -16
- metadata +24 -8
@@ -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.
|
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
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
46
|
-
|
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
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
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
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
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
|
-
|
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
|
-
|
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 = []
|
29
|
+
@source_names = []
|
30
|
+
@geometries = {}
|
31
|
+
@gravities = {}
|
18
32
|
|
19
|
-
|
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
|
31
|
-
|
32
|
-
|
38
|
+
def split_params
|
39
|
+
ArgSplitter
|
40
|
+
.new(Utils.liquid_lookup(@raw_params))
|
41
|
+
.words
|
33
42
|
end
|
34
43
|
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
48
|
+
parse_param(@params.first) until stop_here? @params.first
|
56
49
|
|
57
|
-
|
50
|
+
@leftovers = @params
|
58
51
|
end
|
59
52
|
|
60
|
-
def
|
61
|
-
|
62
|
-
|
63
|
-
close_escape char
|
53
|
+
def parse_param(param)
|
54
|
+
if param.match?(/[\w\-]+:$/)
|
55
|
+
add_media_source
|
64
56
|
|
65
|
-
|
66
|
-
|
67
|
-
handle_special char
|
57
|
+
elsif Utils::GRAVITIES.include?(param.downcase)
|
58
|
+
@gravities[@media_presets.last] = @params.shift
|
68
59
|
|
69
|
-
|
70
|
-
|
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
|
-
|
64
|
+
raise_error(param)
|
76
65
|
end
|
77
66
|
end
|
78
67
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
89
|
-
|
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
|
-
|
96
|
-
|
97
|
-
@
|
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
|
-
#
|
72
|
+
# GeneratedImage class, not HTML
|
73
73
|
def build_fallback_image
|
74
|
-
|
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:
|
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 >
|
131
|
+
if target > source_width
|
103
132
|
Utils.warning "#{source.shortname} is smaller than the " \
|
104
|
-
"requested fallback width of #{target}px. Using #{
|
133
|
+
"requested fallback width of #{target}px. Using #{source_width}" \
|
105
134
|
' px instead.'
|
106
|
-
|
135
|
+
source_width
|
107
136
|
else
|
108
137
|
target
|
109
138
|
end
|