jekyll-imgflow 0.3.2 → 0.4.0

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.
@@ -110,44 +110,51 @@ module JekyllImgFlow
110
110
  operations = []
111
111
  operation_options = options.slice(*OPERATION_PARAMS)
112
112
 
113
- # Handle crop+resize combination (needs sequential processing)
114
- has_crop = operation_options[:ratio] || operation_options[:aspect_ratio]
115
- has_resize = operation_options[:width] || operation_options[:height]
113
+ add_crop_resize_operations(operations, operation_options)
114
+ add_simple_operations(operations, operation_options)
115
+ add_format_operation(operations, operation_options)
116
+ add_optimize_operation(operations, operation_options)
117
+
118
+ operations
119
+ end
120
+
121
+ def self.add_crop_resize_operations(operations, opts)
122
+ has_crop = opts[:ratio] || opts[:aspect_ratio]
123
+ has_resize = opts[:width] || opts[:height]
124
+ crop_params = opts.slice(:ratio, :aspect_ratio, :keep, :position)
116
125
 
117
126
  if has_crop && has_resize
118
- crop_params = operation_options.slice(:ratio, :aspect_ratio, :keep, :position)
119
- resize_params = operation_options.except(:ratio, :aspect_ratio, :keep, :position)
120
127
  operations << { type: :crop, params: crop_params }
121
- operations << { type: :resize, params: resize_params }
128
+ operations << { type: :resize, params: opts.except(:ratio, :aspect_ratio, :keep, :position) }
122
129
  elsif has_crop
123
- crop_params = operation_options.slice(:ratio, :aspect_ratio, :keep, :position)
124
130
  operations << { type: :crop, params: crop_params }
125
131
  elsif has_resize
126
- operations << { type: :resize, params: operation_options }
132
+ operations << { type: :resize, params: opts }
127
133
  end
134
+ end
128
135
 
129
- # Add simple operations using mappings
136
+ def self.add_simple_operations(operations, opts)
130
137
  SIMPLE_OPERATIONS.each do |key, builder|
131
- operations << builder.call(operation_options[key]) if operation_options[key]
138
+ operations << builder.call(opts[key]) if opts[key]
132
139
  end
140
+ end
133
141
 
134
- # Handle format operation (can be array or single value)
135
- if operation_options[:format] || operation_options[:formats]
136
- format_value = operation_options[:format] || operation_options[:formats]
137
- operations << if format_value.is_a?(Array)
138
- { type: :format, params: { formats: format_value } }
139
- else
140
- { type: :format, params: { format: format_value } }
141
- end
142
- end
142
+ def self.add_format_operation(operations, opts)
143
+ return unless opts[:format] || opts[:formats]
143
144
 
144
- # Handle optimize operation
145
- if operation_options[:optimize] || operation_options[:level]
146
- operations << { type: :optimize,
147
- params: { level: operation_options[:level] || :medium } }
148
- end
145
+ format_value = opts[:format] || opts[:formats]
146
+ operations << if format_value.is_a?(Array)
147
+ { type: :format, params: { formats: format_value } }
148
+ else
149
+ { type: :format, params: { format: format_value } }
150
+ end
151
+ end
149
152
 
150
- operations
153
+ def self.add_optimize_operation(operations, opts)
154
+ return unless opts[:optimize] || opts[:level]
155
+
156
+ operations << { type: :optimize,
157
+ params: { level: opts[:level] || :medium } }
151
158
  end
152
159
 
153
160
  def self.extract_html_attributes(options)
@@ -16,26 +16,19 @@ module JekyllImgFlow
16
16
  result = translate_to_imgflow(picture_markup)
17
17
  return "" if result[:markup].empty?
18
18
 
19
- # Build ImgFlow tag with attributes
20
- tag_parts = ["{% imgflow", result[:markup]]
21
-
22
- # Add HTML attributes
23
- tag_parts << "alt:\"#{result[:attributes][:alt]}\"" if result[:attributes][:alt]
24
-
25
- if result[:attributes][:img]&.any?
26
- result[:attributes][:img].each do |name, value|
27
- tag_parts << "img-#{name}:\"#{value}\""
28
- end
29
- end
19
+ "{% imgflow #{[result[:markup], *translated_attributes(result[:attributes]), '%}'].join(' ')}"
20
+ end
30
21
 
31
- if result[:attributes][:picture]&.any?
32
- result[:attributes][:picture].each do |name, value|
33
- tag_parts << "picture-#{name}:\"#{value}\""
34
- end
35
- end
22
+ def translated_attributes(attributes)
23
+ parts = []
24
+ parts << "alt:\"#{attributes[:alt]}\"" if attributes[:alt]
25
+ parts.concat(prefixed_attributes(attributes[:img], "img"))
26
+ parts.concat(prefixed_attributes(attributes[:picture], "picture"))
27
+ parts
28
+ end
36
29
 
37
- tag_parts << "%}"
38
- tag_parts.join(" ")
30
+ def prefixed_attributes(attributes, prefix)
31
+ attributes.to_h.map { |name, value| "#{prefix}-#{name}:\"#{value}\"" }
39
32
  end
40
33
 
41
34
  # Convert Picture Tag markup to ImgFlow markup with HTML attributes
@@ -57,17 +50,9 @@ module JekyllImgFlow
57
50
  categorized = categorize_arguments(args)
58
51
  return { markup: "", attributes: {} } unless categorized[:image]
59
52
 
60
- # Step 2: Translate each category to ImgFlow syntax
61
- imgflow_parts = []
62
- imgflow_parts << categorized[:image]
63
- imgflow_parts += translate_media_queries(categorized[:media_queries])
64
- imgflow_parts += translate_operations(categorized[:operations])
65
- imgflow_parts << "formats:webp,jpg" unless formats?(imgflow_parts)
66
- imgflow_parts << "markup:#{categorized[:markup_format]}" if categorized[:markup_format] && categorized[:markup_format] != "auto"
67
-
68
- # Step 3: Assemble result
53
+ imgflow_parts = translated_parts(categorized)
69
54
  {
70
- markup: imgflow_parts.compact.join(" "),
55
+ markup: imgflow_parts.join(" "),
71
56
  attributes: categorized[:html_attributes],
72
57
  markup_format: categorized[:markup_format]
73
58
  }
@@ -75,6 +60,16 @@ module JekyllImgFlow
75
60
 
76
61
  private
77
62
 
63
+ def translated_parts(categorized)
64
+ parts = [categorized[:image]]
65
+ parts.concat(translate_media_queries(categorized[:media_queries]))
66
+ parts.concat(translate_operations(categorized[:operations]))
67
+ parts << "formats:webp,jpg" unless formats?(parts)
68
+ format = categorized[:markup_format]
69
+ parts << "markup:#{format}" if format && format != "auto"
70
+ parts.compact
71
+ end
72
+
78
73
  def extract_picture_content(markup)
79
74
  offset = 0
80
75
  while (opening = markup.index("{%", offset))
@@ -105,37 +100,43 @@ module JekyllImgFlow
105
100
  # @param content [String] Raw content
106
101
  # @return [Array] Parsed arguments
107
102
  def parse_arguments(content)
108
- args = []
109
- current = +"" # Create unfrozen string
110
- in_quotes = false
111
- quote_char = nil
112
-
113
- content.dup.each_char do |char|
114
- case char
115
- when '"', "'"
116
- if !in_quotes
117
- in_quotes = true
118
- quote_char = char
119
- elsif char == quote_char
120
- in_quotes = false
121
- quote_char = nil
122
- else
123
- current << char
124
- end
125
- when " "
126
- if in_quotes
127
- current << char
128
- elsif !current.empty?
129
- args << current.dup
130
- current = +""
131
- end
132
- else
133
- current << char
134
- end
103
+ state = { args: [], current: +"", in_quotes: false, quote_char: nil }
104
+ content.each_char { |char| consume_argument_character(state, char) }
105
+ state[:args] << state[:current].dup unless state[:current].empty?
106
+ state[:args]
107
+ end
108
+
109
+ def consume_argument_character(state, char)
110
+ if quote_character?(char)
111
+ consume_quote(state, char)
112
+ elsif char == " " && !state[:in_quotes]
113
+ append_argument(state)
114
+ else
115
+ state[:current] << char
116
+ end
117
+ end
118
+
119
+ def quote_character?(char)
120
+ ["\"", "'"].include?(char)
121
+ end
122
+
123
+ def consume_quote(state, char)
124
+ if !state[:in_quotes]
125
+ state[:in_quotes] = true
126
+ state[:quote_char] = char
127
+ elsif char == state[:quote_char]
128
+ state[:in_quotes] = false
129
+ state[:quote_char] = nil
130
+ else
131
+ state[:current] << char
135
132
  end
133
+ end
134
+
135
+ def append_argument(state)
136
+ return if state[:current].empty?
136
137
 
137
- args << current.dup unless current.empty?
138
- args
138
+ state[:args] << state[:current].dup
139
+ state[:current] = +""
139
140
  end
140
141
 
141
142
  # Categorize arguments into types
@@ -149,59 +150,65 @@ module JekyllImgFlow
149
150
  html_attributes: default_html_attributes,
150
151
  markup_format: nil
151
152
  }
153
+ index = 0
154
+ index = categorize_argument(args, result, index) while index < args.length
155
+ result
156
+ end
152
157
 
153
- i = 0
154
- while i < args.length
155
- arg = args[i]
158
+ def categorize_argument(args, result, index)
159
+ arg = args[index]
160
+ return parse_alt_argument(args, result, index) if arg == "--alt"
161
+ return parse_link_argument(args, result, index) if arg == "--link"
162
+ return parse_element_arguments(args, result, index) if arg.match?(/^--(img|picture|source|a|parent)$/)
163
+ return parse_media_argument(args, result, index) if arg.match?(/(mobile|tablet|desktop):/)
156
164
 
157
- case arg
158
- when /^--alt$/
159
- # Collect alt text until next --
160
- i += 1
161
- alt_parts = []
162
- while i < args.length && !args[i].start_with?("--")
163
- alt_parts << args[i]
164
- i += 1
165
- end
166
- result[:html_attributes][:alt] = strip_quotes(alt_parts.join(" "))
167
- next
168
- when /^--link$/
169
- result[:html_attributes][:link] = strip_quotes(args[i + 1]) if i + 1 < args.length
170
- i += 2
171
- next
172
- when /^--(img|picture|source|a|parent)$/
173
- element = ::Regexp.last_match(1).to_sym
174
- i += 1
175
- while i < args.length && !args[i].start_with?("--")
176
- parse_element_attribute(args[i], result[:html_attributes][element])
177
- i += 1
178
- end
179
- next
180
- when /(mobile|tablet|desktop):/
181
- device = ::Regexp.last_match(1)
182
- i += 1
183
- if i < args.length && args[i].include?(".")
184
- image = args[i]
185
- i += 1
186
- crop = parse_crop_from_arg(args[i]) if i < args.length && args[i] =~ /^\d+:\d+/
187
- i += 1 if crop
188
- result[:media_queries][device] = { image: image, crop: crop }
189
- end
190
- next
191
- when /^(auto|data_auto|picture|img)$/
192
- result[:markup_format] = arg
193
- when /\./
194
- # Image path (has extension, no colon)
195
- result[:image] ||= arg unless arg.include?(":")
196
- else
197
- # Operation argument
198
- result[:operations] << arg unless arg.start_with?("--")
199
- end
165
+ categorize_simple_argument(arg, result)
166
+ index + 1
167
+ end
200
168
 
201
- i += 1
202
- end
169
+ def parse_alt_argument(args, result, index)
170
+ values, next_index = values_until_option(args, index + 1)
171
+ result[:html_attributes][:alt] = strip_quotes(values.join(" "))
172
+ next_index
173
+ end
203
174
 
204
- result
175
+ def parse_link_argument(args, result, index)
176
+ result[:html_attributes][:link] = strip_quotes(args[index + 1]) if args[index + 1]
177
+ index + 2
178
+ end
179
+
180
+ def parse_element_arguments(args, result, index)
181
+ element = args[index].match(/^--(img|picture|source|a|parent)$/)[1].to_sym
182
+ values, next_index = values_until_option(args, index + 1)
183
+ values.each { |value| parse_element_attribute(value, result[:html_attributes][element]) }
184
+ next_index
185
+ end
186
+
187
+ def values_until_option(args, index)
188
+ start = index
189
+ index += 1 while index < args.length && !args[index].start_with?("--")
190
+ [args[start...index], index]
191
+ end
192
+
193
+ def parse_media_argument(args, result, index)
194
+ device = args[index].match(/(mobile|tablet|desktop):/)[1]
195
+ image = args[index + 1]
196
+ return index + 1 unless image&.include?(".")
197
+
198
+ crop = parse_crop_from_arg(args[index + 2])
199
+ result[:media_queries][device] = { image: image, crop: crop }
200
+ index + 2 + (crop ? 1 : 0)
201
+ end
202
+
203
+ def categorize_simple_argument(arg, result)
204
+ case arg
205
+ when /^(auto|data_auto|picture|img)$/
206
+ result[:markup_format] = arg
207
+ when /\./
208
+ result[:image] ||= arg unless arg.include?(":")
209
+ else
210
+ result[:operations] << arg unless arg.start_with?("--")
211
+ end
205
212
  end
206
213
 
207
214
  # Translate media queries to ImgFlow crop syntax
@@ -99,61 +99,11 @@ module JekyllImgFlow
99
99
  # @return [Hash, nil] ImgFlow preset data or nil if not convertible
100
100
  def convert_preset_to_imgflow(preset_name, preset_data)
101
101
  operations = []
102
-
103
- # Handle multi-width presets - use the largest width as primary
104
- if preset_data["widths"]
105
- max_width = Array(preset_data["widths"]).max
106
- operations << { "resize" => { "width" => max_width } }
107
- elsif preset_data["base_width"]
108
- # For pixel-ratio presets, use base_width
109
- operations << { "resize" => { "width" => preset_data["base_width"] } }
110
- elsif preset_data["width"]
111
- # Direct width setting
112
- operations << { "resize" => { "width" => preset_data["width"] } }
113
- end
114
-
115
- # Handle height (less common in Picture Tag)
116
- if preset_data["height"]
117
- if operations.any? && operations.last["resize"]
118
- # Add height to existing resize operation
119
- operations.last["resize"]["height"] = preset_data["height"]
120
- else
121
- operations << { "resize" => { "height" => preset_data["height"] } }
122
- end
123
- end
124
-
125
- # Convert quality (if specified)
126
- operations << { "quality" => { "quality" => preset_data["quality"] } } if preset_data["quality"]
127
-
128
- # Convert formats - Picture Tag uses 'original', ImgFlow uses actual format
129
- if preset_data["formats"]
130
- formats = Array(preset_data["formats"]).map do |format|
131
- case format
132
- when "original"
133
- "jpg" # Default to jpg for 'original'
134
- else
135
- format
136
- end
137
- end
138
- operations << { "format" => { "formats" => formats } }
139
- end
140
-
141
- # Convert crop (aspect ratio) - handle both ratio and aspect_ratio with precedence
142
- ratio_value = preset_data["ratio"] || preset_data["aspect_ratio"] || preset_data["crop"]
143
- operations << { "crop" => { "ratio" => ratio_value } } if ratio_value
144
-
145
- # Convert gravity/position - handle both position and gravity with precedence
146
- position_value = preset_data["position"] || preset_data["gravity"]
147
- if position_value
148
- if operations.any? { |op| op["crop"] }
149
- # Add to existing crop operation
150
- crop_op = operations.find { |op| op["crop"] }
151
- crop_op["crop"]["position"] = position_value
152
- else
153
- # Create new crop operation
154
- operations << { "crop" => { "position" => position_value } }
155
- end
156
- end
102
+ add_resize_operation(operations, preset_data)
103
+ add_quality_operation(operations, preset_data)
104
+ add_format_operation(operations, preset_data)
105
+ add_crop_operation(operations, preset_data)
106
+ add_position_operation(operations, preset_data)
157
107
 
158
108
  return if operations.empty?
159
109
 
@@ -171,30 +121,84 @@ module JekyllImgFlow
171
121
  }
172
122
  end
173
123
 
124
+ def add_resize_operation(operations, preset_data)
125
+ width = preset_width(preset_data)
126
+ operations << { "resize" => { "width" => width } } if width
127
+
128
+ return unless preset_data["height"]
129
+
130
+ if operations.any? && operations.last["resize"]
131
+ operations.last["resize"]["height"] = preset_data["height"]
132
+ else
133
+ operations << { "resize" => { "height" => preset_data["height"] } }
134
+ end
135
+ end
136
+
137
+ def preset_width(preset_data)
138
+ return Array(preset_data["widths"]).max if preset_data["widths"]
139
+ return preset_data["base_width"] if preset_data["base_width"]
140
+
141
+ preset_data["width"] if preset_data["width"]
142
+ end
143
+
144
+ def add_quality_operation(operations, preset_data)
145
+ return unless preset_data["quality"]
146
+
147
+ operations << { "quality" => { "quality" => preset_data["quality"] } }
148
+ end
149
+
150
+ def add_format_operation(operations, preset_data)
151
+ return unless preset_data["formats"]
152
+
153
+ formats = Array(preset_data["formats"]).map do |format|
154
+ format == "original" ? "jpg" : format
155
+ end
156
+ operations << { "format" => { "formats" => formats } }
157
+ end
158
+
159
+ def add_crop_operation(operations, preset_data)
160
+ ratio_value = preset_data["ratio"] || preset_data["aspect_ratio"] || preset_data["crop"]
161
+ operations << { "crop" => { "ratio" => ratio_value } } if ratio_value
162
+ end
163
+
164
+ def add_position_operation(operations, preset_data)
165
+ position_value = preset_data["position"] || preset_data["gravity"]
166
+ return unless position_value
167
+
168
+ crop_op = operations.find { |op| op["crop"] }
169
+ if crop_op
170
+ crop_op["crop"]["position"] = position_value
171
+ else
172
+ operations << { "crop" => { "position" => position_value } }
173
+ end
174
+ end
175
+
174
176
  # Generate description for ImgFlow preset
175
177
  # @param preset_name [String] Preset name
176
178
  # @param preset_data [Hash] Original preset data
177
179
  # @return [String] Description
178
180
  def generate_description(preset_name, preset_data)
179
- operations = []
181
+ parts = []
182
+ add_width_description(parts, preset_data)
183
+ parts << "height: #{preset_data['height']}" if preset_data["height"]
184
+ parts << "quality: #{preset_data['quality']}" if preset_data["quality"]
185
+ parts << "formats: #{Array(preset_data['formats']).join(',')}" if preset_data["formats"]
186
+ parts << "crop: #{preset_data['crop']}" if preset_data["crop"]
187
+ parts << "gravity: #{preset_data['gravity']}" if preset_data["gravity"]
188
+
189
+ "Migrated from Picture Tag preset '#{preset_name}'. #{parts.join(', ')}"
190
+ end
180
191
 
192
+ def add_width_description(parts, preset_data)
181
193
  if preset_data["widths"]
182
- operations << "widths: #{Array(preset_data['widths']).join(',')}"
183
- operations << "using max width: #{Array(preset_data['widths']).max}"
194
+ parts << "widths: #{Array(preset_data['widths']).join(',')}"
195
+ parts << "using max width: #{Array(preset_data['widths']).max}"
184
196
  elsif preset_data["base_width"]
185
- operations << "base_width: #{preset_data['base_width']}"
186
- operations << "pixel_ratios: #{Array(preset_data['pixel_ratios']).join(',')}"
197
+ parts << "base_width: #{preset_data['base_width']}"
198
+ parts << "pixel_ratios: #{Array(preset_data['pixel_ratios']).join(',')}"
187
199
  elsif preset_data["width"]
188
- operations << "width: #{preset_data['width']}"
200
+ parts << "width: #{preset_data['width']}"
189
201
  end
190
-
191
- operations << "height: #{preset_data['height']}" if preset_data["height"]
192
- operations << "quality: #{preset_data['quality']}" if preset_data["quality"]
193
- operations << "formats: #{Array(preset_data['formats']).join(',')}" if preset_data["formats"]
194
- operations << "crop: #{preset_data['crop']}" if preset_data["crop"]
195
- operations << "gravity: #{preset_data['gravity']}" if preset_data["gravity"]
196
-
197
- "Migrated from Picture Tag preset '#{preset_name}'. #{operations.join(', ')}"
198
202
  end
199
203
 
200
204
  # Save ImgFlow preset to YAML file
@@ -132,55 +132,59 @@ module JekyllImgFlow
132
132
  # @return [Hash] Tags in key => value format
133
133
  def yaml_to_tags(preset)
134
134
  tags = {}
135
- preset_operations = preset["operations"] || []
136
-
137
- preset_operations.each do |op|
135
+ (preset["operations"] || []).each do |op|
138
136
  next unless op.is_a?(Hash)
139
137
 
140
138
  op.each do |op_type, params|
141
139
  next unless params.is_a?(Hash)
142
140
 
143
- # Store operation type for operations that don't have specific params
144
- # This allows Parser to detect the operation type
145
- case op_type.to_s
146
- when "resize"
147
- # Resize params: width, height (both optional, at least one required)
148
- tags[:width] = params["width"] if params["width"]
149
- tags[:height] = params["height"] if params["height"]
150
- when "crop"
151
- # Crop params: ratio or width/height
152
- tags[:ratio] = params["ratio"] if params["ratio"]
153
- tags[:aspect_ratio] = params["aspect_ratio"] if params["aspect_ratio"]
154
- tags[:width] = params["width"] if params["width"] && !tags[:width]
155
- tags[:height] = params["height"] if params["height"] && !tags[:height]
156
- when "format"
157
- # Format params: format or formats
158
- if params["formats"]
159
- tag_value = params["formats"].is_a?(Array) ? params["formats"].join(",") : params["formats"]
160
- tags[:formats] = tag_value
161
- elsif params["format"]
162
- tags[:format] = params["format"]
163
- end
164
- when "quality"
165
- tags[:quality] = params["quality"] if params["quality"]
166
- when "optimize"
167
- tags[:optimize] = true
168
- tags[:level] = params["level"] if params["level"]
169
- when "opacity"
170
- tags[:opacity] = params["opacity"] if params["opacity"]
171
- else
172
- # Generic handling for unknown operations
173
- params.each do |key, value|
174
- tag_value = value.is_a?(Array) ? value.join(",") : value
175
- tags[key.to_sym] = tag_value
176
- end
177
- end
141
+ merge_operation_tags(tags, op_type.to_s, params)
178
142
  end
179
143
  end
180
144
 
181
145
  tags
182
146
  end
183
147
 
148
+ def merge_operation_tags(tags, op_type, params)
149
+ case op_type
150
+ when "resize"
151
+ tags[:width] = params["width"] if params["width"]
152
+ tags[:height] = params["height"] if params["height"]
153
+ when "crop"
154
+ tags[:ratio] = params["ratio"] if params["ratio"]
155
+ tags[:aspect_ratio] = params["aspect_ratio"] if params["aspect_ratio"]
156
+ tags[:width] = params["width"] if params["width"] && !tags[:width]
157
+ tags[:height] = params["height"] if params["height"] && !tags[:height]
158
+ when "format"
159
+ merge_format_tag(tags, params)
160
+ when "quality"
161
+ tags[:quality] = params["quality"] if params["quality"]
162
+ when "optimize"
163
+ tags[:optimize] = true
164
+ tags[:level] = params["level"] if params["level"]
165
+ when "opacity"
166
+ tags[:opacity] = params["opacity"] if params["opacity"]
167
+ else
168
+ merge_generic_tags(tags, params)
169
+ end
170
+ end
171
+
172
+ def merge_format_tag(tags, params)
173
+ if params["formats"]
174
+ tag_value = params["formats"].is_a?(Array) ? params["formats"].join(",") : params["formats"]
175
+ tags[:formats] = tag_value
176
+ elsif params["format"]
177
+ tags[:format] = params["format"]
178
+ end
179
+ end
180
+
181
+ def merge_generic_tags(tags, params)
182
+ params.each do |key, value|
183
+ tag_value = value.is_a?(Array) ? value.join(",") : value
184
+ tags[key.to_sym] = tag_value
185
+ end
186
+ end
187
+
184
188
  # Merge preset tags with user options (user options override)
185
189
  # @param preset_tags [Hash] Tags from preset
186
190
  # @param user_options [Hash] User-provided options