jekyll-uj-powertools 1.6.0 → 1.6.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: bee69fe37cc10927e8f51898ed41337c5f71a2a013f1270f3ff0dea6b9318a42
4
- data.tar.gz: 6a07ff1145e4d2fc93dd2f269ce38ed51d42606206d435afb9a47c909b8575ff
3
+ metadata.gz: 035a3dbc797be68fa2e9dd84f80417b63672994e25362ce47897a254da7faa12
4
+ data.tar.gz: b7e4edc28279b80e0aadcae6435dd1283517d6f3aa68b9f65dfe99edfe0ae94a
5
5
  SHA512:
6
- metadata.gz: 7b87aa8225fd7cbebb64c20a21711e04f1faefd7221ddfc351a322424f46fb26c3b9459ce1b62938c430842c27b8b4aebf910c49517c846ceacab01fc6664c19
7
- data.tar.gz: 7b8e91ed9fb513e952163f53519f4eaf065a516832422dbc11ad9f5c39c0e0d535c4bfba312f36d9272f0ff11bb234475121dee9ad5367b04c98fc76e458c887
6
+ metadata.gz: 9c7c040655729bec85fc2c08b36bc84a759b518884c5f408bd75b0b735d05fdfee891b182f24b4b1f53004fe002bc7e23e31ff20fa6722871c82b83fe12964e1
7
+ data.tar.gz: 8ebda7bd0080113e1496fa8092889b79f80573a2c8241df1e07708b95e581dd1c143769561c295254070edcf40709cf3a5b48ba3e6965e5a3db524774e33ca0e
data/README.md CHANGED
@@ -70,6 +70,33 @@ Convert a string to title case.
70
70
  {{ "hello world" | uj_title_case }}
71
71
  ```
72
72
 
73
+ ### `uj_content_format` Filter
74
+ Process content with Liquid templating and Markdown conversion, automatically transforming markdown images to responsive `uj_image` tags.
75
+
76
+ ```liquid
77
+ {{ post.content | uj_content_format }}
78
+ ```
79
+
80
+ This filter:
81
+ - Transforms markdown images `![alt](url)` to `{% uj_image "url", alt="alt", class="..." %}`
82
+ - Automatically pulls image class from `page.resolved.theme.blog.image.class`
83
+ - Processes Liquid tags in the content
84
+ - Converts Markdown to HTML (for .md files)
85
+
86
+ If no class is specified in frontmatter, the `uj_image` tag will be rendered without a class attribute.
87
+
88
+ #### Frontmatter Configuration Example
89
+ ```yaml
90
+ ---
91
+ theme:
92
+ blog:
93
+ image:
94
+ class: "img-fluid rounded-3 shadow"
95
+ ---
96
+ ```
97
+
98
+ With this frontmatter, all markdown images in the post will automatically use the specified class.
99
+
73
100
  ## Global Variables
74
101
  ### `site.uj.cache_breaker` Variable
75
102
  Use the `site.uj.cache_breaker` variable to append a cache-busting query parameter to your assets.
@@ -143,6 +170,73 @@ A custom Liquid tag that checks if a variable is falsy (nil, false, empty string
143
170
  {% endifalsy %}
144
171
  ```
145
172
 
173
+ ### `uj_icon` Tag
174
+ A custom Liquid tag that renders a Font Awesome icon with the specified style and name. It supports `name` and `class` parameters.
175
+ ```liquid
176
+ {% uj_icon "rocket", "fa-lg me-2" %}
177
+ ```
178
+
179
+ ### `uj_fake_comments` Tag
180
+ Generates a fake comment count based on content word count for demonstration purposes.
181
+ ```liquid
182
+ {% uj_fake_comments %}
183
+ {% uj_fake_comments page.content %}
184
+ ```
185
+
186
+ ### `uj_image` Tag
187
+ Renders responsive images with WebP support and lazy loading.
188
+ ```liquid
189
+ {% uj_image "/assets/images/hero.jpg", max_width="1024", alt="Hero image" %}
190
+ {% uj_image page.featured_image, class="img-fluid", webp="false" %}
191
+ ```
192
+
193
+ ### `uj_language` Tag
194
+ Converts ISO language codes to language names in English or native format.
195
+ ```liquid
196
+ {% uj_language "es" %}
197
+ {% uj_language page.language, "native" %}
198
+ ```
199
+
200
+ ### `uj_member` Tag
201
+ Retrieves member information from site team collection.
202
+ ```liquid
203
+ {% uj_member "john-doe", "name" %}
204
+ {% uj_member page.author, "url" %}
205
+ {% uj_member member_id, "image" %}
206
+ {% uj_member "john-doe", "image-tag", max_width="640", class="team-photo" %}
207
+ ```
208
+
209
+ The `image-tag` property renders a responsive image using the `uj_image` tag with all its features (WebP, lazy loading, responsive sizes). You can pass any `uj_image` options as additional parameters.
210
+
211
+ ### `uj_post` Tag
212
+ Fetches post data from site collections.
213
+ ```liquid
214
+ {% uj_post "my-post-slug", "title" %}
215
+ {% uj_post post.id, "description" %}
216
+ {% uj_post current_post, "image-url" %}
217
+ ```
218
+
219
+ ### `uj_readtime` Tag
220
+ Calculates estimated reading time based on content (200 words per minute).
221
+ ```liquid
222
+ {% uj_readtime %}
223
+ {% uj_readtime page.content %}
224
+ ```
225
+
226
+ ### `uj_social` Tag
227
+ Generates social media URLs from platform handles.
228
+ ```liquid
229
+ {% uj_social "twitter" %}
230
+ {% uj_social "github" %}
231
+ ```
232
+
233
+ ### `uj_translation_url` Tag
234
+ Creates language-specific URLs for multilingual sites.
235
+ ```liquid
236
+ {% uj_translation_url "es", page.url %}
237
+ {% uj_translation_url target_lang, "/pricing" %}
238
+ ```
239
+
146
240
  ## Final notes
147
241
  These examples show how you can use the features of `jekyll-uj-powertools` in your Jekyll site.
148
242
 
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  Gem::Specification.new do |spec|
6
6
  # Gem info
7
7
  spec.name = "jekyll-uj-powertools"
8
- spec.version = "1.6.0"
8
+ spec.version = "1.6.2"
9
9
 
10
10
  # Author info
11
11
  spec.authors = ["ITW Creative Works"]
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency "bundler"
31
31
  spec.add_development_dependency "rake"
32
32
  spec.add_development_dependency "rspec"
33
+ spec.add_development_dependency "simplecov"
33
34
 
34
35
  # Translation and HTML manipulation requires Nokogiri
35
36
  spec.add_runtime_dependency 'nokogiri', '>= 1.17'
data/lib/filters/main.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  # Libraries
2
2
  require "jekyll"
3
+ require "json"
3
4
 
4
5
  # Filters
5
6
  module Jekyll
6
7
  module UJPowertools
7
- # Initialize a timestamp that will remain consistent across calls
8
- @cache_timestamp = Time.now.to_i.to_s
8
+ # Initialize a timestamp that will remain consistent across calls (with milliseconds)
9
+ @cache_timestamp = (Time.now.to_f * 1000).to_i.to_s
9
10
 
10
11
  # Strip ads from the input
11
12
  def uj_strip_ads(input)
@@ -48,11 +49,6 @@ module Jekyll
48
49
  rand(input)
49
50
  end
50
51
 
51
- # Return the current year
52
- def uj_year(input)
53
- Time.now.year
54
- end
55
-
56
52
  # Title case
57
53
  def uj_title_case(input)
58
54
  input.split(' ').map(&:capitalize).join(' ')
@@ -79,11 +75,18 @@ module Jekyll
79
75
 
80
76
  # Format content based on file extension - apply liquify and markdownify for .md files
81
77
  def uj_content_format(input)
78
+ # Return empty string if input is nil
79
+ return '' unless input
80
+
82
81
  # Get the current page from context
83
82
  page = @context.registers[:page] if @context.respond_to?(:registers)
84
83
  page ||= @context[:registers][:page] if @context.is_a?(Hash)
84
+
85
+ # Get site from context
86
+ site = @context.registers[:site] if @context.respond_to?(:registers)
87
+ site ||= @context[:registers][:site] if @context.is_a?(Hash)
85
88
 
86
- # Apply liquify first
89
+ # Simply apply liquify first (markdown images are already converted to uj_image tags by the hook)
87
90
  liquified = if @context.respond_to?(:registers)
88
91
  Liquid::Template.parse(input).render(@context)
89
92
  else
@@ -91,22 +94,37 @@ module Jekyll
91
94
  end
92
95
 
93
96
  # Check if the page extension is .md
94
- if page && page['extension'] == '.md'
97
+ if page && page['extension'] == '.md' && site
95
98
  # Apply markdownify for markdown files
96
- site = @context.registers[:site] if @context.respond_to?(:registers)
97
- site ||= @context[:registers][:site] if @context.is_a?(Hash)
98
-
99
- if site
100
- converter = site.find_converter_instance(Jekyll::Converters::Markdown)
101
- converter.convert(liquified)
102
- else
103
- liquified
104
- end
99
+ converter = site.find_converter_instance(Jekyll::Converters::Markdown)
100
+ converter.convert(liquified)
105
101
  else
106
102
  # Return just liquified content for non-markdown files
107
103
  liquified
108
104
  end
109
105
  end
106
+
107
+ # Pretty print JSON with configurable indentation (default 2 spaces)
108
+ def uj_jsonify(input, indent_size = 2)
109
+ indent_string = ' ' * indent_size.to_i
110
+ JSON.pretty_generate(input, indent: indent_string)
111
+ end
112
+
113
+ private
114
+
115
+ # Helper method to safely dig through nested hashes
116
+ def dig_value(hash, *keys)
117
+ return nil unless hash
118
+
119
+ value = hash
120
+ keys.each do |key|
121
+ return nil unless value.is_a?(Hash)
122
+ value = value[key]
123
+ return nil if value.nil?
124
+ end
125
+
126
+ value
127
+ end
110
128
  end
111
129
  end
112
130
 
@@ -3,7 +3,7 @@
3
3
 
4
4
  # Generator
5
5
  module Jekyll
6
- class InjectData < Generator
6
+ class InjectProperties < Generator
7
7
  safe true
8
8
  priority :low
9
9
 
@@ -114,6 +114,27 @@ module Jekyll
114
114
  item.data['extension'] = File.extname(item.path)
115
115
  end
116
116
 
117
+
118
+ # Inject canonical URL
119
+ if item.respond_to?(:url)
120
+ page_url_stripped = item.url.sub(/index\.html$/, '')
121
+ page_url_stripped = '' if page_url_stripped == '/'
122
+ site_url = site.config['url'] || ''
123
+ item.data['canonical'] = {
124
+ 'url' => site_url + page_url_stripped,
125
+ 'path' => page_url_stripped.empty? ? '/' : page_url_stripped
126
+ }
127
+ end
128
+
129
+ # Inject page type based on post or member properties
130
+ if item.data['post']
131
+ item.data['type'] = 'post'
132
+ elsif item.data['member']
133
+ item.data['type'] = 'member'
134
+ else
135
+ item.data['type'] = 'basic'
136
+ end
137
+
117
138
  # Set resolved data for site, layout, and page
118
139
  # Create a deep merge of site -> child layouts -> parent layouts -> page data
119
140
  # Priority: page (highest) -> parent layouts -> child layouts -> site (lowest)
@@ -155,5 +176,6 @@ module Jekyll
155
176
  # Add the resolved data to the item
156
177
  item.data['resolved'] = resolved
157
178
  end
179
+
158
180
  end
159
181
  end
@@ -3,6 +3,16 @@
3
3
 
4
4
  # Hook
5
5
  Jekyll::Hooks.register :site, :pre_render do |site|
6
+ # Ensure uj config exists
6
7
  site.config['uj'] ||= {}
8
+
9
+ # Set cache breaker
7
10
  site.config['uj']['cache_breaker'] = Jekyll::UJPowertools.cache_timestamp
11
+
12
+ # Add date properties
13
+ site.config['uj']['date'] ||= {}
14
+ now = Time.now
15
+ site.config['uj']['date']['year'] = now.year
16
+ site.config['uj']['date']['month'] = now.month
17
+ site.config['uj']['date']['day'] = now.day
8
18
  end
@@ -0,0 +1,40 @@
1
+ # Libraries
2
+ require "jekyll"
3
+
4
+ module Jekyll
5
+ # Hook into the pre_render phase to transform markdown images before conversion
6
+ Jekyll::Hooks.register [:posts, :pages, :documents], :pre_render do |doc|
7
+ # Only process markdown files
8
+ if doc.extname == ".md"
9
+ # Get image class from resolved data if available
10
+ image_class = nil
11
+ if doc.data['resolved'] && doc.data['resolved']['theme']
12
+ theme = doc.data['resolved']['theme']
13
+ if theme['post'] && theme['post']['image'] && theme['post']['image']['class']
14
+ image_class = theme['post']['image']['class']
15
+ end
16
+ end
17
+
18
+ # Transform markdown images by parsing and rendering Liquid template
19
+ doc.content = doc.content.gsub(/!\[([^\]]*)\]\(([^)]+)\)/) do
20
+ alt_text = $1
21
+ image_path = $2
22
+
23
+ # Build the Liquid tag string
24
+ if image_class
25
+ liquid_tag = "{% uj_image \"#{image_path}\", alt=\"#{alt_text}\", class=\"#{image_class}\" %}"
26
+ else
27
+ liquid_tag = "{% uj_image \"#{image_path}\", alt=\"#{alt_text}\" %}"
28
+ end
29
+
30
+ # Parse and render the Liquid template immediately
31
+ template = Liquid::Template.parse(liquid_tag)
32
+ context = doc.site.site_payload.merge({'page' => doc.to_liquid})
33
+ result = template.render(Liquid::Context.new(context))
34
+
35
+ # Return the HTML with blank lines to ensure markdown treats it as raw HTML
36
+ "\n\n#{result}\n\n"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -10,8 +10,19 @@ module Jekyll
10
10
 
11
11
  # Load Hooks
12
12
  require_relative "hooks/inject-properties"
13
+ require_relative "hooks/markdown-images"
13
14
 
14
15
  # Load Tags
15
- require_relative "tags/iftruthy"
16
+ require_relative "tags/fake_comments"
17
+ require_relative "tags/icon"
16
18
  require_relative "tags/iffalsy"
19
+ require_relative "tags/iffile"
20
+ require_relative "tags/iftruthy"
21
+ require_relative "tags/image"
22
+ require_relative "tags/language"
23
+ require_relative "tags/member"
24
+ require_relative "tags/post"
25
+ require_relative "tags/readtime"
26
+ require_relative "tags/social"
27
+ require_relative "tags/translation_url"
17
28
  end
@@ -0,0 +1,72 @@
1
+ # Libraries
2
+ require "jekyll"
3
+
4
+ module Jekyll
5
+ class UJCommentsTag < Liquid::Tag
6
+ def initialize(tag_name, markup, tokens)
7
+ super
8
+ @markup = markup.strip
9
+ end
10
+
11
+ def render(context)
12
+ # Get the content to analyze
13
+ content = resolve_content(context)
14
+ return '0' unless content
15
+
16
+ # Strip HTML tags
17
+ stripped_content = strip_html(content)
18
+
19
+ # Count words
20
+ words = count_words(stripped_content)
21
+
22
+ # Generate comment count based on word count modulo 13
23
+ comments = words % 13
24
+
25
+ comments.to_s
26
+ end
27
+
28
+ private
29
+
30
+ def resolve_content(context)
31
+ if @markup.empty?
32
+ # No argument, use page content
33
+ page = context['page']
34
+ return nil unless page
35
+ page['content']
36
+ else
37
+ # Resolve the variable name
38
+ resolve_variable(context, @markup)
39
+ end
40
+ end
41
+
42
+ def resolve_variable(context, variable_name)
43
+ # Handle nested variable access like page.content or include.content
44
+ parts = variable_name.split('.')
45
+ current = context
46
+
47
+ parts.each do |part|
48
+ return nil unless current.respond_to?(:[]) || current.is_a?(Hash)
49
+ current = current[part]
50
+ return nil if current.nil?
51
+ end
52
+
53
+ current
54
+ end
55
+
56
+ def strip_html(content)
57
+ # Remove HTML tags
58
+ content = content.to_s.gsub(/<script.*?<\/script>/m, '')
59
+ content = content.gsub(/<style.*?<\/style>/m, '')
60
+ content = content.gsub(/<[^>]+>/, ' ')
61
+ content = content.gsub(/\s+/, ' ')
62
+ content.strip
63
+ end
64
+
65
+ def count_words(text)
66
+ # Count words (split by whitespace)
67
+ text.split(/\s+/).length
68
+ end
69
+ end
70
+ end
71
+
72
+ Liquid::Template.register_tag('uj_fake_comments', Jekyll::UJCommentsTag)
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)