jekyll-imgflow 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b95efe6605d712e14895c4152a68b4b751eea6954416e428acbe96518bd4284
4
- data.tar.gz: 434fc1985138599142a94d32dda9c986c63893c4acfc587e085d8e3486d700d4
3
+ metadata.gz: 2f902d95441ebd7a1c3a98200b140c2bf109343a79cf43700221a6cfec2902df
4
+ data.tar.gz: c89e222e5f37bb795f44ec7b6e3ecc865ca3fb66fefac0481ea31211f8fa7dfb
5
5
  SHA512:
6
- metadata.gz: f588617884137bcf8054e2a1bce6489b9aebfe4829ce9a279bb65654871dd13d41df6e6f0e0a15ddeb7fb39b9f4f9776206a2f6ff16b83db6e11c82b76dd78ef
7
- data.tar.gz: 480ba9208f2952793bff054b24b8f7e79a47de1b83d322d0f0f261242cbd8d01f4e532b0571397d4f0efb18c31f27867546ba987f51add3a664fb5b7049bcfb1
6
+ metadata.gz: cfc171eb11a3f533033671d2b8d8bfdc90bd3a9790bc9b764cf1ada4e58817179261419c80af2cd0eb2bc8c6bc1c130d754f31a0415664bd4262c43f0c3adb49
7
+ data.tar.gz: 1f92eb5e21c373a01e570bcaa843a51ccfd093655c46a64a4ae21248c9d4862d8cd6acac84830e2f1d023a8de4a8b8b2662bfbfe44e83c0a6a633c6eb23cc6d1
@@ -20,11 +20,12 @@ module JekyllImgFlow
20
20
  # Format used for the <img> fallback in <picture> elements.
21
21
  # All other formats in `formats` get <source> tags for browsers that support them.
22
22
  DEFAULT_FALLBACK_FORMAT = "jpg"
23
+ DEFAULT_IMAGE_MODAL = true
23
24
 
24
25
  attr_reader :site, :originals, :output, :input_formats, :sizes, :formats,
25
26
  :quality, :backend_priority, :imgproxy_url,
26
27
  :weserv_url, :flyimg_url,
27
- :optimize_qualities, :fallback_format
28
+ :optimize_qualities, :fallback_format, :image_modal
28
29
 
29
30
  def initialize(site)
30
31
  shared = site.config["shared_images_configs"] || {}
@@ -40,6 +41,7 @@ module JekyllImgFlow
40
41
  @quality = cfg["quality"] || DEFAULT_QUALITY
41
42
  @backend_priority = cfg["backend_priority"] || DEFAULT_BACKEND_PRIORITY
42
43
  @fallback_format = cfg["fallback_format"] || DEFAULT_FALLBACK_FORMAT
44
+ @image_modal = cfg.fetch("image_modal", DEFAULT_IMAGE_MODAL)
43
45
 
44
46
  @imgproxy_url = cfg["imgproxy_url"]
45
47
  @weserv_url = cfg["weserv_url"]
@@ -47,6 +47,13 @@ module JekyllImgFlow
47
47
  end
48
48
  end
49
49
 
50
+ # Inject modal CSS/JS into pages that contain data-imgflow-modal triggers
51
+ %i[pages documents].each do |type|
52
+ Jekyll::Hooks.register type, :post_render do |doc|
53
+ doc.output = ModalAssets.inject(doc.output) if doc.output
54
+ end
55
+ end
56
+
50
57
  def self.cleanup_orphaned_images(site, manifest = nil)
51
58
  Jekyll.logger.info "🧹 ImgFlow: Checking for orphaned specialized images..."
52
59
 
@@ -4,6 +4,8 @@ module JekyllImgFlow
4
4
  # Unified HTML Generator for all markup formats
5
5
  # Supports Picture Tag compatibility with multiple markup formats and attributes
6
6
  class HtmlGenerator
7
+ include ModalWrapper
8
+
7
9
  # Generate HTML based on markup format and attributes
8
10
  # @param results [Array<String>] Processed image paths
9
11
  # @param attributes [Hash] HTML attributes by element
@@ -47,6 +49,9 @@ module JekyllImgFlow
47
49
  # Wrap in link if specified
48
50
  html = wrap_with_link(html) if @attributes[:link]
49
51
 
52
+ # Wrap in modal trigger if enabled (only for HTML elements, not raw URLs)
53
+ html = wrap_with_modal(html) if modal_enabled? && html_element?
54
+
50
55
  # Wrap in parent container if specified
51
56
  html = wrap_with_parent(html) if @attributes[:parent]&.any?
52
57
 
@@ -77,7 +82,8 @@ module JekyllImgFlow
77
82
  a: attributes[:a] || {},
78
83
  parent: attributes[:parent] || {},
79
84
  alt: attributes[:alt],
80
- link: attributes[:link]
85
+ link: attributes[:link],
86
+ modal: attributes[:modal]
81
87
  }
82
88
  end
83
89
 
@@ -89,7 +95,8 @@ module JekyllImgFlow
89
95
  a: {},
90
96
  parent: {},
91
97
  alt: nil,
92
- link: nil
98
+ link: nil,
99
+ modal: nil
93
100
  }
94
101
  end
95
102
 
@@ -60,7 +60,8 @@ module Jekyll
60
60
  # Combine image path with preset markup. Keep the path quoted because
61
61
  # filenames may contain spaces.
62
62
  image_markup = if image_path.include?(" ")
63
- "\"#{image_path.gsub('"', '\\"')}\""
63
+ escaped_path = image_path.gsub("\\") { "\\\\" }.gsub('"', '\\"')
64
+ "\"#{escaped_path}\""
64
65
  else
65
66
  image_path
66
67
  end
@@ -193,7 +194,8 @@ module Jekyll
193
194
  a: extract_prefixed_attrs(base_attrs, "a-"),
194
195
  parent: extract_prefixed_attrs(base_attrs, "parent-"),
195
196
  alt: base_attrs[:alt],
196
- link: base_attrs[:link]
197
+ link: base_attrs[:link],
198
+ modal: base_attrs[:modal]
197
199
  }
198
200
  end
199
201
 
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllImgFlow
4
+ # Inline CSS and JS for the image modal/lightbox feature.
5
+ # Injected by hooks.rb into pages that contain data-imgflow-modal triggers.
6
+ module ModalAssets
7
+ CSS = <<~CSS
8
+ .imgflow-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.85);
9
+ display:flex;align-items:center;justify-content:center;z-index:9999;
10
+ cursor:pointer;animation:imgflow-fade .15s ease-out}
11
+ .imgflow-modal-image{max-width:90vw;max-height:90vh;object-fit:contain;
12
+ cursor:auto;border-radius:4px;box-shadow:0 4px 30px rgba(0,0,0,.5)}
13
+ .imgflow-modal-close{position:absolute;top:1rem;right:1rem;
14
+ background:rgba(255,255,255,.2);border:none;color:#fff;font-size:2rem;
15
+ width:3rem;height:3rem;border-radius:50%;cursor:pointer;
16
+ display:flex;align-items:center;justify-content:center;line-height:1}
17
+ .imgflow-modal-close:hover{background:rgba(255,255,255,.4)}
18
+ @keyframes imgflow-fade{from{opacity:0}to{opacity:1}}
19
+ CSS
20
+
21
+ JS = <<~JS
22
+ (function(){
23
+ function openModal(src,alt){
24
+ var o=document.createElement('div');
25
+ o.className='imgflow-modal-overlay';
26
+ var i=document.createElement('img');
27
+ i.src=src;i.alt=alt||'';i.className='imgflow-modal-image';
28
+ var b=document.createElement('button');
29
+ b.className='imgflow-modal-close';b.setAttribute('aria-label','Close');
30
+ b.innerHTML='&times;';
31
+ o.appendChild(b);o.appendChild(i);document.body.appendChild(o);
32
+ function c(){document.body.removeChild(o);
33
+ document.removeEventListener('keydown',k);}
34
+ function k(e){if(e.key==='Escape')c();}
35
+ o.addEventListener('click',function(e){
36
+ if(e.target===o||e.target===b)c();});
37
+ document.addEventListener('keydown',k);}
38
+ document.addEventListener('click',function(e){
39
+ var t=e.target.closest('[data-imgflow-modal]');
40
+ if(!t)return;e.preventDefault();
41
+ openModal(t.getAttribute('href'),t.getAttribute('data-imgflow-alt'));});
42
+ })();
43
+ JS
44
+
45
+ def self.inject(html)
46
+ return html unless html&.include?("data-imgflow-modal")
47
+
48
+ style_tag = "<style>\n#{CSS}</style>"
49
+ script_tag = "<script>\n#{JS}</script>"
50
+
51
+ if html.include?("</head>")
52
+ html = html.sub("</head>", "#{style_tag}\n</head>")
53
+ html = html.sub("</body>", "#{script_tag}\n</body>") if html.include?("</body>")
54
+ elsif html.include?("</body>")
55
+ html = html.sub("</body>", "#{style_tag}\n#{script_tag}\n</body>")
56
+ else
57
+ html = "#{html}\n#{style_tag}\n#{script_tag}"
58
+ end
59
+
60
+ html
61
+ end
62
+ end
63
+
64
+ # Modal wrapping logic for HtmlGenerator — extracted to keep HtmlGenerator
65
+ # under the RuboCop class length limit.
66
+ module ModalWrapper
67
+ # Formats that produce raw URLs/srcsets (not wrappable HTML elements)
68
+ NON_HTML_FORMATS = %w[direct_url naked_srcset].freeze
69
+
70
+ # Check if the current markup format produces an HTML element
71
+ def html_element?
72
+ !NON_HTML_FORMATS.include?(@markup_format)
73
+ end
74
+
75
+ # Determine if modal behavior is enabled for this image
76
+ # Per-image modal:true/false overrides config default
77
+ # Modal is skipped when an explicit link is set
78
+ def modal_enabled?
79
+ return false if @attributes[:link]
80
+
81
+ modal_attr = @attributes[:modal]
82
+ return modal_attr.to_s == "true" if modal_attr
83
+
84
+ @config&.image_modal ? true : false
85
+ end
86
+
87
+ # Pick the best image path for the modal — prefer the fallback format
88
+ # (e.g. jpg) for maximum browser compatibility
89
+ 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
93
+ end
94
+
95
+ # Wrap HTML in modal trigger anchor
96
+ def wrap_with_modal(html)
97
+ modal_href = html_path(modal_image_path)
98
+ return html unless modal_href
99
+
100
+ data_attrs = " data-imgflow-modal"
101
+ alt = @attributes[:alt]
102
+ data_attrs += " data-imgflow-alt=\"#{escape_attr_value(alt)}\"" if alt
103
+
104
+ "<a href=\"#{modal_href}\"#{data_attrs}>#{html}</a>"
105
+ end
106
+ end
107
+ end
@@ -9,7 +9,7 @@ module JekyllImgFlow
9
9
  # Define valid operation parameters and attribute types
10
10
  OPERATION_PARAMS = %i[width height ratio aspect_ratio quality format formats
11
11
  optimize level watermark opacity position keep preset].freeze
12
- HTML_ATTRIBUTES = %i[alt class title loading].freeze
12
+ HTML_ATTRIBUTES = %i[alt class title loading link modal].freeze
13
13
 
14
14
  # Simple operation mappings for single-parameter operations
15
15
  SIMPLE_OPERATIONS = {
@@ -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.2.0"
4
+ VERSION = "0.3.1"
5
5
  end
@@ -23,6 +23,7 @@ require_relative "jekyll-imgflow/provider_registry"
23
23
  require_relative "jekyll-imgflow/build_time_processor"
24
24
  require_relative "jekyll-imgflow/parser"
25
25
  require_relative "jekyll-imgflow/tags/tag_registry"
26
+ require_relative "jekyll-imgflow/modal_assets"
26
27
  require_relative "jekyll-imgflow/html_generator"
27
28
  require_relative "jekyll-imgflow/imgflow_tag"
28
29
  require_relative "jekyll-imgflow/picture_tag_adaptor"
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.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svend Gundestrup
@@ -74,6 +74,7 @@ files:
74
74
  - lib/jekyll-imgflow/html_generator.rb
75
75
  - lib/jekyll-imgflow/imgflow_tag.rb
76
76
  - lib/jekyll-imgflow/manifest_manager.rb
77
+ - lib/jekyll-imgflow/modal_assets.rb
77
78
  - lib/jekyll-imgflow/operation_processor.rb
78
79
  - lib/jekyll-imgflow/parser.rb
79
80
  - lib/jekyll-imgflow/path_resolver.rb