jekyll-imgflow 0.1.5
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 +7 -0
- data/LICENSE +661 -0
- data/README.md +214 -0
- data/lib/jekyll-imgflow/batch_manager.rb +263 -0
- data/lib/jekyll-imgflow/build_time_processor.rb +152 -0
- data/lib/jekyll-imgflow/config.rb +130 -0
- data/lib/jekyll-imgflow/filename_generator.rb +102 -0
- data/lib/jekyll-imgflow/helpers/http_downloader.rb +68 -0
- data/lib/jekyll-imgflow/hooks.rb +57 -0
- data/lib/jekyll-imgflow/html_generator.rb +335 -0
- data/lib/jekyll-imgflow/imgflow_tag.rb +247 -0
- data/lib/jekyll-imgflow/manifest_manager.rb +297 -0
- data/lib/jekyll-imgflow/operation_processor.rb +183 -0
- data/lib/jekyll-imgflow/parser.rb +199 -0
- data/lib/jekyll-imgflow/path_resolver.rb +130 -0
- data/lib/jekyll-imgflow/picture_tag_adaptor.rb +299 -0
- data/lib/jekyll-imgflow/picture_tag_preset_migrator.rb +210 -0
- data/lib/jekyll-imgflow/preset_manager.rb +149 -0
- data/lib/jekyll-imgflow/provider_registry.rb +106 -0
- data/lib/jekyll-imgflow/providers/base_provider.rb +198 -0
- data/lib/jekyll-imgflow/providers/flyimg.rb +187 -0
- data/lib/jekyll-imgflow/providers/imagemagick.rb +138 -0
- data/lib/jekyll-imgflow/providers/imgproxy.rb +164 -0
- data/lib/jekyll-imgflow/providers/libvips.rb +292 -0
- data/lib/jekyll-imgflow/providers/sharp.rb +213 -0
- data/lib/jekyll-imgflow/providers/weserv.rb +160 -0
- data/lib/jekyll-imgflow/tag_scanner.rb +227 -0
- data/lib/jekyll-imgflow/tags/base_tag.rb +52 -0
- data/lib/jekyll-imgflow/tags/crop_tag.rb +220 -0
- data/lib/jekyll-imgflow/tags/format_tag.rb +64 -0
- data/lib/jekyll-imgflow/tags/opacity_tag.rb +36 -0
- data/lib/jekyll-imgflow/tags/optimize_tag.rb +52 -0
- data/lib/jekyll-imgflow/tags/quality_tag.rb +31 -0
- data/lib/jekyll-imgflow/tags/resize_tag.rb +58 -0
- data/lib/jekyll-imgflow/tags/tag_registry.rb +70 -0
- data/lib/jekyll-imgflow/tags/watermark_tag.rb +51 -0
- data/lib/jekyll-imgflow/version.rb +5 -0
- data/lib/jekyll-imgflow.rb +30 -0
- metadata +261 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jekyll
|
|
4
|
+
class ImgflowTag < Liquid::Tag
|
|
5
|
+
def initialize(tag_name, markup, tokens)
|
|
6
|
+
super
|
|
7
|
+
@markup = markup.strip
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def render(context)
|
|
11
|
+
# Get components
|
|
12
|
+
components = get_imgflow_components(context)
|
|
13
|
+
|
|
14
|
+
# Check if markup contains preset
|
|
15
|
+
markup_to_parse = if @markup.include?("preset:")
|
|
16
|
+
# Route through PresetManager: preset → tags:value → Parser
|
|
17
|
+
expand_preset_markup(@markup, components[:preset_manager])
|
|
18
|
+
else
|
|
19
|
+
# Direct route: markup → Parser
|
|
20
|
+
@markup
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Use central parser to parse markup (uniform for both paths)
|
|
24
|
+
parsed = JekyllImgFlow::Parser.parse(markup_to_parse, context)
|
|
25
|
+
|
|
26
|
+
# Process operations through the new architecture
|
|
27
|
+
process_operations(components, parsed, context)
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
# Handle errors gracefully - don't crash the build for one image
|
|
30
|
+
Jekyll.logger.error "ImgFlow Error: #{e.message}"
|
|
31
|
+
Jekyll.logger.debug "ImgFlow Backtrace: #{e.backtrace.first(5).join("\n")}"
|
|
32
|
+
"<!-- ImgFlow Error: #{e.message} -->"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def expand_preset_markup(markup, preset_manager)
|
|
38
|
+
# Parse markup to extract image path, preset name, and user options
|
|
39
|
+
parts = markup.split
|
|
40
|
+
image_path = parts.first
|
|
41
|
+
|
|
42
|
+
# Extract preset name and user options
|
|
43
|
+
preset_name = nil
|
|
44
|
+
user_options = {}
|
|
45
|
+
|
|
46
|
+
parts[1..].each do |part|
|
|
47
|
+
if part.start_with?("preset:")
|
|
48
|
+
preset_name = part.split(":", 2).last
|
|
49
|
+
elsif part.include?(":")
|
|
50
|
+
key, value = part.split(":", 2)
|
|
51
|
+
user_options[key.to_sym] = value
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get preset markup from PresetManager
|
|
56
|
+
preset_markup = preset_manager.build_markup_from_preset(preset_name, user_options)
|
|
57
|
+
|
|
58
|
+
# Combine image path with preset markup
|
|
59
|
+
"#{image_path} #{preset_markup}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def process_operations(components, parsed, context)
|
|
63
|
+
return "" unless parsed[:image_path]
|
|
64
|
+
|
|
65
|
+
site = context.registers[:site]
|
|
66
|
+
page = context.registers[:page]
|
|
67
|
+
original_name = File.basename(parsed[:image_path])
|
|
68
|
+
|
|
69
|
+
# Get page path for manifest tracking
|
|
70
|
+
# Try multiple attributes to get the page identifier
|
|
71
|
+
page_path = if page
|
|
72
|
+
page["path"] || page["url"] || page["name"] || "unknown"
|
|
73
|
+
else
|
|
74
|
+
"unknown"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Resolve input path
|
|
78
|
+
input_path = resolve_image_path(parsed[:image_path], site, components[:config])
|
|
79
|
+
raise ArgumentError, "Input image file not found: #{input_path}" unless File.file?(input_path)
|
|
80
|
+
|
|
81
|
+
# Process operations using clean architecture
|
|
82
|
+
operations = parsed[:operations]
|
|
83
|
+
|
|
84
|
+
result = if operations.empty?
|
|
85
|
+
# No operations - use original image
|
|
86
|
+
input_path
|
|
87
|
+
else
|
|
88
|
+
# Generate filename early to check existence
|
|
89
|
+
operation = operations.first
|
|
90
|
+
params = operation[:params]
|
|
91
|
+
|
|
92
|
+
# For default versions, include default format and quality for proper identification
|
|
93
|
+
if determine_version_type(params, components[:config]) == :default
|
|
94
|
+
params = params.dup
|
|
95
|
+
params[:format] ||= components[:config].formats.first # Use default format
|
|
96
|
+
params[:quality] ||= components[:config].quality # Use default quality
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
filename = components[:filename_generator].generate_filename(input_path, params)
|
|
100
|
+
output_path = components[:path_resolver].resolve_output_path(filename)
|
|
101
|
+
|
|
102
|
+
# Determine version type
|
|
103
|
+
version_type = determine_version_type(params, components[:config])
|
|
104
|
+
|
|
105
|
+
# Check if version exists in manifest
|
|
106
|
+
Jekyll.logger.debug "🔍 ImgflowTag: Checking existence - original_name: #{original_name}, version_type: #{version_type}, page_path: #{page_path}"
|
|
107
|
+
Jekyll.logger.debug "🔍 ImgflowTag: Params being checked: #{params.inspect}"
|
|
108
|
+
|
|
109
|
+
if components[:manifest].version_exists?(original_name, params, version_type)
|
|
110
|
+
# Version exists - update page usage and return path
|
|
111
|
+
Jekyll.logger.debug "✅ ImgflowTag: Version exists! Updating page usage"
|
|
112
|
+
components[:manifest].update_page_usage(original_name, params, version_type,
|
|
113
|
+
page_path)
|
|
114
|
+
output_path
|
|
115
|
+
else
|
|
116
|
+
# Version doesn't exist - create it
|
|
117
|
+
Jekyll.logger.debug "❌ ImgflowTag: Version doesn't exist - creating"
|
|
118
|
+
versions = components[:manifest].get_versions(original_name)
|
|
119
|
+
Jekyll.logger.debug "🔍 ImgflowTag: Manifest has - default: #{versions['default']&.length || 0}, specialized: #{versions['specialized']&.length || 0}"
|
|
120
|
+
|
|
121
|
+
components[:operation_processor].process_operation(
|
|
122
|
+
original_name,
|
|
123
|
+
operation,
|
|
124
|
+
input_path,
|
|
125
|
+
page_path
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Convert absolute paths to relative for HTML generation
|
|
131
|
+
relative_result = if result.start_with?(site.dest)
|
|
132
|
+
# Processed images - remove site.dest and leading /
|
|
133
|
+
result.sub(%r{^#{Regexp.escape(site.dest)}/}, "")
|
|
134
|
+
elsif result.start_with?(site.source)
|
|
135
|
+
# Original images - convert from source to relative and remove leading /
|
|
136
|
+
result.sub(%r{^#{Regexp.escape(site.source)}/}, "")
|
|
137
|
+
else
|
|
138
|
+
# Already relative or other format - remove leading / if present
|
|
139
|
+
result.start_with?("/") ? result[1..] : result
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Generate HTML for the result
|
|
143
|
+
generate_html([relative_result], parsed, context)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def generate_html(results, parsed, context)
|
|
147
|
+
return "" if results.empty?
|
|
148
|
+
|
|
149
|
+
# Extract attributes and markup format from parsed data
|
|
150
|
+
attributes = extract_element_attributes(parsed)
|
|
151
|
+
markup_format = parsed[:markup_format] || "img"
|
|
152
|
+
|
|
153
|
+
# Get config from components
|
|
154
|
+
components = get_imgflow_components(context)
|
|
155
|
+
config = components[:config]
|
|
156
|
+
|
|
157
|
+
# Use unified HTML generator with config
|
|
158
|
+
JekyllImgFlow::HtmlGenerator.generate(results, attributes, markup_format, context, config)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Extract element-specific attributes from parsed data
|
|
162
|
+
def extract_element_attributes(parsed)
|
|
163
|
+
# Start with basic HTML attributes
|
|
164
|
+
base_attrs = parsed[:html_attributes] || {}
|
|
165
|
+
|
|
166
|
+
# Build element-specific attribute structure
|
|
167
|
+
{
|
|
168
|
+
img: base_attrs.reject do |k, _v|
|
|
169
|
+
k.to_s.start_with?("picture-", "source-", "a-", "parent-")
|
|
170
|
+
end,
|
|
171
|
+
picture: extract_prefixed_attrs(base_attrs, "picture-"),
|
|
172
|
+
source: extract_prefixed_attrs(base_attrs, "source-"),
|
|
173
|
+
a: extract_prefixed_attrs(base_attrs, "a-"),
|
|
174
|
+
parent: extract_prefixed_attrs(base_attrs, "parent-"),
|
|
175
|
+
alt: base_attrs[:alt],
|
|
176
|
+
link: base_attrs[:link]
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Extract attributes with a specific prefix
|
|
181
|
+
def extract_prefixed_attrs(attrs, prefix)
|
|
182
|
+
attrs.select { |k, _v| k.to_s.start_with?(prefix) }
|
|
183
|
+
.transform_keys { |k| k.to_s.sub(prefix, "") }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def render_liquid_variable(variable, context)
|
|
187
|
+
# Simple liquid variable rendering
|
|
188
|
+
variable_name = variable.tr("{}", "").strip
|
|
189
|
+
context[variable_name] || variable
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Determine if params represent a default or specialized version
|
|
193
|
+
def determine_version_type(params, config)
|
|
194
|
+
config.determine_version_type(params)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def resolve_image_path(image_path, site, config)
|
|
198
|
+
# Use PathResolver for consistent path handling
|
|
199
|
+
path_resolver = JekyllImgFlow::PathResolver.new(config)
|
|
200
|
+
|
|
201
|
+
# Handle different path formats
|
|
202
|
+
if image_path.start_with?("/") || image_path.include?("/")
|
|
203
|
+
# Absolute or relative path
|
|
204
|
+
File.join(site.source, image_path)
|
|
205
|
+
else
|
|
206
|
+
# Use PathResolver to get CLI path (full filesystem path)
|
|
207
|
+
path_resolver.cli_path(image_path)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def get_imgflow_components(context)
|
|
212
|
+
# Get or create ImgFlow components
|
|
213
|
+
site = context.registers[:site]
|
|
214
|
+
|
|
215
|
+
# Cache components on site object
|
|
216
|
+
unless site.imgflow_components
|
|
217
|
+
config = JekyllImgFlow::Config.new(site)
|
|
218
|
+
manifest = JekyllImgFlow::ManifestManager.new(site)
|
|
219
|
+
path_resolver = JekyllImgFlow::PathResolver.new(config)
|
|
220
|
+
filename_generator = JekyllImgFlow::FilenameGenerator.new
|
|
221
|
+
registry = JekyllImgFlow::ProviderRegistry.new(config)
|
|
222
|
+
provider = registry.current_provider
|
|
223
|
+
operation_processor = JekyllImgFlow::OperationProcessor.new(provider, path_resolver,
|
|
224
|
+
manifest, config)
|
|
225
|
+
preset_manager = JekyllImgFlow::PresetManager.new(site, config)
|
|
226
|
+
|
|
227
|
+
site.imgflow_components = {
|
|
228
|
+
config: config,
|
|
229
|
+
manifest: manifest,
|
|
230
|
+
path_resolver: path_resolver,
|
|
231
|
+
filename_generator: filename_generator,
|
|
232
|
+
registry: registry,
|
|
233
|
+
provider: provider,
|
|
234
|
+
operation_processor: operation_processor,
|
|
235
|
+
preset_manager: preset_manager
|
|
236
|
+
}
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Manifest is shared with BuildTimeProcessor for page usage tracking
|
|
240
|
+
|
|
241
|
+
site.imgflow_components
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Register the tag with Liquid
|
|
247
|
+
Liquid::Template.register_tag("imgflow", Jekyll::ImgflowTag)
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "digest"
|
|
6
|
+
|
|
7
|
+
module JekyllImgFlow
|
|
8
|
+
# Manages the manifest of generated images
|
|
9
|
+
# Tracks default vs specialized versions and page usage for cleanup
|
|
10
|
+
class ManifestManager
|
|
11
|
+
VERSION_TYPES = %w[default specialized].freeze
|
|
12
|
+
attr_reader :manifest_path
|
|
13
|
+
|
|
14
|
+
def initialize(site)
|
|
15
|
+
@site = site
|
|
16
|
+
# Place manifest in _site - rebuilt on each Jekyll build
|
|
17
|
+
@manifest_path = File.join(@site.dest, "assets", "images", "imgflow-manifest.json")
|
|
18
|
+
@manifest = load_manifest
|
|
19
|
+
@current_provider = current_provider
|
|
20
|
+
|
|
21
|
+
# Check if provider changed and invalidate cache if needed
|
|
22
|
+
handle_provider_change if provider_changed?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Load existing manifest or create new one
|
|
26
|
+
def load_manifest
|
|
27
|
+
if File.exist?(@manifest_path)
|
|
28
|
+
begin
|
|
29
|
+
data = JSON.parse(File.read(@manifest_path))
|
|
30
|
+
|
|
31
|
+
# Handle new format with provider at top level
|
|
32
|
+
if data.is_a?(Hash) && data.key?("images")
|
|
33
|
+
@cached_provider = data["provider"]
|
|
34
|
+
data["images"] || {}
|
|
35
|
+
else
|
|
36
|
+
# Old format - just image data
|
|
37
|
+
@cached_provider = nil
|
|
38
|
+
data
|
|
39
|
+
end
|
|
40
|
+
rescue JSON::ParserError => e
|
|
41
|
+
Jekyll.logger.warn "ImgFlow: Corrupt manifest file, starting fresh: #{e.message}"
|
|
42
|
+
@cached_provider = nil
|
|
43
|
+
{}
|
|
44
|
+
end
|
|
45
|
+
else
|
|
46
|
+
@cached_provider = nil
|
|
47
|
+
{}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Save manifest to disk
|
|
52
|
+
def save
|
|
53
|
+
# Store current provider at top level of manifest
|
|
54
|
+
manifest_data = {
|
|
55
|
+
"provider" => @current_provider,
|
|
56
|
+
"images" => @manifest
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
FileUtils.mkdir_p(File.dirname(@manifest_path))
|
|
60
|
+
File.write(@manifest_path, JSON.pretty_generate(manifest_data))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# 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
|
|
69
|
+
|
|
70
|
+
# Clean up manifest entries for deleted original images
|
|
71
|
+
# @param current_originals [Array<String>] Array of current original image basenames
|
|
72
|
+
def cleanup_deleted_originals(current_originals)
|
|
73
|
+
current_set = current_originals.to_set
|
|
74
|
+
|
|
75
|
+
@manifest.each_key do |original_name|
|
|
76
|
+
# Extract basename without extension
|
|
77
|
+
basename = File.basename(original_name, ".*")
|
|
78
|
+
|
|
79
|
+
# If original no longer exists, remove from manifest
|
|
80
|
+
unless current_set.include?(basename)
|
|
81
|
+
@manifest.delete(original_name)
|
|
82
|
+
Jekyll.logger.debug "🗑️ Removed manifest entry for deleted original: #{original_name}"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check if a specific image version exists and is up-to-date
|
|
88
|
+
def version_exists?(original_name, operations, type = :specialized)
|
|
89
|
+
return false unless @manifest[original_name]
|
|
90
|
+
|
|
91
|
+
# Provider change is already handled in initialize, so just check operations
|
|
92
|
+
versions = @manifest.dig(original_name, "versions", type.to_s) || []
|
|
93
|
+
Jekyll.logger.debug "🔍 ManifestManager: Looking for operations: #{operations.inspect} in #{versions.length} #{type} versions"
|
|
94
|
+
|
|
95
|
+
result = versions.any? { |v| v["operations"] == operations }
|
|
96
|
+
Jekyll.logger.debug "🔍 ManifestManager: Final result: #{result}"
|
|
97
|
+
result
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if the provider has changed since the manifest was created
|
|
101
|
+
def provider_changed?
|
|
102
|
+
# If no cached provider, this is a new manifest
|
|
103
|
+
return false if @cached_provider.nil?
|
|
104
|
+
|
|
105
|
+
# Provider changed if current doesn't match cached
|
|
106
|
+
@cached_provider != @current_provider
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Handle provider change by invalidating cache and cleaning optimized directory
|
|
110
|
+
def handle_provider_change
|
|
111
|
+
Jekyll.logger.warn "ImgFlow:",
|
|
112
|
+
"Provider changed from '#{@cached_provider}' to '#{@current_provider}'"
|
|
113
|
+
Jekyll.logger.warn "ImgFlow:", "Invalidating cache and cleaning optimized directory..."
|
|
114
|
+
|
|
115
|
+
# Get optimized directory path
|
|
116
|
+
config = JekyllImgFlow::Config.new(@site)
|
|
117
|
+
optimized_dir = File.join(@site.dest, config.output)
|
|
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
|
|
134
|
+
|
|
135
|
+
# Clear manifest cache
|
|
136
|
+
@manifest.clear
|
|
137
|
+
Jekyll.logger.warn "ImgFlow:",
|
|
138
|
+
"Manifest cache cleared - all images will be regenerated with '#{@current_provider}'"
|
|
139
|
+
|
|
140
|
+
# Update cached provider to current
|
|
141
|
+
@cached_provider = @current_provider
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Get output path for a specific version
|
|
145
|
+
def get_version_output(original_name, operations, type = :specialized)
|
|
146
|
+
return unless @manifest[original_name]
|
|
147
|
+
|
|
148
|
+
versions = @manifest.dig(original_name, "versions", type.to_s) || []
|
|
149
|
+
version = versions.find { |v| v["operations"] == operations }
|
|
150
|
+
version&.dig("output")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Update page usage for an existing version
|
|
154
|
+
# This is a convenience method that calls register_version
|
|
155
|
+
def update_page_usage(original_name, operations, type, page_path)
|
|
156
|
+
Jekyll.logger.debug "🔍 update_page_usage: original_name=#{original_name}, operations=#{operations.inspect}, type=#{type}, page_path=#{page_path}"
|
|
157
|
+
|
|
158
|
+
# Get the existing version's output path (already relative)
|
|
159
|
+
output_path = get_version_output(original_name, operations, type)
|
|
160
|
+
Jekyll.logger.debug "🔍 update_page_usage: output_path=#{output_path}"
|
|
161
|
+
return unless output_path
|
|
162
|
+
|
|
163
|
+
# Get the existing version to preserve provider
|
|
164
|
+
versions = @manifest.dig(original_name, "versions", type.to_s) || []
|
|
165
|
+
version = versions.find { |v| v["operations"] == operations }
|
|
166
|
+
provider = version&.dig("provider")
|
|
167
|
+
Jekyll.logger.debug "🔍 update_page_usage: provider=#{provider}, current used_on=#{version&.dig('used_on')&.inspect}"
|
|
168
|
+
|
|
169
|
+
# Call register_version to update page usage (output_path is already relative)
|
|
170
|
+
register_version(original_name, output_path, operations, type, page_path, nil, provider)
|
|
171
|
+
Jekyll.logger.debug "🔍 update_page_usage: completed register_version call"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Register a new image version
|
|
175
|
+
def register_version(original_name, output_path, operations, type, page_path,
|
|
176
|
+
file_digest = nil, provider = nil)
|
|
177
|
+
@manifest[original_name] ||= {
|
|
178
|
+
"versions" => { "default" => [], "specialized" => [] },
|
|
179
|
+
"file_digest" => file_digest # Store SHA256 digest of original file
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# Update file digest if provided
|
|
183
|
+
@manifest[original_name]["file_digest"] = file_digest if file_digest
|
|
184
|
+
|
|
185
|
+
versions = @manifest[original_name]["versions"][type.to_s] ||= []
|
|
186
|
+
|
|
187
|
+
# Find or create version entry
|
|
188
|
+
version = versions.find { |v| v["operations"] == operations }
|
|
189
|
+
|
|
190
|
+
# Normalize page_path to array
|
|
191
|
+
page_paths = if page_path.is_a?(Array)
|
|
192
|
+
page_path
|
|
193
|
+
else
|
|
194
|
+
(page_path ? [page_path] : [])
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
if version
|
|
198
|
+
# Add pages to used_on list if not already present
|
|
199
|
+
version["used_on"] ||= []
|
|
200
|
+
page_paths.each do |path|
|
|
201
|
+
version["used_on"] << path if path && !version["used_on"].include?(path)
|
|
202
|
+
end
|
|
203
|
+
# Update provider if provided
|
|
204
|
+
version["provider"] = provider if provider
|
|
205
|
+
else
|
|
206
|
+
# Create new version entry
|
|
207
|
+
versions << {
|
|
208
|
+
"output" => output_path,
|
|
209
|
+
"operations" => operations,
|
|
210
|
+
"type" => type.to_s,
|
|
211
|
+
"used_on" => page_paths,
|
|
212
|
+
"created_at" => Time.now.to_i,
|
|
213
|
+
"provider" => provider
|
|
214
|
+
}
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Check if image version is default type
|
|
219
|
+
def default_version?(original_name, operations)
|
|
220
|
+
return false unless @manifest[original_name]
|
|
221
|
+
|
|
222
|
+
versions = @manifest.dig(original_name, "versions", "default") || []
|
|
223
|
+
versions.any? { |v| v["operations"] == operations }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Find orphaned specialized images (not used on any page)
|
|
227
|
+
def find_orphans
|
|
228
|
+
orphans = []
|
|
229
|
+
|
|
230
|
+
@manifest.each do |original_name, data|
|
|
231
|
+
specialized = data.dig("versions", "specialized") || []
|
|
232
|
+
specialized.each do |version|
|
|
233
|
+
next unless version["used_on"].nil? || version["used_on"].empty?
|
|
234
|
+
|
|
235
|
+
orphans << {
|
|
236
|
+
"original" => original_name,
|
|
237
|
+
"output" => version["output"],
|
|
238
|
+
"operations" => version["operations"]
|
|
239
|
+
}
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
orphans
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Remove page from all image version usage tracking
|
|
247
|
+
def remove_page_usage(page_path)
|
|
248
|
+
@manifest.each_value do |data|
|
|
249
|
+
VERSION_TYPES.each do |type|
|
|
250
|
+
versions = data.dig("versions", type) || []
|
|
251
|
+
versions.each do |version|
|
|
252
|
+
version["used_on"]&.delete(page_path)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Cleanup orphaned specialized images
|
|
259
|
+
def cleanup_orphans
|
|
260
|
+
orphans = find_orphans
|
|
261
|
+
cleaned = []
|
|
262
|
+
|
|
263
|
+
orphans.each do |orphan|
|
|
264
|
+
output_file = if orphan["output"].start_with?("/")
|
|
265
|
+
orphan["output"]
|
|
266
|
+
else
|
|
267
|
+
File.join(@site.dest,
|
|
268
|
+
orphan["output"])
|
|
269
|
+
end
|
|
270
|
+
if File.exist?(output_file)
|
|
271
|
+
File.delete(output_file)
|
|
272
|
+
cleaned << orphan["output"]
|
|
273
|
+
Jekyll.logger.info "🗑️ Cleaned orphaned image: #{orphan['output']}"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Remove from manifest
|
|
277
|
+
versions = @manifest.dig(orphan["original"], "versions", "specialized") || []
|
|
278
|
+
versions.reject! { |v| v["operations"] == orphan["operations"] }
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
cleaned
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Get all versions for an original image
|
|
285
|
+
def get_versions(original_name)
|
|
286
|
+
@manifest.dig(original_name, "versions") || { "default" => [], "specialized" => [] }
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Check if original image has any versions
|
|
290
|
+
def versions?(original_name)
|
|
291
|
+
return false unless @manifest[original_name]
|
|
292
|
+
|
|
293
|
+
versions = @manifest[original_name]["versions"]
|
|
294
|
+
(versions["default"]&.any? || false) || (versions["specialized"]&.any? || false)
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
end
|