jekyll-uj-powertools 1.5.2 → 1.6.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.
data/lib/tags/icon.rb ADDED
@@ -0,0 +1,262 @@
1
+ # Libraries
2
+ require "jekyll"
3
+
4
+ module Jekyll
5
+ class UJIconTag < Liquid::Tag
6
+ # Default icon to show when requested icon is not found
7
+ DEFAULT_ICON = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M320 64C334.7 64 348.2 72.1 355.2 85L571.2 485C577.9 497.4 577.6 512.4 570.4 524.5C563.2 536.6 550.1 544 536 544L104 544C89.9 544 76.9 536.6 69.6 524.5C62.3 512.4 62.1 497.4 68.8 485L284.8 85C291.8 72.1 305.3 64 320 64zM320 232C306.7 232 296 242.7 296 256L296 368C296 381.3 306.7 392 320 392C333.3 392 344 381.3 344 368L344 256C344 242.7 333.3 232 320 232zM346.7 448C347.3 438.1 342.4 428.7 333.9 423.5C325.4 418.4 314.7 418.4 306.2 423.5C297.7 428.7 292.8 438.1 293.4 448C292.8 457.9 297.7 467.3 306.2 472.5C314.7 477.6 325.4 477.6 333.9 472.5C342.4 467.3 347.3 457.9 346.7 448z"/></svg>'
8
+
9
+ # Language code to country code mapping for flags
10
+ LANGUAGE_TO_COUNTRY = {
11
+ 'en' => 'us', # English -> United States (could also be 'gb' for Great Britain)
12
+ 'es' => 'es', # Spanish -> Spain
13
+ 'fr' => 'fr', # French -> France
14
+ 'de' => 'de', # German -> Germany
15
+ 'it' => 'it', # Italian -> Italy
16
+ 'pt' => 'pt', # Portuguese -> Portugal
17
+ 'ru' => 'ru', # Russian -> Russia
18
+ 'ja' => 'jp', # Japanese -> Japan
19
+ 'ko' => 'kr', # Korean -> South Korea
20
+ 'zh' => 'cn', # Chinese -> China
21
+ 'ar' => 'sa', # Arabic -> Saudi Arabia
22
+ 'hi' => 'in', # Hindi -> India
23
+ 'tr' => 'tr', # Turkish -> Turkey
24
+ 'pl' => 'pl', # Polish -> Poland
25
+ 'nl' => 'nl', # Dutch -> Netherlands
26
+ 'sv' => 'se', # Swedish -> Sweden
27
+ 'no' => 'no', # Norwegian -> Norway
28
+ 'da' => 'dk', # Danish -> Denmark
29
+ 'fi' => 'fi', # Finnish -> Finland
30
+ 'he' => 'il', # Hebrew -> Israel
31
+ 'th' => 'th', # Thai -> Thailand
32
+ 'vi' => 'vn', # Vietnamese -> Vietnam
33
+ 'uk' => 'ua', # Ukrainian -> Ukraine
34
+ 'cs' => 'cz', # Czech -> Czech Republic
35
+ 'hu' => 'hu', # Hungarian -> Hungary
36
+ 'ro' => 'ro', # Romanian -> Romania
37
+ 'bg' => 'bg', # Bulgarian -> Bulgaria
38
+ 'hr' => 'hr', # Croatian -> Croatia
39
+ 'sk' => 'sk', # Slovak -> Slovakia
40
+ 'sl' => 'si', # Slovenian -> Slovenia
41
+ 'et' => 'ee', # Estonian -> Estonia
42
+ 'lv' => 'lv', # Latvian -> Latvia
43
+ 'lt' => 'lt', # Lithuanian -> Lithuania
44
+ 'mt' => 'mt', # Maltese -> Malta
45
+ 'ga' => 'ie', # Irish -> Ireland
46
+ 'cy' => 'gb', # Welsh -> Great Britain
47
+ 'ca' => 'es', # Catalan -> Spain (could also be ad for Andorra)
48
+ 'eu' => 'es', # Basque -> Spain
49
+ 'gl' => 'es', # Galician -> Spain
50
+ }
51
+
52
+ # Font Awesome size mappings - commented out for now
53
+ # FA_SIZES = {
54
+ # 'fa-2xs' => '0.625em',
55
+ # 'fa-xs' => '0.75em',
56
+ # 'fa-sm' => '0.875em',
57
+ # 'fa-md' => '1em',
58
+ # 'fa-lg' => '1.25em',
59
+ # 'fa-xl' => '1.5em',
60
+ # 'fa-2xl' => '2em'
61
+ # }
62
+
63
+ # Cache for loaded icons to improve performance
64
+ @@icon_cache = {}
65
+
66
+ def initialize(tag_name, markup, tokens)
67
+ super
68
+ @markup = markup.strip
69
+ end
70
+
71
+ def render(context)
72
+ # Parse arguments that can be quoted or unquoted
73
+ parts = parse_arguments(@markup)
74
+ icon_name_input = parts[0]
75
+ css_classes = parts[1]
76
+
77
+ # Check if the input was originally quoted (literal string)
78
+ is_quoted = @markup.strip.match(/^['"]/)
79
+
80
+ # If quoted, use as literal. Otherwise, try to resolve as variable
81
+ if is_quoted
82
+ icon_name = icon_name_input
83
+ else
84
+ # Try to resolve as a variable
85
+ icon_name = resolve_variable(context, icon_name_input)
86
+ # If it didn't resolve to a string, use the input as literal
87
+ icon_name = icon_name_input if icon_name.nil? || !icon_name.is_a?(String)
88
+ end
89
+
90
+ # Strip quotes from resolved icon name if present
91
+ if icon_name.is_a?(String) && icon_name.match(/^['"].*['"]$/)
92
+ icon_name = icon_name[1..-2]
93
+ end
94
+
95
+ # Get site from context
96
+ site = context.registers[:site]
97
+ return '' unless site
98
+
99
+ # Load the icon SVG from file
100
+ icon_svg = load_icon_from_file(site, icon_name.to_s)
101
+ return '' unless icon_svg
102
+
103
+ # Process SVG to inject required attributes
104
+ processed_svg = inject_svg_attributes(icon_svg)
105
+
106
+ # Determine CSS classes
107
+ # font_size = '1em' # default
108
+ # if size_input && !size_input.empty?
109
+ # # Check if it's a Font Awesome preset size
110
+ # font_size = FA_SIZES[size_input] || size_input
111
+ # end
112
+
113
+ # Wrap in i tag with CSS classes (always include 'fa' class)
114
+ if css_classes && !css_classes.empty?
115
+ "<i class=\"fa #{css_classes}\">#{processed_svg}</i>"
116
+ else
117
+ "<i class=\"fa\">#{processed_svg}</i>"
118
+ end
119
+ end
120
+
121
+ private
122
+
123
+ def inject_svg_attributes(svg_content)
124
+ # Inject width, height, and fill attributes into the SVG tag
125
+ if svg_content.include?('<svg')
126
+ # Replace the opening SVG tag to include our required attributes
127
+ svg_content.sub(/<svg([^>]*)>/) do |match|
128
+ existing_attrs = $1
129
+ # Only add attributes if they don't already exist
130
+ attrs_to_add = []
131
+ attrs_to_add << 'width="1em"' unless existing_attrs.include?('width=')
132
+ attrs_to_add << 'height="1em"' unless existing_attrs.include?('height=')
133
+ attrs_to_add << 'fill="currentColor"' unless existing_attrs.include?('fill=')
134
+
135
+ if attrs_to_add.any?
136
+ "<svg#{existing_attrs} #{attrs_to_add.join(' ')}>"
137
+ else
138
+ match
139
+ end
140
+ end
141
+ else
142
+ svg_content
143
+ end
144
+ end
145
+
146
+ def load_icon_from_file(site, icon_name)
147
+ # Get the style from site config
148
+ style = site.config.dig('icons', 'style') || 'solid'
149
+
150
+ # Create cache key
151
+ cache_key = "#{style}/#{icon_name}"
152
+
153
+ # Return cached version if available
154
+ return @@icon_cache[cache_key] if @@icon_cache.key?(cache_key)
155
+
156
+ # Try to load icon from multiple sources in order
157
+ icon_svg = try_load_fontawesome_icon(icon_name, style) ||
158
+ try_load_flag_icon(icon_name) ||
159
+ DEFAULT_ICON
160
+
161
+ # Cache the result
162
+ @@icon_cache[cache_key] = icon_svg
163
+ return icon_svg
164
+ end
165
+
166
+ def try_load_fontawesome_icon(icon_name, style)
167
+ # Build file path for the configured style
168
+ icon_path = File.join(Dir.pwd, 'node_modules', 'ultimate-jekyll-manager', 'assets', 'icons', 'font-awesome', style, "#{icon_name}.svg")
169
+
170
+ # Read file if it exists in the configured style
171
+ if File.exist?(icon_path)
172
+ return File.read(icon_path)
173
+ end
174
+
175
+ # If not found and style is not 'brands', try brands style as fallback
176
+ if style != 'brands'
177
+ brands_path = File.join(Dir.pwd, 'node_modules', 'ultimate-jekyll-manager', 'assets', 'icons', 'font-awesome', 'brands', "#{icon_name}.svg")
178
+
179
+ if File.exist?(brands_path)
180
+ return File.read(brands_path)
181
+ end
182
+ end
183
+
184
+ nil
185
+ end
186
+
187
+ def try_load_flag_icon(icon_name)
188
+ # First try direct country code (e.g., 'us', 'gb')
189
+ flag_path = File.join(Dir.pwd, 'node_modules', 'ultimate-jekyll-manager', 'assets', 'icons', 'flags', 'modern-square', "#{icon_name}.svg")
190
+
191
+ if File.exist?(flag_path)
192
+ return File.read(flag_path)
193
+ end
194
+
195
+ # If not found, try language code to country code mapping (e.g., 'en' -> 'us')
196
+ country_code = LANGUAGE_TO_COUNTRY[icon_name.downcase]
197
+ if country_code
198
+ mapped_flag_path = File.join(Dir.pwd, 'node_modules', 'ultimate-jekyll-manager', 'assets', 'icons', 'flags', 'modern-square', "#{country_code}.svg")
199
+
200
+ if File.exist?(mapped_flag_path)
201
+ return File.read(mapped_flag_path)
202
+ end
203
+ end
204
+
205
+ nil
206
+ end
207
+
208
+ def parse_arguments(markup)
209
+ # Parse arguments that can be quoted or unquoted
210
+ # Examples: award, fa-md OR 'award', 'fa-md' OR myVar, "2em"
211
+ args = []
212
+ current_arg = ''
213
+ in_quotes = false
214
+ quote_char = nil
215
+
216
+ markup.each_char.with_index do |char, i|
217
+ if !in_quotes && (char == '"' || char == "'")
218
+ # Start of quoted string
219
+ in_quotes = true
220
+ quote_char = char
221
+ elsif in_quotes && char == quote_char
222
+ # End of quoted string
223
+ in_quotes = false
224
+ quote_char = nil
225
+ elsif !in_quotes && char == ','
226
+ # Argument separator
227
+ args << current_arg.strip
228
+ current_arg = ''
229
+ else
230
+ # Regular character
231
+ current_arg += char
232
+ end
233
+ end
234
+
235
+ # Add the last argument
236
+ args << current_arg.strip if current_arg.strip.length > 0
237
+
238
+ args
239
+ end
240
+
241
+ def resolve_variable(context, variable_name)
242
+ # Handle nested variable access like page.icon
243
+ parts = variable_name.split('.')
244
+ current = context
245
+
246
+ parts.each do |part|
247
+ if current.respond_to?(:[])
248
+ current = current[part]
249
+ elsif current.respond_to?(:key?) && current.key?(part)
250
+ current = current[part]
251
+ else
252
+ return nil
253
+ end
254
+ return nil if current.nil?
255
+ end
256
+
257
+ current
258
+ end
259
+ end
260
+ end
261
+
262
+ Liquid::Template.register_tag('uj_icon', Jekyll::UJIconTag)
@@ -0,0 +1,38 @@
1
+ # Libraries
2
+ # ...
3
+
4
+ # Tag
5
+ module Jekyll
6
+ module UJPowertools
7
+ class IfFalsyTag < Liquid::Block
8
+ def initialize(tag_name, markup, tokens)
9
+ super
10
+ @variable = markup.strip
11
+ end
12
+
13
+ def render(context)
14
+ # Use Liquid's variable lookup to handle nested properties
15
+ value = context.scopes.last[@variable] || context[@variable]
16
+
17
+ # For nested properties like page.my.variable
18
+ if @variable.include?('.')
19
+ parts = @variable.split('.')
20
+ value = context[parts.first]
21
+ parts[1..-1].each do |part|
22
+ value = value.is_a?(Hash) ? value[part] : nil
23
+ break if value.nil?
24
+ end
25
+ end
26
+
27
+ # Check if the value is falsy (nil, false, empty string, or 0)
28
+ if value.nil? || value == false || value == "" || value == 0
29
+ super
30
+ else
31
+ ""
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ Liquid::Template.register_tag('iffalsy', Jekyll::UJPowertools::IfFalsyTag)
@@ -0,0 +1,38 @@
1
+ # Libraries
2
+ # ...
3
+
4
+ # Tag
5
+ module Jekyll
6
+ module UJPowertools
7
+ class IfTruthyTag < Liquid::Block
8
+ def initialize(tag_name, markup, tokens)
9
+ super
10
+ @variable = markup.strip
11
+ end
12
+
13
+ def render(context)
14
+ # Use Liquid's variable lookup to handle nested properties
15
+ value = context.scopes.last[@variable] || context[@variable]
16
+
17
+ # For nested properties like page.my.variable
18
+ if @variable.include?('.')
19
+ parts = @variable.split('.')
20
+ value = context[parts.first]
21
+ parts[1..-1].each do |part|
22
+ value = value.is_a?(Hash) ? value[part] : nil
23
+ break if value.nil?
24
+ end
25
+ end
26
+
27
+ # Check if the value is truthy (not nil, not false, not empty string, not 0)
28
+ if value && value != false && value != "" && value != 0
29
+ super
30
+ else
31
+ ""
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ Liquid::Template.register_tag('iftruthy', Jekyll::UJPowertools::IfTruthyTag)
data/lib/tags/image.rb ADDED
@@ -0,0 +1,208 @@
1
+ # Libraries
2
+ require "jekyll"
3
+
4
+ module Jekyll
5
+ class UJImageTag < Liquid::Tag
6
+ def initialize(tag_name, markup, tokens)
7
+ super
8
+ @markup = markup.strip
9
+ end
10
+
11
+ def render(context)
12
+ # Parse arguments
13
+ args = parse_arguments(@markup)
14
+ src_input = args[0]
15
+ options = parse_options(args[1..-1])
16
+
17
+ # Resolve source path
18
+ src = resolve_variable(context, src_input)
19
+ return '' unless src
20
+
21
+ # Check if this is an external URL
22
+ is_external = !!(src =~ /^https?:\/\//)
23
+
24
+ if is_external
25
+ # For external URLs, just create a simple responsive img tag
26
+ build_external_image(src, options)
27
+ else
28
+ # Extract file extension
29
+ extension = File.extname(src)
30
+ src_path = src.chomp(extension)
31
+
32
+ # Determine max width
33
+ max_width = options['max_width'] || options['max-width'] || false
34
+ max_width = max_width.to_s if max_width
35
+
36
+ # Build picture element for local images
37
+ build_picture_element(src, src_path, extension, max_width, options)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def parse_arguments(markup)
44
+ args = []
45
+ current_arg = ''
46
+ in_quotes = false
47
+ quote_char = nil
48
+
49
+ markup.each_char do |char|
50
+ if !in_quotes && (char == '"' || char == "'")
51
+ in_quotes = true
52
+ quote_char = char
53
+ current_arg += char
54
+ elsif in_quotes && char == quote_char
55
+ in_quotes = false
56
+ quote_char = nil
57
+ current_arg += char
58
+ elsif !in_quotes && char == ','
59
+ args << current_arg.strip
60
+ current_arg = ''
61
+ else
62
+ current_arg += char
63
+ end
64
+ end
65
+
66
+ args << current_arg.strip if current_arg.strip.length > 0
67
+ args
68
+ end
69
+
70
+ def parse_options(option_args)
71
+ options = {}
72
+
73
+ option_args.each do |arg|
74
+ if arg.include?('=')
75
+ key, value = arg.split('=', 2)
76
+ key = key.strip.gsub(/^['"]|['"]$/, '')
77
+ value = value.strip.gsub(/^['"]|['"]$/, '')
78
+ options[key] = value
79
+ end
80
+ end
81
+
82
+ options
83
+ end
84
+
85
+ def resolve_variable(context, variable_input)
86
+ # Strip quotes if present
87
+ if variable_input.match(/^['"]/)
88
+ variable_input.gsub(/^['"]|['"]$/, '')
89
+ else
90
+ # Resolve as variable
91
+ parts = variable_input.split('.')
92
+ current = context
93
+
94
+ parts.each do |part|
95
+ return nil unless current.respond_to?(:[]) || current.is_a?(Hash)
96
+ current = current[part]
97
+ return nil if current.nil?
98
+ end
99
+
100
+ current
101
+ end
102
+ end
103
+
104
+ def build_picture_element(src, src_path, extension, max_width, options)
105
+ html = "<picture>\n"
106
+
107
+ # Add WebP sources unless disabled
108
+ unless options['webp'] == 'false'
109
+ html += build_webp_sources(src_path, max_width)
110
+ end
111
+
112
+ # Add original format sources
113
+ html += build_original_sources(src_path, extension, max_width, src)
114
+
115
+ # Build img tag
116
+ alt = options['alt'] || ''
117
+ css_class = options['class'] || ''
118
+ style = options['style'] || ''
119
+ width = options['width'] || ''
120
+ height = options['height'] || ''
121
+
122
+ html += "<img\n"
123
+ html += "src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\"\n"
124
+ html += "data-lazy=\"@src #{src}\"\n"
125
+ html += "class=\"#{css_class}\"\n" unless css_class.empty?
126
+ html += "alt=\"#{alt}\"\n"
127
+ html += "style=\"#{style}\"\n" unless style.empty?
128
+ html += "width=\"#{width}\"\n" unless width.empty?
129
+ html += "height=\"#{height}\"\n" unless height.empty?
130
+ html += ">\n"
131
+ html += "</picture>"
132
+
133
+ html
134
+ end
135
+
136
+ def build_webp_sources(src_path, max_width)
137
+ html = ""
138
+
139
+ case max_width
140
+ when "320"
141
+ html += "<source data-lazy=\"@srcset #{src_path}-320px.webp\" type=\"image/webp\">\n"
142
+ when "640"
143
+ html += "<source data-lazy=\"@srcset #{src_path}-320px.webp\" media=\"(max-width: 320px)\" type=\"image/webp\">\n"
144
+ html += "<source data-lazy=\"@srcset #{src_path}-640px.webp\" type=\"image/webp\">\n"
145
+ when "1024"
146
+ html += "<source data-lazy=\"@srcset #{src_path}-320px.webp\" media=\"(max-width: 320px)\" type=\"image/webp\">\n"
147
+ html += "<source data-lazy=\"@srcset #{src_path}-640px.webp\" media=\"(max-width: 640px)\" type=\"image/webp\">\n"
148
+ html += "<source data-lazy=\"@srcset #{src_path}-1024px.webp\" type=\"image/webp\">\n"
149
+ else
150
+ html += "<source data-lazy=\"@srcset #{src_path}-320px.webp\" media=\"(max-width: 320px)\" type=\"image/webp\">\n"
151
+ html += "<source data-lazy=\"@srcset #{src_path}-640px.webp\" media=\"(max-width: 640px)\" type=\"image/webp\">\n"
152
+ html += "<source data-lazy=\"@srcset #{src_path}-1024px.webp\" media=\"(max-width: 1024px)\" type=\"image/webp\">\n"
153
+ html += "<source data-lazy=\"@srcset #{src_path}.webp\" type=\"image/webp\">\n"
154
+ end
155
+
156
+ html
157
+ end
158
+
159
+ def build_original_sources(src_path, extension, max_width, src)
160
+ html = ""
161
+
162
+ case max_width
163
+ when "320"
164
+ html += "<source data-lazy=\"@srcset #{src_path}-320px#{extension}\">\n"
165
+ when "640"
166
+ html += "<source data-lazy=\"@srcset #{src_path}-320px#{extension}\" media=\"(max-width: 320px)\">\n"
167
+ html += "<source data-lazy=\"@srcset #{src_path}-640px#{extension}\">\n"
168
+ when "1024"
169
+ html += "<source data-lazy=\"@srcset #{src_path}-320px#{extension}\" media=\"(max-width: 320px)\">\n"
170
+ html += "<source data-lazy=\"@srcset #{src_path}-640px#{extension}\" media=\"(max-width: 640px)\">\n"
171
+ html += "<source data-lazy=\"@srcset #{src_path}-1024px#{extension}\">\n"
172
+ else
173
+ html += "<source data-lazy=\"@srcset #{src_path}-320px#{extension}\" media=\"(max-width: 320px)\">\n"
174
+ html += "<source data-lazy=\"@srcset #{src_path}-640px#{extension}\" media=\"(max-width: 640px)\">\n"
175
+ html += "<source data-lazy=\"@srcset #{src_path}-1024px#{extension}\" media=\"(max-width: 1024px)\">\n"
176
+ html += "<source data-lazy=\"@srcset #{src}\" media=\"(min-width: 1025px)\">\n"
177
+ end
178
+
179
+ html
180
+ end
181
+
182
+ def build_external_image(src, options)
183
+ # Build responsive img tag for external URLs
184
+ alt = options['alt'] || ''
185
+ css_class = options['class'] || ''
186
+ style = options['style'] || ''
187
+ width = options['width'] || ''
188
+ height = options['height'] || ''
189
+ loading = options['loading'] || 'lazy'
190
+
191
+ # Build img tag on a single line to prevent markdown parsing issues
192
+ html = "<img"
193
+ html += " src=\"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==\""
194
+ html += " data-lazy=\"@src #{src}\""
195
+ html += " class=\"#{css_class}\"" unless css_class.empty?
196
+ html += " alt=\"#{alt}\""
197
+ html += " style=\"#{style}\"" unless style.empty?
198
+ html += " width=\"#{width}\"" unless width.empty?
199
+ html += " height=\"#{height}\"" unless height.empty?
200
+ html += " loading=\"#{loading}\""
201
+ html += ">"
202
+
203
+ html
204
+ end
205
+ end
206
+ end
207
+
208
+ Liquid::Template.register_tag('uj_image', Jekyll::UJImageTag)