trmnl_preview 0.5.3 → 0.5.4
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 +14 -0
- data/README.md +11 -2
- data/bin/rake +27 -0
- data/lib/trmnlp/app.rb +7 -3
- data/lib/trmnlp/config/project.rb +2 -2
- data/lib/trmnlp/screen_generator.rb +146 -87
- data/lib/trmnlp/version.rb +1 -1
- data/templates/init/src/shared.liquid +1 -0
- data/trmnl_preview.gemspec +1 -2
- data/web/public/index.css +6 -0
- data/web/views/index.erb +1 -1
- metadata +4 -19
- data/web/public/black-case.jpg +0 -0
- data/web/public/clear-case.jpg +0 -0
- data/web/public/white-case.jpg +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd5804f039fe8932f996c097798d5a4c9211725fad19f71c230ac3d99c9c8875
|
4
|
+
data.tar.gz: d13908a4314d5a397667e59ed4576ab1cb9a396b47bf2962df12bc80f53e9d14
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e51b14f3cc1320a96a948c5c1b44622dc13a53eeb348f7b9f6ca4e928628dbccec6fd4de54598408c7d778f1891d82577ebc3853b884a122432b89d74dd11d56
|
7
|
+
data.tar.gz: 7962920070178a0701251b5f847b1b35630d6643323b1d87cb8c11c0f91f8478e842d88722f1d04743a28c69f25c78c394538daa9aa3e4777beb8647b3b53f9b
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.5.4
|
4
|
+
|
5
|
+
- Added `shared.liquid` file to template (@mariovisic)
|
6
|
+
- Stringified custom field values to match production (@mariovisic)
|
7
|
+
- Optimized image generation (@sd416)
|
8
|
+
- Fixed preview from growing when JSON data becomes too wide (@stephenyeargin)
|
9
|
+
|
10
|
+
## 0.5.3
|
11
|
+
|
12
|
+
- Added support for [reusable markup](https://docs.usetrmnl.com/go/reusing-markup) in `shared.liquid`
|
13
|
+
- Replaced custom case images with [\<trmnl-frame\> component](https://github.com/usetrmnl/trmnl-component)
|
14
|
+
- Updated custom Liquid filters
|
15
|
+
- Added API key validation during `trmnlp login`
|
16
|
+
|
3
17
|
## 0.5.2
|
4
18
|
|
5
19
|
- Added `time_zone` project config option, which is injected into `trmnl.user` variables
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
A basic self-hosted web server to ease the development and sharing of [TRMNL](https://usetrmnl.com/) plugins.
|
4
4
|
|
5
|
-
[Liquid](https://shopify.github.io/liquid/) templates are rendered leveraging the [TRMNL Design System](https://usetrmnl.com/framework). They may be generated as HTML (faster, and a good approximation of the final result) or as
|
5
|
+
[Liquid](https://shopify.github.io/liquid/) templates are rendered leveraging the [TRMNL Design System](https://usetrmnl.com/framework). They may be generated as HTML (faster, and a good approximation of the final result) or as PNG images (slower, but more accurate).
|
6
6
|
|
7
7
|
The server watches the filesystem for changes to the Liquid templates, seamlessly updating the preview without the need to refresh.
|
8
8
|
|
@@ -57,7 +57,7 @@ trmnlp push # upload
|
|
57
57
|
Prerequisites:
|
58
58
|
|
59
59
|
- Ruby 3.x
|
60
|
-
- For
|
60
|
+
- For PNG rendering (optional):
|
61
61
|
- Firefox
|
62
62
|
- ImageMagick
|
63
63
|
|
@@ -114,6 +114,15 @@ The `settings.yml` file is part of the plugin definition.
|
|
114
114
|
|
115
115
|
See [TRMNL documentation](https://help.usetrmnl.com/en/articles/10542599-importing-and-exporting-private-plugins#h_581fb988f0) for details on this file's contents.
|
116
116
|
|
117
|
+
|
118
|
+
## Tests
|
119
|
+
|
120
|
+
To test, run:
|
121
|
+
|
122
|
+
```sh
|
123
|
+
bin/rake
|
124
|
+
```
|
125
|
+
|
117
126
|
## Contributing
|
118
127
|
|
119
128
|
Bug reports and pull requests are welcome on GitHub at https://github.com/usetrmnl/trmnlp.
|
data/bin/rake
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# This file was generated by Bundler.
|
6
|
+
#
|
7
|
+
# The application 'rake' is installed as part of a gem, and
|
8
|
+
# this file is here to facilitate running it.
|
9
|
+
#
|
10
|
+
|
11
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
12
|
+
|
13
|
+
bundle_binstub = File.expand_path("bundle", __dir__)
|
14
|
+
|
15
|
+
if File.file?(bundle_binstub)
|
16
|
+
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
|
17
|
+
load(bundle_binstub)
|
18
|
+
else
|
19
|
+
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
20
|
+
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
require "rubygems"
|
25
|
+
require "bundler/setup"
|
26
|
+
|
27
|
+
load Gem.bin_path("rake", "rake")
|
data/lib/trmnlp/app.rb
CHANGED
@@ -86,12 +86,16 @@ module TRMNLP
|
|
86
86
|
end
|
87
87
|
end
|
88
88
|
|
89
|
-
get "/render/#{view}.
|
89
|
+
get "/render/#{view}.png" do
|
90
90
|
@view = view
|
91
91
|
html = @context.render_full_page(view)
|
92
92
|
generator = ScreenGenerator.new(html, image: true)
|
93
|
-
|
94
|
-
|
93
|
+
temp_image = generator.process
|
94
|
+
|
95
|
+
send_file temp_image.path, type: 'image/png', disposition: 'inline'
|
96
|
+
|
97
|
+
temp_image.close
|
98
|
+
temp_image.unlink
|
95
99
|
end
|
96
100
|
end
|
97
101
|
end
|
@@ -26,7 +26,7 @@ module TRMNLP
|
|
26
26
|
(@config['watch'] || []).map { |watch_path| paths.expand(watch_path) }.uniq
|
27
27
|
end
|
28
28
|
|
29
|
-
def custom_fields = @config
|
29
|
+
def custom_fields = @config.fetch('custom_fields', {}).transform_values(&:to_s)
|
30
30
|
|
31
31
|
def user_data_overrides = @config['variables'] || {}
|
32
32
|
|
@@ -46,4 +46,4 @@ module TRMNLP
|
|
46
46
|
end
|
47
47
|
end
|
48
48
|
end
|
49
|
-
end
|
49
|
+
end
|
@@ -1,11 +1,105 @@
|
|
1
|
-
require 'ferrum'
|
2
1
|
require 'mini_magick'
|
3
2
|
require 'puppeteer-ruby'
|
4
3
|
require 'base64'
|
4
|
+
require 'thread'
|
5
5
|
|
6
6
|
module TRMNLP
|
7
7
|
class ScreenGenerator
|
8
|
-
|
8
|
+
# Browser pool management for efficient resource usage
|
9
|
+
class BrowserPool
|
10
|
+
def initialize(max_size: 2)
|
11
|
+
@browsers = []
|
12
|
+
@available = Queue.new
|
13
|
+
@mutex = Mutex.new
|
14
|
+
@max_size = max_size
|
15
|
+
@shutdown = false
|
16
|
+
|
17
|
+
# Register cleanup on exit
|
18
|
+
at_exit { shutdown }
|
19
|
+
end
|
20
|
+
|
21
|
+
def with_page
|
22
|
+
browser = nil
|
23
|
+
page = nil
|
24
|
+
|
25
|
+
begin
|
26
|
+
browser = checkout_browser
|
27
|
+
page = browser.new_page
|
28
|
+
yield page
|
29
|
+
ensure
|
30
|
+
# Clean up page but keep browser alive
|
31
|
+
page&.close rescue nil
|
32
|
+
checkin_browser(browser) if browser
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def shutdown
|
37
|
+
@mutex.synchronize do
|
38
|
+
return if @shutdown
|
39
|
+
@shutdown = true
|
40
|
+
|
41
|
+
# Close all browsers
|
42
|
+
@browsers.each do |browser|
|
43
|
+
browser.close rescue nil
|
44
|
+
end
|
45
|
+
@browsers.clear
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def checkout_browser
|
52
|
+
# Try to get an available browser
|
53
|
+
browser = @available.pop(true) rescue nil
|
54
|
+
|
55
|
+
# If no browser available and we haven't reached max size, create a new one
|
56
|
+
if browser.nil?
|
57
|
+
@mutex.synchronize do
|
58
|
+
if @browsers.size < @max_size
|
59
|
+
browser = create_browser
|
60
|
+
@browsers << browser
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# If still no browser, wait for one to become available
|
66
|
+
browser ||= @available.pop
|
67
|
+
|
68
|
+
# Verify browser is still alive
|
69
|
+
begin
|
70
|
+
browser.targets # Simple check to see if browser responds
|
71
|
+
browser
|
72
|
+
rescue
|
73
|
+
# Browser is dead, create a new one
|
74
|
+
@mutex.synchronize do
|
75
|
+
@browsers.delete(browser)
|
76
|
+
browser = create_browser
|
77
|
+
@browsers << browser
|
78
|
+
end
|
79
|
+
browser
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def checkin_browser(browser)
|
84
|
+
return if @shutdown
|
85
|
+
@available.push(browser)
|
86
|
+
end
|
87
|
+
|
88
|
+
def create_browser
|
89
|
+
Puppeteer.launch(
|
90
|
+
product: 'firefox',
|
91
|
+
headless: true,
|
92
|
+
args: [
|
93
|
+
"--window-size=800,480",
|
94
|
+
"--disable-web-security"
|
95
|
+
]
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Class-level browser pool shared across all instances
|
101
|
+
@@browser_pool = BrowserPool.new
|
102
|
+
|
9
103
|
def initialize(html, opts = {})
|
10
104
|
self.input = html
|
11
105
|
self.image = !!opts[:image]
|
@@ -16,115 +110,80 @@ module TRMNLP
|
|
16
110
|
def process
|
17
111
|
convert_to_image
|
18
112
|
image ? mono_image(output) : mono(output)
|
19
|
-
output
|
20
|
-
# IO.copy_stream(output, img_path)
|
113
|
+
output
|
21
114
|
end
|
22
115
|
|
23
116
|
private
|
24
117
|
|
25
|
-
# def img_path
|
26
|
-
# "#{Dir.pwd}/public/images/generated/#{SecureRandom.hex(3)}.bmp"
|
27
|
-
# end
|
28
|
-
|
29
|
-
# Constructs the command and passes the input to the vendor/puppeteer.js
|
30
|
-
# script for processing. Returns a base64 encoded string
|
31
118
|
def convert_to_image
|
32
119
|
retry_count = 0
|
120
|
+
|
33
121
|
begin
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
()
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
122
|
+
@@browser_pool.with_page do |page|
|
123
|
+
# Configure page
|
124
|
+
page.viewport = Puppeteer::Viewport.new(width: width, height: height)
|
125
|
+
|
126
|
+
# Set content with appropriate wait strategy
|
127
|
+
page.set_content(input, timeout: 10000)
|
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";
|
134
|
+
}
|
135
|
+
JAVASCRIPT
|
136
|
+
|
137
|
+
# Take screenshot
|
138
|
+
self.output = Tempfile.new(['screenshot', '.png'])
|
139
|
+
page.screenshot(path: output.path, type: 'png')
|
140
|
+
end
|
141
|
+
rescue Puppeteer::TimeoutError, Puppeteer::FrameManager::NavigationError => e
|
142
|
+
retry_count += 1
|
143
|
+
if retry_count <= 1
|
144
|
+
retry
|
145
|
+
else
|
146
|
+
puts "ERROR -> ScreenGenerator#convert_to_image -> #{e.message}"
|
147
|
+
raise
|
148
|
+
end
|
59
149
|
end
|
60
150
|
end
|
61
151
|
|
62
|
-
# Refer this PR where the author reused the browser instance https://github.com/YusukeIwaki/puppeteer-ruby/pull/100/files
|
63
|
-
# This will increase the throughput of our image rendering process by 60-70%, saving about ~1.5 second per image generation.
|
64
|
-
# On local it takes < 1 second now to generate the subsequent image.
|
65
|
-
def firefox_browser
|
66
|
-
@browser ||= Puppeteer.launch(
|
67
|
-
product: 'firefox',
|
68
|
-
headless: true,
|
69
|
-
args: [
|
70
|
-
"--window-size=#{width},#{height}",
|
71
|
-
"--disable-web-security"
|
72
|
-
# "--hide-scrollbars" #works only on chrome, using page.evaluate for firefox
|
73
|
-
]
|
74
|
-
)
|
75
|
-
end
|
76
|
-
|
77
|
-
def Ferrum.cached_browser
|
78
|
-
return nil unless $cached_browser
|
79
|
-
|
80
|
-
$cached_browser
|
81
|
-
end
|
82
|
-
|
83
|
-
def Ferrum.cached_browser=(value)
|
84
|
-
$cached_browser = value
|
85
|
-
end
|
86
|
-
|
87
|
-
# Overall at max wait for 2.5 seconds
|
88
|
-
def wait_for_stop_loading(page)
|
89
|
-
count = 0
|
90
|
-
while page.frames.first.state != :stopped_loading && count < 20
|
91
|
-
count += 1
|
92
|
-
sleep 0.1
|
93
|
-
end
|
94
|
-
sleep 0.5 # wait_until: DomContentLoaded event is not available in ferrum
|
95
|
-
end
|
96
|
-
|
97
152
|
def mono(img)
|
98
153
|
MiniMagick::Tool::Convert.new do |m|
|
99
154
|
m << img.path
|
100
155
|
m.monochrome # Use built-in smart monochrome dithering (but it's not working as expected)
|
101
156
|
m.depth(color_depth) # Should be set to 1 for 1-bit output
|
102
157
|
m.strip # Remove any additional metadata
|
103
|
-
m <<
|
158
|
+
m << img.path
|
104
159
|
end
|
105
160
|
end
|
106
161
|
|
107
162
|
def mono_image(img)
|
108
|
-
#
|
109
|
-
#
|
110
|
-
# The same seems to be broken with imagemagick 7.XX
|
111
|
-
# So in order to reduce the channel from 8 to 1, I just rerun the command, and it's working
|
112
|
-
# TODO for future, find a better way to generate image screens.
|
163
|
+
# Convert to monochrome bitmap with proper dithering
|
164
|
+
# This implementation works with both ImageMagick 6.x and 7.x
|
113
165
|
MiniMagick::Tool::Convert.new do |m|
|
114
166
|
m << img.path
|
167
|
+
|
168
|
+
# First convert to grayscale to ensure proper channel handling
|
169
|
+
m.colorspace << 'Gray'
|
170
|
+
|
171
|
+
# Apply Floyd-Steinberg dithering for better quality
|
115
172
|
m.dither << 'FloydSteinberg'
|
173
|
+
|
174
|
+
# Remap to a 50% gray pattern for better dithering
|
116
175
|
m.remap << 'pattern:gray50'
|
117
|
-
|
118
|
-
|
119
|
-
m <<
|
120
|
-
|
121
|
-
|
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
|
184
|
+
m.strip
|
185
|
+
|
122
186
|
m << img.path
|
123
|
-
m.dither << 'FloydSteinberg'
|
124
|
-
m.remap << 'pattern:gray50'
|
125
|
-
m.depth(color_depth) # Should be set to 1 for 1-bit output
|
126
|
-
m.strip # Remove any additional metadata
|
127
|
-
m << ('bmp3:' << img.path) # Converts to Bitmap.
|
128
187
|
end
|
129
188
|
end
|
130
189
|
|
@@ -134,4 +193,4 @@ module TRMNLP
|
|
134
193
|
|
135
194
|
def color_depth = 1
|
136
195
|
end
|
137
|
-
end
|
196
|
+
end
|
data/lib/trmnlp/version.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
|
data/trmnl_preview.gemspec
CHANGED
@@ -47,8 +47,7 @@ Gem::Specification.new do |spec|
|
|
47
47
|
spec.add_dependency "activesupport", "~> 8.0"
|
48
48
|
spec.add_dependency "actionview", "~> 8.0"
|
49
49
|
|
50
|
-
#
|
51
|
-
spec.add_dependency "ferrum", "~> 0.16"
|
50
|
+
# PNG rendering
|
52
51
|
spec.add_dependency 'puppeteer-ruby', '~> 0.45.6'
|
53
52
|
spec.add_dependency 'mini_magick', '~> 4.12.0'
|
54
53
|
|
data/web/public/index.css
CHANGED
@@ -14,6 +14,7 @@ menu {
|
|
14
14
|
margin: 0;
|
15
15
|
display: flex;
|
16
16
|
justify-content: space-between;
|
17
|
+
max-width: 950px;
|
17
18
|
}
|
18
19
|
|
19
20
|
menu a {
|
@@ -34,6 +35,11 @@ menu a.active {
|
|
34
35
|
color: white;
|
35
36
|
}
|
36
37
|
|
38
|
+
pre {
|
39
|
+
max-width: 950px;
|
40
|
+
overflow-x: scroll;
|
41
|
+
}
|
42
|
+
|
37
43
|
.spinner {
|
38
44
|
width: 22px;
|
39
45
|
height: 22px;
|
data/web/views/index.erb
CHANGED
@@ -24,7 +24,7 @@
|
|
24
24
|
<div style="display: flex; gap: 0.3em; align-items: center;">
|
25
25
|
<div class="spinner" style="display: none;"></div>
|
26
26
|
<select class="select-format">
|
27
|
-
<option value="
|
27
|
+
<option value="png">PNG</option>
|
28
28
|
<option value="html" selected>HTML</option>
|
29
29
|
</select>
|
30
30
|
<select class="select-case">
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: trmnl_preview
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rockwell Schrock
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-06-09 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: sinatra
|
@@ -107,20 +107,6 @@ dependencies:
|
|
107
107
|
- - "~>"
|
108
108
|
- !ruby/object:Gem::Version
|
109
109
|
version: '8.0'
|
110
|
-
- !ruby/object:Gem::Dependency
|
111
|
-
name: ferrum
|
112
|
-
requirement: !ruby/object:Gem::Requirement
|
113
|
-
requirements:
|
114
|
-
- - "~>"
|
115
|
-
- !ruby/object:Gem::Version
|
116
|
-
version: '0.16'
|
117
|
-
type: :runtime
|
118
|
-
prerelease: false
|
119
|
-
version_requirements: !ruby/object:Gem::Requirement
|
120
|
-
requirements:
|
121
|
-
- - "~>"
|
122
|
-
- !ruby/object:Gem::Version
|
123
|
-
version: '0.16'
|
124
110
|
- !ruby/object:Gem::Dependency
|
125
111
|
name: puppeteer-ruby
|
126
112
|
requirement: !ruby/object:Gem::Requirement
|
@@ -272,6 +258,7 @@ files:
|
|
272
258
|
- CHANGELOG.md
|
273
259
|
- LICENSE.txt
|
274
260
|
- README.md
|
261
|
+
- bin/rake
|
275
262
|
- bin/trmnlp
|
276
263
|
- lib/markup/custom_liquid_filters.rb
|
277
264
|
- lib/markup/inline_templates_file_system.rb
|
@@ -305,13 +292,11 @@ files:
|
|
305
292
|
- templates/init/src/half_vertical.liquid
|
306
293
|
- templates/init/src/quadrant.liquid
|
307
294
|
- templates/init/src/settings.yml
|
295
|
+
- templates/init/src/shared.liquid
|
308
296
|
- trmnl_preview.gemspec
|
309
|
-
- web/public/black-case.jpg
|
310
|
-
- web/public/clear-case.jpg
|
311
297
|
- web/public/index.css
|
312
298
|
- web/public/index.js
|
313
299
|
- web/public/trmnl-component.js
|
314
|
-
- web/public/white-case.jpg
|
315
300
|
- web/views/index.erb
|
316
301
|
- web/views/render_html.erb
|
317
302
|
homepage: https://github.com/usetrmnl/trmnlp
|
data/web/public/black-case.jpg
DELETED
Binary file
|
data/web/public/clear-case.jpg
DELETED
Binary file
|
data/web/public/white-case.jpg
DELETED
Binary file
|