jekyll-imgflow 0.1.10 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +22 -13
- data/lib/jekyll-imgflow/batch_manager.rb +10 -7
- data/lib/jekyll-imgflow/build_time_processor.rb +70 -61
- data/lib/jekyll-imgflow/config.rb +6 -7
- data/lib/jekyll-imgflow/filename_generator.rb +8 -10
- data/lib/jekyll-imgflow/generated_file_cleaner.rb +81 -0
- data/lib/jekyll-imgflow/hooks.rb +19 -5
- data/lib/jekyll-imgflow/html_generator.rb +22 -20
- data/lib/jekyll-imgflow/imgflow_tag.rb +86 -70
- data/lib/jekyll-imgflow/manifest_manager.rb +132 -53
- data/lib/jekyll-imgflow/modal_assets.rb +107 -0
- data/lib/jekyll-imgflow/operation_processor.rb +43 -22
- data/lib/jekyll-imgflow/parser.rb +1 -1
- data/lib/jekyll-imgflow/picture_tag_adaptor.rb +1 -1
- data/lib/jekyll-imgflow/preset_manager.rb +58 -7
- data/lib/jekyll-imgflow/presets/gallery.yml +7 -0
- data/lib/jekyll-imgflow/presets/hero.yml +7 -0
- data/lib/jekyll-imgflow/presets/thumbnail.yml +7 -0
- data/lib/jekyll-imgflow/processing_stats.rb +80 -0
- data/lib/jekyll-imgflow/provider_registry.rb +18 -3
- data/lib/jekyll-imgflow/providers/base_provider.rb +12 -1
- data/lib/jekyll-imgflow/providers/flyimg.rb +2 -1
- data/lib/jekyll-imgflow/providers/imagemagick.rb +72 -3
- data/lib/jekyll-imgflow/providers/imgproxy.rb +2 -1
- data/lib/jekyll-imgflow/providers/libvips.rb +152 -100
- data/lib/jekyll-imgflow/providers/sharp.rb +2 -1
- data/lib/jekyll-imgflow/providers/weserv.rb +3 -2
- data/lib/jekyll-imgflow/tags/opacity_tag.rb +8 -1
- data/lib/jekyll-imgflow/tags/resize_tag.rb +2 -0
- data/lib/jekyll-imgflow/tags/watermark_tag.rb +8 -1
- data/lib/jekyll-imgflow/tasks.rb +129 -0
- data/lib/jekyll-imgflow/version.rb +1 -1
- data/lib/jekyll-imgflow.rb +3 -0
- metadata +25 -157
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
3
5
|
module Jekyll
|
|
4
6
|
class ImgflowTag < Liquid::Tag
|
|
5
7
|
def initialize(tag_name, markup, tokens)
|
|
@@ -36,7 +38,7 @@ module Jekyll
|
|
|
36
38
|
|
|
37
39
|
def expand_preset_markup(markup, preset_manager)
|
|
38
40
|
# Parse markup to extract image path, preset name, and user options
|
|
39
|
-
parts =
|
|
41
|
+
parts = Shellwords.split(markup)
|
|
40
42
|
image_path = parts.first
|
|
41
43
|
|
|
42
44
|
# Extract preset name and user options
|
|
@@ -55,8 +57,14 @@ module Jekyll
|
|
|
55
57
|
# Get preset markup from PresetManager
|
|
56
58
|
preset_markup = preset_manager.build_markup_from_preset(preset_name, user_options)
|
|
57
59
|
|
|
58
|
-
# Combine image path with preset markup
|
|
59
|
-
|
|
60
|
+
# Combine image path with preset markup. Keep the path quoted because
|
|
61
|
+
# filenames may contain spaces.
|
|
62
|
+
image_markup = if image_path.include?(" ")
|
|
63
|
+
"\"#{image_path.gsub('"', '\\"')}\""
|
|
64
|
+
else
|
|
65
|
+
image_path
|
|
66
|
+
end
|
|
67
|
+
"#{image_markup} #{preset_markup}"
|
|
60
68
|
end
|
|
61
69
|
|
|
62
70
|
def process_operations(components, parsed, context)
|
|
@@ -82,73 +90,77 @@ module Jekyll
|
|
|
82
90
|
"unknown"
|
|
83
91
|
end
|
|
84
92
|
|
|
85
|
-
# Process operations using clean architecture
|
|
86
93
|
operations = parsed[:operations]
|
|
94
|
+
results = if operations.empty?
|
|
95
|
+
[input_path]
|
|
96
|
+
else
|
|
97
|
+
process_variants(components, operations.first, original_name, input_path,
|
|
98
|
+
page_path)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
relative_results = results.uniq.map { |result| relative_result_path(result, site) }
|
|
102
|
+
parsed = parsed.merge(markup_format: "picture") if relative_results.length > 1
|
|
103
|
+
generate_html(relative_results, parsed, context)
|
|
104
|
+
end
|
|
87
105
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
version_type = determine_version_type(params, components[:config])
|
|
112
|
-
|
|
113
|
-
# Check if version exists in manifest
|
|
114
|
-
Jekyll.logger.debug "🔍 ImgflowTag: Checking existence - original_name: #{original_name}, version_type: #{version_type}, page_path: #{page_path}"
|
|
115
|
-
Jekyll.logger.debug "🔍 ImgflowTag: Params being checked: #{params.inspect}"
|
|
116
|
-
|
|
117
|
-
if components[:manifest].version_exists?(original_name, params, version_type)
|
|
118
|
-
# Version exists - update page usage and return path
|
|
119
|
-
Jekyll.logger.debug "✅ ImgflowTag: Version exists! Updating page usage"
|
|
120
|
-
components[:manifest].update_page_usage(original_name, params, version_type,
|
|
121
|
-
page_path)
|
|
122
|
-
output_path
|
|
123
|
-
else
|
|
124
|
-
# Version doesn't exist - create it
|
|
125
|
-
Jekyll.logger.debug "❌ ImgflowTag: Version doesn't exist - creating"
|
|
126
|
-
versions = components[:manifest].get_versions(original_name)
|
|
127
|
-
Jekyll.logger.debug "🔍 ImgflowTag: Manifest has - default: #{versions['default']&.length || 0}, specialized: #{versions['specialized']&.length || 0}"
|
|
128
|
-
|
|
129
|
-
components[:operation_processor].process_operation(
|
|
130
|
-
original_name,
|
|
131
|
-
operation,
|
|
132
|
-
input_path,
|
|
133
|
-
page_path
|
|
134
|
-
)
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Convert absolute paths to relative for HTML generation
|
|
139
|
-
relative_result = if result.start_with?(site.dest)
|
|
140
|
-
# Processed images - remove site.dest and leading /
|
|
141
|
-
result.sub(%r{^#{Regexp.escape(site.dest)}/}, "")
|
|
142
|
-
elsif result.start_with?(site.source)
|
|
143
|
-
# Original images - convert from source to relative and remove leading /
|
|
144
|
-
result.sub(%r{^#{Regexp.escape(site.source)}/}, "")
|
|
145
|
-
else
|
|
146
|
-
# Already relative or other format - remove leading / if present
|
|
147
|
-
result.start_with?("/") ? result[1..] : result
|
|
148
|
-
end
|
|
106
|
+
def process_variants(components, operation, original_name, input_path, page_path)
|
|
107
|
+
params = operation[:params].dup
|
|
108
|
+
formats = Array(params.delete(:formats) || params[:format])
|
|
109
|
+
# When no format is explicitly specified, generate all configured formats
|
|
110
|
+
# (avif, webp, png, jpg) so browsers get a <picture> with <source> tags for
|
|
111
|
+
# modern formats and an <img> fallback. This applies to both default-width
|
|
112
|
+
# versions (which find pre-generated files in the manifest cache) and
|
|
113
|
+
# specialized versions (which are generated on-demand).
|
|
114
|
+
formats = components[:config].formats if formats.empty?
|
|
115
|
+
|
|
116
|
+
formats.map do |format|
|
|
117
|
+
variant_params = params.dup
|
|
118
|
+
variant_params[:format] = format
|
|
119
|
+
process_variant(components, operation, variant_params, original_name, input_path, page_path)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def process_variant(components, operation, params, original_name, input_path, page_path)
|
|
124
|
+
config = components[:config]
|
|
125
|
+
if determine_version_type(params, config) == :default
|
|
126
|
+
params[:format] ||= config.formats.first
|
|
127
|
+
params[:quality] ||= config.quality
|
|
128
|
+
end
|
|
149
129
|
|
|
150
|
-
|
|
151
|
-
|
|
130
|
+
digest = components[:filename_generator].file_digest(input_path)
|
|
131
|
+
variant = operation.merge(params: params, file_digest: digest)
|
|
132
|
+
subdir = File.dirname(original_name)
|
|
133
|
+
subdir = nil if subdir == "."
|
|
134
|
+
filename = components[:filename_generator].generate_filename(input_path, params)
|
|
135
|
+
output_path = components[:path_resolver].resolve_source_output_path(filename, subdir)
|
|
136
|
+
version_type = determine_version_type(params, config)
|
|
137
|
+
|
|
138
|
+
if components[:manifest].version_exists?(original_name, params, version_type, digest) &&
|
|
139
|
+
File.file?(output_path)
|
|
140
|
+
components[:stats]&.record_cache_hit
|
|
141
|
+
components[:manifest].update_page_usage(original_name, params, version_type, page_path)
|
|
142
|
+
return output_path
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
unless components[:operation_processor]
|
|
146
|
+
Jekyll.logger.warn "ImgFlow:", "No image provider available — " \
|
|
147
|
+
"rendering original without optimization."
|
|
148
|
+
return input_path
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
components[:operation_processor].process_operation(
|
|
152
|
+
original_name, variant.merge(force_processing: true), input_path, page_path
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def relative_result_path(result, site)
|
|
157
|
+
if result.start_with?(site.dest)
|
|
158
|
+
result.sub(%r{^#{Regexp.escape(site.dest)}/}, "")
|
|
159
|
+
elsif result.start_with?(site.source)
|
|
160
|
+
result.sub(%r{^#{Regexp.escape(site.source)}/}, "")
|
|
161
|
+
else
|
|
162
|
+
result.delete_prefix("/")
|
|
163
|
+
end
|
|
152
164
|
end
|
|
153
165
|
|
|
154
166
|
def generate_html(results, parsed, context)
|
|
@@ -181,7 +193,8 @@ module Jekyll
|
|
|
181
193
|
a: extract_prefixed_attrs(base_attrs, "a-"),
|
|
182
194
|
parent: extract_prefixed_attrs(base_attrs, "parent-"),
|
|
183
195
|
alt: base_attrs[:alt],
|
|
184
|
-
link: base_attrs[:link]
|
|
196
|
+
link: base_attrs[:link],
|
|
197
|
+
modal: base_attrs[:modal]
|
|
185
198
|
}
|
|
186
199
|
end
|
|
187
200
|
|
|
@@ -232,8 +245,10 @@ module Jekyll
|
|
|
232
245
|
filename_generator = JekyllImgFlow::FilenameGenerator.new
|
|
233
246
|
registry = JekyllImgFlow::ProviderRegistry.new(config)
|
|
234
247
|
provider = registry.current_provider
|
|
235
|
-
|
|
236
|
-
|
|
248
|
+
if provider
|
|
249
|
+
operation_processor = JekyllImgFlow::OperationProcessor.new(provider, path_resolver,
|
|
250
|
+
manifest, config)
|
|
251
|
+
end
|
|
237
252
|
preset_manager = JekyllImgFlow::PresetManager.new(site, config)
|
|
238
253
|
|
|
239
254
|
site.imgflow_components = {
|
|
@@ -244,6 +259,7 @@ module Jekyll
|
|
|
244
259
|
registry: registry,
|
|
245
260
|
provider: provider,
|
|
246
261
|
operation_processor: operation_processor,
|
|
262
|
+
stats: operation_processor&.stats,
|
|
247
263
|
preset_manager: preset_manager
|
|
248
264
|
}
|
|
249
265
|
end
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "fileutils"
|
|
5
|
-
require "digest"
|
|
6
5
|
|
|
7
6
|
module JekyllImgFlow
|
|
8
7
|
# Manages the manifest of generated images
|
|
@@ -13,9 +12,11 @@ module JekyllImgFlow
|
|
|
13
12
|
|
|
14
13
|
def initialize(site)
|
|
15
14
|
@site = site
|
|
16
|
-
|
|
17
|
-
@
|
|
18
|
-
@
|
|
15
|
+
@config = JekyllImgFlow::Config.new(site)
|
|
16
|
+
@file_cleaner = GeneratedFileCleaner.new(site, @config)
|
|
17
|
+
@manifest_path = File.join(@site.source, @config.cache_dir, "imgflow-manifest.json")
|
|
18
|
+
@legacy_manifest_paths = legacy_manifest_paths
|
|
19
|
+
@manifest = load_manifest(manifest_load_path)
|
|
19
20
|
@current_provider = current_provider
|
|
20
21
|
|
|
21
22
|
# Check if provider changed and invalidate cache if needed
|
|
@@ -23,20 +24,22 @@ module JekyllImgFlow
|
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
# Load existing manifest or create new one
|
|
26
|
-
def load_manifest
|
|
27
|
-
if File.exist?(
|
|
27
|
+
def load_manifest(path = @manifest_path)
|
|
28
|
+
if path && File.exist?(path)
|
|
28
29
|
begin
|
|
29
|
-
data = JSON.parse(File.read(
|
|
30
|
+
data = JSON.parse(File.read(path))
|
|
30
31
|
|
|
31
32
|
# Handle new format with provider at top level
|
|
32
33
|
if data.is_a?(Hash) && data.key?("images")
|
|
33
34
|
@cached_provider = data["provider"]
|
|
34
|
-
data["images"] || {}
|
|
35
|
+
images = data["images"] || {}
|
|
35
36
|
else
|
|
36
37
|
# Old format - just image data
|
|
37
38
|
@cached_provider = nil
|
|
38
|
-
data
|
|
39
|
+
images = data
|
|
39
40
|
end
|
|
41
|
+
|
|
42
|
+
migrate_legacy_manifest(images)
|
|
40
43
|
rescue JSON::ParserError => e
|
|
41
44
|
Jekyll.logger.warn "ImgFlow: Corrupt manifest file, starting fresh: #{e.message}"
|
|
42
45
|
@cached_provider = nil
|
|
@@ -48,6 +51,23 @@ module JekyllImgFlow
|
|
|
48
51
|
end
|
|
49
52
|
end
|
|
50
53
|
|
|
54
|
+
# Detect and migrate legacy manifest entries (pre-0.1.11 format).
|
|
55
|
+
# Old entries have operations like {"width": 400} without format/quality
|
|
56
|
+
# and no file_digest. Clearing them lets the build re-register existing
|
|
57
|
+
# output files without reprocessing (output_up_to_date? check handles this).
|
|
58
|
+
def migrate_legacy_manifest(images)
|
|
59
|
+
legacy = images.any? do |_name, data|
|
|
60
|
+
versions = data.dig("versions", "default") || []
|
|
61
|
+
versions.any? { |v| v["file_digest"].nil? || !v["operations"].key?("format") }
|
|
62
|
+
end
|
|
63
|
+
if legacy
|
|
64
|
+
Jekyll.logger.info "ImgFlow: Detected legacy manifest format, " \
|
|
65
|
+
"clearing entries for re-registration."
|
|
66
|
+
images.clear
|
|
67
|
+
end
|
|
68
|
+
images
|
|
69
|
+
end
|
|
70
|
+
|
|
51
71
|
# Save manifest to disk
|
|
52
72
|
def save
|
|
53
73
|
# Store current provider at top level of manifest
|
|
@@ -55,44 +75,98 @@ module JekyllImgFlow
|
|
|
55
75
|
"provider" => @current_provider,
|
|
56
76
|
"images" => @manifest
|
|
57
77
|
}
|
|
78
|
+
content = JSON.pretty_generate(manifest_data)
|
|
79
|
+
if File.exist?(@manifest_path) && File.binread(@manifest_path) == content
|
|
80
|
+
cleanup_legacy_manifests
|
|
81
|
+
return
|
|
82
|
+
end
|
|
58
83
|
|
|
59
|
-
|
|
60
|
-
|
|
84
|
+
directory = File.dirname(@manifest_path)
|
|
85
|
+
temporary_path = "#{@manifest_path}.tmp-#{Process.pid}-#{Thread.current.object_id}"
|
|
86
|
+
FileUtils.mkdir_p(directory)
|
|
87
|
+
File.open(temporary_path, "wb") do |file|
|
|
88
|
+
file.write(content)
|
|
89
|
+
file.flush
|
|
90
|
+
file.fsync
|
|
91
|
+
end
|
|
92
|
+
File.rename(temporary_path, @manifest_path)
|
|
93
|
+
cleanup_legacy_manifests
|
|
94
|
+
ensure
|
|
95
|
+
FileUtils.rm_f(temporary_path) if temporary_path
|
|
61
96
|
end
|
|
62
97
|
|
|
63
98
|
# Get current provider from site config
|
|
64
|
-
def current_provider
|
|
65
|
-
config = JekyllImgFlow::Config.new(@site)
|
|
66
|
-
# Get first available provider from backend_priority
|
|
67
|
-
config.backend_priority&.first || "unknown"
|
|
68
|
-
end
|
|
99
|
+
def current_provider = @config.backend_priority&.first || "unknown"
|
|
69
100
|
|
|
70
101
|
# Clean up manifest entries for deleted original images
|
|
71
|
-
# @param current_originals [Array<String>]
|
|
102
|
+
# @param current_originals [Array<String>] Paths relative to the originals directory
|
|
72
103
|
def cleanup_deleted_originals(current_originals)
|
|
73
104
|
current_set = current_originals.to_set
|
|
74
105
|
|
|
75
106
|
@manifest.each_key do |original_name|
|
|
76
|
-
|
|
77
|
-
|
|
107
|
+
next if current_set.include?(original_name)
|
|
108
|
+
|
|
109
|
+
data = @manifest.delete(original_name)
|
|
110
|
+
VERSION_TYPES.each do |type|
|
|
111
|
+
(data.dig("versions", type) || []).each do |version|
|
|
112
|
+
@file_cleaner.delete(version["output"])
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
Jekyll.logger.debug "🗑️ Removed manifest entry for deleted original: #{original_name}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def cleanup_obsolete_defaults(expected_operations)
|
|
120
|
+
@manifest.each_value do |data|
|
|
121
|
+
versions = data.dig("versions", "default") || []
|
|
122
|
+
versions.reject! do |version|
|
|
123
|
+
obsolete = expected_operations.none? do |operations|
|
|
124
|
+
same_operations?(version["operations"], operations)
|
|
125
|
+
end
|
|
126
|
+
@file_cleaner.delete(version["output"]) if obsolete
|
|
127
|
+
obsolete
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def reset_page_usage
|
|
133
|
+
@manifest.each_value do |data|
|
|
134
|
+
(data.dig("versions", "specialized") || []).each do |version|
|
|
135
|
+
version["used_on"] = []
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
78
139
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
140
|
+
# Normalize operations for consistent comparison after JSON persistence.
|
|
141
|
+
def normalize_operations(operations)
|
|
142
|
+
case operations
|
|
143
|
+
when Hash
|
|
144
|
+
operations.to_h do |key, value|
|
|
145
|
+
[key.to_s, normalize_operations(value)]
|
|
83
146
|
end
|
|
147
|
+
when Array
|
|
148
|
+
operations.map { |value| normalize_operations(value) }
|
|
149
|
+
when Symbol
|
|
150
|
+
operations.to_s
|
|
151
|
+
else
|
|
152
|
+
operations
|
|
84
153
|
end
|
|
85
154
|
end
|
|
86
155
|
|
|
156
|
+
def same_operations?(left, right) = normalize_operations(left) == normalize_operations(right)
|
|
157
|
+
|
|
87
158
|
# Check if a specific image version exists and is up-to-date
|
|
88
|
-
def version_exists?(original_name, operations, type = :specialized)
|
|
159
|
+
def version_exists?(original_name, operations, type = :specialized, file_digest = nil)
|
|
89
160
|
return false unless @manifest[original_name]
|
|
90
161
|
|
|
91
162
|
# Provider change is already handled in initialize, so just check operations
|
|
92
163
|
versions = @manifest.dig(original_name, "versions", type.to_s) || []
|
|
93
164
|
Jekyll.logger.debug "🔍 ManifestManager: Looking for operations: #{operations.inspect} in #{versions.length} #{type} versions"
|
|
94
165
|
|
|
95
|
-
result = versions.any?
|
|
166
|
+
result = versions.any? do |version|
|
|
167
|
+
same_operations?(version["operations"], operations) &&
|
|
168
|
+
(!file_digest || version["file_digest"] == file_digest)
|
|
169
|
+
end
|
|
96
170
|
Jekyll.logger.debug "🔍 ManifestManager: Final result: #{result}"
|
|
97
171
|
result
|
|
98
172
|
end
|
|
@@ -112,25 +186,9 @@ module JekyllImgFlow
|
|
|
112
186
|
"Provider changed from '#{@cached_provider}' to '#{@current_provider}'"
|
|
113
187
|
Jekyll.logger.warn "ImgFlow:", "Invalidating cache and cleaning optimized directory..."
|
|
114
188
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
# Safety guard: prevent deletion of _site itself
|
|
120
|
-
raise "Refusing to delete optimized directory: path resolves to site dest or is empty" if optimized_dir == @site.dest || optimized_dir.empty?
|
|
121
|
-
|
|
122
|
-
# Delete all files in optimized directory
|
|
123
|
-
if Dir.exist?(optimized_dir)
|
|
124
|
-
deleted_count = 0
|
|
125
|
-
Dir.glob(File.join(optimized_dir, "**", "*")).each do |file|
|
|
126
|
-
if File.file?(file)
|
|
127
|
-
File.delete(file)
|
|
128
|
-
deleted_count += 1
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
Jekyll.logger.warn "ImgFlow:",
|
|
132
|
-
"Deleted #{deleted_count} cached images from optimized directory"
|
|
133
|
-
end
|
|
189
|
+
deleted_count = @file_cleaner.clear
|
|
190
|
+
Jekyll.logger.warn "ImgFlow:",
|
|
191
|
+
"Deleted #{deleted_count} cached images from optimized directory"
|
|
134
192
|
|
|
135
193
|
# Clear manifest cache
|
|
136
194
|
@manifest.clear
|
|
@@ -146,7 +204,7 @@ module JekyllImgFlow
|
|
|
146
204
|
return unless @manifest[original_name]
|
|
147
205
|
|
|
148
206
|
versions = @manifest.dig(original_name, "versions", type.to_s) || []
|
|
149
|
-
version = versions.find { |v| v["operations"]
|
|
207
|
+
version = versions.find { |v| same_operations?(v["operations"], operations) }
|
|
150
208
|
version&.dig("output")
|
|
151
209
|
end
|
|
152
210
|
|
|
@@ -162,7 +220,7 @@ module JekyllImgFlow
|
|
|
162
220
|
|
|
163
221
|
# Get the existing version to preserve provider
|
|
164
222
|
versions = @manifest.dig(original_name, "versions", type.to_s) || []
|
|
165
|
-
version = versions.find { |v| v["operations"]
|
|
223
|
+
version = versions.find { |v| same_operations?(v["operations"], operations) }
|
|
166
224
|
provider = version&.dig("provider")
|
|
167
225
|
Jekyll.logger.debug "🔍 update_page_usage: provider=#{provider}, current used_on=#{version&.dig('used_on')&.inspect}"
|
|
168
226
|
|
|
@@ -183,9 +241,10 @@ module JekyllImgFlow
|
|
|
183
241
|
@manifest[original_name]["file_digest"] = file_digest if file_digest
|
|
184
242
|
|
|
185
243
|
versions = @manifest[original_name]["versions"][type.to_s] ||= []
|
|
244
|
+
normalized_operations = normalize_operations(operations)
|
|
186
245
|
|
|
187
246
|
# Find or create version entry
|
|
188
|
-
version = versions.find { |v| v["operations"]
|
|
247
|
+
version = versions.find { |v| same_operations?(v["operations"], normalized_operations) }
|
|
189
248
|
|
|
190
249
|
# Normalize page_path to array
|
|
191
250
|
page_paths = if page_path.is_a?(Array)
|
|
@@ -200,16 +259,19 @@ module JekyllImgFlow
|
|
|
200
259
|
page_paths.each do |path|
|
|
201
260
|
version["used_on"] << path if path && !version["used_on"].include?(path)
|
|
202
261
|
end
|
|
262
|
+
version["output"] = output_path
|
|
263
|
+
version["file_digest"] = file_digest if file_digest
|
|
203
264
|
# Update provider if provided
|
|
204
265
|
version["provider"] = provider if provider
|
|
205
266
|
else
|
|
206
267
|
# Create new version entry
|
|
207
268
|
versions << {
|
|
208
269
|
"output" => output_path,
|
|
209
|
-
"operations" =>
|
|
270
|
+
"operations" => normalized_operations,
|
|
210
271
|
"type" => type.to_s,
|
|
211
272
|
"used_on" => page_paths,
|
|
212
273
|
"created_at" => Time.now.to_i,
|
|
274
|
+
"file_digest" => file_digest,
|
|
213
275
|
"provider" => provider
|
|
214
276
|
}
|
|
215
277
|
end
|
|
@@ -220,7 +282,7 @@ module JekyllImgFlow
|
|
|
220
282
|
return false unless @manifest[original_name]
|
|
221
283
|
|
|
222
284
|
versions = @manifest.dig(original_name, "versions", "default") || []
|
|
223
|
-
versions.any? { |v| v["operations"]
|
|
285
|
+
versions.any? { |v| same_operations?(v["operations"], operations) }
|
|
224
286
|
end
|
|
225
287
|
|
|
226
288
|
# Find orphaned specialized images (not used on any page)
|
|
@@ -261,16 +323,14 @@ module JekyllImgFlow
|
|
|
261
323
|
cleaned = []
|
|
262
324
|
|
|
263
325
|
orphans.each do |orphan|
|
|
264
|
-
|
|
265
|
-
if File.exist?(output_file)
|
|
266
|
-
File.delete(output_file)
|
|
326
|
+
if @file_cleaner.delete(orphan["output"])
|
|
267
327
|
cleaned << orphan["output"]
|
|
268
328
|
Jekyll.logger.info "🗑️ Cleaned orphaned image: #{orphan['output']}"
|
|
269
329
|
end
|
|
270
330
|
|
|
271
331
|
# Remove from manifest
|
|
272
332
|
versions = @manifest.dig(orphan["original"], "versions", "specialized") || []
|
|
273
|
-
versions.reject! { |v| v["operations"]
|
|
333
|
+
versions.reject! { |v| same_operations?(v["operations"], orphan["operations"]) }
|
|
274
334
|
end
|
|
275
335
|
|
|
276
336
|
cleaned
|
|
@@ -288,5 +348,24 @@ module JekyllImgFlow
|
|
|
288
348
|
versions = @manifest[original_name]["versions"]
|
|
289
349
|
(versions["default"]&.any? || false) || (versions["specialized"]&.any? || false)
|
|
290
350
|
end
|
|
351
|
+
|
|
352
|
+
private
|
|
353
|
+
|
|
354
|
+
def legacy_manifest_paths
|
|
355
|
+
[
|
|
356
|
+
File.join(@site.source, "assets", "images", "imgflow-manifest.json"),
|
|
357
|
+
File.join(@site.dest, "assets", "images", "imgflow-manifest.json")
|
|
358
|
+
].reject { |path| path == @manifest_path }
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def manifest_load_path
|
|
362
|
+
return @manifest_path if File.exist?(@manifest_path)
|
|
363
|
+
|
|
364
|
+
@legacy_manifest_paths.find { |path| File.exist?(path) }
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def cleanup_legacy_manifests
|
|
368
|
+
@legacy_manifest_paths.each { |path| FileUtils.rm_f(path) }
|
|
369
|
+
end
|
|
291
370
|
end
|
|
292
371
|
end
|
|
@@ -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='×';
|
|
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
|