jekyll-chatgpt-translate 0.0.29 → 0.0.31

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: dc3e9a30a7d065c15ac18a15b5047f94881809857f65ed77330351f95a129d18
4
- data.tar.gz: f6254349103e49e105451d773ac459be452027a5bdafc5a7a004a1d926e7203e
3
+ metadata.gz: 8c50e5ce734d7a12ddcedbbcc4602453d5c0a7e78352a6e12bfbe04abfcf149e
4
+ data.tar.gz: 66f6a5668e0d37fd32a1f9573e34036b3155cf864238afb48116c8be4cd19e64
5
5
  SHA512:
6
- metadata.gz: 15a82d1c87dda2069162db76f00c3a0ade671a76806eeefeeef0239ba3dff9c62c109a1b5772e2fa002ee8da13c26ac8a0b8466235c32a88df557f5896008329
7
- data.tar.gz: 61637d5cd8a3ce93f8e10b5df542b6a3319d23ddd168a3293df44e062a95b000b0137cc1d6e336ef24cc4cc7b4025ec095dbea3960d2099cbdfc517016efc921
6
+ metadata.gz: b5adeb2316daff862f639e9318f5636d3651c69ea5fd50c42c3ddc86e13a7ac97406491fe7f0acf5bfe5d0550135a0568abeed790d1b70ccb3a593a89fc279b6
7
+ data.tar.gz: cf233bb2b67710883367515aaffffe4304d4c7f2b4c9571e276ed1323c90793bbddef1045067d5e84ca04dbb93fb946614bb5d05bbe71571bbfc589011af3c66
data/.gitignore CHANGED
@@ -3,4 +3,4 @@ Gemfile.lock
3
3
  .bundle/
4
4
  .DS_Store
5
5
  coverage/
6
- _chatgpt-translated/
6
+ _chatgpt-translate/
data/Gemfile CHANGED
@@ -29,7 +29,7 @@ gem 'cucumber', '8.0.0', require: false
29
29
  gem 'kramdown-parser-gfm', '1.1.0', require: false
30
30
  gem 'minitest', '5.19.0', require: false
31
31
  gem 'rake', '13.0.6', require: false
32
- gem 'rubocop', '1.56.0', require: false
32
+ gem 'rubocop', '1.56.2', require: false
33
33
  gem 'rubocop-rspec', '2.23.2', require: false
34
34
  gem 'simplecov', '0.22.0', require: false
35
35
  gem 'webmock', '3.18.1', require: false
data/README.md CHANGED
@@ -42,13 +42,13 @@ OpenAI API KEY must be set in `OPENAI_API_KEY` environment variable, otherwise
42
42
  the plugin will not do any translation and won't generate translated pages.
43
43
  You can get your key [here](https://help.openai.com/en/articles/4936850-where-do-i-find-my-secret-api-key).
44
44
 
45
- Inside the original page you can use `{{ page.translated-XX-url }}` in order to render the URL
45
+ Inside the original page you can use `{{ page.chatgpt-translate.urls[XX] }}` in order to render the URL
46
46
  of the translated page, where `XX` is the [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes)
47
47
  code of the target language.
48
- Inside the translated page you can use `{{ page.translated-original-url }}` in order
48
+ Inside the translated page you can use `{{ page.chatgpt-translate.original-url }}` in order
49
49
  to get the URL of the page that was translated.
50
50
 
51
- You can also use `{{ page.chatgpt-model }}`
51
+ You can also use `{{ page.chatgpt-translate.model }}`
52
52
  inside both the original page and the translated one, to refer to the model of ChatGPT.
53
53
  The presence of this attribute in the `{{ page }}` means that the
54
54
  page was translated or the translated HTML was downloaded and placed into the `_site` directory.
@@ -80,7 +80,8 @@ Full list of options available to specify in `_config.yml`:
80
80
  number big enough, to avoid silly translations. The default is 128.
81
81
 
82
82
  * `layout` (optional) — is name of the file in `_layouts` directory, without the extension.
83
- This layout will be specified for the pages generated by this plugin.
83
+ This layout will be specified for the pages generated by this plugin.
84
+ The default value is `translated` (expecting you to have `_layouts/translated.html` file available).
84
85
 
85
86
  * `targets` (mandatory) — an array of target languages, each of which has the following attributes
86
87
 
@@ -100,6 +101,9 @@ This layout will be specified for the pages generated by this plugin.
100
101
  when the `version` is changed on another hand. By default, the version of
101
102
  this plugin will be used, unless you set your own value.
102
103
 
104
+ * `tmpdir` (optional) — the name of the directory where to keep temporary files,
105
+ `_chatgpt-translate` is the default value.
106
+
103
107
  ## How to Contribute
104
108
 
105
109
  Make a fork and then test it locally like this:
data/features/cli.feature CHANGED
@@ -22,19 +22,19 @@ Feature: Simple site building
22
22
  """
23
23
  And I have a "_layouts/default.html" file with content:
24
24
  """
25
- The Chinese: {{ page.translated-zh-url }}
26
- The French: {{ page.translated-fr-url }}
25
+ The Chinese: {{ page.chatgpt-translate.urls['zh'] }}
26
+ The French: {{ page.chatgpt-translate.urls['fr'] }}
27
27
  {{ content }}
28
28
  """
29
29
  And I have a "_layouts/chinese-translated.html" file with content:
30
30
  """
31
31
  Chinese: {{ content }}
32
- The original: {{ page.translated-original-url }}
32
+ The original: {{ page.chatgpt-translate.original-url }}
33
33
  """
34
34
  And I have a "_layouts/translated.html" file with content:
35
35
  """
36
36
  French: {{ content }}
37
- The original: {{ page.translated-original-url }}
37
+ The original: {{ page.chatgpt-translate.original-url }}
38
38
  """
39
39
  And I have a "_posts/2023-01-01-hello.md" file with content:
40
40
  """
@@ -46,9 +46,9 @@ Feature: Simple site building
46
46
  """
47
47
  Then I build Jekyll site
48
48
  And Exit code is zero
49
- And File "_chatgpt-translated/zh/2023-01-01-hello-zh.md" exists
50
- And File "_chatgpt-translated/zh/2023-01-01-hello-zh.md" contains "/2023-01-01-hello-chinese.html"
51
- And File "_chatgpt-translated/zh/2023-01-01-hello-zh.md" contains "translated-language: \"zh\""
49
+ And File "_chatgpt-translate/zh/2023-01-01-hello-zh.md" exists
50
+ And File "_chatgpt-translate/zh/2023-01-01-hello-zh.md" contains "/2023-01-01-hello-chinese.html"
51
+ And File "_chatgpt-translate/zh/2023-01-01-hello-zh.md" contains "language: \"zh\""
52
52
  And File "_site/2023/01/01/hello.html" exists
53
53
  And File "_site/2023/01/01/hello.html" contains "The Chinese: /2023-01-01-hello-chinese.html"
54
54
  And File "_site/2023-01-01-hello-chinese.html" exists
@@ -69,7 +69,7 @@ Feature: Simple site building
69
69
  layout: should-not-be-used
70
70
  targets:
71
71
  -
72
- language: en
72
+ language: ru
73
73
  permalink: about-me.html
74
74
  """
75
75
  And I have a "_posts/2023-01-01-hello.md" file with content:
@@ -77,12 +77,13 @@ Feature: Simple site building
77
77
  ---
78
78
  title: foo
79
79
  ---
80
- foo
80
+ see translated page: {{ page.chatgpt-translate.urls['ru'] }}
81
81
  """
82
82
  Then I build Jekyll site
83
83
  And Exit code is zero
84
- And Stdout contains "No need to translate, the page exists"
84
+ And Stdout contains "Re-translation not required, since version is empty"
85
85
  And File "_site/2023/01/01/hello.html" exists
86
+ And File "_site/2023/01/01/hello.html" contains "see translated page: /about-me.html"
86
87
  And File "_site/about-me.html" exists
87
88
  And File "_site/about-me.html" contains "Yegor Bugayenko"
88
89
 
@@ -125,3 +126,45 @@ Feature: Simple site building
125
126
  And File "_site/about-me.html" exists
126
127
  And File "_site/about-me.html" contains "foo-file-foo"
127
128
  And File "_site/boom.html" exists
129
+
130
+ Scenario: Simple translation with links to other pages
131
+ Given I have a "_config.yml" file with content:
132
+ """
133
+ url: https://www.yegor256.com
134
+ markdown: kramdown
135
+ plugins:
136
+ - jekyll-chatgpt-translate
137
+ chatgpt-translate:
138
+ source: en
139
+ api_key: "it-is-not-used, because EN to EN translation"
140
+ layout: default
141
+ targets:
142
+ -
143
+ language: en
144
+ permalink: :slug.html
145
+ """
146
+ And I have a "_layouts/default.html" file with content:
147
+ """
148
+ {{ content }}
149
+ """
150
+ And I have a "_posts/2023-01-01-hello.md" file with content:
151
+ """
152
+ ---
153
+ title: foo
154
+ ---
155
+ See {% post_url 2023-02-02-bye %}
156
+ """
157
+ And I have a "_posts/2023-02-02-bye.md" file with content:
158
+ """
159
+ ---
160
+ title: foo
161
+ ---
162
+ See {% post_url 2023-01-01-hello %}
163
+ """
164
+ Then I build Jekyll site
165
+ And Exit code is zero
166
+ And Stdout contains "The page is absent, need to translate"
167
+ And File "_site/2023/01/01/hello.html" exists
168
+ And File "_site/2023/01/01/hello.html" contains "/bye.html"
169
+ And File "_site/2023/02/02/bye.html" exists
170
+ And File "_site/2023/02/02/bye.html" contains "/hello.html"
@@ -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.29'
31
+ s.version = '0.0.31'
32
32
  s.license = 'MIT'
33
33
  s.summary = 'Translate Jekyll Pages Through ChatGPT'
34
34
  s.description = [
@@ -23,6 +23,7 @@
23
23
  # SOFTWARE.
24
24
 
25
25
  require 'jekyll'
26
+ require 'fileutils'
26
27
  require 'json'
27
28
  require_relative 'chatgpt'
28
29
  require_relative 'permalink'
@@ -44,7 +45,7 @@ class GptTranslate::Generator < Jekyll::Generator
44
45
  # Main plugin action, called by Jekyll-core
45
46
  def generate(site)
46
47
  config ||= site.config['chatgpt-translate'] || {}
47
- home = '_chatgpt-translated'
48
+ home = config['tmpdir'] || '_chatgpt-translate'
48
49
  key = api_key(config)
49
50
  if key.nil?
50
51
  Jekyll.logger.info('jekyll-chatgpt-translate requires OPENAI_API_KEY environment variable')
@@ -76,18 +77,34 @@ class GptTranslate::Generator < Jekyll::Generator
76
77
  "title: #{doc['title'].to_json}",
77
78
  "description: #{doc['description'].to_json}",
78
79
  "permalink: #{link.to_json}",
79
- "translated-original-url: #{doc.url.to_json}",
80
- "translated-language: #{lang.to_json}",
81
- "chatgpt-model: #{model.to_json}",
80
+ 'chatgpt-translate:',
81
+ " original-url: #{doc.url.to_json}",
82
+ " language: #{lang.to_json}",
83
+ " model: #{model.to_json}",
82
84
  '---'
83
85
  ].join("\n")
84
86
  )
85
- ping = GptTranslate::Ping.new(site, link)
86
- if config['no_download'].nil? && ping.found?(version.empty? ? '' : marker)
87
- copied += 1
88
- elsif translated >= threshold
89
- next
87
+ html = config['no_download'].nil? ? GptTranslate::Ping.new(site, link).download : nil
88
+ needed = false
89
+ if html.nil?
90
+ Jekyll.logger.info("The page is absent, need to translate #{link.inspect}")
91
+ needed = true
90
92
  else
93
+ copied += 1
94
+ site.static_files << DownloadedFile.new(site, link, html)
95
+ if version.empty?
96
+ Jekyll.logger.info("Re-translation not required, since version is empty: #{link.inspect}")
97
+ elsif html.include?(marker)
98
+ Jekyll.logger.info("No need to translate, the page exists at \
99
+ #{link.inspect} (#{html.split.count} words)")
100
+ else
101
+ Jekyll.logger.info("Re-translation required for #{link.inspect}")
102
+ needed = true
103
+ end
104
+ end
105
+ if translated >= threshold
106
+ Jekyll.logger.info("We are over the threshold: #{translated} > #{threshold}")
107
+ elsif needed
91
108
  gpt = GptTranslate::ChatGPT.new(
92
109
  key,
93
110
  model,
@@ -106,19 +123,39 @@ class GptTranslate::Generator < Jekyll::Generator
106
123
  mode: 'a+'
107
124
  )
108
125
  site.pages << Jekyll::Page.new(site, site.source, File.dirname(path), File.basename(path))
109
- site.static_files.delete_if { |f| f.is_a?(GptTranslate::Ping::DownloadedFile) && f.link == link }
126
+ site.static_files.delete_if { |f| f.is_a?(DownloadedFile) && f.link == link }
110
127
  translated += 1
111
128
  Jekyll.logger.info("Translated via ChatGPT \
112
129
  in #{(Time.now - start).round(2)}s: #{path} (#{File.size(path)} bytes)")
113
130
  end
114
- doc.data["translated-#{lang}-url"] = link
115
- doc.data['chatgpt-model'] = model
131
+ doc.data['chatgpt-translate'] ||= {}
132
+ doc.data['chatgpt-translate']['model'] ||= model
133
+ doc.data['chatgpt-translate']['urls'] ||= {}
134
+ doc.data['chatgpt-translate']['urls'][lang] = link
116
135
  end
117
136
  end
118
137
  Jekyll.logger.info("jekyll-chatgpt-translate #{GptTranslate::VERSION}: \
119
138
  #{translated} pages translated and #{copied} pages copied in #{(Time.now - start).round(2)}s")
120
139
  end
121
140
 
141
+ # The file we just downloaded.
142
+ class DownloadedFile < Jekyll::StaticFile
143
+ attr_reader :link
144
+
145
+ def initialize(site, link, html)
146
+ super(site, site.dest, '', link)
147
+ @html = html
148
+ @link = link
149
+ end
150
+
151
+ def write(_dest)
152
+ FileUtils.mkdir_p(File.dirname(path))
153
+ File.write(path, @html)
154
+ Jekyll.logger.info("Saved #{@html.split.count} words to #{path.inspect}")
155
+ true
156
+ end
157
+ end
158
+
122
159
  private
123
160
 
124
161
  # Try to find the KEY, either in the environment, a file, etc.
@@ -25,8 +25,6 @@
25
25
  require 'iri'
26
26
  require 'net/http'
27
27
  require 'uri'
28
- require 'jekyll'
29
- require 'fileutils'
30
28
  require_relative 'version'
31
29
 
32
30
  # see https://stackoverflow.com/a/6048451/187141
@@ -48,47 +46,20 @@ class GptTranslate::Ping
48
46
  @path = path
49
47
  end
50
48
 
51
- def found?(marker)
49
+ # Downloads the page from the Internet and returns HTML or NIL, if the page is absent
50
+ def download
52
51
  home = @site.config['url']
53
- return false if home.nil?
52
+ return nil if home.nil?
54
53
  uri = Iri.new(home).path(@path).to_s
54
+ html = nil
55
55
  begin
56
- before = Net::HTTP.get_response(URI(uri))
57
- if before.is_a?(Net::HTTPSuccess)
58
- html = before.body
59
- @site.static_files << DownloadedFile.new(@site, @path, html)
60
- if html.include?(marker)
61
- Jekyll.logger.info("No need to translate, the page exists at \
62
- #{uri.inspect} (#{html.split.count} words)")
63
- return true
64
- end
65
- Jekyll.logger.info("Re-translation required for #{uri.inspect}")
66
- else
67
- Jekyll.logger.info("The page is absent, will translate #{uri.inspect} (#{before.code})")
68
- end
69
- Jekyll.logger.debug("GET #{uri.inspect}: #{before.code}")
56
+ response = Net::HTTP.get_response(URI(uri))
57
+ html = response.body if response.is_a?(Net::HTTPSuccess)
58
+ Jekyll.logger.debug("GET #{uri.inspect}: #{response.code}")
70
59
  rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL => e
71
60
  Jekyll.logger.debug("Failed to ping #{uri.inspect}: #{e.message}")
72
61
  Jekyll.logger.info("The page is absent (#{e.class.name}): #{uri.inspect}")
73
62
  end
74
- false
75
- end
76
-
77
- # The file we just downloaded.
78
- class DownloadedFile < Jekyll::StaticFile
79
- attr_reader :link
80
-
81
- def initialize(site, link, html)
82
- super(site, site.dest, '', link)
83
- @html = html
84
- @link = link
85
- end
86
-
87
- def write(_dest)
88
- FileUtils.mkdir_p(File.dirname(path))
89
- File.write(path, @html)
90
- Jekyll.logger.info("Saved #{@html.split.count} words to #{path.inspect}")
91
- true
92
- end
63
+ html
93
64
  end
94
65
  end
@@ -46,6 +46,8 @@ class GptTranslate::Plain
46
46
  .gsub(/<!--.+?-->/m, '')
47
47
  .gsub(/{{[^}]+}}/, '')
48
48
  .gsub(/{%.+?%}/, '')
49
+ .gsub(/^\{.+?\}\n/, '')
50
+ .gsub(/\n\{.+?\}$/, '')
49
51
  ).strip
50
52
  end
51
53
 
@@ -119,7 +121,11 @@ class GptTranslate::Plain
119
121
  end
120
122
 
121
123
  def link(link, _title, content)
122
- "[#{content}](#{link})"
124
+ if !link.nil? && link.start_with?('/', 'https://', 'http://')
125
+ "[#{content}](#{link})"
126
+ else
127
+ content
128
+ end
123
129
  end
124
130
  end
125
131
  end
@@ -23,5 +23,5 @@
23
23
  # SOFTWARE.
24
24
 
25
25
  module GptTranslate
26
- VERSION = '0.0.29'
26
+ VERSION = '0.0.31'
27
27
  end
data/test/test_chatgpt.rb CHANGED
@@ -38,22 +38,19 @@ class GptTranslate::ChatGPTTest < Minitest::Test
38
38
  end
39
39
 
40
40
  def test_start_with_link
41
- stub_request(:any, 'https://api.openai.com/v1/chat/completions')
42
- .to_return(body: '{"choices":[{"message":{"content": "done!"}}]}')
41
+ stub_it!
43
42
  chat = GptTranslate::ChatGPT.new('fake-key', 'gpt-3.5-turbo', 'en', 'ru')
44
43
  assert_equal('done!', chat.translate('[OpenAI](https://openai.com) is the creator of ChatGPT', min: 10))
45
44
  end
46
45
 
47
46
  def test_unordered_list_item
48
- stub_request(:any, 'https://api.openai.com/v1/chat/completions')
49
- .to_return(body: '{"choices":[{"message":{"content": "done!"}}]}')
47
+ stub_it!
50
48
  chat = GptTranslate::ChatGPT.new('fake-key', 'gpt-3.5-turbo', 'en', 'ru')
51
49
  assert_equal("* done!\n\n* done!", chat.translate("* First\n\n* Second", min: 1))
52
50
  end
53
51
 
54
52
  def test_ordered_list_item
55
- stub_request(:any, 'https://api.openai.com/v1/chat/completions')
56
- .to_return(body: '{"choices":[{"message":{"content": "done!"}}]}')
53
+ stub_it!
57
54
  chat = GptTranslate::ChatGPT.new('fake-key', 'gpt-3.5-turbo', 'en', 'ru')
58
55
  assert_equal("1. done!\n\n1. done!", chat.translate("1. First\n\n2. Second", min: 1))
59
56
  end
@@ -94,9 +91,15 @@ class GptTranslate::ChatGPTTest < Minitest::Test
94
91
  end
95
92
 
96
93
  def test_through_webmock
97
- stub_request(:any, 'https://api.openai.com/v1/chat/completions')
98
- .to_return(body: '{"choices":[{"message":{"content": "boom!"}}]}')
94
+ stub_it!
99
95
  chat = GptTranslate::ChatGPT.new('fake-key', 'gpt-3.5-turbo', 'en', 'ru')
100
- assert_equal('boom!', chat.translate('This is the text to send to OpenAI'))
96
+ assert_equal('done!', chat.translate('This is the text to send to OpenAI'))
97
+ end
98
+
99
+ private
100
+
101
+ def stub_it!
102
+ stub_request(:any, 'https://api.openai.com/v1/chat/completions')
103
+ .to_return(body: '{"choices":[{"message":{"content": "done!"}}]}')
101
104
  end
102
105
  end
data/test/test_ping.rb CHANGED
@@ -38,24 +38,21 @@ class GptTranslate::PingTest < Minitest::Test
38
38
  stub_request(:any, 'https://www.yegor256.com/about-me.html').to_return(body: 'Hello!')
39
39
  site = GptTranslate::FakeSite.new({ 'url' => 'https://www.yegor256.com/' })
40
40
  ping = GptTranslate::Ping.new(site, '/about-me.html')
41
- assert(ping.found?(''))
42
- assert_equal(1, site.static_files.size)
41
+ assert(!ping.download.nil?)
43
42
  end
44
43
 
45
44
  def test_when_not_exists
46
45
  stub_request(:any, 'https://www.yegor256.com/absent.html').to_return(status: 404)
47
46
  site = GptTranslate::FakeSite.new({ 'url' => 'https://www.yegor256.com/' })
48
47
  ping = GptTranslate::Ping.new(site, '/absent.html')
49
- assert(!ping.found?(''))
50
- assert_equal(0, site.static_files.size)
48
+ assert(ping.download.nil?)
51
49
  end
52
50
 
53
51
  def test_wrong_address
54
52
  WebMock.allow_net_connect!
55
53
  site = GptTranslate::FakeSite.new({ 'url' => 'https://localhost:1/' })
56
54
  ping = GptTranslate::Ping.new(site, '/boom.html')
57
- assert(!ping.found?(''))
58
- assert_equal(0, site.static_files.size)
55
+ assert(ping.download.nil?)
59
56
  end
60
57
 
61
58
  def test_relative_path
data/test/test_plain.rb CHANGED
@@ -40,6 +40,11 @@ class GptTranslate::PlainTest < Minitest::Test
40
40
  assert_equal('Hi, dude!', GptTranslate::Plain.new(" Hi,\ndude!\n").to_s)
41
41
  end
42
42
 
43
+ def test_strip_meta_markup
44
+ assert_equal('Hello, world!', GptTranslate::Plain.new("{:name='boom'}\nHello, world!").to_s)
45
+ assert_equal('Hello, world!', GptTranslate::Plain.new("Hello, world!\n{: .foo-class}").to_s)
46
+ end
47
+
43
48
  def test_lists
44
49
  assert_equal(
45
50
  "* first\n\n* second\n\n* third",
@@ -70,6 +75,14 @@ class GptTranslate::PlainTest < Minitest::Test
70
75
  'Hello, [dude](/a.html)!',
71
76
  GptTranslate::Plain.new('Hello, [dude](/a.html)!').to_s
72
77
  )
78
+ assert_equal(
79
+ 'Hello, dude!',
80
+ GptTranslate::Plain.new('Hello, [dude]()!').to_s
81
+ )
82
+ assert_equal(
83
+ 'Hello, dude!',
84
+ GptTranslate::Plain.new('Hello, [dude]({% post_url 2023-01-01-hello %})!').to_s
85
+ )
73
86
  end
74
87
 
75
88
  def test_code
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-chatgpt-translate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.29
4
+ version: 0.0.31
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-24 00:00:00.000000000 Z
11
+ date: 2023-08-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: iri