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/post.rb ADDED
@@ -0,0 +1,258 @@
1
+ # Libraries
2
+ require "jekyll"
3
+
4
+ module Jekyll
5
+ class UJPostTag < 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_with_quotes(@markup)
14
+ post_input = args[0]
15
+ property_input = args[1] || "'title'" # Default to title if no property specified
16
+
17
+ # Strip quotes from property if present
18
+ property = property_input.gsub(/^['"]|['"]$/, '')
19
+
20
+ # Check if the post input was originally quoted
21
+ is_quoted = post_input && post_input.match(/^['"]/)
22
+
23
+ # Resolve post ID
24
+ if is_quoted
25
+ # If quoted, strip quotes and use as literal
26
+ post_id = post_input.gsub(/^['"]|['"]$/, '')
27
+ else
28
+ # Otherwise resolve as variable
29
+ post_id = resolve_post_id(context, post_input)
30
+ end
31
+ return '' unless post_id
32
+
33
+ # Find post in site collections
34
+ site = context.registers[:site]
35
+ post = find_post(site, post_id)
36
+ return '' unless post
37
+
38
+ # Return the requested property
39
+ case property
40
+ when 'title'
41
+ post.data['title'] || ''
42
+ when 'description'
43
+ post.data['description'] || post.data['excerpt'] || ''
44
+ when 'url'
45
+ site_url = site.config['url'] || ''
46
+ site_url + post.url
47
+ when 'path'
48
+ post.url
49
+ when 'image'
50
+ # Use the custom post.post.id if available, otherwise fall back to extracting from post.id
51
+ custom_id = (post.data['post'] && post.data['post']['id']) || post.id.gsub(/^\/(\w+)\//, '')
52
+ # Extract the slug from the Jekyll post ID
53
+ post_id_clean = post.id.gsub(/^\/(\w+)\//, '')
54
+ slug = post_id_clean.gsub(/^\d{4}-\d{2}-\d{2}-/, '')
55
+ "/assets/images/blog/post-#{custom_id}/#{slug}.jpg"
56
+ when 'date'
57
+ post.data['date'] ? post.data['date'].strftime('%Y-%m-%d') : ''
58
+ when 'author'
59
+ (post.data['post'] && post.data['post']['author']) || post.data['author'] || ''
60
+ when 'category'
61
+ post.data['category'] || post.data['categories']&.first || ''
62
+ when 'categories'
63
+ Array(post.data['categories']).join(', ')
64
+ when 'tags'
65
+ Array(post.data['tags']).join(', ')
66
+ when 'id'
67
+ post.id
68
+ when 'image-tag'
69
+ # Generate image path
70
+ # Use the custom post.post.id if available, otherwise fall back to extracting from post.id
71
+ custom_id = (post.data['post'] && post.data['post']['id']) || post.id.gsub(/^\/(\w+)\//, '')
72
+ # Extract the slug from the Jekyll post ID
73
+ post_id_clean = post.id.gsub(/^\/(\w+)\//, '')
74
+ slug = post_id_clean.gsub(/^\d{4}-\d{2}-\d{2}-/, '')
75
+ image_path = "/assets/images/blog/post-#{custom_id}/#{slug}.jpg"
76
+
77
+ # Parse additional options for the image tag
78
+ image_options = parse_image_options(args[2..-1], context)
79
+
80
+ # Set default alt text if not provided
81
+ if !image_options['alt']
82
+ # Try to get the title from post.post.title first, then fall back to post.title
83
+ default_alt = (post.data['post'] && post.data['post']['title']) || post.data['title']
84
+ image_options['alt'] = default_alt if default_alt
85
+ end
86
+
87
+ # Build the markup string for uj_image tag
88
+ image_markup = build_image_markup(image_path, image_options)
89
+
90
+ # Parse and render the uj_image tag using Liquid template
91
+ template_content = "{% uj_image #{image_markup} %}"
92
+ template = Liquid::Template.parse(template_content)
93
+ template.render!(context)
94
+ else
95
+ # Try to access any other property dynamically
96
+ (post.data['post'] && post.data['post'][property]) || post.data[property] || ''
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def parse_arguments_with_quotes(markup)
103
+ # Parse arguments preserving quotes for detection
104
+ args = []
105
+ current_arg = ''
106
+ in_quotes = false
107
+ quote_char = nil
108
+
109
+ markup.each_char.with_index do |char, i|
110
+ if !in_quotes && (char == '"' || char == "'")
111
+ in_quotes = true
112
+ quote_char = char
113
+ current_arg += char
114
+ elsif in_quotes && char == quote_char
115
+ in_quotes = false
116
+ current_arg += char
117
+ quote_char = nil
118
+ elsif !in_quotes && char == ','
119
+ args << current_arg.strip
120
+ current_arg = ''
121
+ else
122
+ current_arg += char
123
+ end
124
+ end
125
+
126
+ args << current_arg.strip if current_arg.strip.length > 0
127
+ args
128
+ end
129
+
130
+ def parse_arguments(markup)
131
+ # Parse arguments that can be quoted or unquoted
132
+ args = []
133
+ current_arg = ''
134
+ in_quotes = false
135
+ quote_char = nil
136
+
137
+ markup.each_char.with_index do |char, i|
138
+ if !in_quotes && (char == '"' || char == "'")
139
+ in_quotes = true
140
+ quote_char = char
141
+ elsif in_quotes && char == quote_char
142
+ in_quotes = false
143
+ quote_char = nil
144
+ elsif !in_quotes && char == ','
145
+ args << current_arg.strip
146
+ current_arg = ''
147
+ else
148
+ current_arg += char
149
+ end
150
+ end
151
+
152
+ args << current_arg.strip if current_arg.strip.length > 0
153
+ args
154
+ end
155
+
156
+ def resolve_post_id(context, post_input)
157
+ if post_input.nil? || post_input.empty?
158
+ # No input, use current page if it's a post
159
+ page = context['page']
160
+ return nil unless page
161
+
162
+ # Check if current page is a post
163
+ if page['post'] || page['collection'] == 'posts'
164
+ page['id']
165
+ else
166
+ nil
167
+ end
168
+ else
169
+ # Resolve the variable
170
+ resolve_variable(context, post_input)
171
+ end
172
+ end
173
+
174
+ def resolve_variable(context, variable_name)
175
+ # Handle nested variable access
176
+ parts = variable_name.split('.')
177
+ current = context
178
+
179
+ parts.each do |part|
180
+ return nil unless current.respond_to?(:[]) || current.is_a?(Hash)
181
+ current = current[part]
182
+ return nil if current.nil?
183
+ end
184
+
185
+ current
186
+ end
187
+
188
+ def find_post(site, post_id)
189
+ post_id_clean = post_id.to_s.strip
190
+
191
+ # Search in posts collection first
192
+ if site.collections['posts']
193
+ post = site.collections['posts'].docs.find do |doc|
194
+ # Check standard ID match
195
+ doc.id == post_id_clean ||
196
+ doc.id.include?(post_id_clean) ||
197
+ # Also check if the post has a custom post.id field that matches
198
+ (doc.data['post'] && doc.data['post']['id'] == post_id_clean)
199
+ end
200
+ return post if post
201
+ end
202
+
203
+ # Search in other collections that might contain posts
204
+ site.collections.each do |name, collection|
205
+ next if name == 'posts' # Already checked
206
+
207
+ post = collection.docs.find do |doc|
208
+ (doc.id == post_id_clean ||
209
+ doc.id.include?(post_id_clean) ||
210
+ # Check custom post.id field
211
+ (doc.data['post'] && doc.data['post']['id'] == post_id_clean)) &&
212
+ doc.data['post']
213
+ end
214
+ return post if post
215
+ end
216
+
217
+ nil
218
+ end
219
+
220
+ def parse_image_options(option_args, context)
221
+ options = {}
222
+
223
+ option_args.each do |arg|
224
+ if arg.include?('=')
225
+ key, value = arg.split('=', 2)
226
+ key = key.strip
227
+
228
+ # Check if the value is quoted (literal) or unquoted (variable)
229
+ if value.strip.match(/^['"].*['"]$/)
230
+ # It's a literal string, strip quotes
231
+ value = value.strip.gsub(/^['"]|['"]$/, '')
232
+ else
233
+ # It's a variable, resolve it
234
+ resolved_value = resolve_variable(context, value.strip)
235
+ value = resolved_value || value.strip
236
+ end
237
+
238
+ options[key] = value
239
+ end
240
+ end
241
+
242
+ options
243
+ end
244
+
245
+ def build_image_markup(image_path, options)
246
+ # Build markup string in the format expected by uj_image tag
247
+ markup_parts = ["\"#{image_path}\""]
248
+
249
+ options.each do |key, value|
250
+ markup_parts << "#{key}=\"#{value}\""
251
+ end
252
+
253
+ markup_parts.join(', ')
254
+ end
255
+ end
256
+ end
257
+
258
+ Liquid::Template.register_tag('uj_post', Jekyll::UJPostTag)
@@ -0,0 +1,73 @@
1
+ # Libraries
2
+ require "jekyll"
3
+
4
+ module Jekyll
5
+ class UJReadtimeTag < 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 '1' 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
+ # Calculate readtime (200 words per minute, minimum 1 minute)
23
+ readtime = (words / 200.0).ceil
24
+ readtime = 1 if readtime < 1
25
+
26
+ readtime.to_s
27
+ end
28
+
29
+ private
30
+
31
+ def resolve_content(context)
32
+ if @markup.empty?
33
+ # No argument, use page content
34
+ page = context['page']
35
+ return nil unless page
36
+ page['content']
37
+ else
38
+ # Resolve the variable name
39
+ resolve_variable(context, @markup)
40
+ end
41
+ end
42
+
43
+ def resolve_variable(context, variable_name)
44
+ # Handle nested variable access like page.content or include.content
45
+ parts = variable_name.split('.')
46
+ current = context
47
+
48
+ parts.each do |part|
49
+ return nil unless current.respond_to?(:[]) || current.is_a?(Hash)
50
+ current = current[part]
51
+ return nil if current.nil?
52
+ end
53
+
54
+ current
55
+ end
56
+
57
+ def strip_html(content)
58
+ # Remove HTML tags
59
+ content = content.to_s.gsub(/<script.*?<\/script>/m, '')
60
+ content = content.gsub(/<style.*?<\/style>/m, '')
61
+ content = content.gsub(/<[^>]+>/, ' ')
62
+ content = content.gsub(/\s+/, ' ')
63
+ content.strip
64
+ end
65
+
66
+ def count_words(text)
67
+ # Count words (split by whitespace)
68
+ text.split(/\s+/).length
69
+ end
70
+ end
71
+ end
72
+
73
+ Liquid::Template.register_tag('uj_readtime', Jekyll::UJReadtimeTag)
@@ -0,0 +1,84 @@
1
+ # Libraries
2
+ require "jekyll"
3
+
4
+ module Jekyll
5
+ class UJSocialTag < Liquid::Tag
6
+ # Social platform URL patterns
7
+ SOCIAL_URLS = {
8
+ 'facebook' => 'https://facebook.com/%s',
9
+ 'twitter' => 'https://twitter.com/%s',
10
+ 'linkedin' => 'https://linkedin.com/in/%s',
11
+ 'youtube' => 'https://youtube.com/@%s',
12
+ 'instagram' => 'https://instagram.com/%s',
13
+ 'tumblr' => 'https://%s.tumblr.com',
14
+ 'slack' => 'https://%s.slack.com',
15
+ 'discord' => 'https://discord.gg/%s',
16
+ 'github' => 'https://github.com/%s',
17
+ 'dev' => 'https://dev.to/%s',
18
+ 'tiktok' => 'https://tiktok.com/@%s',
19
+ 'twitch' => 'https://twitch.tv/%s',
20
+ 'soundcloud' => 'https://soundcloud.com/%s',
21
+ 'spotify' => 'https://open.spotify.com/user/%s',
22
+ 'mixcloud' => 'https://mixcloud.com/%s'
23
+ }
24
+
25
+ def initialize(tag_name, markup, tokens)
26
+ super
27
+ @markup = markup.strip
28
+ end
29
+
30
+ def render(context)
31
+ # Parse the platform name (can be quoted or unquoted)
32
+ platform_input = parse_argument(@markup)
33
+
34
+ # Resolve the platform name (could be a variable or literal string)
35
+ platform = resolve_variable(context, platform_input)
36
+
37
+ # If it didn't resolve to anything, use the input as a literal string
38
+ platform = platform_input if platform.nil? || platform.empty?
39
+
40
+ # Get the social handle from page.resolved.socials.{platform}
41
+ page = context['page']
42
+ return '' unless page
43
+
44
+ social_handle = page['resolved'] && page['resolved']['socials'] && page['resolved']['socials'][platform]
45
+ return '' unless social_handle && !social_handle.empty?
46
+
47
+ # Get the URL pattern for this platform
48
+ url_pattern = SOCIAL_URLS[platform]
49
+ return '' unless url_pattern
50
+
51
+ # Build the URL
52
+ url_pattern % social_handle
53
+ end
54
+
55
+ private
56
+
57
+ def parse_argument(markup)
58
+ # Remove quotes if present
59
+ cleaned = markup.strip
60
+ if (cleaned.start_with?('"') && cleaned.end_with?('"')) ||
61
+ (cleaned.start_with?("'") && cleaned.end_with?("'"))
62
+ cleaned[1..-2]
63
+ else
64
+ cleaned
65
+ end
66
+ end
67
+
68
+ def resolve_variable(context, variable_name)
69
+ # Handle nested variable access like page.social
70
+ parts = variable_name.split('.')
71
+ current = context
72
+
73
+ parts.each do |part|
74
+ return nil unless current.respond_to?(:[]) || current.is_a?(Hash)
75
+ current = current[part]
76
+ return nil if current.nil?
77
+ end
78
+
79
+ current
80
+ end
81
+ end
82
+ end
83
+
84
+ Liquid::Template.register_tag('uj_social', Jekyll::UJSocialTag)
@@ -0,0 +1,154 @@
1
+ # Libraries
2
+ require "jekyll"
3
+
4
+ module Jekyll
5
+ class UJTranslationUrlTag < 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 that can be quoted or unquoted
13
+ parts = parse_arguments(@markup)
14
+
15
+ # Return root if no arguments
16
+ return '/' if parts.empty? || parts[0].nil?
17
+
18
+ language_code_input = parts[0]
19
+ url_path_input = parts[1] || '/'
20
+
21
+ # Resolve language code (literal or variable)
22
+ language_code = resolve_argument_value(context, language_code_input)
23
+ # Resolve URL path (literal or variable)
24
+ url_path = resolve_argument_value(context, url_path_input)
25
+
26
+ # Get site and translation config from context
27
+ site = context.registers[:site]
28
+ return '/' unless site
29
+
30
+ translation_config = site.config['translation'] || {}
31
+ default_language = translation_config['default'] || 'en'
32
+ available_languages = translation_config['languages'] || [default_language]
33
+
34
+ # Validate that the requested language is available
35
+ unless available_languages.include?(language_code)
36
+ # Fall back to default language if requested language is not available
37
+ language_code = default_language
38
+ end
39
+
40
+ # Normalize the URL path
41
+ normalized_path = normalize_path(url_path)
42
+
43
+ # Generate the language-specific URL
44
+ generate_language_url(language_code, normalized_path, default_language)
45
+ end
46
+
47
+ private
48
+
49
+ def parse_arguments(markup)
50
+ # Parse arguments that can be quoted or unquoted
51
+ # Examples: 'es', '/pricing' OR language, page.canonical.path OR 'es', page.url
52
+ args = []
53
+ current_arg = ''
54
+ in_quotes = false
55
+ quote_char = nil
56
+
57
+ markup.each_char.with_index do |char, i|
58
+ if !in_quotes && (char == '"' || char == "'")
59
+ # Start of quoted string - include the quote in the arg
60
+ in_quotes = true
61
+ quote_char = char
62
+ current_arg += char
63
+ elsif in_quotes && char == quote_char
64
+ # End of quoted string - include the quote in the arg
65
+ current_arg += char
66
+ in_quotes = false
67
+ quote_char = nil
68
+ elsif !in_quotes && char == ','
69
+ # Argument separator
70
+ args << current_arg.strip
71
+ current_arg = ''
72
+ else
73
+ # Regular character
74
+ current_arg += char
75
+ end
76
+ end
77
+
78
+ # Add the last argument
79
+ args << current_arg.strip if current_arg.strip.length > 0
80
+
81
+ args
82
+ end
83
+
84
+ def resolve_argument_value(context, argument_input)
85
+ return '' if argument_input.nil? || argument_input.empty?
86
+
87
+ # Check if the argument was originally quoted (literal string)
88
+ is_quoted = argument_input.match(/^['"].*['"]$/)
89
+
90
+ # If quoted, remove quotes and use as literal. Otherwise, try to resolve as variable
91
+ if is_quoted
92
+ # Remove quotes from literal string
93
+ resolved_value = argument_input[1..-2]
94
+ else
95
+ # Try to resolve as a variable
96
+ resolved_value = resolve_variable(context, argument_input)
97
+ # If variable resolved to nil, return empty string
98
+ return '' if resolved_value.nil?
99
+ # If it didn't resolve to a string, use the resolved value
100
+ resolved_value = resolved_value.to_s if resolved_value
101
+ end
102
+
103
+ resolved_value.to_s
104
+ end
105
+
106
+ def resolve_variable(context, variable_name)
107
+ parts = variable_name.split('.')
108
+ current = context
109
+
110
+ parts.each do |part|
111
+ if current.respond_to?(:[])
112
+ current = current[part]
113
+ elsif current.respond_to?(:key?) && current.key?(part)
114
+ current = current[part]
115
+ else
116
+ return nil
117
+ end
118
+ return nil if current.nil?
119
+ end
120
+
121
+ current
122
+ end
123
+
124
+ def normalize_path(path)
125
+ return '' if path.nil? || path.empty?
126
+
127
+ # Remove leading slash for processing
128
+ clean_path = path.start_with?('/') ? path[1..-1] : path
129
+
130
+ # Handle empty path (home page)
131
+ return '' if clean_path.empty?
132
+
133
+ clean_path
134
+ end
135
+
136
+ def generate_language_url(language_code, normalized_path, default_language)
137
+ # If it's the default language, return the original path
138
+ if language_code == default_language
139
+ return normalized_path.empty? ? '/' : "/#{normalized_path}"
140
+ end
141
+
142
+ # For non-default languages, prefix with language code
143
+ if normalized_path.empty?
144
+ # Home page: /es
145
+ "/#{language_code}"
146
+ else
147
+ # Other pages: /es/pricing
148
+ "/#{language_code}/#{normalized_path}"
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ Liquid::Template.register_tag('uj_translation_url', Jekyll::UJTranslationUrlTag)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-uj-powertools
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.2
4
+ version: 1.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - ITW Creative Works
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-16 00:00:00.000000000 Z
11
+ date: 2025-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -72,6 +72,20 @@ dependencies:
72
72
  - - ">="
73
73
  - !ruby/object:Gem::Version
74
74
  version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: simplecov
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
75
89
  - !ruby/object:Gem::Dependency
76
90
  name: nokogiri
77
91
  requirement: !ruby/object:Gem::Requirement
@@ -102,8 +116,19 @@ files:
102
116
  - lib/filters/main.rb
103
117
  - lib/generators/inject-properties.rb
104
118
  - lib/hooks/inject-properties.rb
119
+ - lib/hooks/markdown-images.rb
105
120
  - lib/jekyll-uj-powertools.rb
106
- - lib/tags/ifistruthy.rb
121
+ - lib/tags/fake_comments.rb
122
+ - lib/tags/icon.rb
123
+ - lib/tags/iffalsy.rb
124
+ - lib/tags/iftruthy.rb
125
+ - lib/tags/image.rb
126
+ - lib/tags/language.rb
127
+ - lib/tags/member.rb
128
+ - lib/tags/post.rb
129
+ - lib/tags/readtime.rb
130
+ - lib/tags/social.rb
131
+ - lib/tags/translation_url.rb
107
132
  homepage: https://github.com/itw-creative-works/jekyll-uj-powertools
108
133
  licenses:
109
134
  - MIT