jekyll-uj-powertools 1.3.1 → 1.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06406f26c558d459244fad81422d210523148d4a90bd42d004fcdfcaede642e0
4
- data.tar.gz: d6e23dd0c11253df4dfef38612049904cb251728beade6139a2d5a74f51f3a19
3
+ metadata.gz: bf2e2fcda199ac3fbca2062352312b94e177a99193a85ee3ebfdfae4315063c2
4
+ data.tar.gz: 6036a0be11527263de051855fe4015338db44f4c8d6b7227dc64bda174fe7b4a
5
5
  SHA512:
6
- metadata.gz: a40bb126326706aee31507e2ad02d83a0a1e83ff8927bf99d90f0869d4898034f282f73e27032013c5018c3553ed435964f7de170d4b502f2d3be900ef7e1666
7
- data.tar.gz: 5c5ff4b05d49cfc03312d7eaa834ef0da25c5a56df0c5ec99fcf7452eda94d40b97809d90edfd1fdfea639008e344c5e7cc22460d00bed30a57fcd48fdd0a05d
6
+ metadata.gz: 180f0afccbbe441fbfdc32e1340893de4e0dea5c4adf688eef606bfadd92a7c4515857e699115204f4c62ffce0412e5170cb8187a86d6a14ae88d9abf13d27f7
7
+ data.tar.gz: 2f71ba1926df6fde7f6d0f1ebd0cc506a9ec5ccf76fc3350aabbecac25d58c26e720e757122288a7e2aae0a5c84ef4ced76371c89a1abe3a2d70165faade6383
data/README.md CHANGED
@@ -46,8 +46,9 @@ gem install jekyll-uj-powertools
46
46
  ```
47
47
 
48
48
  ## ⚡️ Usage
49
- Now you can use the `uj_strip_ads` and `uj_json_escape` filters in your Jekyll site:
49
+ Now you can use all the custom filters and variables provided by `jekyll-uj-powertools` in your Jekyll site.
50
50
 
51
+ ## Filters
51
52
  ### `uj_strip_ads` Filter
52
53
  Remove ads from a string, such as a blog post or article.
53
54
 
@@ -69,11 +70,26 @@ Convert a string to title case.
69
70
  {{ "hello world" | uj_title_case }}
70
71
  ```
71
72
 
72
- ### `uj.cache_breaker` Variable
73
- Use the `uj.cache_breaker` variable to append a cache-busting query parameter to your assets.
73
+ ## Global Variables
74
+ ### `site.uj.cache_breaker` Variable
75
+ Use the `site.uj.cache_breaker` variable to append a cache-busting query parameter to your assets.
74
76
 
75
77
  ```liquid
76
- <link rel="stylesheet" href="{{ "/assets/css/style.css" | prepend: site.baseurl }}?v={{ uj.cache_breaker }}">
78
+ <link rel="stylesheet" href="{{ "/assets/css/style.css" | prepend: site.baseurl }}?v={{ site.uj.cache_breaker }}">
79
+ ```
80
+
81
+ ### Page Variables
82
+ ### `page.random_id` Variable
83
+ Generate a random ID for each page, useful for sorting randomly or for unique identifiers.
84
+
85
+ ### `page.extension` Variable
86
+ Get the file extension of the current page, useful for determining how to process or display the page.
87
+
88
+ ### `page.layout_data` Variable
89
+ Access the layout data of the current page, which can be useful for debugging or displaying layout-specific information.
90
+
91
+ ```liquid
92
+ {{ page.extension }}
77
93
  ```
78
94
 
79
95
  These examples show how you can use the features of `jekyll-uj-powertools` in your Jekyll site.
@@ -81,7 +97,9 @@ These examples show how you can use the features of `jekyll-uj-powertools` in yo
81
97
  ## 🔧 Development
82
98
  After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
83
99
 
84
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
100
+ To install this gem onto your local machine, run `bundle exec rake install`.
101
+
102
+ To release a new version, update the version number in the `.gemspec` and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
85
103
 
86
104
  ## ⚠️ Testing
87
105
  Run the tests
@@ -1,12 +1,11 @@
1
1
  # coding: utf-8
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'jekyll-uj-powertools/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
6
  # Gem info
8
7
  spec.name = "jekyll-uj-powertools"
9
- spec.version = Jekyll::UJPowertools::VERSION
8
+ spec.version = "1.4.0"
10
9
 
11
10
  # Author info
12
11
  spec.authors = ["ITW Creative Works"]
@@ -0,0 +1,114 @@
1
+ # Libraries
2
+ require "jekyll"
3
+
4
+ # Module
5
+ module Jekyll
6
+ module UJPowertools
7
+ # Initialize a timestamp that will remain consistent across calls
8
+ @cache_timestamp = Time.now.to_i.to_s
9
+
10
+ # Strip ads from the input
11
+ def uj_strip_ads(input)
12
+ input
13
+ # Remove HTML <ad-units>
14
+ .gsub(/\s*<ad-unit>[\s\S]*?<\/ad-unit>\s*/m, '')
15
+ # Remove includes starting with "/master/modules/adunits/"
16
+ .gsub(/\s*\{% include \/master\/modules\/adunits\/.*? %\}\s*/m, '')
17
+ end
18
+
19
+ # Escape a string for use in JSON
20
+ # def uj_json_escape(value)
21
+ # value
22
+ # .gsub('\\', '\\\\') # Escape backslashes
23
+ # .gsub('"', '\"') # Escape double quotes
24
+ # .gsub("\b", '\\b') # Escape backspace
25
+ # .gsub("\f", '\\f') # Escape formfeed
26
+ # .gsub("\n", '\\n') # Escape newline
27
+ # .gsub("\r", '\\r') # Escape carriage return
28
+ # .gsub("\t", '\\t') # Escape tab
29
+ # end
30
+ def uj_json_escape(value)
31
+ value.to_json[1..-2] # Convert to JSON and remove the surrounding quotes
32
+ end
33
+
34
+ # Increment a global counter that can be accessed from any page then return the new value
35
+ # def uj_increment_return(input)
36
+ # @context.registers[:uj_incremental_return] ||= 0
37
+ # @context.registers[:uj_incremental_return]
38
+ # @context.registers[:uj_incremental_return] += input
39
+ # end
40
+ def uj_increment_return(input)
41
+ @context ||= { registers: {} }
42
+ @context[:registers][:uj_incremental_return] ||= 0
43
+ @context[:registers][:uj_incremental_return] += input
44
+ end
45
+
46
+ # Return a random number between 0 and the input
47
+ def uj_random(input)
48
+ rand(input)
49
+ end
50
+
51
+ # Return the current year
52
+ def uj_year(input)
53
+ Time.now.year
54
+ end
55
+
56
+ # Title case
57
+ def uj_title_case(input)
58
+ input.split(' ').map(&:capitalize).join(' ')
59
+ end
60
+
61
+ # Check if a value is truthy (not nil, empty string, or 'null')
62
+ # def uj_istruthy(input)
63
+ # return false if input.nil?
64
+ # return false if input.respond_to?(:empty?) && input.empty?
65
+ # return false if input.to_s.downcase == 'null'
66
+ # return false if input == false
67
+ # true
68
+ # end
69
+
70
+ # Accessor for the consistent timestamp
71
+ def self.cache_timestamp
72
+ @cache_timestamp
73
+ end
74
+
75
+ # Check if a string ends with a specific suffix
76
+ # def uj_ends_with(input, suffix)
77
+ # input.end_with?(suffix)
78
+ # end
79
+
80
+ # Format content based on file extension - apply liquify and markdownify for .md files
81
+ def uj_content_format(input)
82
+ # Get the current page from context
83
+ page = @context.registers[:page] if @context.respond_to?(:registers)
84
+ page ||= @context[:registers][:page] if @context.is_a?(Hash)
85
+
86
+ # Apply liquify first
87
+ liquified = if @context.respond_to?(:registers)
88
+ Liquid::Template.parse(input).render(@context)
89
+ else
90
+ Liquid::Template.parse(input).render(@context[:registers] || {})
91
+ end
92
+
93
+ # Check if the page extension is .md
94
+ if page && page['extension'] == '.md'
95
+ # 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
105
+ else
106
+ # Return just liquified content for non-markdown files
107
+ liquified
108
+ end
109
+ end
110
+ end
111
+ end
112
+
113
+ # Register the filter
114
+ Liquid::Template.register_filter(Jekyll::UJPowertools)
@@ -1,6 +1,7 @@
1
1
  # Libraries
2
2
  # ...
3
3
 
4
+ # Module
4
5
  module Jekyll
5
6
  class InjectData < Generator
6
7
  safe true
@@ -29,7 +30,13 @@ module Jekyll
29
30
  # Inject a random number into the item's data
30
31
  item.data['random_id'] = rand(100) # Random number between 0 and 99
31
32
 
32
- return unless item.data['layout'] # Skip items without layouts
33
+ # Inject the file extension into the item's data
34
+ if item.respond_to?(:path)
35
+ item.data['extension'] = File.extname(item.path)
36
+ end
37
+
38
+ # Skip items without layouts
39
+ return unless item.data['layout']
33
40
 
34
41
  # Find the layout file by its name
35
42
  layout_name = item.data['layout']
@@ -1,76 +1,16 @@
1
1
  # Libraries
2
2
  require "jekyll"
3
3
 
4
- # Module
5
4
  module Jekyll
6
- module UJPowertools
7
- # Initialize a timestamp that will remain consistent across calls
8
- @cache_timestamp = Time.now.to_i.to_s
9
-
10
- # Strip ads from the input
11
- def uj_strip_ads(input)
12
- input
13
- # Remove HTML <ad-units>
14
- .gsub(/\s*<ad-unit>[\s\S]*?<\/ad-unit>\s*/m, '')
15
- # Remove includes starting with "/master/modules/adunits/"
16
- .gsub(/\s*\{% include \/master\/modules\/adunits\/.*? %\}\s*/m, '')
17
- end
18
-
19
- # Escape a string for use in JSON
20
- # def uj_json_escape(value)
21
- # value
22
- # .gsub('\\', '\\\\') # Escape backslashes
23
- # .gsub('"', '\"') # Escape double quotes
24
- # .gsub("\b", '\\b') # Escape backspace
25
- # .gsub("\f", '\\f') # Escape formfeed
26
- # .gsub("\n", '\\n') # Escape newline
27
- # .gsub("\r", '\\r') # Escape carriage return
28
- # .gsub("\t", '\\t') # Escape tab
29
- # end
30
- def uj_json_escape(value)
31
- value.to_json[1..-2] # Convert to JSON and remove the surrounding quotes
32
- end
33
-
34
- # Increment a global counter that can be accessed from any page then return the new value
35
- # def uj_increment_return(input)
36
- # @context.registers[:uj_incremental_return] ||= 0
37
- # @context.registers[:uj_incremental_return]
38
- # @context.registers[:uj_incremental_return] += input
39
- # end
40
- def uj_increment_return(input)
41
- @context ||= { registers: {} }
42
- @context[:registers][:uj_incremental_return] ||= 0
43
- @context[:registers][:uj_incremental_return] += input
44
- end
45
-
46
- # Return a random number between 0 and the input
47
- def uj_random(input)
48
- rand(input)
49
- end
50
-
51
- # Return the current year
52
- def uj_year(input)
53
- Time.now.year
54
- end
55
-
56
- # Title case
57
- def uj_title_case(input)
58
- input.split(' ').map(&:capitalize).join(' ')
59
- end
60
-
61
- # Accessor for the consistent timestamp
62
- def self.cache_timestamp
63
- @cache_timestamp
64
- end
65
- end
5
+ # Load Filters
6
+ require_relative "filters/main"
66
7
 
67
8
  # Load Generators
68
9
  require_relative "generators/inject-properties"
69
10
 
70
11
  # Load Hooks
71
12
  require_relative "hooks/inject-properties"
72
- # require_relative "hooks/translate-pages"
73
- end
74
13
 
75
- # Register the filter
76
- Liquid::Template.register_filter(Jekyll::UJPowertools)
14
+ # Load Tags
15
+ # require_relative "tags/ifistruthy"
16
+ end
@@ -0,0 +1,161 @@
1
+ module Jekyll
2
+ module UJPowertools
3
+ class IfIsTruthyTag < Liquid::Block
4
+ Syntax = /(\w+)/
5
+
6
+ def initialize(tag_name, markup, tokens)
7
+ super
8
+
9
+ if markup =~ Syntax
10
+ @variable_name = $1
11
+ else
12
+ raise SyntaxError, "Invalid syntax for ifistruthy tag. Usage: {% ifistruthy variable_name %}"
13
+ end
14
+ end
15
+
16
+ def render(context)
17
+ variable = context[@variable_name]
18
+ is_truthy = check_truthy(variable)
19
+
20
+ # Split content at else tag
21
+ else_index = nil
22
+ @nodelist.each_with_index do |node, index|
23
+ if node.respond_to?(:tag_name) && node.tag_name == 'else'
24
+ else_index = index
25
+ break
26
+ end
27
+ end
28
+
29
+ if is_truthy
30
+ if else_index
31
+ render_nodelist(@nodelist[0...else_index], context)
32
+ else
33
+ super(context)
34
+ end
35
+ else
36
+ if else_index
37
+ render_nodelist(@nodelist[(else_index + 1)..-1], context)
38
+ else
39
+ ''
40
+ end
41
+ end
42
+ end
43
+
44
+ def unknown_tag(tag_name, markup, tokens)
45
+ if tag_name == 'else'
46
+ @nodelist << Liquid::ElseTag.new(tag_name, markup, tokens)
47
+ else
48
+ super
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def check_truthy(value)
55
+ return false if value.nil?
56
+ return false if value.respond_to?(:empty?) && value.empty?
57
+ return false if value.to_s.downcase == 'null'
58
+ return false if value == false
59
+ true
60
+ end
61
+
62
+ def render_nodelist(nodelist, context)
63
+ output = []
64
+ nodelist.each do |token|
65
+ case token
66
+ when String
67
+ output << token
68
+ else
69
+ if token.respond_to?(:render)
70
+ output << token.render(context)
71
+ else
72
+ output << token.to_s
73
+ end
74
+ end
75
+ end
76
+ output.join
77
+ end
78
+ end
79
+
80
+ class UnlessIsTruthyTag < Liquid::Block
81
+ Syntax = /(\w+)/
82
+
83
+ def initialize(tag_name, markup, tokens)
84
+ super
85
+
86
+ if markup =~ Syntax
87
+ @variable_name = $1
88
+ else
89
+ raise SyntaxError, "Invalid syntax for unlessistruthy tag. Usage: {% unlessistruthy variable_name %}"
90
+ end
91
+ end
92
+
93
+ def render(context)
94
+ variable = context[@variable_name]
95
+ is_truthy = check_truthy(variable)
96
+
97
+ # Split content at else tag
98
+ else_index = nil
99
+ @nodelist.each_with_index do |node, index|
100
+ if node.respond_to?(:tag_name) && node.tag_name == 'else'
101
+ else_index = index
102
+ break
103
+ end
104
+ end
105
+
106
+ if !is_truthy
107
+ if else_index
108
+ render_nodelist(@nodelist[0...else_index], context)
109
+ else
110
+ super(context)
111
+ end
112
+ else
113
+ if else_index
114
+ render_nodelist(@nodelist[(else_index + 1)..-1], context)
115
+ else
116
+ ''
117
+ end
118
+ end
119
+ end
120
+
121
+ def unknown_tag(tag_name, markup, tokens)
122
+ if tag_name == 'else'
123
+ @nodelist << Liquid::ElseTag.new(tag_name, markup, tokens)
124
+ else
125
+ super
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ def check_truthy(value)
132
+ return false if value.nil?
133
+ return false if value.respond_to?(:empty?) && value.empty?
134
+ return false if value.to_s.downcase == 'null'
135
+ return false if value == false
136
+ true
137
+ end
138
+
139
+ def render_nodelist(nodelist, context)
140
+ output = []
141
+ nodelist.each do |token|
142
+ case token
143
+ when String
144
+ output << token
145
+ else
146
+ if token.respond_to?(:render)
147
+ output << token.render(context)
148
+ else
149
+ output << token.to_s
150
+ end
151
+ end
152
+ end
153
+ output.join
154
+ end
155
+ end
156
+ end
157
+ end
158
+
159
+ # Register the tags
160
+ Liquid::Template.register_tag('ifistruthy', Jekyll::UJPowertools::IfIsTruthyTag)
161
+ Liquid::Template.register_tag('unlessistruthy', Jekyll::UJPowertools::UnlessIsTruthyTag)
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.3.1
4
+ version: 1.4.0
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-06-17 00:00:00.000000000 Z
11
+ date: 2025-07-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -99,13 +99,11 @@ files:
99
99
  - README.md
100
100
  - Rakefile
101
101
  - jekyll-uj-powertools.gemspec
102
- - lib/_old/generators/translate-pages.rb
103
- - lib/_old/hooks/translate-pages.rb
102
+ - lib/filters/main.rb
104
103
  - lib/generators/inject-properties.rb
105
104
  - lib/hooks/inject-properties.rb
106
105
  - lib/jekyll-uj-powertools.rb
107
- - lib/jekyll-uj-powertools/version.rb
108
- - spec/jekyll-uj-powertools_spec.rb
106
+ - lib/tags/ifistruthy.rb
109
107
  homepage: https://github.com/itw-creative-works/jekyll-uj-powertools
110
108
  licenses:
111
109
  - MIT
@@ -129,5 +127,4 @@ rubygems_version: 3.2.3
129
127
  signing_key:
130
128
  specification_version: 4
131
129
  summary: A powerful set of utilities for Jekyll
132
- test_files:
133
- - spec/jekyll-uj-powertools_spec.rb
130
+ test_files: []
@@ -1,223 +0,0 @@
1
- require 'json'
2
- require 'net/http'
3
- require 'fileutils'
4
- require 'nokogiri'
5
- require 'digest'
6
-
7
- module Jekyll
8
- class TranslatePages < Generator
9
- safe true
10
- priority :low
11
-
12
- # Variables
13
- # Translation path
14
- CACHE_DIR = '.temp/translations'
15
- # Re-translate pages older than this many days
16
- RECHECK_DAYS = 30
17
-
18
- def generate(site)
19
- # Get target languages from site config
20
- target_langs = site.config.dig('translation', 'languages') || []
21
-
22
- # Log
23
- puts "🔁 Starting translation process for supported languages (#{target_langs.length}): #{target_langs.join(', ')}"
24
- # puts "📂 Cache directory: #{CACHE_DIR}"
25
- # puts "🔍 All environment variables:"
26
- # ENV.each { |k, v| puts " #{k}=#{v}" }
27
- puts "🔍 UJ_ environment variables:"
28
- ENV.select { |k, _| k.start_with?('UJ_') }.each { |k, v| puts " #{k}=#{v}" }
29
-
30
- # Skip if site config translation is disabled
31
- unless site.config.dig('translation', 'enabled')
32
- puts "🚫 Translation is disabled in _config.yml (translation.enabled: false)"
33
- return
34
- end
35
-
36
- # Quit if UJ_BUILD_MODE is false
37
- if ENV['UJ_BUILD_MODE'] == 'false' && ENV['UJ_TRANSLATION_FORCE'] != 'true'
38
- puts "🚫 UJ_BUILD_MODE is set to 'false' (set UJ_TRANSLATION_FORCE=true). Exiting translation process."
39
- return
40
- end
41
-
42
- # Ensure OpenAI API key is set
43
- unless ENV['OPENAI_API_KEY'] && !ENV['OPENAI_API_KEY'].strip.empty?
44
- puts "❌ OPENAI_API_KEY not found in environment. Exiting translation process."
45
- return
46
- end
47
-
48
- # Quit if no languages are configured
49
- if target_langs.empty?
50
- puts "🚫 No target languages configured in _config.yml (translation.languages). Exiting translation process."
51
- return
52
- end
53
-
54
- # Keep track of skipped files
55
- skipped_files = []
56
-
57
- # Loop through all pages in the site
58
- site.pages.clone.each do |page|
59
- next unless page.output_ext == '.html'
60
- original_content = page.content
61
- original_hash = Digest::SHA256.hexdigest(original_content)
62
- page_path = page.path.sub(/^_?site\//, '')
63
-
64
- # Skip if page.translation.enabled is false
65
- if page.data['translation'] && page.data['translation']['enabled'] == false
66
- skipped_files << "#{page_path} (translation disabled)"
67
- next
68
- end
69
-
70
- # Skip if page.redirect.url is set
71
- if page.data['redirect'] && page.data['redirect']['url']
72
- skipped_files << "#{page_path} (redirect set)"
73
- next
74
- end
75
-
76
- target_langs.each do |lang|
77
- translated_path = File.join(CACHE_DIR, lang, page_path)
78
- meta_path = "#{translated_path}.meta.json"
79
-
80
- # @TODO: Remove this
81
- # Unless its pages/legal/terms.md, QUIT
82
- uj_translation_only = ENV['UJ_TRANSLATION_ONLY']
83
- if uj_translation_only && page_path != uj_translation_only
84
- skipped_files << "#{page_path} (UJ_TRANSLATION_ONLY is set)"
85
- next
86
- end
87
-
88
- # Log
89
- puts "🌐 Processing page '#{page_path}' for language '#{lang}'"
90
-
91
- # Either read cached translation or generate a new one
92
- translated = read_or_translate(original_content, original_hash, lang, translated_path, meta_path)
93
-
94
- next unless translated # skip this lang if translation failed
95
-
96
- # Build new page with translated content
97
- new_page = page.dup
98
- new_page.data = page.data.dup
99
- new_page.data['lang'] = lang
100
- new_page.data['permalink'] = "/#{lang}#{page.url}"
101
- new_page.content = rewrite_links(translated, lang)
102
-
103
- site.pages << new_page
104
- puts "✅ Added translated page: /#{lang}#{page.url}"
105
- end
106
- end
107
-
108
- # Log skipped files at the end
109
- if skipped_files.any?
110
- puts "\n🚫 Skipped files:"
111
- skipped_files.each { |f| puts " - #{f}" }
112
- end
113
-
114
- puts "🎉 Translation process complete."
115
- end
116
-
117
- private
118
-
119
- # Return cached translation or generate new one via API
120
- def read_or_translate(content, hash, lang, path, meta_path)
121
- if File.exist?(path) && File.exist?(meta_path)
122
- meta = JSON.parse(File.read(meta_path)) rescue {}
123
- last_hash = meta['hash']
124
- last_time = Time.at(meta['timestamp'].to_i) rescue Time.at(0)
125
-
126
- age_days = ((Time.now - last_time) / (60 * 60 * 24)).round
127
- puts "📅 Cache age: #{age_days}/#{RECHECK_DAYS} days"
128
-
129
- # Determine whether we should re-check based on age
130
- recheck_due_to_age = RECHECK_DAYS && RECHECK_DAYS > 0 && age_days >= RECHECK_DAYS
131
-
132
- # Re-translate if hash changed or translation is too old (only if RECHECK_DAYS is truthy)
133
- if last_hash == hash && !recheck_due_to_age
134
- puts "📦 Using cached translation at: #{path}"
135
- return File.read(path)
136
- else
137
- puts "🔁 Cache stale or hash changed, regenerating translation..."
138
- end
139
- else
140
- puts "📭 No cache found, generating translation..."
141
- end
142
-
143
- # Log before/after content
144
- puts "\n--- BEFORE CONTENT (#{lang}) ---\n#{content[0..500]}..."
145
-
146
- # Translate the content using OpenAI API
147
- begin
148
- result = translate_with_api(content, lang)
149
- rescue => e
150
- puts "❌ Skipping translation for '#{lang}' due to error: #{e.message}"
151
- return nil
152
- end
153
-
154
- # Log the first 500 characters of the result
155
- puts "\n--- AFTER TRANSLATION (#{lang}) ---\n#{result[0..500]}..."
156
-
157
- # Save the translation and metadata
158
- FileUtils.mkdir_p(File.dirname(path))
159
- File.write(path, result)
160
- File.write(meta_path, {
161
- timestamp: Time.now.to_i,
162
- hash: hash
163
- }.to_json)
164
-
165
- puts "📝 Cached translation and metadata written to: #{path}"
166
-
167
- result
168
- end
169
-
170
- # Perform translation via OpenAI API
171
- def translate_with_api(content, lang)
172
- system_prompt = "You are a professional translator. Translate the provided HTML content, preserving all original formatting, HTML structure, metadata, and links. Do not explain anything — just return the translated HTML. Translate to #{lang}."
173
- user_message = content
174
-
175
- uri = URI('https://api.openai.com/v1/chat/completions')
176
-
177
- res = Net::HTTP.post(
178
- uri,
179
- {
180
- model: 'gpt-4',
181
- messages: [
182
- { role: 'system', content: system_prompt },
183
- { role: 'user', content: user_message }
184
- ],
185
- temperature: 0.3
186
- }.to_json,
187
- {
188
- 'Authorization' => "Bearer #{ENV['OPENAI_API_KEY']}",
189
- 'Content-Type' => 'application/json'
190
- }
191
- )
192
-
193
- if res.code.to_i != 200
194
- raise "HTTP #{res.code}: #{res.body}"
195
- end
196
-
197
- json = JSON.parse(res.body)
198
- # Log json
199
- puts "🔍 API response: #{json.inspect}"
200
- result = json.dig('choices', 0, 'message', 'content')
201
-
202
- if result.nil? || result.strip.empty?
203
- raise "Translation returned empty or invalid content"
204
- end
205
-
206
- puts "🔤 Translation complete."
207
- result
208
- end
209
-
210
- # Rewrite internal links to language-prefixed versions
211
- def rewrite_links(html, lang)
212
- doc = Nokogiri::HTML::DocumentFragment.parse(html)
213
- doc.css('a[href^="/"]').each do |a|
214
- href = a['href']
215
- next if href.start_with?("/#{lang}") || href.include?('.') || href.start_with?('//')
216
- new_href = "/#{lang}#{href}"
217
- puts "🔗 Rewriting link: #{href} -> #{new_href}"
218
- a['href'] = new_href
219
- end
220
- doc.to_html
221
- end
222
- end
223
- end
@@ -1,253 +0,0 @@
1
- # Libraries
2
- require 'json'
3
- require 'net/http'
4
- require 'fileutils'
5
- require 'nokogiri'
6
- require 'digest'
7
-
8
- # Hook
9
- Jekyll::Hooks.register :site, :post_write do |site|
10
- # Variables
11
- # Translation path
12
- CACHE_DIR = '.temp/translations'
13
- # Re-translate pages older than this many days
14
- RECHECK_DAYS = 30
15
-
16
- # Get target languages from site config
17
- target_langs = site.config.dig('translation', 'languages') || []
18
-
19
- # Log
20
- puts "🔁 Starting translation process for supported languages (#{target_langs.length}): #{target_langs.join(', ')}"
21
- puts "🔍 UJ_ environment variables:"
22
- ENV.select { |k, _| k.start_with?('UJ_') }.each { |k, v| puts " #{k}=#{v}" }
23
-
24
- # Skip if site config translation is disabled
25
- unless site.config.dig('translation', 'enabled')
26
- puts "🚫 Translation is disabled in _config.yml (translation.enabled: false)"
27
- next
28
- end
29
-
30
- # Quit if UJ_BUILD_MODE is false
31
- if ENV['UJ_BUILD_MODE'] == 'false' && ENV['UJ_TRANSLATION_FORCE'] != 'true'
32
- puts "🚫 UJ_BUILD_MODE is set to 'false' (set UJ_TRANSLATION_FORCE=true). Exiting translation process."
33
- next
34
- end
35
-
36
- # Ensure OpenAI API key is set
37
- unless ENV['OPENAI_API_KEY'] && !ENV['OPENAI_API_KEY'].strip.empty?
38
- puts "❌ OPENAI_API_KEY not found in environment. Exiting translation process."
39
- next
40
- end
41
-
42
- # Quit if no languages are configured
43
- if target_langs.empty?
44
- puts "🚫 No target languages configured in _config.yml (translation.languages). Exiting translation process."
45
- next
46
- end
47
-
48
- # Keep track of skipped files
49
- skipped_files = []
50
-
51
- # Loop through all pages in the site
52
- site.pages.clone.each do |page|
53
- # Quit if its not an HTML page
54
- next unless page.output_ext == '.html'
55
-
56
- # Get original content
57
- original_content = page.output
58
-
59
- # Extract body content
60
- doc = Nokogiri::HTML(original_content)
61
- original_content_body = doc.at('body')&.inner_html.to_s
62
-
63
- # Compute original hash
64
- original_hash = Digest::SHA256.hexdigest(original_content_body)
65
-
66
- # Get page path and URL
67
- page_path = page.path.sub(/^_?site\//, '')
68
- page_url = page.url
69
-
70
- # Skip if page.translation.enabled is false
71
- if page.data['translation'] && page.data['translation']['enabled'] == false
72
- skipped_files << "#{page_path} (translation disabled)"
73
- next
74
- end
75
-
76
- # Skip if page.redirect.url is set
77
- if page.data['redirect'] && page.data['redirect']['url']
78
- skipped_files << "#{page_path} (redirect set)"
79
- next
80
- end
81
-
82
- # Loop through target languages
83
- target_langs.each do |lang|
84
- page_new_url = "/#{lang}#{page.url}"
85
- page_new_path = File.join(CACHE_DIR, lang, page_new_url)
86
- page_new_meta_path = "#{page_new_path}.meta.json"
87
-
88
- # See if we only want to test a specific page
89
- uj_translation_only = ENV['UJ_TRANSLATION_ONLY']
90
- if uj_translation_only && page_path != uj_translation_only
91
- skipped_files << "#{page_path} (UJ_TRANSLATION_ONLY is set)"
92
- next
93
- end
94
-
95
- # Log
96
- puts "🌐 Processing page '#{page_url}' for language '#{lang}'"
97
-
98
- # LOG new_page.data
99
- # Log permalink
100
- puts "🔗 New permalink: #{page_new_url}"
101
-
102
- # Either read cached translation or generate a new one
103
- translated = read_or_translate(original_content_body, original_hash, lang, page_new_path, page_new_meta_path)
104
-
105
- # Fallback if translation failed
106
- if translated.nil?
107
- puts "⚠️ Translation failed for #{page_url}, using original content and marking for retry"
108
-
109
- # Force a retry next time by setting bad hash + old timestamp
110
- FileUtils.mkdir_p(File.dirname(page_new_meta_path))
111
- File.write(page_new_meta_path, {
112
- timestamp: 0,
113
- hash: '__fail__'
114
- }.to_json)
115
-
116
- translated = original_content_body
117
- end
118
-
119
- # Rewrite internal links
120
- translated_html = rewrite_links(translated, lang)
121
-
122
- # Inject translated content into original HTML structure
123
- translated_doc = Nokogiri::HTML(original_content)
124
- translated_doc.at('body').inner_html = translated_html
125
- final_html = translated_doc.to_html
126
-
127
- # Determine output path
128
- output_dir = site.config['destination']
129
- translated_output_path = File.join(output_dir, lang, page.url)
130
- translated_output_path = File.join(translated_output_path, 'index.html') if translated_output_path.end_with?('/')
131
-
132
- # Write translated page to disk
133
- FileUtils.mkdir_p(File.dirname(translated_output_path))
134
- File.write(translated_output_path, final_html)
135
- puts "✅ Wrote translated file: #{translated_output_path}"
136
- end
137
- end
138
-
139
- # Log skipped files at the end
140
- if skipped_files.any?
141
- puts "\n🚫 Skipped files:"
142
- skipped_files.each { |f| puts " - #{f}" }
143
- end
144
-
145
- # Log
146
- puts "🎉 Translation process complete."
147
- end
148
-
149
- def read_or_translate(content, hash, lang, path, page_new_meta_path)
150
- if File.exist?(path) && File.exist?(page_new_meta_path)
151
- meta = JSON.parse(File.read(page_new_meta_path)) rescue {}
152
- last_hash = meta['hash']
153
- last_time = Time.at(meta['timestamp'].to_i) rescue Time.at(0)
154
-
155
- age_days = ((Time.now - last_time) / (60 * 60 * 24)).round
156
- puts "📅 Cache age: #{age_days}/#{RECHECK_DAYS} days"
157
-
158
- # Determine whether we should re-check based on age
159
- recheck_due_to_age = RECHECK_DAYS && RECHECK_DAYS > 0 && age_days >= RECHECK_DAYS
160
-
161
- # Re-translate if hash changed or translation is too old (only if RECHECK_DAYS is truthy)
162
- if last_hash == hash && !recheck_due_to_age
163
- puts "📦 Using cached translation at: #{path}"
164
- return File.read(path)
165
- else
166
- puts "🔁 Cache stale or hash changed, regenerating translation..."
167
- end
168
- else
169
- puts "📭 No cache found, generating translation..."
170
- end
171
-
172
- # Log before/after content
173
- puts "\n--- BEFORE CONTENT (#{lang}) ---\n#{content[0..500]}..."
174
-
175
- # Translate the content using OpenAI API
176
- begin
177
- result = translate_with_api(content, lang)
178
- rescue => e
179
- puts "❌ Skipping translation for '#{lang}' due to error: #{e.message}"
180
- return nil
181
- end
182
-
183
- # Log the first 500 characters of the result
184
- puts "\n--- AFTER TRANSLATION (#{lang}) ---\n#{result[0..500]}..."
185
-
186
- # Save the translation and metadata
187
- FileUtils.mkdir_p(File.dirname(path))
188
- File.write(path, result)
189
- File.write(page_new_meta_path, {
190
- timestamp: Time.now.to_i,
191
- hash: hash
192
- }.to_json)
193
-
194
- puts "📝 Cached translation and metadata written to: #{path}"
195
-
196
- result
197
- end
198
-
199
- def translate_with_api(content, lang)
200
- system_prompt = "You are a professional translator. Translate the provided HTML content, preserving all original formatting, HTML structure, metadata, and links. Do not explain anything — just return the translated HTML. Translate to #{lang}."
201
- user_message = content
202
-
203
- uri = URI('https://api.openai.com/v1/chat/completions')
204
-
205
- http = Net::HTTP.new(uri.host, uri.port)
206
- http.use_ssl = true
207
- http.read_timeout = 30 # seconds
208
- http.open_timeout = 10 # seconds
209
-
210
- request = Net::HTTP::Post.new(uri.path, {
211
- 'Authorization' => "Bearer #{ENV['OPENAI_API_KEY']}",
212
- 'Content-Type' => 'application/json'
213
- })
214
-
215
- request.body = {
216
- model: 'gpt-4o',
217
- messages: [
218
- { role: 'system', content: system_prompt },
219
- { role: 'user', content: user_message }
220
- ],
221
- temperature: 0.3,
222
- max_tokens: 4096
223
- }.to_json
224
-
225
- response = http.request(request)
226
-
227
- if response.code.to_i != 200
228
- raise "HTTP #{response.code}: #{response.body}"
229
- end
230
-
231
- json = JSON.parse(response.body)
232
- puts "🔍 API response: #{json.inspect}"
233
- result = json.dig('choices', 0, 'message', 'content')
234
-
235
- if result.nil? || result.strip.empty?
236
- raise "Translation returned empty or invalid content"
237
- end
238
-
239
- puts "🔤 Translation complete."
240
- result
241
- end
242
-
243
- def rewrite_links(html, lang)
244
- doc = Nokogiri::HTML::DocumentFragment.parse(html)
245
- doc.css('a[href^="/"]').each do |a|
246
- href = a['href']
247
- next if href.start_with?("/#{lang}") || href.include?('.') || href.start_with?('//')
248
- new_href = "/#{lang}#{href}"
249
- puts "🔗 Rewriting link: #{href} -> #{new_href}"
250
- a['href'] = new_href
251
- end
252
- doc.to_html
253
- end
@@ -1,5 +0,0 @@
1
- module Jekyll
2
- module UJPowertools
3
- VERSION = "1.3.1"
4
- end
5
- end
@@ -1,75 +0,0 @@
1
- require 'jekyll-uj-powertools'
2
-
3
- RSpec.describe Jekyll::UJPowertools do
4
- # Dummy class to include the filter methods
5
- class DummyClass
6
- include Jekyll::UJPowertools
7
- end
8
-
9
- let(:dummy) { DummyClass.new }
10
-
11
- # Test Strip Ads method
12
- describe '.uj_strip_ads' do
13
- it 'removes ads from the string with custom HTML elements' do
14
- expect(dummy.uj_strip_ads('This is <ad-unit>and ad</ad-unit>')).to eq('This is')
15
- end
16
-
17
- it 'returns the original string if no ads are present' do
18
- expect(dummy.uj_strip_ads('No ads here')).to eq('No ads here')
19
- end
20
-
21
- it 'removes multiple ads from the string' do
22
- expect(dummy.uj_strip_ads("First part\n<ad-unit>ad content</ad-unit>\nSecond part\n<ad-unit>more ad content</ad-unit>\nThird part")).to eq('First partSecond partThird part')
23
- end
24
-
25
- it 'removes surrounding whitespace' do
26
- expect(dummy.uj_strip_ads("Start\n<ad-unit>\n ad content\n</ad-unit>\nEnd")).to eq("StartEnd")
27
- end
28
- end
29
-
30
- # Test JSON Escape method
31
- describe '.uj_json_escape' do
32
- it 'escapes double quotes in JSON string' do
33
- expect(dummy.uj_json_escape('this is a "quote"')).to eq('this is a \"quote\"')
34
- end
35
-
36
- it 'escapes backslashes in JSON string' do
37
- expect(dummy.uj_json_escape('this is a nothing')).to eq('this is a nothing')
38
- end
39
- end
40
-
41
- # Test Increment Return method
42
- describe '.uj_increment_return' do
43
- it 'increments a global counter' do
44
- expect(dummy.uj_increment_return(1)).to eq(1)
45
- expect(dummy.uj_increment_return(1)).to eq(2)
46
- expect(dummy.uj_increment_return(1)).to eq(3)
47
- end
48
- end
49
-
50
- # Test Random method
51
- describe '.uj_random' do
52
- it 'returns a random number between 0 and the input' do
53
- srand(1)
54
- expect(dummy.uj_random(10)).to eq(5)
55
- srand(2)
56
- expect(dummy.uj_random(10)).to eq(8)
57
- srand(3)
58
- expect(dummy.uj_random(10)).to eq(8)
59
- end
60
- end
61
-
62
- # Test Cache Buster method
63
- describe '.uj_cache' do
64
- it 'returns the current timestamp as a string' do
65
- expect(dummy.uj_cache('unused')).to eq(Time.now.to_i.to_s)
66
- end
67
- end
68
-
69
- # Test Title Case method
70
- describe '.uj_title_case' do
71
- it 'capitalizes the first letter of each word' do
72
- expect(dummy.uj_title_case('this is a title')).to eq('This Is A Title')
73
- end
74
- end
75
- end