trmnl_preview 0.7.1 → 0.7.2
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/bin/trmnlp +15 -13
- metadata +17 -301
- data/CHANGELOG.md +0 -139
- data/LICENSE.txt +0 -21
- data/README.md +0 -144
- data/bin/rake +0 -27
- data/lib/trmnlp/api_client.rb +0 -86
- data/lib/trmnlp/app.rb +0 -103
- data/lib/trmnlp/cli.rb +0 -68
- data/lib/trmnlp/commands/base.rb +0 -40
- data/lib/trmnlp/commands/build.rb +0 -21
- data/lib/trmnlp/commands/clone.rb +0 -30
- data/lib/trmnlp/commands/init.rb +0 -55
- data/lib/trmnlp/commands/login.rb +0 -27
- data/lib/trmnlp/commands/pull.rb +0 -43
- data/lib/trmnlp/commands/push.rb +0 -67
- data/lib/trmnlp/commands/serve.rb +0 -25
- data/lib/trmnlp/commands.rb +0 -1
- data/lib/trmnlp/config/app.rb +0 -47
- data/lib/trmnlp/config/plugin.rb +0 -75
- data/lib/trmnlp/config/project.rb +0 -54
- data/lib/trmnlp/config.rb +0 -15
- data/lib/trmnlp/context.rb +0 -238
- data/lib/trmnlp/paths.rb +0 -58
- data/lib/trmnlp/screen_generator.rb +0 -240
- data/lib/trmnlp/version.rb +0 -5
- data/lib/trmnlp.rb +0 -14
- data/templates/init/.trmnlp.yml +0 -14
- data/templates/init/bin/trmnlp +0 -44
- data/templates/init/src/full.liquid +0 -1
- data/templates/init/src/half_horizontal.liquid +0 -1
- data/templates/init/src/half_vertical.liquid +0 -1
- data/templates/init/src/quadrant.liquid +0 -1
- data/templates/init/src/settings.yml +0 -15
- data/templates/init/src/shared.liquid +0 -1
- data/trmnl_preview.gemspec +0 -68
- data/web/public/highlight/highlight.min.js +0 -315
- data/web/public/highlight/styles/atom-one-dark.min.css +0 -1
- data/web/public/index.css +0 -113
- data/web/public/index.js +0 -83
- data/web/public/trmnl-picker.js +0 -656
- data/web/views/index.erb +0 -68
- data/web/views/render_html.erb +0 -23
data/lib/trmnlp/config/plugin.rb
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
require 'yaml'
|
|
2
|
-
|
|
3
|
-
module TRMNLP
|
|
4
|
-
class Config
|
|
5
|
-
class Plugin
|
|
6
|
-
def initialize(paths, project_config)
|
|
7
|
-
@paths = paths
|
|
8
|
-
@project_config = project_config
|
|
9
|
-
reload!
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def reload!
|
|
13
|
-
if paths.plugin_config.exist?
|
|
14
|
-
@config = YAML.load_file(paths.plugin_config)
|
|
15
|
-
else
|
|
16
|
-
@config = {}
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def strategy = @config['strategy']
|
|
21
|
-
def polling? = strategy == 'polling'
|
|
22
|
-
def webhook? = strategy == 'webhook'
|
|
23
|
-
def static? = strategy == 'static'
|
|
24
|
-
|
|
25
|
-
def polling_urls
|
|
26
|
-
# allow project-level config to override
|
|
27
|
-
urls = project_config.user_data_overrides.dig('trmnl', 'plugin_settings', 'polling_url') || @config['polling_url']
|
|
28
|
-
|
|
29
|
-
return [] if urls.nil?
|
|
30
|
-
|
|
31
|
-
with_custom_fields(urls).strip.split("\n")
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def polling_url_text = polling_urls.join("\r\n") # for {{ trmnl }}
|
|
35
|
-
|
|
36
|
-
def polling_verb = @config['polling_verb'] || 'GET'
|
|
37
|
-
|
|
38
|
-
def polling_headers
|
|
39
|
-
string_to_hash(@config['polling_headers'] || '').transform_values { |v| with_custom_fields(v) }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def polling_headers_encoded = polling_headers.map { |k, v| "#{k}=#{v}" }.join('&') # for {{ trmnl }}
|
|
43
|
-
|
|
44
|
-
def polling_body = with_custom_fields(@config['polling_body'] || '')
|
|
45
|
-
|
|
46
|
-
def dark_mode = @config['dark_mode'] || 'no'
|
|
47
|
-
|
|
48
|
-
def no_screen_padding = @config['no_screen_padding'] || 'no'
|
|
49
|
-
|
|
50
|
-
def id = @config['id']
|
|
51
|
-
|
|
52
|
-
def static_data
|
|
53
|
-
JSON.parse(@config['static_data'] || '{}')
|
|
54
|
-
rescue JSON::ParserError
|
|
55
|
-
raise Error, 'invalid JSON in static_data'
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
attr_reader :paths, :project_config
|
|
61
|
-
|
|
62
|
-
def with_custom_fields(value) = project_config.with_custom_fields(value)
|
|
63
|
-
|
|
64
|
-
# copied from TRMNL core
|
|
65
|
-
def string_to_hash(str, delimiter: '=')
|
|
66
|
-
str.split('&').map do |k_v|
|
|
67
|
-
key, value = k_v.split(delimiter)
|
|
68
|
-
next if value.nil?
|
|
69
|
-
|
|
70
|
-
{ key => CGI.unescape_uri_component(value) }
|
|
71
|
-
end.compact.reduce({}, :merge)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
require 'trmnl/liquid'
|
|
2
|
-
require 'yaml'
|
|
3
|
-
|
|
4
|
-
module TRMNLP
|
|
5
|
-
class Config
|
|
6
|
-
class Project
|
|
7
|
-
attr_reader :paths
|
|
8
|
-
|
|
9
|
-
def initialize(paths)
|
|
10
|
-
@paths = paths
|
|
11
|
-
reload!
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def reload!
|
|
15
|
-
if paths.trmnlp_config.exist?
|
|
16
|
-
@config = YAML.load_file(paths.trmnlp_config)
|
|
17
|
-
else
|
|
18
|
-
@config = {}
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def user_filters = @config['custom_filters'] || []
|
|
23
|
-
|
|
24
|
-
def live_render? = !watch_paths.empty?
|
|
25
|
-
|
|
26
|
-
def watch_paths
|
|
27
|
-
(@config['watch'] || []).map { |watch_path| paths.expand(watch_path) }.uniq
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def custom_fields = @config.fetch('custom_fields', {}).transform_values(&:to_s)
|
|
31
|
-
|
|
32
|
-
def user_data_overrides = @config['variables'] || {}
|
|
33
|
-
|
|
34
|
-
# for interpolating custom_fields into polling_* options
|
|
35
|
-
def with_custom_fields(value)
|
|
36
|
-
custom_fields_with_env = custom_fields.transform_values { |v| with_env(v) }
|
|
37
|
-
parse_liquid(value).render(custom_fields_with_env)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def time_zone = @config['time_zone'] || 'UTC'
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
# for interpolating ENV vars into custom_fields
|
|
45
|
-
def with_env(value)
|
|
46
|
-
parse_liquid(value).render({ 'env' => ENV.to_h })
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def parse_liquid(contents)
|
|
50
|
-
Liquid::Template.parse(contents, environment: TRMNL::Liquid.build_environment)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
data/lib/trmnlp/config.rb
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
require_relative 'config/app'
|
|
2
|
-
require_relative 'config/plugin'
|
|
3
|
-
require_relative 'config/project'
|
|
4
|
-
|
|
5
|
-
module TRMNLP
|
|
6
|
-
class Config
|
|
7
|
-
attr_reader :app, :project, :plugin
|
|
8
|
-
|
|
9
|
-
def initialize(path)
|
|
10
|
-
@app = App.new(path)
|
|
11
|
-
@project = Project.new(path)
|
|
12
|
-
@plugin = Plugin.new(path, @project)
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
end
|
data/lib/trmnlp/context.rb
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
require 'active_support/time'
|
|
2
|
-
require 'active_support/core_ext/hash/conversions'
|
|
3
|
-
require 'erb'
|
|
4
|
-
require 'faraday'
|
|
5
|
-
require 'filewatcher'
|
|
6
|
-
require 'json'
|
|
7
|
-
require 'trmnl/liquid'
|
|
8
|
-
|
|
9
|
-
require_relative 'config'
|
|
10
|
-
require_relative 'paths'
|
|
11
|
-
|
|
12
|
-
module TRMNLP
|
|
13
|
-
class Context
|
|
14
|
-
attr_reader :config, :paths
|
|
15
|
-
|
|
16
|
-
def initialize(root_dir)
|
|
17
|
-
@paths = Paths.new(root_dir)
|
|
18
|
-
@config = Config.new(paths)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def validate!
|
|
22
|
-
raise Error, "not a plugin directory (did not find #{paths.trmnlp_config})" unless paths.valid?
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def start_filewatcher
|
|
26
|
-
@filewatcher_thread ||= Thread.new do
|
|
27
|
-
loop do
|
|
28
|
-
begin
|
|
29
|
-
Filewatcher.new(config.project.watch_paths).watch do |changes|
|
|
30
|
-
config.project.reload!
|
|
31
|
-
config.plugin.reload!
|
|
32
|
-
new_user_data = user_data
|
|
33
|
-
|
|
34
|
-
views = changes.map { |path, _change| File.basename(path, '.liquid') }
|
|
35
|
-
views.each do |view|
|
|
36
|
-
@view_change_callback.call(view, new_user_data) if @view_change_callback
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
rescue => e
|
|
40
|
-
puts "Error during live render: #{e}"
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def on_view_change(&block)
|
|
47
|
-
@view_change_callback = block
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def user_data
|
|
51
|
-
merged_data = base_trmnl_data
|
|
52
|
-
|
|
53
|
-
if config.plugin.static?
|
|
54
|
-
merged_data.merge!(config.plugin.static_data)
|
|
55
|
-
elsif paths.user_data.exist?
|
|
56
|
-
merged_data.merge!(JSON.parse(paths.user_data.read))
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Praise be to ActiveSupport
|
|
60
|
-
merged_data.deep_merge!(config.project.user_data_overrides)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def poll_data
|
|
64
|
-
return unless config.plugin.polling?
|
|
65
|
-
|
|
66
|
-
data = {}
|
|
67
|
-
|
|
68
|
-
if config.plugin.polling_urls.empty?
|
|
69
|
-
raise Error, "config must specify polling_url or polling_urls"
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
config.plugin.polling_urls.each.with_index do |url, i|
|
|
73
|
-
verb = config.plugin.polling_verb.upcase
|
|
74
|
-
|
|
75
|
-
print "#{verb} #{url}... "
|
|
76
|
-
|
|
77
|
-
conn = Faraday.new(url:, headers: config.plugin.polling_headers)
|
|
78
|
-
|
|
79
|
-
case verb
|
|
80
|
-
when 'GET'
|
|
81
|
-
response = conn.get
|
|
82
|
-
when 'POST'
|
|
83
|
-
response = conn.post do |req|
|
|
84
|
-
req.body = config.plugin.polling_body
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
puts "received #{response.body.length} bytes (#{response.status} status)"
|
|
89
|
-
if response.status == 200
|
|
90
|
-
content_type = response.headers['content-type'].split(';').first.strip if response.headers.include?('content-type')
|
|
91
|
-
case content_type
|
|
92
|
-
when 'application/json', /^application\/.+\+json/
|
|
93
|
-
json = wrap_array(JSON.parse(response.body))
|
|
94
|
-
when 'text/xml', 'application/xml', /^application\/.+\+xml/
|
|
95
|
-
json = wrap_array(Hash.from_xml(response.body))
|
|
96
|
-
else
|
|
97
|
-
puts "unknown content type received: #{response.headers['content-type']}"
|
|
98
|
-
json = {}
|
|
99
|
-
end
|
|
100
|
-
else
|
|
101
|
-
json = {}
|
|
102
|
-
puts response.body
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
if config.plugin.polling_urls.count == 1
|
|
106
|
-
# For a single polling URL, we just return the JSON directly
|
|
107
|
-
data = json
|
|
108
|
-
break
|
|
109
|
-
else
|
|
110
|
-
# Multiple URLs are namespaced by index
|
|
111
|
-
data["IDX_#{i}"] = json
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
write_user_data(data)
|
|
116
|
-
|
|
117
|
-
data
|
|
118
|
-
rescue StandardError => e
|
|
119
|
-
puts "error: #{e.message}"
|
|
120
|
-
{}
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def put_webhook(payload)
|
|
124
|
-
data = wrap_array(JSON.parse(payload))
|
|
125
|
-
write_user_data(data)
|
|
126
|
-
rescue
|
|
127
|
-
puts "webhook error: #{e.message}"
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def render_liquid_template(view)
|
|
131
|
-
template_path = paths.template(view)
|
|
132
|
-
return "Missing template: #{template_path}" unless template_path.exist?
|
|
133
|
-
|
|
134
|
-
shared_template_path = paths.shared_template
|
|
135
|
-
if shared_template_path.exist?
|
|
136
|
-
full_markup = shared_template_path.read + template_path.read
|
|
137
|
-
else
|
|
138
|
-
full_markup = template_path.read
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
user_template = Liquid::Template.parse(full_markup, environment: liquid_environment)
|
|
142
|
-
user_template.render(user_data)
|
|
143
|
-
rescue StandardError => e
|
|
144
|
-
e.message
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def render_full_page(view, params = {})
|
|
148
|
-
template = paths.render_template.read
|
|
149
|
-
|
|
150
|
-
ERB.new(template).result(TemplateBinding.new(self, view, params).get_binding do
|
|
151
|
-
render_liquid_template(view)
|
|
152
|
-
end)
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def screen_classes(classes = 'screen')
|
|
156
|
-
classes << ' screen--no-bleed' if config.plugin.no_screen_padding == 'yes'
|
|
157
|
-
classes
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
private
|
|
161
|
-
|
|
162
|
-
# bindings must match the `GET /render/{view}.html` route in app.rb
|
|
163
|
-
class TemplateBinding
|
|
164
|
-
def initialize(context, view, params)
|
|
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
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def get_binding = binding
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def wrap_array(json)
|
|
182
|
-
json.is_a?(Array) ? { data: json } : json
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def base_trmnl_data
|
|
186
|
-
tz = ActiveSupport::TimeZone.find_tzinfo(config.project.time_zone)
|
|
187
|
-
time_zone_iana = tz.name
|
|
188
|
-
time_zone_name = ActiveSupport::TimeZone::MAPPING.invert[time_zone_iana] || time_zone_iana
|
|
189
|
-
utc_offset = tz.utc_offset
|
|
190
|
-
|
|
191
|
-
{
|
|
192
|
-
'trmnl' => {
|
|
193
|
-
'user' => {
|
|
194
|
-
'name' => 'name',
|
|
195
|
-
'first_name' => 'first_name',
|
|
196
|
-
'last_name' => 'last_name',
|
|
197
|
-
'locale' => 'en',
|
|
198
|
-
'time_zone' => time_zone_name,
|
|
199
|
-
'time_zone_iana' => time_zone_iana,
|
|
200
|
-
'utc_offset' => utc_offset,
|
|
201
|
-
},
|
|
202
|
-
'device' => {
|
|
203
|
-
'friendly_id' => 'ABC123',
|
|
204
|
-
'percent_charged' => 85.0,
|
|
205
|
-
'wifi_strength' => 90,
|
|
206
|
-
'height' => 480,
|
|
207
|
-
'width' => 800
|
|
208
|
-
},
|
|
209
|
-
'system' => {
|
|
210
|
-
'timestamp_utc' => Time.now.utc.to_i,
|
|
211
|
-
},
|
|
212
|
-
'plugin_settings' => {
|
|
213
|
-
'instance_name' => 'instance_name',
|
|
214
|
-
'strategy' => config.plugin.strategy,
|
|
215
|
-
'dark_mode' => config.plugin.dark_mode,
|
|
216
|
-
'polling_headers' => config.plugin.polling_headers_encoded,
|
|
217
|
-
'polling_url' => config.plugin.polling_url_text,
|
|
218
|
-
'custom_fields_values' => config.project.custom_fields
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def liquid_environment
|
|
225
|
-
@liquid_environment ||= TRMNL::Liquid.build_environment do |env|
|
|
226
|
-
config.project.user_filters.each do |module_name, relative_path|
|
|
227
|
-
require paths.root_dir.join(relative_path)
|
|
228
|
-
env.register_filter(Object.const_get(module_name))
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
def write_user_data(data)
|
|
234
|
-
paths.create_cache_dir
|
|
235
|
-
paths.user_data.write(JSON.generate(data))
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
end
|
data/lib/trmnlp/paths.rb
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
require 'xdg'
|
|
2
|
-
|
|
3
|
-
module TRMNLP
|
|
4
|
-
class Paths
|
|
5
|
-
attr_reader :root_dir
|
|
6
|
-
|
|
7
|
-
def initialize(root_dir)
|
|
8
|
-
@root_dir = Pathname.new(root_dir)
|
|
9
|
-
@xdg = XDG.new
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# --- trmnlp library ---
|
|
13
|
-
|
|
14
|
-
def gem_dir = Pathname.new(__dir__).join('..', '..').expand_path
|
|
15
|
-
|
|
16
|
-
def templates_dir = gem_dir.join('templates')
|
|
17
|
-
|
|
18
|
-
# --- directories ---
|
|
19
|
-
|
|
20
|
-
def src_dir = root_dir.join('src')
|
|
21
|
-
|
|
22
|
-
def build_dir = root_dir.join('_build')
|
|
23
|
-
def create_build_dir = build_dir.mkpath
|
|
24
|
-
|
|
25
|
-
def app_config_dir = xdg.config_home.join('trmnlp')
|
|
26
|
-
|
|
27
|
-
def cache_dir = xdg.cache_home.join('trmnl')
|
|
28
|
-
def create_cache_dir = cache_dir.mkpath
|
|
29
|
-
|
|
30
|
-
def valid? = trmnlp_config.exist?
|
|
31
|
-
|
|
32
|
-
# --- files ---
|
|
33
|
-
|
|
34
|
-
def trmnlp_config = root_dir.join('.trmnlp.yml')
|
|
35
|
-
|
|
36
|
-
def plugin_config = src_dir.join('settings.yml')
|
|
37
|
-
|
|
38
|
-
def template(view) = src_dir.join("#{view}.liquid")
|
|
39
|
-
|
|
40
|
-
def shared_template = template('shared')
|
|
41
|
-
|
|
42
|
-
def app_config = app_config_dir.join('config.yml')
|
|
43
|
-
|
|
44
|
-
def user_data = cache_dir.join('data.json')
|
|
45
|
-
|
|
46
|
-
def render_template = Pathname.new(__dir__).join('..', '..', 'web', 'views', 'render_html.erb')
|
|
47
|
-
|
|
48
|
-
def src_files = src_dir.glob('*').select(&:file?)
|
|
49
|
-
|
|
50
|
-
# --- utilities ---
|
|
51
|
-
|
|
52
|
-
def expand(path) = Pathname.new(path).expand_path(root_dir)
|
|
53
|
-
|
|
54
|
-
private
|
|
55
|
-
|
|
56
|
-
attr_reader :xdg
|
|
57
|
-
end
|
|
58
|
-
end
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
require 'mini_magick'
|
|
2
|
-
require 'selenium-webdriver'
|
|
3
|
-
require 'base64'
|
|
4
|
-
require 'thread'
|
|
5
|
-
require 'tempfile'
|
|
6
|
-
require 'fileutils'
|
|
7
|
-
require 'uri'
|
|
8
|
-
|
|
9
|
-
module TRMNLP
|
|
10
|
-
class ScreenGenerator
|
|
11
|
-
# Browser pool management for efficient resource usage
|
|
12
|
-
class BrowserPool
|
|
13
|
-
def initialize(max_size: 2)
|
|
14
|
-
@drivers = []
|
|
15
|
-
@available = Queue.new
|
|
16
|
-
@mutex = Mutex.new
|
|
17
|
-
@max_size = max_size
|
|
18
|
-
@shutdown = false
|
|
19
|
-
|
|
20
|
-
at_exit { shutdown }
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def with_driver
|
|
24
|
-
driver = nil
|
|
25
|
-
|
|
26
|
-
begin
|
|
27
|
-
driver = checkout_driver
|
|
28
|
-
yield driver
|
|
29
|
-
ensure
|
|
30
|
-
checkin_driver(driver) if driver
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def shutdown
|
|
35
|
-
@mutex.synchronize do
|
|
36
|
-
return if @shutdown
|
|
37
|
-
@shutdown = true
|
|
38
|
-
|
|
39
|
-
@drivers.each do |driver|
|
|
40
|
-
driver.quit rescue nil
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
@drivers.clear
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def checkout_driver
|
|
50
|
-
driver = @available.pop(true) rescue nil
|
|
51
|
-
|
|
52
|
-
if driver.nil?
|
|
53
|
-
@mutex.synchronize do
|
|
54
|
-
if @drivers.size < @max_size
|
|
55
|
-
driver = create_driver
|
|
56
|
-
@drivers << driver
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
driver ||= @available.pop
|
|
62
|
-
|
|
63
|
-
begin
|
|
64
|
-
# Ping the driver
|
|
65
|
-
driver.title
|
|
66
|
-
driver
|
|
67
|
-
rescue
|
|
68
|
-
@mutex.synchronize do
|
|
69
|
-
@drivers.delete(driver)
|
|
70
|
-
driver = create_driver
|
|
71
|
-
@drivers << driver
|
|
72
|
-
end
|
|
73
|
-
driver
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def checkin_driver(driver)
|
|
78
|
-
return if @shutdown
|
|
79
|
-
@available.push(driver)
|
|
80
|
-
end
|
|
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
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
@@browser_pool = BrowserPool.new
|
|
95
|
-
|
|
96
|
-
def initialize(html, opts = {})
|
|
97
|
-
self.input = html
|
|
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]
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
attr_accessor :input, :output, :image
|
|
107
|
-
|
|
108
|
-
def process
|
|
109
|
-
convert_to_image
|
|
110
|
-
image ? mono_image(output) : mono(output)
|
|
111
|
-
output
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
private
|
|
115
|
-
|
|
116
|
-
def convert_to_image
|
|
117
|
-
retry_count = 0
|
|
118
|
-
|
|
119
|
-
begin
|
|
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
|
|
126
|
-
}
|
|
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)
|
|
132
|
-
|
|
133
|
-
sleep(0.1)
|
|
134
|
-
|
|
135
|
-
prepare_page(driver)
|
|
136
|
-
|
|
137
|
-
self.output = Tempfile.new(['screenshot', '.png'])
|
|
138
|
-
driver.save_screenshot(output.path)
|
|
139
|
-
output.close
|
|
140
|
-
end
|
|
141
|
-
rescue Selenium::WebDriver::Error::TimeoutError,
|
|
142
|
-
Selenium::WebDriver::Error::WebDriverError => e
|
|
143
|
-
retry_count += 1
|
|
144
|
-
retry if retry_count <= 1
|
|
145
|
-
raise
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
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'
|
|
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
|
|
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
|
|
176
|
-
|
|
177
|
-
MiniMagick::Tool::Convert.new do |m|
|
|
178
|
-
m << img.path
|
|
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}"
|
|
186
|
-
m.strip
|
|
187
|
-
m << tmp.path
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
FileUtils.mv(tmp.path, img.path, force: true)
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def mono(img)
|
|
194
|
-
depth = [[color_depth.to_i, 1].max, 8].min
|
|
195
|
-
|
|
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
|
|
202
|
-
|
|
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
|
|
239
|
-
end
|
|
240
|
-
end
|
data/lib/trmnlp/version.rb
DELETED
data/lib/trmnlp.rb
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module TRMNLP; end
|
|
4
|
-
require 'oj'
|
|
5
|
-
Oj.mimic_JSON()
|
|
6
|
-
require_relative "trmnlp/config"
|
|
7
|
-
require_relative "trmnlp/context"
|
|
8
|
-
require_relative "trmnlp/version"
|
|
9
|
-
|
|
10
|
-
module TRMNLP
|
|
11
|
-
VIEWS = %w{full half_horizontal half_vertical quadrant}
|
|
12
|
-
|
|
13
|
-
class Error < StandardError; end
|
|
14
|
-
end
|