jekyll-chatgpt-translate 0.0.12 → 0.0.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/features/cli.feature +7 -6
- data/features/step_definitions/steps.rb +2 -1
- data/jekyll-chatgpt-translate.gemspec +1 -1
- data/lib/jekyll-chatgpt-translate/chatgpt.rb +7 -12
- data/lib/jekyll-chatgpt-translate/generator.rb +13 -9
- data/lib/jekyll-chatgpt-translate/ping.rb +18 -13
- data/lib/jekyll-chatgpt-translate/plain.rb +16 -13
- data/lib/jekyll-chatgpt-translate/version.rb +1 -1
- data/test/test_generator.rb +119 -1
- data/test/test_ping.rb +9 -0
- data/test/test_plain.rb +29 -0
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b8a34c60ab2378b8dc26ab4b189ff3fa168cbb864c80db92a854d68b58def59
|
4
|
+
data.tar.gz: 3654fe32f201e8a155cd5f154129c24350333b6f2459cd86bb511c0acc22c0c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea02f9faba8ffedaa3e25bd428bde6aaba7a7a484bda745cc270a9f97f1a40211b776cff7d5a8c5d297854f107ba311b10dd5885bb6eff8de353f4a8aa9778d6
|
7
|
+
data.tar.gz: 6f3cc93df6fff8290f3a6d45bc980a9c5b273154a98ab7c18ddff1571c55dd8c2796e9ef965c3a0df72ff175c42ed54b72f4b4ecb5d2d8bb21864ae3b4aa0c7c
|
data/.gitignore
CHANGED
data/features/cli.feature
CHANGED
@@ -40,11 +40,12 @@ Feature: Simple site building
|
|
40
40
|
Hello, world!
|
41
41
|
"""
|
42
42
|
Then I build Jekyll site
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
43
|
+
And File "_chatgpt-translated/cn/2023-01-01-hello-cn.md" exists
|
44
|
+
And File "_chatgpt-translated/cn/2023-01-01-hello-cn.md" contains "/2023-01-01-hello-chinese.html"
|
45
|
+
And File "_site/2023/01/01/hello.html" exists
|
46
|
+
And File "_site/2023/01/01/hello.html" contains "The Chinese: /2023-01-01-hello-chinese.html"
|
47
|
+
And File "_site/2023-01-01-hello-chinese.html" exists
|
48
|
+
And File "_site/2023-01-01-hello-chinese.html" contains "The original: /2023/01/01/hello.html"
|
49
|
+
And File "_site/2023/hello-french.html" exists
|
49
50
|
And Exit code is zero
|
50
51
|
|
@@ -50,10 +50,11 @@ Then(/^Stdout contains "([^"]*)"$/) do |txt|
|
|
50
50
|
end
|
51
51
|
|
52
52
|
Then(/^File "([^"]*)" exists$/) do |name|
|
53
|
-
raise "The file \"#{name}\" is absent:\n#{`tree`}" unless File.exist?(name)
|
53
|
+
raise "The file \"#{name}\" is absent:\n#{`tree -s`}" unless File.exist?(name)
|
54
54
|
end
|
55
55
|
|
56
56
|
Then(/^File "([^"]*)" contains "([^"]*)"$/) do |name, text|
|
57
|
+
raise "The file \"#{name}\" is absent" unless File.exist?(name)
|
57
58
|
content = File.read(name)
|
58
59
|
raise "The file \"#{name}\" doesn't contain \"#{text}\":\n#{content}" unless content.include?(text)
|
59
60
|
end
|
@@ -28,7 +28,7 @@ Gem::Specification.new do |s|
|
|
28
28
|
s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
|
29
29
|
s.required_ruby_version = '>= 2.6'
|
30
30
|
s.name = 'jekyll-chatgpt-translate'
|
31
|
-
s.version = '0.0.
|
31
|
+
s.version = '0.0.14'
|
32
32
|
s.license = 'MIT'
|
33
33
|
s.summary = 'Translate Jekyll Pages Through ChatGPT'
|
34
34
|
s.description = [
|
@@ -92,17 +92,12 @@ through #{@model} in #{(Time.now - start).round(2)}s")
|
|
92
92
|
end
|
93
93
|
|
94
94
|
def prompt
|
95
|
-
|
96
|
-
'
|
97
|
-
|
98
|
-
'
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
ISO_639.find_by_code(@source),
|
103
|
-
'to',
|
104
|
-
ISO_639.find_by_code(@target)
|
105
|
-
].join(' ')
|
106
|
-
end
|
95
|
+
[
|
96
|
+
'Please, translate this paragraph from',
|
97
|
+
ISO_639.find_by_code(@source),
|
98
|
+
'to',
|
99
|
+
ISO_639.find_by_code(@target),
|
100
|
+
', don\'t change proper nouns'
|
101
|
+
].join(' ')
|
107
102
|
end
|
108
103
|
end
|
@@ -23,6 +23,7 @@
|
|
23
23
|
# SOFTWARE.
|
24
24
|
|
25
25
|
require 'jekyll'
|
26
|
+
require 'json'
|
26
27
|
require_relative 'chatgpt'
|
27
28
|
require_relative 'permalink'
|
28
29
|
require_relative 'ping'
|
@@ -60,13 +61,19 @@ class GptTranslate::Generator < Jekyll::Generator
|
|
60
61
|
plain = GptTranslate::Plain.new(doc.content).to_s
|
61
62
|
config['targets'].each do |target|
|
62
63
|
link = GptTranslate::Permalink.new(doc, target['permalink']).to_path
|
63
|
-
next if GptTranslate::Ping.new(site, link).found?(doc.path, version)
|
64
64
|
lang = target['language']
|
65
65
|
raise 'Language must be defined for each target' if target.nil?
|
66
66
|
if total >= threshold
|
67
67
|
Jekyll.logger.info("Already generated #{total} pages, that's enough for today")
|
68
68
|
break
|
69
69
|
end
|
70
|
+
path = File.join(home, lang, doc.basename.gsub(/\.md$/, "-#{lang}.md"))
|
71
|
+
FileUtils.mkdir_p(File.dirname(path))
|
72
|
+
File.write(path, '') # in order to surpress warnings in Page ctor
|
73
|
+
dest = Jekyll::Page.new(site, site.source, File.dirname(path), File.basename(path)).destination(site.dest)
|
74
|
+
doc.data["translated-#{lang}-url"] = link
|
75
|
+
doc.data['chatgpt-model'] = model
|
76
|
+
next if GptTranslate::Ping.new(site, link).found?(dest, version)
|
70
77
|
gpt = GptTranslate::ChatGPT.new(
|
71
78
|
key,
|
72
79
|
model,
|
@@ -74,17 +81,16 @@ class GptTranslate::Generator < Jekyll::Generator
|
|
74
81
|
lang
|
75
82
|
)
|
76
83
|
translated = gpt.translate(plain)
|
77
|
-
path = File.join(home, lang, doc.basename.gsub(/\.md$/, "-#{lang}.md"))
|
78
|
-
FileUtils.mkdir_p(File.dirname(path))
|
79
84
|
File.write(
|
80
85
|
path,
|
81
86
|
[
|
82
87
|
'---',
|
83
88
|
"layout: #{target['layout'] || layout}",
|
84
|
-
"title: #{doc
|
85
|
-
"
|
86
|
-
"
|
87
|
-
"
|
89
|
+
"title: #{doc['title'].to_json}",
|
90
|
+
"description: #{doc['description'].to_json}",
|
91
|
+
"permalink: #{link.to_json}",
|
92
|
+
"translated-original-url: #{doc.url.to_json}",
|
93
|
+
"chatgpt-model: #{model.to_json}",
|
88
94
|
'---',
|
89
95
|
'',
|
90
96
|
translated,
|
@@ -92,8 +98,6 @@ class GptTranslate::Generator < Jekyll::Generator
|
|
92
98
|
"#{marker}\n{: .jekyll-chatgpt-translate}"
|
93
99
|
].join("\n")
|
94
100
|
)
|
95
|
-
doc.data["translated-#{lang}-url"] = link
|
96
|
-
doc.data['chatgpt-model'] = model
|
97
101
|
site.pages << Jekyll::Page.new(site, site.source, File.dirname(path), File.basename(path))
|
98
102
|
total += 1
|
99
103
|
Jekyll.logger.info("Translated via ChatGPT: #{path}")
|
@@ -51,21 +51,26 @@ class GptTranslate::Ping
|
|
51
51
|
home = @site.config['url']
|
52
52
|
return false if home.nil?
|
53
53
|
uri = Iri.new(home).path(@path)
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
54
|
+
begin
|
55
|
+
before = Net::HTTP.get_response(URI(uri.to_s))
|
56
|
+
if before.is_a?(Net::HTTPSuccess)
|
57
|
+
html = before.body
|
58
|
+
if html.include?(marker)
|
59
|
+
Jekyll.logger.info("No need to translate, the page exists at \
|
60
|
+
\"#{uri}\" (#{html.split.count} words), saved to \"#{file}\"")
|
61
|
+
FileUtils.mkdir_p(File.dirname(file))
|
62
|
+
File.write(file, html)
|
63
|
+
return true
|
64
|
+
end
|
65
|
+
Jekyll.logger.info("Re-translation required for \"#{uri}\"")
|
66
|
+
else
|
67
|
+
Jekyll.logger.info("The page is absent, will translate \"#{uri}\"")
|
63
68
|
end
|
64
|
-
Jekyll.logger.
|
65
|
-
|
66
|
-
Jekyll.logger.
|
69
|
+
Jekyll.logger.debug("GET \"#{uri}\": #{before.code}")
|
70
|
+
rescue StandardError => e
|
71
|
+
Jekyll.logger.debug("Failed to ping \"#{uri}\": #{e.message}")
|
72
|
+
Jekyll.logger.info("The page is absent: \"#{uri}\"")
|
67
73
|
end
|
68
|
-
Jekyll.logger.debug("GET #{uri}: #{before.code}")
|
69
74
|
false
|
70
75
|
end
|
71
76
|
end
|
@@ -38,28 +38,31 @@ class GptTranslate::Plain
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def to_s
|
41
|
-
|
42
|
-
|
43
|
-
par.gsub!(
|
41
|
+
# To turn compact lists into proper lists
|
42
|
+
@markdown.gsub(/([^\n])\n(\s*\*)/, "\\1\n\n\\2").split(/\n{2,}/).compact.map do |par|
|
43
|
+
# par.gsub!("\n", ' ')
|
44
|
+
par.gsub!("\t", ' ')
|
45
|
+
par.gsub!(/ {2,}/, ' ')
|
44
46
|
# Liquid tags are removed, but this implementation is primitive
|
45
47
|
# Seehttps://stackoverflow.com/questions/
|
46
48
|
par.gsub!(/{{[^}]+}}/, '')
|
47
|
-
par.gsub!(/{
|
49
|
+
par.gsub!(/{%.+?%}/, '')
|
50
|
+
par.gsub!(/<!--.+?-->/m, '')
|
48
51
|
par.strip!
|
49
52
|
Redcarpet::Markdown.new(Strip).render(par)
|
50
53
|
end.join("\n\n").gsub(/\n{2,}/, "\n\n").strip
|
51
54
|
end
|
52
55
|
|
53
|
-
# To ignore/remove Liquid tags.
|
54
|
-
class NullDrop < Liquid::Drop
|
55
|
-
|
56
|
-
|
57
|
-
|
56
|
+
# # To ignore/remove Liquid tags.
|
57
|
+
# class NullDrop < Liquid::Drop
|
58
|
+
# def method_missing(*)
|
59
|
+
# nil
|
60
|
+
# end
|
58
61
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
end
|
62
|
+
# def respond_to_missing?(*)
|
63
|
+
# true
|
64
|
+
# end
|
65
|
+
# end
|
63
66
|
|
64
67
|
# Markdown to pain text.
|
65
68
|
class Strip < Redcarpet::Render::Base
|
data/test/test_generator.rb
CHANGED
@@ -23,6 +23,8 @@
|
|
23
23
|
# SOFTWARE.
|
24
24
|
|
25
25
|
require 'minitest/autorun'
|
26
|
+
require 'tmpdir'
|
27
|
+
require 'webmock/minitest'
|
26
28
|
require_relative '../lib/jekyll-chatgpt-translate/generator'
|
27
29
|
|
28
30
|
# Generator test.
|
@@ -30,7 +32,123 @@ require_relative '../lib/jekyll-chatgpt-translate/generator'
|
|
30
32
|
# Copyright:: Copyright (c) 2023 Yegor Bugayenko
|
31
33
|
# License:: MIT
|
32
34
|
class GptTranslate::GeneratorTest < Minitest::Test
|
35
|
+
class FakeSite
|
36
|
+
attr_reader :config
|
37
|
+
|
38
|
+
def initialize(config, doc)
|
39
|
+
@config = config
|
40
|
+
@doc = doc
|
41
|
+
end
|
42
|
+
|
43
|
+
def posts
|
44
|
+
FakePosts.new(@doc)
|
45
|
+
end
|
46
|
+
|
47
|
+
def pages
|
48
|
+
[]
|
49
|
+
end
|
50
|
+
|
51
|
+
def permalink_style
|
52
|
+
''
|
53
|
+
end
|
54
|
+
|
55
|
+
def frontmatter_defaults
|
56
|
+
Jekyll::FrontmatterDefaults.new(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
def converters
|
60
|
+
[Jekyll::Converters::Markdown.new({ 'markdown_ext' => 'md' })]
|
61
|
+
end
|
62
|
+
|
63
|
+
def source
|
64
|
+
''
|
65
|
+
end
|
66
|
+
|
67
|
+
def dest
|
68
|
+
''
|
69
|
+
end
|
70
|
+
|
71
|
+
def in_theme_dir(base, _foo = nil, _bar = nil)
|
72
|
+
base
|
73
|
+
end
|
74
|
+
|
75
|
+
def in_dest_dir(*paths)
|
76
|
+
paths[0].dup
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class FakeDocument
|
81
|
+
def initialize(path)
|
82
|
+
@path = path
|
83
|
+
end
|
84
|
+
|
85
|
+
def content
|
86
|
+
'Hello, world!'
|
87
|
+
end
|
88
|
+
|
89
|
+
def data
|
90
|
+
{}
|
91
|
+
end
|
92
|
+
|
93
|
+
def []=(key, value); end
|
94
|
+
|
95
|
+
def [](key)
|
96
|
+
if key == 'date'
|
97
|
+
Time.now
|
98
|
+
elsif key == 'title'
|
99
|
+
'Hello!'
|
100
|
+
else
|
101
|
+
''
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def relative_path
|
106
|
+
@path
|
107
|
+
end
|
108
|
+
|
109
|
+
def url
|
110
|
+
'2023-01-01-hello.html'
|
111
|
+
end
|
112
|
+
|
113
|
+
def basename
|
114
|
+
'2023-01-01-hello.md'
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class FakePosts
|
119
|
+
attr_reader :config
|
120
|
+
|
121
|
+
def initialize(doc)
|
122
|
+
@doc = doc
|
123
|
+
end
|
124
|
+
|
125
|
+
def docs
|
126
|
+
[FakeDocument.new(@doc)]
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
33
130
|
def test_simple_scenario
|
34
|
-
|
131
|
+
Dir.mktmpdir do |home|
|
132
|
+
post = File.join(home, '2023-01-01-hello.md')
|
133
|
+
File.write(post, "---\ntitle: Hello\n---\n\nHello, world!")
|
134
|
+
site = FakeSite.new(
|
135
|
+
{
|
136
|
+
'url' => 'https://www.yegor256.com/',
|
137
|
+
'chatgpt-translate' => {
|
138
|
+
'targets' => [
|
139
|
+
{
|
140
|
+
'language' => 'cn',
|
141
|
+
'layout' => 'chinese',
|
142
|
+
'permalink' => ':slug.html'
|
143
|
+
}
|
144
|
+
]
|
145
|
+
}
|
146
|
+
},
|
147
|
+
FakeDocument.new({})
|
148
|
+
)
|
149
|
+
gen = GptTranslate::Generator.new
|
150
|
+
stub_request(:get, 'https://www.yegor256.com/.html').to_return(body: '')
|
151
|
+
gen.generate(site)
|
152
|
+
end
|
35
153
|
end
|
36
154
|
end
|
data/test/test_ping.rb
CHANGED
@@ -59,6 +59,15 @@ class GptTranslate::PingTest < Minitest::Test
|
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
62
|
+
def test_wrong_address
|
63
|
+
WebMock.allow_net_connect!
|
64
|
+
site = FakeSite.new({ 'url' => 'https://localhost:1/' })
|
65
|
+
ping = GptTranslate::Ping.new(site, '/boom.html')
|
66
|
+
Tempfile.open do |f|
|
67
|
+
assert(!ping.found?(f, ''))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
62
71
|
def test_relative_path
|
63
72
|
assert_raises do
|
64
73
|
GptTranslate::Ping.new({}, '404.html')
|
data/test/test_plain.rb
CHANGED
@@ -47,6 +47,20 @@ class GptTranslate::PlainTest < Minitest::Test
|
|
47
47
|
)
|
48
48
|
end
|
49
49
|
|
50
|
+
def test_ordered_list
|
51
|
+
assert_equal(
|
52
|
+
"first\n\nsecond\n\nthird",
|
53
|
+
GptTranslate::Plain.new("1. first\n\n2. second\n\n3. third").to_s
|
54
|
+
)
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_compact_list
|
58
|
+
assert_equal(
|
59
|
+
"first\n\nsecond\n\nthird",
|
60
|
+
GptTranslate::Plain.new("* first\n* second\n* third").to_s
|
61
|
+
)
|
62
|
+
end
|
63
|
+
|
50
64
|
def test_links
|
51
65
|
assert_equal(
|
52
66
|
'Hello, dude!',
|
@@ -89,5 +103,20 @@ class GptTranslate::PlainTest < Minitest::Test
|
|
89
103
|
'Hello, !',
|
90
104
|
GptTranslate::Plain.new('Hello, {% Java %}!').to_s
|
91
105
|
)
|
106
|
+
assert_equal(
|
107
|
+
'Hello, !',
|
108
|
+
GptTranslate::Plain.new('Hello, {% plantuml "width=50%" %}!').to_s
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_html_comments
|
113
|
+
assert_equal(
|
114
|
+
'Hello, !',
|
115
|
+
GptTranslate::Plain.new('Hello, <!-- Java -->!').to_s
|
116
|
+
)
|
117
|
+
assert_equal(
|
118
|
+
'Hello, !',
|
119
|
+
GptTranslate::Plain.new("Hello, <!-- \nJava\n -->!").to_s
|
120
|
+
)
|
92
121
|
end
|
93
122
|
end
|