trmnl_preview 0.6.1 → 0.7.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 +4 -4
- data/CHANGELOG.md +8 -0
- data/lib/trmnlp/app.rb +10 -18
- data/lib/trmnlp/context.rb +17 -8
- data/lib/trmnlp/screen_generator.rb +160 -116
- data/lib/trmnlp/version.rb +1 -1
- data/trmnl_preview.gemspec +2 -1
- data/web/public/index.js +15 -1
- data/web/views/render_html.erb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5728e75c9a9b9508254942f91d4e64c09440014d2b40ede681b60e5fe054a847
|
|
4
|
+
data.tar.gz: 1330a50632f9757807e35aee4525f573d7923d639bf345c4e6efacb5ed994c78
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca4d3138556081762e15ffc3ed2644bed18c4bdd6edd3bfc2f6ed6e002653f47eae7d025ddc1c477b9fbc2092a5c4c5f09192489fd3ccfd9c44c13e1511b2f4b
|
|
7
|
+
data.tar.gz: 890fcbe509e699e77970de5590b26595e52c20ad0772f234fa25f837c73d8f489b5681eabceecad017ab6830ef2b8a233002b140559cbd7b90fe3e7748e39937
|
data/CHANGELOG.md
CHANGED
data/lib/trmnlp/app.rb
CHANGED
|
@@ -78,27 +78,19 @@ module TRMNLP
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
get "/render/#{view}.html" do
|
|
81
|
-
@view
|
|
82
|
-
@screen_classes = @context.screen_classes(params[:screen_classes])
|
|
83
|
-
|
|
84
|
-
case view
|
|
85
|
-
when 'half_horizontal'
|
|
86
|
-
@mashup_classes = 'mashup mashup--1Tx1B'
|
|
87
|
-
when 'half_vertical'
|
|
88
|
-
@mashup_classes = 'mashup mashup--1Lx1R'
|
|
89
|
-
when 'quadrant'
|
|
90
|
-
@mashup_classes = 'mashup mashup--2x2'
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
erb :render_html do
|
|
94
|
-
@context.render_template(view)
|
|
95
|
-
end
|
|
81
|
+
@context.render_full_page(view, params)
|
|
96
82
|
end
|
|
97
|
-
|
|
83
|
+
|
|
98
84
|
get "/render/#{view}.png" do
|
|
99
85
|
@view = view
|
|
100
|
-
html = @context.render_full_page(view)
|
|
101
|
-
|
|
86
|
+
html = @context.render_full_page(view, params)
|
|
87
|
+
|
|
88
|
+
# Parse optional rendering params (sent by the web UI for PNG output)
|
|
89
|
+
width = params[:width] && params[:width].to_i
|
|
90
|
+
height = params[:height] && params[:height].to_i
|
|
91
|
+
color_depth = params[:color_depth] && params[:color_depth].to_i
|
|
92
|
+
|
|
93
|
+
generator = ScreenGenerator.new(html, image: true, width: width, height: height, color_depth: color_depth)
|
|
102
94
|
temp_image = generator.process
|
|
103
95
|
|
|
104
96
|
send_file temp_image.path, type: 'image/png', disposition: 'inline'
|
data/lib/trmnlp/context.rb
CHANGED
|
@@ -89,9 +89,9 @@ module TRMNLP
|
|
|
89
89
|
if response.status == 200
|
|
90
90
|
content_type = response.headers['content-type'].split(';').first.strip if response.headers.include?('content-type')
|
|
91
91
|
case content_type
|
|
92
|
-
when 'application/json'
|
|
92
|
+
when 'application/json', /^application\/.+\+json/
|
|
93
93
|
json = wrap_array(JSON.parse(response.body))
|
|
94
|
-
when 'text/xml', 'application/xml',
|
|
94
|
+
when 'text/xml', 'application/xml', /^application\/.+\+xml/
|
|
95
95
|
json = wrap_array(Hash.from_xml(response.body))
|
|
96
96
|
else
|
|
97
97
|
puts "unknown content type received: #{response.headers['content-type']}"
|
|
@@ -127,7 +127,7 @@ module TRMNLP
|
|
|
127
127
|
puts "webhook error: #{e.message}"
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
-
def
|
|
130
|
+
def render_liquid_template(view)
|
|
131
131
|
template_path = paths.template(view)
|
|
132
132
|
return "Missing template: #{template_path}" unless template_path.exist?
|
|
133
133
|
|
|
@@ -144,11 +144,11 @@ module TRMNLP
|
|
|
144
144
|
e.message
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
-
def render_full_page(view)
|
|
147
|
+
def render_full_page(view, params = {})
|
|
148
148
|
template = paths.render_template.read
|
|
149
149
|
|
|
150
|
-
ERB.new(template).result(TemplateBinding.new(self, view).get_binding do
|
|
151
|
-
|
|
150
|
+
ERB.new(template).result(TemplateBinding.new(self, view, params).get_binding do
|
|
151
|
+
render_liquid_template(view)
|
|
152
152
|
end)
|
|
153
153
|
end
|
|
154
154
|
|
|
@@ -161,9 +161,18 @@ module TRMNLP
|
|
|
161
161
|
|
|
162
162
|
# bindings must match the `GET /render/{view}.html` route in app.rb
|
|
163
163
|
class TemplateBinding
|
|
164
|
-
def initialize(context, view)
|
|
165
|
-
@screen_classes = context.screen_classes
|
|
164
|
+
def initialize(context, view, params)
|
|
166
165
|
@view = view
|
|
166
|
+
@screen_classes = context.screen_classes(params[:screen_classes])
|
|
167
|
+
|
|
168
|
+
case view
|
|
169
|
+
when 'half_horizontal'
|
|
170
|
+
@mashup_classes = 'mashup mashup--1Tx1B'
|
|
171
|
+
when 'half_vertical'
|
|
172
|
+
@mashup_classes = 'mashup mashup--1Lx1R'
|
|
173
|
+
when 'quadrant'
|
|
174
|
+
@mashup_classes = 'mashup mashup--2x2'
|
|
175
|
+
end
|
|
167
176
|
end
|
|
168
177
|
|
|
169
178
|
def get_binding = binding
|
|
@@ -1,111 +1,109 @@
|
|
|
1
1
|
require 'mini_magick'
|
|
2
|
-
require '
|
|
2
|
+
require 'selenium-webdriver'
|
|
3
3
|
require 'base64'
|
|
4
4
|
require 'thread'
|
|
5
|
+
require 'tempfile'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
require 'uri'
|
|
5
8
|
|
|
6
9
|
module TRMNLP
|
|
7
10
|
class ScreenGenerator
|
|
8
11
|
# Browser pool management for efficient resource usage
|
|
9
12
|
class BrowserPool
|
|
10
13
|
def initialize(max_size: 2)
|
|
11
|
-
@
|
|
14
|
+
@drivers = []
|
|
12
15
|
@available = Queue.new
|
|
13
16
|
@mutex = Mutex.new
|
|
14
17
|
@max_size = max_size
|
|
15
18
|
@shutdown = false
|
|
16
|
-
|
|
17
|
-
# Register cleanup on exit
|
|
19
|
+
|
|
18
20
|
at_exit { shutdown }
|
|
19
21
|
end
|
|
20
|
-
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
|
|
23
|
+
def with_driver
|
|
24
|
+
driver = nil
|
|
25
|
+
|
|
25
26
|
begin
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
yield page
|
|
27
|
+
driver = checkout_driver
|
|
28
|
+
yield driver
|
|
29
29
|
ensure
|
|
30
|
-
|
|
31
|
-
page&.close rescue nil
|
|
32
|
-
checkin_browser(browser) if browser
|
|
30
|
+
checkin_driver(driver) if driver
|
|
33
31
|
end
|
|
34
32
|
end
|
|
35
|
-
|
|
33
|
+
|
|
36
34
|
def shutdown
|
|
37
35
|
@mutex.synchronize do
|
|
38
36
|
return if @shutdown
|
|
39
37
|
@shutdown = true
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
browser.close rescue nil
|
|
38
|
+
|
|
39
|
+
@drivers.each do |driver|
|
|
40
|
+
driver.quit rescue nil
|
|
44
41
|
end
|
|
45
|
-
|
|
42
|
+
|
|
43
|
+
@drivers.clear
|
|
46
44
|
end
|
|
47
45
|
end
|
|
48
|
-
|
|
46
|
+
|
|
49
47
|
private
|
|
50
|
-
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# If no browser available and we haven't reached max size, create a new one
|
|
56
|
-
if browser.nil?
|
|
48
|
+
|
|
49
|
+
def checkout_driver
|
|
50
|
+
driver = @available.pop(true) rescue nil
|
|
51
|
+
|
|
52
|
+
if driver.nil?
|
|
57
53
|
@mutex.synchronize do
|
|
58
|
-
if @
|
|
59
|
-
|
|
60
|
-
@
|
|
54
|
+
if @drivers.size < @max_size
|
|
55
|
+
driver = create_driver
|
|
56
|
+
@drivers << driver
|
|
61
57
|
end
|
|
62
58
|
end
|
|
63
59
|
end
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# Verify browser is still alive
|
|
60
|
+
|
|
61
|
+
driver ||= @available.pop
|
|
62
|
+
|
|
69
63
|
begin
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
# Ping the driver
|
|
65
|
+
driver.title
|
|
66
|
+
driver
|
|
72
67
|
rescue
|
|
73
|
-
# Browser is dead, create a new one
|
|
74
68
|
@mutex.synchronize do
|
|
75
|
-
@
|
|
76
|
-
|
|
77
|
-
@
|
|
69
|
+
@drivers.delete(driver)
|
|
70
|
+
driver = create_driver
|
|
71
|
+
@drivers << driver
|
|
78
72
|
end
|
|
79
|
-
|
|
73
|
+
driver
|
|
80
74
|
end
|
|
81
75
|
end
|
|
82
|
-
|
|
83
|
-
def
|
|
76
|
+
|
|
77
|
+
def checkin_driver(driver)
|
|
84
78
|
return if @shutdown
|
|
85
|
-
@available.push(
|
|
79
|
+
@available.push(driver)
|
|
86
80
|
end
|
|
87
|
-
|
|
88
|
-
def
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
81
|
+
|
|
82
|
+
def create_driver
|
|
83
|
+
options = Selenium::WebDriver::Firefox::Options.new
|
|
84
|
+
options.add_argument('--headless')
|
|
85
|
+
options.add_argument('--disable-web-security')
|
|
86
|
+
|
|
87
|
+
driver = Selenium::WebDriver.for(:firefox, options: options)
|
|
88
|
+
# Set a default window size that will be consistent
|
|
89
|
+
driver.manage.window.maximize
|
|
90
|
+
driver
|
|
97
91
|
end
|
|
98
92
|
end
|
|
99
|
-
|
|
100
|
-
# Class-level browser pool shared across all instances
|
|
93
|
+
|
|
101
94
|
@@browser_pool = BrowserPool.new
|
|
102
|
-
|
|
95
|
+
|
|
103
96
|
def initialize(html, opts = {})
|
|
104
97
|
self.input = html
|
|
105
98
|
self.image = !!opts[:image]
|
|
99
|
+
|
|
100
|
+
# Accept optional rendering parameters (width/height/color depth/dark mode)
|
|
101
|
+
@requested_width = opts[:width]
|
|
102
|
+
@requested_height = opts[:height]
|
|
103
|
+
@requested_color_depth = opts[:color_depth]
|
|
106
104
|
end
|
|
107
105
|
|
|
108
|
-
attr_accessor :input, :output, :image
|
|
106
|
+
attr_accessor :input, :output, :image
|
|
109
107
|
|
|
110
108
|
def process
|
|
111
109
|
convert_to_image
|
|
@@ -117,80 +115,126 @@ module TRMNLP
|
|
|
117
115
|
|
|
118
116
|
def convert_to_image
|
|
119
117
|
retry_count = 0
|
|
120
|
-
|
|
118
|
+
|
|
121
119
|
begin
|
|
122
|
-
@@browser_pool.
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
# Hide scrollbars
|
|
130
|
-
page.evaluate(<<~JAVASCRIPT)
|
|
131
|
-
() => {
|
|
132
|
-
document.getElementsByTagName('html')[0].style.overflow = "hidden";
|
|
133
|
-
document.getElementsByTagName('body')[0].style.overflow = "hidden";
|
|
120
|
+
@@browser_pool.with_driver do |driver|
|
|
121
|
+
# determine dimensions of toolbars, etc
|
|
122
|
+
borders = driver.execute_script(<<~JS)
|
|
123
|
+
return {
|
|
124
|
+
width: window.outerWidth - window.innerWidth,
|
|
125
|
+
height: window.outerHeight - window.innerHeight
|
|
134
126
|
}
|
|
135
|
-
|
|
127
|
+
JS
|
|
128
|
+
|
|
129
|
+
window_width = width + borders['width']
|
|
130
|
+
window_height = height + borders['height']
|
|
131
|
+
driver.manage.window.size = Selenium::WebDriver::Dimension.new(window_width, window_height)
|
|
136
132
|
|
|
137
|
-
|
|
133
|
+
sleep(0.1)
|
|
134
|
+
|
|
135
|
+
prepare_page(driver)
|
|
136
|
+
|
|
138
137
|
self.output = Tempfile.new(['screenshot', '.png'])
|
|
139
|
-
|
|
138
|
+
driver.save_screenshot(output.path)
|
|
139
|
+
output.close
|
|
140
140
|
end
|
|
141
|
-
rescue
|
|
141
|
+
rescue Selenium::WebDriver::Error::TimeoutError,
|
|
142
|
+
Selenium::WebDriver::Error::WebDriverError => e
|
|
142
143
|
retry_count += 1
|
|
143
|
-
if retry_count <= 1
|
|
144
|
-
|
|
145
|
-
else
|
|
146
|
-
puts "ERROR -> ScreenGenerator#convert_to_image -> #{e.message}"
|
|
147
|
-
raise
|
|
148
|
-
end
|
|
144
|
+
retry if retry_count <= 1
|
|
145
|
+
raise
|
|
149
146
|
end
|
|
150
147
|
end
|
|
151
148
|
|
|
152
|
-
def
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
149
|
+
def prepare_page(driver)
|
|
150
|
+
driver.navigate.to('about:blank')
|
|
151
|
+
|
|
152
|
+
driver.execute_script(<<~JS, input)
|
|
153
|
+
document.open();
|
|
154
|
+
document.write(arguments[0]);
|
|
155
|
+
document.close();
|
|
156
|
+
JS
|
|
157
|
+
|
|
158
|
+
Selenium::WebDriver::Wait.new(timeout: 5).until do
|
|
159
|
+
driver.execute_script('return document.readyState') == 'complete'
|
|
159
160
|
end
|
|
161
|
+
|
|
162
|
+
# Wait for fonts (prevents layout shifts)
|
|
163
|
+
driver.execute_script('return document.fonts && document.fonts.ready')
|
|
164
|
+
|
|
165
|
+
driver.execute_script(<<~JS)
|
|
166
|
+
document.documentElement.style.overflow = 'hidden';
|
|
167
|
+
document.body.style.overflow = 'hidden';
|
|
168
|
+
JS
|
|
160
169
|
end
|
|
170
|
+
|
|
171
|
+
def convert_with_mini_magick(img, depth)
|
|
172
|
+
tmp = Tempfile.new(['mono', '.png'])
|
|
173
|
+
tmp.close
|
|
174
|
+
|
|
175
|
+
levels = 2**depth
|
|
161
176
|
|
|
162
|
-
def mono_image(img)
|
|
163
|
-
# Convert to monochrome bitmap with proper dithering
|
|
164
|
-
# This implementation works with both ImageMagick 6.x and 7.x
|
|
165
177
|
MiniMagick::Tool::Convert.new do |m|
|
|
166
178
|
m << img.path
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
m.
|
|
173
|
-
|
|
174
|
-
# Remap to a 50% gray pattern for better dithering
|
|
175
|
-
m.remap << 'pattern:gray50'
|
|
176
|
-
|
|
177
|
-
# Set the image type to bilevel (1-bit black and white)
|
|
178
|
-
m.type << 'Bilevel'
|
|
179
|
-
|
|
180
|
-
# Set color depth to 1 bit
|
|
181
|
-
m.depth << color_depth
|
|
182
|
-
|
|
183
|
-
# Remove any metadata to reduce file size
|
|
179
|
+
m.colorspace 'Gray'
|
|
180
|
+
m.dither 'FloydSteinberg'
|
|
181
|
+
|
|
182
|
+
yield(m, depth, levels)
|
|
183
|
+
|
|
184
|
+
m.depth depth
|
|
185
|
+
m.define "png:bit-depth=#{depth}"
|
|
184
186
|
m.strip
|
|
185
|
-
|
|
186
|
-
m << img.path
|
|
187
|
+
m << tmp.path
|
|
187
188
|
end
|
|
189
|
+
|
|
190
|
+
FileUtils.mv(tmp.path, img.path, force: true)
|
|
188
191
|
end
|
|
189
192
|
|
|
190
|
-
def
|
|
193
|
+
def mono(img)
|
|
194
|
+
depth = [[color_depth.to_i, 1].max, 8].min
|
|
191
195
|
|
|
192
|
-
|
|
196
|
+
convert_with_mini_magick(img, depth) do |m, d, levels|
|
|
197
|
+
m.posterize levels
|
|
198
|
+
m.colors levels
|
|
199
|
+
m.type 'Bilevel' if d == 1
|
|
200
|
+
end
|
|
201
|
+
end
|
|
193
202
|
|
|
194
|
-
def
|
|
203
|
+
def mono_image(img)
|
|
204
|
+
depth = [[color_depth.to_i, 1].max, 8].min
|
|
205
|
+
|
|
206
|
+
convert_with_mini_magick(img, depth) do |m, d, levels|
|
|
207
|
+
if d == 1
|
|
208
|
+
# For true 1-bit, use a halftone/remap and bilevel output
|
|
209
|
+
m.remap 'pattern:gray50'
|
|
210
|
+
m.posterize 2
|
|
211
|
+
m.colors 2
|
|
212
|
+
m.type 'Bilevel'
|
|
213
|
+
else
|
|
214
|
+
m.posterize levels
|
|
215
|
+
m.colors levels
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def width
|
|
222
|
+
@requested_width || 800
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def height
|
|
226
|
+
@requested_height || 480
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def color_depth
|
|
230
|
+
return @requested_color_depth if @requested_color_depth
|
|
231
|
+
|
|
232
|
+
# Try to infer color depth from the rendered HTML's screen classes
|
|
233
|
+
if input && input.match(/screen--(\d+)bit/)
|
|
234
|
+
return $1.to_i
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
1
|
|
238
|
+
end
|
|
195
239
|
end
|
|
196
240
|
end
|
data/lib/trmnlp/version.rb
CHANGED
data/trmnl_preview.gemspec
CHANGED
|
@@ -47,7 +47,8 @@ Gem::Specification.new do |spec|
|
|
|
47
47
|
spec.add_dependency "trmnl-liquid", "~> 0.4.0"
|
|
48
48
|
|
|
49
49
|
# PNG rendering
|
|
50
|
-
spec.add_dependency 'puppeteer-ruby', '~> 0.45.6'
|
|
50
|
+
# spec.add_dependency 'puppeteer-ruby', '~> 0.45.6'
|
|
51
|
+
spec.add_dependency 'selenium-webdriver', '~> 4.39'
|
|
51
52
|
spec.add_dependency 'mini_magick', '~> 4.12.0'
|
|
52
53
|
|
|
53
54
|
# Utilities
|
data/web/public/index.js
CHANGED
|
@@ -27,7 +27,21 @@ trmnlp.connectLiveRender = function () {
|
|
|
27
27
|
trmnlp.fetchPreview = function (pickerState) {
|
|
28
28
|
const screenClasses = (pickerState?.screenClasses || trmnlp.picker.state.screenClasses).join(" ");
|
|
29
29
|
const encodedScreenClasses = encodeURIComponent(screenClasses);
|
|
30
|
-
|
|
30
|
+
let src = `/render/${trmnlp.view}.${trmnlp.formatSelect.value}?screen_classes=${encodedScreenClasses}`;
|
|
31
|
+
|
|
32
|
+
// If requesting a PNG, also include dimensions, dark mode, and color depth
|
|
33
|
+
if (trmnlp.formatSelect.value === 'png') {
|
|
34
|
+
const state = pickerState || trmnlp.picker.state;
|
|
35
|
+
const width = encodeURIComponent(state.width);
|
|
36
|
+
const height = encodeURIComponent(state.height);
|
|
37
|
+
const isDarkMode = state.isDarkMode ? 1 : 0;
|
|
38
|
+
|
|
39
|
+
// derive numeric color depth from classes like 'screen--1bit'
|
|
40
|
+
const grays = state.palette.grays || 2;
|
|
41
|
+
const colorDepth = Math.ceil(Math.log2(grays));
|
|
42
|
+
|
|
43
|
+
src += `&width=${width}&height=${height}&color_depth=${colorDepth}`;
|
|
44
|
+
}
|
|
31
45
|
|
|
32
46
|
trmnlp.spinner.style.display = "inline-block";
|
|
33
47
|
trmnlp.iframe.src = src;
|
data/web/views/render_html.erb
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<!-- End Inter font -->
|
|
12
12
|
</head>
|
|
13
13
|
|
|
14
|
-
<body class="environment trmnl"
|
|
14
|
+
<body class="environment trmnl">
|
|
15
15
|
<div class="<%= @screen_classes %>">
|
|
16
16
|
<% if @mashup_classes %><div class="<%= @mashup_classes %>"><% end %>
|
|
17
17
|
<div class="view view--<%= @view %>">
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trmnl_preview
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rockwell Schrock
|
|
@@ -94,19 +94,19 @@ dependencies:
|
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: 0.4.0
|
|
96
96
|
- !ruby/object:Gem::Dependency
|
|
97
|
-
name:
|
|
97
|
+
name: selenium-webdriver
|
|
98
98
|
requirement: !ruby/object:Gem::Requirement
|
|
99
99
|
requirements:
|
|
100
100
|
- - "~>"
|
|
101
101
|
- !ruby/object:Gem::Version
|
|
102
|
-
version:
|
|
102
|
+
version: '4.39'
|
|
103
103
|
type: :runtime
|
|
104
104
|
prerelease: false
|
|
105
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
106
106
|
requirements:
|
|
107
107
|
- - "~>"
|
|
108
108
|
- !ruby/object:Gem::Version
|
|
109
|
-
version:
|
|
109
|
+
version: '4.39'
|
|
110
110
|
- !ruby/object:Gem::Dependency
|
|
111
111
|
name: mini_magick
|
|
112
112
|
requirement: !ruby/object:Gem::Requirement
|