jekyll-imgflow 0.3.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d37c208e7250fe6da8291bfab933d5df7391347d0ce95492e32e1d94e54df7a8
4
- data.tar.gz: cd222c0e6dbdcffe168ec0919a12d53c57324a277cb9efeecb5173a98c4c5b04
3
+ metadata.gz: fa56ff31b7cfe312dfe202726e049c99db5511dc35b2e82d721abde41c76488e
4
+ data.tar.gz: 88ff3c89e44a1dfe0fe022a016bbeba79185c137bca201c4e2c66c9d06cc5a92
5
5
  SHA512:
6
- metadata.gz: 2f566b0e2d70993a209a5353cf1b91d54e3b3f3143397146f58bbbcf19428eab9797ed5def60efdd3d8f926f214fa2ec6670d47ccfd6c000fe2f9c35b2bac00c
7
- data.tar.gz: 0b601aea8c16d71133816f034b4277e399e6108569af169c0984416ef6e5d90edc30490d17af254f736f2897eae5c6b55d515888a4b71af35dd8020b63f37a88
6
+ metadata.gz: c1f92e12c629652780356a972969ee435992b29dd62dea03ea263582084e8b512b35eb25e8bb1845c111834a5506cd1c3ffd5c9b4c5392ff6e17e20d3ea5c4ae
7
+ data.tar.gz: 92b73297116fd7dd7d38a5aeb349c81aeab97709f3a85886469dc0443e3fa9e5e35c2c971b1c01bd6682db2c59776c54112682a31e3610c64a6254b3d4067441
@@ -83,7 +83,8 @@ module JekyllImgFlow
83
83
  parent: attributes[:parent] || {},
84
84
  alt: attributes[:alt],
85
85
  link: attributes[:link],
86
- modal: attributes[:modal]
86
+ modal: attributes[:modal],
87
+ modal_results: attributes[:modal_results] || []
87
88
  }
88
89
  end
89
90
 
@@ -96,7 +97,8 @@ module JekyllImgFlow
96
97
  parent: {},
97
98
  alt: nil,
98
99
  link: nil,
99
- modal: nil
100
+ modal: nil,
101
+ modal_results: []
100
102
  }
101
103
  end
102
104
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "shellwords"
4
+ require "fastimage"
4
5
 
5
6
  module Jekyll
6
7
  class ImgflowTag < Liquid::Tag
@@ -60,7 +61,8 @@ module Jekyll
60
61
  # Combine image path with preset markup. Keep the path quoted because
61
62
  # filenames may contain spaces.
62
63
  image_markup = if image_path.include?(" ")
63
- "\"#{image_path.gsub('"', '\\"')}\""
64
+ escaped_path = image_path.gsub("\\") { "\\\\" }.gsub('"', '\\"')
65
+ "\"#{escaped_path}\""
64
66
  else
65
67
  image_path
66
68
  end
@@ -98,11 +100,51 @@ module Jekyll
98
100
  page_path)
99
101
  end
100
102
 
103
+ modal_results = if operations.empty?
104
+ []
105
+ else
106
+ process_modal_variants(components, operations.first, parsed, original_name,
107
+ input_path, page_path)
108
+ end
101
109
  relative_results = results.uniq.map { |result| relative_result_path(result, site) }
110
+ relative_modal_results = modal_results.uniq.map do |result|
111
+ relative_result_path(result, site)
112
+ end
102
113
  parsed = parsed.merge(markup_format: "picture") if relative_results.length > 1
114
+ parsed = parsed.merge(modal_results: relative_modal_results)
103
115
  generate_html(relative_results, parsed, context)
104
116
  end
105
117
 
118
+ def process_modal_variants(components, operation, parsed, original_name, input_path, page_path)
119
+ config = components[:config]
120
+ return [] unless modal_requested?(parsed, config)
121
+
122
+ original_width = FastImage.size(input_path)&.first
123
+ return [] unless original_width
124
+
125
+ params = operation[:params].dup
126
+ formats = Array(params.delete(:formats) || params[:format])
127
+ formats = config.formats if formats.empty?
128
+ modal_width = [original_width, config.sizes.values.max].min
129
+
130
+ formats.map do |format|
131
+ modal_params = params.merge(width: modal_width, format: format)
132
+ process_variant(components, operation, modal_params, original_name, input_path, page_path)
133
+ end
134
+ rescue FastImage::ImageFetchError, FastImage::UnknownImageType
135
+ []
136
+ end
137
+
138
+ def modal_requested?(parsed, config)
139
+ return false if %w[direct_url naked_srcset].include?(parsed[:markup_format])
140
+
141
+ attrs = parsed[:html_attributes] || {}
142
+ return false if attrs[:link]
143
+
144
+ modal = attrs[:modal]
145
+ modal.nil? ? (config.respond_to?(:image_modal) && config.image_modal) : modal.to_s == "true"
146
+ end
147
+
106
148
  def process_variants(components, operation, original_name, input_path, page_path)
107
149
  params = operation[:params].dup
108
150
  formats = Array(params.delete(:formats) || params[:format])
@@ -194,7 +236,8 @@ module Jekyll
194
236
  parent: extract_prefixed_attrs(base_attrs, "parent-"),
195
237
  alt: base_attrs[:alt],
196
238
  link: base_attrs[:link],
197
- modal: base_attrs[:modal]
239
+ modal: base_attrs[:modal],
240
+ modal_results: parsed[:modal_results] || []
198
241
  }
199
242
  end
200
243
 
@@ -20,15 +20,24 @@ module JekyllImgFlow
20
20
 
21
21
  JS = <<~JS
22
22
  (function(){
23
- function openModal(src,alt){
23
+ function openModal(trigger,alt){
24
24
  var o=document.createElement('div');
25
25
  o.className='imgflow-modal-overlay';
26
+ var p=document.createElement('picture');
27
+ var href=trigger.getAttribute('href');
28
+ var types={avif:'image/avif',webp:'image/webp',png:'image/png',jpg:'image/jpeg',jpeg:'image/jpeg'};
29
+ var formats=(trigger.getAttribute('data-imgflow-modal-formats')||'').split(',');
30
+ formats.forEach(function(format){
31
+ if(!format)return;
32
+ var s=document.createElement('source');
33
+ s.srcset=href.replace(/.[^.]+$/,'.'+format);s.type=types[format];p.appendChild(s);});
26
34
  var i=document.createElement('img');
27
- i.src=src;i.alt=alt||'';i.className='imgflow-modal-image';
35
+ i.src=href;
36
+ i.alt=alt||'';i.className='imgflow-modal-image';p.appendChild(i);
28
37
  var b=document.createElement('button');
29
38
  b.className='imgflow-modal-close';b.setAttribute('aria-label','Close');
30
39
  b.innerHTML='&times;';
31
- o.appendChild(b);o.appendChild(i);document.body.appendChild(o);
40
+ o.appendChild(b);o.appendChild(p);document.body.appendChild(o);
32
41
  function c(){document.body.removeChild(o);
33
42
  document.removeEventListener('keydown',k);}
34
43
  function k(e){if(e.key==='Escape')c();}
@@ -38,7 +47,7 @@ module JekyllImgFlow
38
47
  document.addEventListener('click',function(e){
39
48
  var t=e.target.closest('[data-imgflow-modal]');
40
49
  if(!t)return;e.preventDefault();
41
- openModal(t.getAttribute('href'),t.getAttribute('data-imgflow-alt'));});
50
+ openModal(t,t.getAttribute('data-imgflow-alt'));});
42
51
  })();
43
52
  JS
44
53
 
@@ -84,20 +93,72 @@ module JekyllImgFlow
84
93
  @config&.image_modal ? true : false
85
94
  end
86
95
 
87
- # Pick the best image path for the modal — prefer the fallback format
88
- # (e.g. jpg) for maximum browser compatibility
96
+ # Pick the largest existing fallback-format variant for the modal.
89
97
  def modal_image_path
90
- fallback_ext = @config&.fallback_format ? ".#{@config.fallback_format}" : ".jpg"
91
- fallback = @results.find { |r| File.extname(r).downcase == fallback_ext }
92
- fallback || @results.last
98
+ variants = modal_variants
99
+ variants[modal_fallback_format] || @results.last
100
+ end
101
+
102
+ def modal_variants
103
+ candidates = @attributes[:modal_results] + @results + generated_modal_results
104
+ max_width = modal_max_width
105
+
106
+ candidates.each_with_object({}) do |result, variants|
107
+ parsed = JekyllImgFlow::FilenameGenerator.new.parse_filename(File.basename(result))
108
+ next if parsed.empty? || (max_width && parsed[:width] > max_width)
109
+ next if variants[parsed[:format]] && modal_width(variants[parsed[:format]]) >= parsed[:width]
110
+
111
+ variants[parsed[:format]] = result
112
+ end
113
+ end
114
+
115
+ def modal_fallback_format
116
+ @config&.fallback_format.to_s
117
+ end
118
+
119
+ def modal_max_width
120
+ return unless @config.respond_to?(:sizes)
121
+
122
+ @config.sizes.values.max
123
+ end
124
+
125
+ def generated_modal_results
126
+ return [] unless @config && @context&.registers&.key?(:site)
127
+
128
+ result = @results.first
129
+ parsed = JekyllImgFlow::FilenameGenerator.new.parse_filename(File.basename(result))
130
+ return [] if parsed.empty?
131
+
132
+ site = @context.registers[:site]
133
+ directory = File.join(site.source, File.dirname(result.delete_prefix("/")))
134
+ return [] unless File.directory?(directory)
135
+
136
+ max_width = @config.sizes.values.max
137
+ Dir.children(directory).filter_map do |filename|
138
+ candidate = JekyllImgFlow::FilenameGenerator.new.parse_filename(filename)
139
+ next unless candidate[:base_name] == parsed[:base_name]
140
+ next unless candidate[:hash] == parsed[:hash]
141
+ next if max_width && candidate[:width] > max_width
142
+
143
+ File.join(File.dirname(result), filename)
144
+ end
145
+ rescue Errno::ENOENT
146
+ []
147
+ end
148
+
149
+ def modal_width(result)
150
+ JekyllImgFlow::FilenameGenerator.new.parse_filename(File.basename(result))[:width] || 0
93
151
  end
94
152
 
95
153
  # Wrap HTML in modal trigger anchor
96
154
  def wrap_with_modal(html)
97
- modal_href = html_path(modal_image_path)
155
+ variants = modal_variants
156
+ modal_href = html_path(variants[modal_fallback_format] || @results.last)
98
157
  return html unless modal_href
99
158
 
159
+ source_formats = variants.keys.reject { |format| format == modal_fallback_format }
100
160
  data_attrs = " data-imgflow-modal"
161
+ data_attrs += " data-imgflow-modal-formats=\"#{source_formats.join(',')}\"" unless source_formats.empty?
101
162
  alt = @attributes[:alt]
102
163
  data_attrs += " data-imgflow-alt=\"#{escape_attr_value(alt)}\"" if alt
103
164
 
@@ -42,11 +42,11 @@ module JekyllImgFlow
42
42
  # @param picture_markup [String] Picture Tag markup like "{% picture hero image.jpg 16:9 --alt Text %}"
43
43
  # @return [Hash] Translation result with markup and attributes
44
44
  def translate_to_imgflow(picture_markup)
45
- # Extract content between {% picture ... %}
46
- match = picture_markup.match(/\{%\s*picture\s+(.+?)%\}/)
47
- return { markup: "", attributes: {} } unless match
45
+ # Extract content between {% picture ... %} without a backtracking regex
46
+ content = extract_picture_content(picture_markup)
47
+ return { markup: "", attributes: {} } unless content
48
48
 
49
- content = match[1].strip
49
+ content = content.strip
50
50
  return { markup: "", attributes: {} } if content.empty?
51
51
 
52
52
  # Parse arguments
@@ -75,6 +75,32 @@ module JekyllImgFlow
75
75
 
76
76
  private
77
77
 
78
+ def extract_picture_content(markup)
79
+ offset = 0
80
+ while (opening = markup.index("{%", offset))
81
+ cursor = skip_whitespace(markup, opening + 2)
82
+ if markup[cursor, 7] == "picture"
83
+ cursor += 7
84
+ if cursor < markup.length && whitespace?(markup[cursor])
85
+ cursor = skip_whitespace(markup, cursor)
86
+ closing = markup.index("%}", cursor)
87
+ return markup[cursor...closing] if closing
88
+ end
89
+ end
90
+ offset = opening + 2
91
+ end
92
+ nil
93
+ end
94
+
95
+ def skip_whitespace(markup, cursor)
96
+ cursor += 1 while cursor < markup.length && whitespace?(markup[cursor])
97
+ cursor
98
+ end
99
+
100
+ def whitespace?(character)
101
+ [" ", "\t", "\r", "\n"].include?(character)
102
+ end
103
+
78
104
  # Parse arguments handling quoted paths and attributes
79
105
  # @param content [String] Raw content
80
106
  # @return [Array] Parsed arguments
@@ -79,7 +79,7 @@ module JekyllImgFlow
79
79
 
80
80
  format_op = @operations.find { |op| op[:type] == :format }
81
81
  quality_op = @operations.find { |op| op[:type] == :quality }
82
- command_parts += ["-f", format_op[:format]] if format_op
82
+ command_parts += ["-f", sharp_format(format_op[:format])] if format_op
83
83
  command_parts += ["-q#{quality_op[:quality]}"] if quality_op
84
84
 
85
85
  if opacity && opacity < 1.0
@@ -142,7 +142,7 @@ module JekyllImgFlow
142
142
  quality_op = @operations.find { |op| op[:type] == :quality }
143
143
  alpha_op = @operations.find { |op| op[:type] == :alpha_opacity }
144
144
 
145
- command_parts += ["-f", format_op[:format]] if format_op
145
+ command_parts += ["-f", sharp_format(format_op[:format])] if format_op
146
146
  command_parts += ["-q#{quality_op[:quality]}"] if quality_op
147
147
  if alpha_op
148
148
  alpha_value = (alpha_op[:opacity] * 255).round
@@ -200,7 +200,7 @@ module JekyllImgFlow
200
200
  quality_op = @operations.find { |op| op[:type] == :quality }
201
201
  alpha_op = @operations.find { |op| op[:type] == :alpha_opacity }
202
202
 
203
- command_parts += ["-f", format_op[:format]] if format_op
203
+ command_parts += ["-f", sharp_format(format_op[:format])] if format_op
204
204
  command_parts += ["-q#{quality_op[:quality]}"] if quality_op
205
205
  if alpha_op
206
206
  alpha_value = (alpha_op[:opacity] * 255).round
@@ -209,6 +209,12 @@ module JekyllImgFlow
209
209
 
210
210
  command_parts.join(" ")
211
211
  end
212
+
213
+ # Sharp CLI calls JPEG "jpeg", while ImgFlow uses the standard "jpg"
214
+ # extension in generated filenames and public configuration.
215
+ def sharp_format(format)
216
+ format.to_s == "jpg" ? "jpeg" : format
217
+ end
212
218
  end
213
219
  end
214
220
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JekyllImgFlow
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-imgflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svend Gundestrup