trmnl_preview 0.5.3 → 0.5.5
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 +21 -0
- data/README.md +20 -7
- data/bin/rake +27 -0
- data/lib/trmnlp/api_client.rb +7 -3
- data/lib/trmnlp/app.rb +7 -3
- data/lib/trmnlp/cli.rb +3 -1
- data/lib/trmnlp/commands/pull.rb +4 -4
- data/lib/trmnlp/commands/push.rb +10 -10
- data/lib/trmnlp/config/plugin.rb +8 -7
- 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/bin/trmnlp +30 -0
- data/templates/init/src/shared.liquid +1 -0
- data/trmnl_preview.gemspec +1 -2
- data/web/public/highlight/highlight.min.js +315 -0
- data/web/public/highlight/styles/atom-one-dark.min.css +1 -0
- data/web/public/index.css +25 -1
- data/web/public/trmnl-component.js +1 -1
- data/web/views/index.erb +7 -2
- metadata +7 -20
- data/templates/init/bin/dev +0 -25
- 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: 16a333009aab09891c12d6fe7bf3b8662912cd6e376f31c1a4997de5a6c93314
|
4
|
+
data.tar.gz: cf57e05fa4ba41059e55fb7281d7644ee07772459b63503ebe4a4023fae14085
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 43048f869b37533061b92b7e06b2623b8d47e35098942f535301655727efc726f09c004330f277fb1ee7d0a73f3b5785283846b1404e0d8a6b92cec4f94b9105
|
7
|
+
data.tar.gz: 3a8672a65e06a537fef9c51e759657076bcda0a320cc4f2ee8a211c27bad31bcab09993ab592c520c3103805e2077a5f907c67c2423184688fd612d0c307e276
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,26 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.5.5
|
4
|
+
|
5
|
+
- Added dark mode (@stephenyeargin)
|
6
|
+
- Added override for `polling_url` in project config (@heroheman)
|
7
|
+
- Reworked `bin/dev` into more generic `bin/trmnlp`
|
8
|
+
- Fixed pull, push, and clone commands on Windows (@eugenio)
|
9
|
+
|
10
|
+
## 0.5.4
|
11
|
+
|
12
|
+
- Added `shared.liquid` file to template (@mariovisic)
|
13
|
+
- Stringified custom field values to match production (@mariovisic)
|
14
|
+
- Optimized image generation (@sd416)
|
15
|
+
- Fixed preview from growing when JSON data becomes too wide (@stephenyeargin)
|
16
|
+
|
17
|
+
## 0.5.3
|
18
|
+
|
19
|
+
- Added support for [reusable markup](https://docs.usetrmnl.com/go/reusing-markup) in `shared.liquid`
|
20
|
+
- Replaced custom case images with [\<trmnl-frame\> component](https://github.com/usetrmnl/trmnl-component)
|
21
|
+
- Updated custom Liquid filters
|
22
|
+
- Added API key validation during `trmnlp login`
|
23
|
+
|
3
24
|
## 0.5.2
|
4
25
|
|
5
26
|
- 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
|
|
@@ -52,12 +52,16 @@ trmnlp push # upload
|
|
52
52
|
|
53
53
|
## Running trmnlp
|
54
54
|
|
55
|
-
|
55
|
+
The `bin/trmnlp` script is provided as a convenience. It will use the local Ruby gem if available, falling back to the `trmnl/trmnlp` Docker image.
|
56
|
+
|
57
|
+
You can modify the `bin/trmnlp` script to set up environment variables (plugin secrets, etc.) before running the server.
|
58
|
+
|
59
|
+
### Installing via RubyGems
|
56
60
|
|
57
61
|
Prerequisites:
|
58
62
|
|
59
63
|
- Ruby 3.x
|
60
|
-
- For
|
64
|
+
- For PNG rendering (optional):
|
61
65
|
- Firefox
|
62
66
|
- ImageMagick
|
63
67
|
|
@@ -66,13 +70,13 @@ gem install trmnl_preview
|
|
66
70
|
trmnlp serve
|
67
71
|
```
|
68
72
|
|
69
|
-
###
|
73
|
+
### Installing via Docker
|
70
74
|
|
71
75
|
```sh
|
72
76
|
docker run \
|
73
|
-
|
74
|
-
|
75
|
-
trmnl/trmnlp
|
77
|
+
--publish 4567:4567 \
|
78
|
+
--volume ".:/plugin" \
|
79
|
+
trmnl/trmnlp serve
|
76
80
|
```
|
77
81
|
|
78
82
|
## `.trmnlp.yml` Reference - Project Config
|
@@ -114,6 +118,15 @@ The `settings.yml` file is part of the plugin definition.
|
|
114
118
|
|
115
119
|
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
120
|
|
121
|
+
|
122
|
+
## Tests
|
123
|
+
|
124
|
+
To test, run:
|
125
|
+
|
126
|
+
```sh
|
127
|
+
bin/rake
|
128
|
+
```
|
129
|
+
|
117
130
|
## Contributing
|
118
131
|
|
119
132
|
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/api_client.rb
CHANGED
@@ -18,20 +18,24 @@ module TRMNLP
|
|
18
18
|
temp_file.write(response.body)
|
19
19
|
temp_file.rewind
|
20
20
|
|
21
|
-
# return the
|
22
|
-
|
21
|
+
# return the temp file IO
|
22
|
+
temp_file
|
23
23
|
else
|
24
24
|
raise Error, "failed to download plugin settings archive: #{response.status} #{response.body}"
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
28
|
def post_plugin_setting_archive(id, path)
|
29
|
+
filepart = Faraday::Multipart::FilePart.new(path, 'application/zip')
|
30
|
+
|
29
31
|
payload = {
|
30
|
-
file:
|
32
|
+
file: filepart
|
31
33
|
}
|
32
34
|
|
33
35
|
response = conn.post("plugin_settings/#{id}/archive", payload)
|
34
36
|
|
37
|
+
filepart.close
|
38
|
+
|
35
39
|
if response.status == 200
|
36
40
|
JSON.parse(response.body)
|
37
41
|
else
|
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
|
data/lib/trmnlp/cli.rb
CHANGED
@@ -14,6 +14,8 @@ module TRMNLP
|
|
14
14
|
|
15
15
|
def self.exit_on_failure? = true
|
16
16
|
|
17
|
+
def self.default_bind = File.exist?('/.dockerenv') ? '0.0.0.0' : '127.0.0.1'
|
18
|
+
|
17
19
|
desc 'build', 'Generate static HTML files'
|
18
20
|
def build
|
19
21
|
Commands::Build.new(options).call
|
@@ -52,7 +54,7 @@ module TRMNLP
|
|
52
54
|
end
|
53
55
|
|
54
56
|
desc 'serve', 'Start a local dev server'
|
55
|
-
method_option :bind, type: :string, default:
|
57
|
+
method_option :bind, type: :string, default: default_bind, aliases: '-b', desc: 'Bind address'
|
56
58
|
method_option :port, type: :numeric, default: 4567, aliases: '-p', desc: 'Port number'
|
57
59
|
def serve
|
58
60
|
Commands::Serve.new(options).call
|
data/lib/trmnlp/commands/pull.rb
CHANGED
@@ -19,11 +19,11 @@ module TRMNLP
|
|
19
19
|
end
|
20
20
|
|
21
21
|
api = APIClient.new(config)
|
22
|
-
|
22
|
+
tempfile = api.get_plugin_setting_archive(plugin_settings_id)
|
23
23
|
size = 0
|
24
24
|
|
25
25
|
begin
|
26
|
-
Zip::File.open(
|
26
|
+
Zip::File.open(tempfile.path) do |zip_file|
|
27
27
|
zip_file.each do |entry|
|
28
28
|
dest_path = paths.src_dir.join(entry.name)
|
29
29
|
dest_path.dirname.mkpath
|
@@ -31,9 +31,9 @@ module TRMNLP
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
size = File.size(
|
34
|
+
size = File.size(tempfile.path)
|
35
35
|
ensure
|
36
|
-
|
36
|
+
tempfile.close
|
37
37
|
end
|
38
38
|
|
39
39
|
puts "Downloaded plugin (#{size} bytes)"
|
data/lib/trmnlp/commands/push.rb
CHANGED
@@ -29,18 +29,18 @@ module TRMNLP
|
|
29
29
|
|
30
30
|
size = 0
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
32
|
+
zip_path = 'upload.zip'
|
33
|
+
f = Zip::File.open(zip_path, Zip::File::CREATE) do |zip_file|
|
34
|
+
paths.src_files.each do |file|
|
35
|
+
zip_file.add(File.basename(file), file)
|
37
36
|
end
|
38
|
-
|
39
|
-
response = api.post_plugin_setting_archive(plugin_settings_id, temp_file.path)
|
40
|
-
paths.plugin_config.write(response.dig('data', 'settings_yaml'))
|
41
|
-
|
42
|
-
size = File.size(temp_file.path)
|
43
37
|
end
|
38
|
+
|
39
|
+
response = api.post_plugin_setting_archive(plugin_settings_id, zip_path)
|
40
|
+
paths.plugin_config.write(response.dig('data', 'settings_yaml'))
|
41
|
+
|
42
|
+
size = File.size(zip_path)
|
43
|
+
File.delete(zip_path)
|
44
44
|
|
45
45
|
output <<~HEREDOC
|
46
46
|
Uploaded plugin (#{size} bytes)
|
data/lib/trmnlp/config/plugin.rb
CHANGED
@@ -3,9 +3,9 @@ require 'yaml'
|
|
3
3
|
module TRMNLP
|
4
4
|
class Config
|
5
5
|
class Plugin
|
6
|
-
def initialize(paths,
|
6
|
+
def initialize(paths, project_config)
|
7
7
|
@paths = paths
|
8
|
-
@
|
8
|
+
@project_config = project_config
|
9
9
|
reload!
|
10
10
|
end
|
11
11
|
|
@@ -23,11 +23,12 @@ module TRMNLP
|
|
23
23
|
def static? = strategy == 'static'
|
24
24
|
|
25
25
|
def polling_urls
|
26
|
-
|
26
|
+
# allow project-level config to override
|
27
|
+
urls = project_config.user_data_overrides.dig('trmnl', 'plugin_settings', 'polling_url') || @config['polling_url']
|
27
28
|
|
28
|
-
|
29
|
+
return [] if urls.nil?
|
29
30
|
|
30
|
-
urls.map { |url| with_custom_fields(url) }
|
31
|
+
urls.strip.split("\n").map { |url| with_custom_fields(url.strip) }
|
31
32
|
end
|
32
33
|
|
33
34
|
def polling_url_text = polling_urls.join("\r\n") # for {{ trmnl }}
|
@@ -56,9 +57,9 @@ module TRMNLP
|
|
56
57
|
|
57
58
|
private
|
58
59
|
|
59
|
-
attr_reader :paths, :
|
60
|
+
attr_reader :paths, :project_config
|
60
61
|
|
61
|
-
def with_custom_fields(value) =
|
62
|
+
def with_custom_fields(value) = project_config.with_custom_fields(value)
|
62
63
|
|
63
64
|
# copied from TRMNL core
|
64
65
|
def string_to_hash(str, delimiter: '=')
|
@@ -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,30 @@
|
|
1
|
+
#! /bin/bash
|
2
|
+
#
|
3
|
+
# This script was automatically generated by `trmnlp init` but it's yours to modify!
|
4
|
+
# Use this opportunity to set up environment variables, install dependencies, etc.
|
5
|
+
#
|
6
|
+
|
7
|
+
if command -v trmnlp &> /dev/null
|
8
|
+
then
|
9
|
+
trmnlp "$@"
|
10
|
+
exit
|
11
|
+
fi
|
12
|
+
|
13
|
+
if command -v docker &> /dev/null
|
14
|
+
then
|
15
|
+
docker run \
|
16
|
+
--publish 4567:4567 \
|
17
|
+
--volume "$(pwd):/plugin" \
|
18
|
+
trmnl/trmnlp "$@"
|
19
|
+
exit
|
20
|
+
fi
|
21
|
+
|
22
|
+
echo "Install the trmnl_preview gem:
|
23
|
+
|
24
|
+
gem install trmnl_preview
|
25
|
+
|
26
|
+
Or install Docker:
|
27
|
+
|
28
|
+
https://docs.docker.com/get-docker/"
|
29
|
+
|
30
|
+
exit 1
|
@@ -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
|
|
@@ -0,0 +1,315 @@
|
|
1
|
+
/*!
|
2
|
+
Highlight.js v11.11.1 (git: 08cb242e7d)
|
3
|
+
(c) 2006-2025 Josh Goebel <hello@joshgoebel.com> and other contributors
|
4
|
+
License: BSD-3-Clause
|
5
|
+
*/
|
6
|
+
var hljs=function(){"use strict";function e(t){
|
7
|
+
return t instanceof Map?t.clear=t.delete=t.set=()=>{
|
8
|
+
throw Error("map is read-only")}:t instanceof Set&&(t.add=t.clear=t.delete=()=>{
|
9
|
+
throw Error("set is read-only")
|
10
|
+
}),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{
|
11
|
+
const i=t[n],s=typeof i;"object"!==s&&"function"!==s||Object.isFrozen(i)||e(i)
|
12
|
+
})),t}class t{constructor(e){
|
13
|
+
void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}
|
14
|
+
ignoreMatch(){this.isMatchIgnored=!0}}function n(e){
|
15
|
+
return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")
|
16
|
+
}function i(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t]
|
17
|
+
;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const s=e=>!!e.scope
|
18
|
+
;class r{constructor(e,t){
|
19
|
+
this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){
|
20
|
+
this.buffer+=n(e)}openNode(e){if(!s(e))return;const t=((e,{prefix:t})=>{
|
21
|
+
if(e.startsWith("language:"))return e.replace("language:","language-")
|
22
|
+
;if(e.includes(".")){const n=e.split(".")
|
23
|
+
;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ")
|
24
|
+
}return`${t}${e}`})(e.scope,{prefix:this.classPrefix});this.span(t)}
|
25
|
+
closeNode(e){s(e)&&(this.buffer+="</span>")}value(){return this.buffer}span(e){
|
26
|
+
this.buffer+=`<span class="${e}">`}}const o=(e={})=>{const t={children:[]}
|
27
|
+
;return Object.assign(t,e),t};class a{constructor(){
|
28
|
+
this.rootNode=o(),this.stack=[this.rootNode]}get top(){
|
29
|
+
return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){
|
30
|
+
this.top.children.push(e)}openNode(e){const t=o({scope:e})
|
31
|
+
;this.add(t),this.stack.push(t)}closeNode(){
|
32
|
+
if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){
|
33
|
+
for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}
|
34
|
+
walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){
|
35
|
+
return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t),
|
36
|
+
t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){
|
37
|
+
"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{
|
38
|
+
a._collapse(e)})))}}class c extends a{constructor(e){super(),this.options=e}
|
39
|
+
addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){
|
40
|
+
this.closeNode()}__addSublanguage(e,t){const n=e.root
|
41
|
+
;t&&(n.scope="language:"+t),this.add(n)}toHTML(){
|
42
|
+
return new r(this,this.options).value()}finalize(){
|
43
|
+
return this.closeAllNodes(),!0}}function l(e){
|
44
|
+
return e?"string"==typeof e?e:e.source:null}function g(e){return h("(?=",e,")")}
|
45
|
+
function u(e){return h("(?:",e,")*")}function d(e){return h("(?:",e,")?")}
|
46
|
+
function h(...e){return e.map((e=>l(e))).join("")}function f(...e){const t=(e=>{
|
47
|
+
const t=e[e.length-1]
|
48
|
+
;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{}
|
49
|
+
})(e);return"("+(t.capture?"":"?:")+e.map((e=>l(e))).join("|")+")"}
|
50
|
+
function p(e){return RegExp(e.toString()+"|").exec("").length-1}
|
51
|
+
const b=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./
|
52
|
+
;function m(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n
|
53
|
+
;let i=l(e),s="";for(;i.length>0;){const e=b.exec(i);if(!e){s+=i;break}
|
54
|
+
s+=i.substring(0,e.index),
|
55
|
+
i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?s+="\\"+(Number(e[1])+t):(s+=e[0],
|
56
|
+
"("===e[0]&&n++)}return s})).map((e=>`(${e})`)).join(t)}
|
57
|
+
const E="[a-zA-Z]\\w*",x="[a-zA-Z_]\\w*",y="\\b\\d+(\\.\\d+)?",_="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",w="\\b(0b[01]+)",O={
|
58
|
+
begin:"\\\\[\\s\\S]",relevance:0},v={scope:"string",begin:"'",end:"'",
|
59
|
+
illegal:"\\n",contains:[O]},k={scope:"string",begin:'"',end:'"',illegal:"\\n",
|
60
|
+
contains:[O]},N=(e,t,n={})=>{const s=i({scope:"comment",begin:e,end:t,
|
61
|
+
contains:[]},n);s.contains.push({scope:"doctag",
|
62
|
+
begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",
|
63
|
+
end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0})
|
64
|
+
;const r=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/)
|
65
|
+
;return s.contains.push({begin:h(/[ ]+/,"(",r,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),s
|
66
|
+
},S=N("//","$"),M=N("/\\*","\\*/"),R=N("#","$");var j=Object.freeze({
|
67
|
+
__proto__:null,APOS_STRING_MODE:v,BACKSLASH_ESCAPE:O,BINARY_NUMBER_MODE:{
|
68
|
+
scope:"number",begin:w,relevance:0},BINARY_NUMBER_RE:w,COMMENT:N,
|
69
|
+
C_BLOCK_COMMENT_MODE:M,C_LINE_COMMENT_MODE:S,C_NUMBER_MODE:{scope:"number",
|
70
|
+
begin:_,relevance:0},C_NUMBER_RE:_,END_SAME_AS_BEGIN:e=>Object.assign(e,{
|
71
|
+
"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{
|
72
|
+
t.data._beginMatch!==e[1]&&t.ignoreMatch()}}),HASH_COMMENT_MODE:R,IDENT_RE:E,
|
73
|
+
MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+x,relevance:0},
|
74
|
+
NUMBER_MODE:{scope:"number",begin:y,relevance:0},NUMBER_RE:y,
|
75
|
+
PHRASAL_WORDS_MODE:{
|
76
|
+
begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
|
77
|
+
},QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/,
|
78
|
+
end:/\/[gimuy]*/,contains:[O,{begin:/\[/,end:/\]/,relevance:0,contains:[O]}]},
|
79
|
+
RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",
|
80
|
+
SHEBANG:(e={})=>{const t=/^#![ ]*\//
|
81
|
+
;return e.binary&&(e.begin=h(t,/.*\b/,e.binary,/\b.*/)),i({scope:"meta",begin:t,
|
82
|
+
end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)},
|
83
|
+
TITLE_MODE:{scope:"title",begin:E,relevance:0},UNDERSCORE_IDENT_RE:x,
|
84
|
+
UNDERSCORE_TITLE_MODE:{scope:"title",begin:x,relevance:0}});function A(e,t){
|
85
|
+
"."===e.input[e.index-1]&&t.ignoreMatch()}function I(e,t){
|
86
|
+
void 0!==e.className&&(e.scope=e.className,delete e.className)}function T(e,t){
|
87
|
+
t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",
|
88
|
+
e.__beforeBegin=A,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,
|
89
|
+
void 0===e.relevance&&(e.relevance=0))}function L(e,t){
|
90
|
+
Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function B(e,t){
|
91
|
+
if(e.match){
|
92
|
+
if(e.begin||e.end)throw Error("begin & end are not supported with match")
|
93
|
+
;e.begin=e.match,delete e.match}}function P(e,t){
|
94
|
+
void 0===e.relevance&&(e.relevance=1)}const D=(e,t)=>{if(!e.beforeMatch)return
|
95
|
+
;if(e.starts)throw Error("beforeMatch cannot be used with starts")
|
96
|
+
;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t]
|
97
|
+
})),e.keywords=n.keywords,e.begin=h(n.beforeMatch,g(n.begin)),e.starts={
|
98
|
+
relevance:0,contains:[Object.assign(n,{endsParent:!0})]
|
99
|
+
},e.relevance=0,delete n.beforeMatch
|
100
|
+
},H=["of","and","for","in","not","or","if","then","parent","list","value"]
|
101
|
+
;function C(e,t,n="keyword"){const i=Object.create(null)
|
102
|
+
;return"string"==typeof e?s(n,e.split(" ")):Array.isArray(e)?s(n,e):Object.keys(e).forEach((n=>{
|
103
|
+
Object.assign(i,C(e[n],t,n))})),i;function s(e,n){
|
104
|
+
t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|")
|
105
|
+
;i[n[0]]=[e,$(n[0],n[1])]}))}}function $(e,t){
|
106
|
+
return t?Number(t):(e=>H.includes(e.toLowerCase()))(e)?0:1}const U={},z=e=>{
|
107
|
+
console.error(e)},W=(e,...t)=>{console.log("WARN: "+e,...t)},X=(e,t)=>{
|
108
|
+
U[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),U[`${e}/${t}`]=!0)
|
109
|
+
},G=Error();function K(e,t,{key:n}){let i=0;const s=e[n],r={},o={}
|
110
|
+
;for(let e=1;e<=t.length;e++)o[e+i]=s[e],r[e+i]=!0,i+=p(t[e-1])
|
111
|
+
;e[n]=o,e[n]._emit=r,e[n]._multi=!0}function F(e){(e=>{
|
112
|
+
e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope,
|
113
|
+
delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={
|
114
|
+
_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope
|
115
|
+
}),(e=>{if(Array.isArray(e.begin)){
|
116
|
+
if(e.skip||e.excludeBegin||e.returnBegin)throw z("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),
|
117
|
+
G
|
118
|
+
;if("object"!=typeof e.beginScope||null===e.beginScope)throw z("beginScope must be object"),
|
119
|
+
G;K(e,e.begin,{key:"beginScope"}),e.begin=m(e.begin,{joinWith:""})}})(e),(e=>{
|
120
|
+
if(Array.isArray(e.end)){
|
121
|
+
if(e.skip||e.excludeEnd||e.returnEnd)throw z("skip, excludeEnd, returnEnd not compatible with endScope: {}"),
|
122
|
+
G
|
123
|
+
;if("object"!=typeof e.endScope||null===e.endScope)throw z("endScope must be object"),
|
124
|
+
G;K(e,e.end,{key:"endScope"}),e.end=m(e.end,{joinWith:""})}})(e)}function Z(e){
|
125
|
+
function t(t,n){
|
126
|
+
return RegExp(l(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":""))
|
127
|
+
}class n{constructor(){
|
128
|
+
this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}
|
129
|
+
addRule(e,t){
|
130
|
+
t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]),
|
131
|
+
this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null)
|
132
|
+
;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(m(e,{joinWith:"|"
|
133
|
+
}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex
|
134
|
+
;const t=this.matcherRe.exec(e);if(!t)return null
|
135
|
+
;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n]
|
136
|
+
;return t.splice(0,n),Object.assign(t,i)}}class s{constructor(){
|
137
|
+
this.rules=[],this.multiRegexes=[],
|
138
|
+
this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){
|
139
|
+
if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n
|
140
|
+
;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))),
|
141
|
+
t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){
|
142
|
+
return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){
|
143
|
+
this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){
|
144
|
+
const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex
|
145
|
+
;let n=t.exec(e)
|
146
|
+
;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{
|
147
|
+
const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)}
|
148
|
+
return n&&(this.regexIndex+=n.position+1,
|
149
|
+
this.regexIndex===this.count&&this.considerAll()),n}}
|
150
|
+
if(e.compilerExtensions||(e.compilerExtensions=[]),
|
151
|
+
e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.")
|
152
|
+
;return e.classNameAliases=i(e.classNameAliases||{}),function n(r,o){const a=r
|
153
|
+
;if(r.isCompiled)return a
|
154
|
+
;[I,B,F,D].forEach((e=>e(r,o))),e.compilerExtensions.forEach((e=>e(r,o))),
|
155
|
+
r.__beforeBegin=null,[T,L,P].forEach((e=>e(r,o))),r.isCompiled=!0;let c=null
|
156
|
+
;return"object"==typeof r.keywords&&r.keywords.$pattern&&(r.keywords=Object.assign({},r.keywords),
|
157
|
+
c=r.keywords.$pattern,
|
158
|
+
delete r.keywords.$pattern),c=c||/\w+/,r.keywords&&(r.keywords=C(r.keywords,e.case_insensitive)),
|
159
|
+
a.keywordPatternRe=t(c,!0),
|
160
|
+
o&&(r.begin||(r.begin=/\B|\b/),a.beginRe=t(a.begin),r.end||r.endsWithParent||(r.end=/\B|\b/),
|
161
|
+
r.end&&(a.endRe=t(a.end)),
|
162
|
+
a.terminatorEnd=l(a.end)||"",r.endsWithParent&&o.terminatorEnd&&(a.terminatorEnd+=(r.end?"|":"")+o.terminatorEnd)),
|
163
|
+
r.illegal&&(a.illegalRe=t(r.illegal)),
|
164
|
+
r.contains||(r.contains=[]),r.contains=[].concat(...r.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>i(e,{
|
165
|
+
variants:null},t)))),e.cachedVariants?e.cachedVariants:V(e)?i(e,{
|
166
|
+
starts:e.starts?i(e.starts):null
|
167
|
+
}):Object.isFrozen(e)?i(e):e))("self"===e?r:e)))),r.contains.forEach((e=>{n(e,a)
|
168
|
+
})),r.starts&&n(r.starts,o),a.matcher=(e=>{const t=new s
|
169
|
+
;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin"
|
170
|
+
}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end"
|
171
|
+
}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function V(e){
|
172
|
+
return!!e&&(e.endsWithParent||V(e.starts))}class q extends Error{
|
173
|
+
constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}}
|
174
|
+
const J=n,Y=i,Q=Symbol("nomatch"),ee=n=>{
|
175
|
+
const i=Object.create(null),s=Object.create(null),r=[];let o=!0
|
176
|
+
;const a="Could not find the language '{}', did you forget to load/include a language module?",l={
|
177
|
+
disableAutodetect:!0,name:"Plain text",contains:[]};let p={
|
178
|
+
ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,
|
179
|
+
languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",
|
180
|
+
cssSelector:"pre code",languages:null,__emitter:c};function b(e){
|
181
|
+
return p.noHighlightRe.test(e)}function m(e,t,n){let i="",s=""
|
182
|
+
;"object"==typeof t?(i=e,
|
183
|
+
n=t.ignoreIllegals,s=t.language):(X("10.7.0","highlight(lang, code, ...args) has been deprecated."),
|
184
|
+
X("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),
|
185
|
+
s=e,i=t),void 0===n&&(n=!0);const r={code:i,language:s};N("before:highlight",r)
|
186
|
+
;const o=r.result?r.result:E(r.language,r.code,n)
|
187
|
+
;return o.code=r.code,N("after:highlight",o),o}function E(e,n,s,r){
|
188
|
+
const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(R)
|
189
|
+
;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(R),n=""
|
190
|
+
;for(;t;){n+=R.substring(e,t.index)
|
191
|
+
;const s=w.case_insensitive?t[0].toLowerCase():t[0],r=(i=s,N.keywords[i]);if(r){
|
192
|
+
const[e,i]=r
|
193
|
+
;if(M.addText(n),n="",c[s]=(c[s]||0)+1,c[s]<=7&&(j+=i),e.startsWith("_"))n+=t[0];else{
|
194
|
+
const n=w.classNameAliases[e]||e;u(t[0],n)}}else n+=t[0]
|
195
|
+
;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(R)}var i
|
196
|
+
;n+=R.substring(e),M.addText(n)}function g(){null!=N.subLanguage?(()=>{
|
197
|
+
if(""===R)return;let e=null;if("string"==typeof N.subLanguage){
|
198
|
+
if(!i[N.subLanguage])return void M.addText(R)
|
199
|
+
;e=E(N.subLanguage,R,!0,S[N.subLanguage]),S[N.subLanguage]=e._top
|
200
|
+
}else e=x(R,N.subLanguage.length?N.subLanguage:null)
|
201
|
+
;N.relevance>0&&(j+=e.relevance),M.__addSublanguage(e._emitter,e.language)
|
202
|
+
})():l(),R=""}function u(e,t){
|
203
|
+
""!==e&&(M.startScope(t),M.addText(e),M.endScope())}function d(e,t){let n=1
|
204
|
+
;const i=t.length-1;for(;n<=i;){if(!e._emit[n]){n++;continue}
|
205
|
+
const i=w.classNameAliases[e[n]]||e[n],s=t[n];i?u(s,i):(R=s,l(),R=""),n++}}
|
206
|
+
function h(e,t){
|
207
|
+
return e.scope&&"string"==typeof e.scope&&M.openNode(w.classNameAliases[e.scope]||e.scope),
|
208
|
+
e.beginScope&&(e.beginScope._wrap?(u(R,w.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),
|
209
|
+
R=""):e.beginScope._multi&&(d(e.beginScope,t),R="")),N=Object.create(e,{parent:{
|
210
|
+
value:N}}),N}function f(e,n,i){let s=((e,t)=>{const n=e&&e.exec(t)
|
211
|
+
;return n&&0===n.index})(e.endRe,i);if(s){if(e["on:end"]){const i=new t(e)
|
212
|
+
;e["on:end"](n,i),i.isMatchIgnored&&(s=!1)}if(s){
|
213
|
+
for(;e.endsParent&&e.parent;)e=e.parent;return e}}
|
214
|
+
if(e.endsWithParent)return f(e.parent,n,i)}function b(e){
|
215
|
+
return 0===N.matcher.regexIndex?(R+=e[0],1):(T=!0,0)}function m(e){
|
216
|
+
const t=e[0],i=n.substring(e.index),s=f(N,e,i);if(!s)return Q;const r=N
|
217
|
+
;N.endScope&&N.endScope._wrap?(g(),
|
218
|
+
u(t,N.endScope._wrap)):N.endScope&&N.endScope._multi?(g(),
|
219
|
+
d(N.endScope,e)):r.skip?R+=t:(r.returnEnd||r.excludeEnd||(R+=t),
|
220
|
+
g(),r.excludeEnd&&(R=t));do{
|
221
|
+
N.scope&&M.closeNode(),N.skip||N.subLanguage||(j+=N.relevance),N=N.parent
|
222
|
+
}while(N!==s.parent);return s.starts&&h(s.starts,e),r.returnEnd?0:t.length}
|
223
|
+
let y={};function _(i,r){const a=r&&r[0];if(R+=i,null==a)return g(),0
|
224
|
+
;if("begin"===y.type&&"end"===r.type&&y.index===r.index&&""===a){
|
225
|
+
if(R+=n.slice(r.index,r.index+1),!o){const t=Error(`0 width match regex (${e})`)
|
226
|
+
;throw t.languageName=e,t.badRule=y.rule,t}return 1}
|
227
|
+
if(y=r,"begin"===r.type)return(e=>{
|
228
|
+
const n=e[0],i=e.rule,s=new t(i),r=[i.__beforeBegin,i["on:begin"]]
|
229
|
+
;for(const t of r)if(t&&(t(e,s),s.isMatchIgnored))return b(n)
|
230
|
+
;return i.skip?R+=n:(i.excludeBegin&&(R+=n),
|
231
|
+
g(),i.returnBegin||i.excludeBegin||(R=n)),h(i,e),i.returnBegin?0:n.length})(r)
|
232
|
+
;if("illegal"===r.type&&!s){
|
233
|
+
const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"<unnamed>")+'"')
|
234
|
+
;throw e.mode=N,e}if("end"===r.type){const e=m(r);if(e!==Q)return e}
|
235
|
+
if("illegal"===r.type&&""===a)return R+="\n",1
|
236
|
+
;if(I>1e5&&I>3*r.index)throw Error("potential infinite loop, way more iterations than matches")
|
237
|
+
;return R+=a,a.length}const w=O(e)
|
238
|
+
;if(!w)throw z(a.replace("{}",e)),Error('Unknown language: "'+e+'"')
|
239
|
+
;const v=Z(w);let k="",N=r||v;const S={},M=new p.__emitter(p);(()=>{const e=[]
|
240
|
+
;for(let t=N;t!==w;t=t.parent)t.scope&&e.unshift(t.scope)
|
241
|
+
;e.forEach((e=>M.openNode(e)))})();let R="",j=0,A=0,I=0,T=!1;try{
|
242
|
+
if(w.__emitTokens)w.__emitTokens(n,M);else{for(N.matcher.considerAll();;){
|
243
|
+
I++,T?T=!1:N.matcher.considerAll(),N.matcher.lastIndex=A
|
244
|
+
;const e=N.matcher.exec(n);if(!e)break;const t=_(n.substring(A,e.index),e)
|
245
|
+
;A=e.index+t}_(n.substring(A))}return M.finalize(),k=M.toHTML(),{language:e,
|
246
|
+
value:k,relevance:j,illegal:!1,_emitter:M,_top:N}}catch(t){
|
247
|
+
if(t.message&&t.message.includes("Illegal"))return{language:e,value:J(n),
|
248
|
+
illegal:!0,relevance:0,_illegalBy:{message:t.message,index:A,
|
249
|
+
context:n.slice(A-100,A+100),mode:t.mode,resultSoFar:k},_emitter:M};if(o)return{
|
250
|
+
language:e,value:J(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:N}
|
251
|
+
;throw t}}function x(e,t){t=t||p.languages||Object.keys(i);const n=(e=>{
|
252
|
+
const t={value:J(e),illegal:!1,relevance:0,_top:l,_emitter:new p.__emitter(p)}
|
253
|
+
;return t._emitter.addText(e),t})(e),s=t.filter(O).filter(k).map((t=>E(t,e,!1)))
|
254
|
+
;s.unshift(n);const r=s.sort(((e,t)=>{
|
255
|
+
if(e.relevance!==t.relevance)return t.relevance-e.relevance
|
256
|
+
;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1
|
257
|
+
;if(O(t.language).supersetOf===e.language)return-1}return 0})),[o,a]=r,c=o
|
258
|
+
;return c.secondBest=a,c}function y(e){let t=null;const n=(e=>{
|
259
|
+
let t=e.className+" ";t+=e.parentNode?e.parentNode.className:""
|
260
|
+
;const n=p.languageDetectRe.exec(t);if(n){const t=O(n[1])
|
261
|
+
;return t||(W(a.replace("{}",n[1])),
|
262
|
+
W("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"}
|
263
|
+
return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return
|
264
|
+
;if(N("before:highlightElement",{el:e,language:n
|
265
|
+
}),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e)
|
266
|
+
;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),
|
267
|
+
console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),
|
268
|
+
console.warn("The element with unescaped HTML:"),
|
269
|
+
console.warn(e)),p.throwUnescapedHTML))throw new q("One of your code blocks includes unescaped HTML.",e.innerHTML)
|
270
|
+
;t=e;const i=t.textContent,r=n?m(i,{language:n,ignoreIllegals:!0}):x(i)
|
271
|
+
;e.innerHTML=r.value,e.dataset.highlighted="yes",((e,t,n)=>{const i=t&&s[t]||n
|
272
|
+
;e.classList.add("hljs"),e.classList.add("language-"+i)
|
273
|
+
})(e,n,r.language),e.result={language:r.language,re:r.relevance,
|
274
|
+
relevance:r.relevance},r.secondBest&&(e.secondBest={
|
275
|
+
language:r.secondBest.language,relevance:r.secondBest.relevance
|
276
|
+
}),N("after:highlightElement",{el:e,result:r,text:i})}let _=!1;function w(){
|
277
|
+
if("loading"===document.readyState)return _||window.addEventListener("DOMContentLoaded",(()=>{
|
278
|
+
w()}),!1),void(_=!0);document.querySelectorAll(p.cssSelector).forEach(y)}
|
279
|
+
function O(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]}
|
280
|
+
function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{
|
281
|
+
s[e.toLowerCase()]=t}))}function k(e){const t=O(e)
|
282
|
+
;return t&&!t.disableAutodetect}function N(e,t){const n=e;r.forEach((e=>{
|
283
|
+
e[n]&&e[n](t)}))}Object.assign(n,{highlight:m,highlightAuto:x,highlightAll:w,
|
284
|
+
highlightElement:y,
|
285
|
+
highlightBlock:e=>(X("10.7.0","highlightBlock will be removed entirely in v12.0"),
|
286
|
+
X("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{p=Y(p,e)},
|
287
|
+
initHighlighting:()=>{
|
288
|
+
w(),X("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")},
|
289
|
+
initHighlightingOnLoad:()=>{
|
290
|
+
w(),X("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")
|
291
|
+
},registerLanguage:(e,t)=>{let s=null;try{s=t(n)}catch(t){
|
292
|
+
if(z("Language definition for '{}' could not be registered.".replace("{}",e)),
|
293
|
+
!o)throw t;z(t),s=l}
|
294
|
+
s.name||(s.name=e),i[e]=s,s.rawDefinition=t.bind(null,n),s.aliases&&v(s.aliases,{
|
295
|
+
languageName:e})},unregisterLanguage:e=>{delete i[e]
|
296
|
+
;for(const t of Object.keys(s))s[t]===e&&delete s[t]},
|
297
|
+
listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:v,
|
298
|
+
autoDetection:k,inherit:Y,addPlugin:e=>{(e=>{
|
299
|
+
e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{
|
300
|
+
e["before:highlightBlock"](Object.assign({block:t.el},t))
|
301
|
+
}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{
|
302
|
+
e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),r.push(e)},
|
303
|
+
removePlugin:e=>{const t=r.indexOf(e);-1!==t&&r.splice(t,1)}}),n.debugMode=()=>{
|
304
|
+
o=!1},n.safeMode=()=>{o=!0},n.versionString="11.11.1",n.regex={concat:h,
|
305
|
+
lookahead:g,either:f,optional:d,anyNumberOfTimes:u}
|
306
|
+
;for(const t in j)"object"==typeof j[t]&&e(j[t]);return Object.assign(n,j),n
|
307
|
+
},te=ee({});return te.newInstance=()=>ee({}),te}()
|
308
|
+
;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `json` grammar compiled for Highlight.js 11.11.1 */
|
309
|
+
(()=>{var e=(()=>{"use strict";return e=>{const a=["true","false","null"],s={
|
310
|
+
scope:"literal",beginKeywords:a.join(" ")};return{name:"JSON",aliases:["jsonc"],
|
311
|
+
keywords:{literal:a},contains:[{className:"attr",
|
312
|
+
begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{match:/[{}[\],:]/,
|
313
|
+
className:"punctuation",relevance:0
|
314
|
+
},e.QUOTE_STRING_MODE,s,e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],
|
315
|
+
illegal:"\\S"}}})();hljs.registerLanguage("json",e)})();
|
@@ -0,0 +1 @@
|
|
1
|
+
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{color:#abb2bf;background:#282c34}.hljs-comment,.hljs-quote{color:#5c6370;font-style:italic}.hljs-doctag,.hljs-formula,.hljs-keyword{color:#c678dd}.hljs-deletion,.hljs-name,.hljs-section,.hljs-selector-tag,.hljs-subst{color:#e06c75}.hljs-literal{color:#56b6c2}.hljs-addition,.hljs-attribute,.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#98c379}.hljs-attr,.hljs-number,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-pseudo,.hljs-template-variable,.hljs-type,.hljs-variable{color:#d19a66}.hljs-bullet,.hljs-link,.hljs-meta,.hljs-selector-id,.hljs-symbol,.hljs-title{color:#61aeee}.hljs-built_in,.hljs-class .hljs-title,.hljs-title.class_{color:#e6c07b}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-link{text-decoration:underline}
|
data/web/public/index.css
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
body {
|
2
2
|
font-family: sans-serif;
|
3
3
|
margin: 10px;
|
4
|
+
background-color: #eaebef;
|
4
5
|
}
|
5
6
|
|
6
7
|
main {
|
@@ -14,11 +15,12 @@ menu {
|
|
14
15
|
margin: 0;
|
15
16
|
display: flex;
|
16
17
|
justify-content: space-between;
|
18
|
+
max-width: 950px;
|
17
19
|
}
|
18
20
|
|
19
21
|
menu a {
|
20
22
|
padding: 0.5em 1em;
|
21
|
-
background: #
|
23
|
+
background: #c7cbd5;
|
22
24
|
border-radius: 0.5em;
|
23
25
|
display: inline-block;
|
24
26
|
text-decoration: none;
|
@@ -34,6 +36,12 @@ menu a.active {
|
|
34
36
|
color: white;
|
35
37
|
}
|
36
38
|
|
39
|
+
pre > code {
|
40
|
+
max-width: 950px;
|
41
|
+
overflow-x: scroll;
|
42
|
+
border-radius: 0.5em;
|
43
|
+
}
|
44
|
+
|
37
45
|
.spinner {
|
38
46
|
width: 22px;
|
39
47
|
height: 22px;
|
@@ -51,3 +59,19 @@ menu a.active {
|
|
51
59
|
transform: rotate(360deg);
|
52
60
|
}
|
53
61
|
}
|
62
|
+
|
63
|
+
@media (prefers-color-scheme: dark) {
|
64
|
+
body {
|
65
|
+
background-color: #191b21;
|
66
|
+
color: #ffffff;
|
67
|
+
}
|
68
|
+
|
69
|
+
menu a {
|
70
|
+
background-color: #262932;
|
71
|
+
color: #b1b6c4;
|
72
|
+
}
|
73
|
+
|
74
|
+
menu a:hover {
|
75
|
+
background-color: #2f333e;
|
76
|
+
}
|
77
|
+
}
|
@@ -204,7 +204,7 @@
|
|
204
204
|
|
205
205
|
|
206
206
|
|
207
|
-
<foreignobject class="node" x="36" y="34" width="
|
207
|
+
<foreignobject class="node" x="36" y="34" width="830" height="520"
|
208
208
|
style="transform:scale(0.98); position: relative; border-radius: 12px; opacity: 0.9; mix-blend-mode: darken;">
|
209
209
|
<div id="${contentWrapperId}"
|
210
210
|
style="position: static; width: 100%; height: 100%; max-width: 100%; max-height: 100%;">
|
data/web/views/index.erb
CHANGED
@@ -11,6 +11,11 @@
|
|
11
11
|
<link rel="stylesheet" href="/index.css">
|
12
12
|
<script src="/trmnl-component.js" defer></script>
|
13
13
|
<script src="/index.js"></script>
|
14
|
+
|
15
|
+
<!-- Highlight.js for syntax highlighting -->
|
16
|
+
<link rel="stylesheet" href="/highlight/styles/atom-one-dark.min.css">
|
17
|
+
<script src="/highlight/highlight.min.js"></script>
|
18
|
+
<script>hljs.highlightAll();</script>
|
14
19
|
</head>
|
15
20
|
<body>
|
16
21
|
<main>
|
@@ -24,7 +29,7 @@
|
|
24
29
|
<div style="display: flex; gap: 0.3em; align-items: center;">
|
25
30
|
<div class="spinner" style="display: none;"></div>
|
26
31
|
<select class="select-format">
|
27
|
-
<option value="
|
32
|
+
<option value="png">PNG</option>
|
28
33
|
<option value="html" selected>HTML</option>
|
29
34
|
</select>
|
30
35
|
<select class="select-case">
|
@@ -41,7 +46,7 @@
|
|
41
46
|
|
42
47
|
<trmnl-frame>Rendering…</trmnl-frame>
|
43
48
|
|
44
|
-
<pre id="user-data"><%= @user_data %></pre>
|
49
|
+
<pre id="user-data"><code><%= @user_data %></code></pre>
|
45
50
|
</main>
|
46
51
|
</body>
|
47
52
|
</html>
|
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.5
|
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-27 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
|
@@ -299,19 +286,19 @@ files:
|
|
299
286
|
- lib/trmnlp/screen_generator.rb
|
300
287
|
- lib/trmnlp/version.rb
|
301
288
|
- templates/init/.trmnlp.yml
|
302
|
-
- templates/init/bin/
|
289
|
+
- templates/init/bin/trmnlp
|
303
290
|
- templates/init/src/full.liquid
|
304
291
|
- templates/init/src/half_horizontal.liquid
|
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/
|
310
|
-
- web/public/
|
297
|
+
- web/public/highlight/highlight.min.js
|
298
|
+
- web/public/highlight/styles/atom-one-dark.min.css
|
311
299
|
- web/public/index.css
|
312
300
|
- web/public/index.js
|
313
301
|
- web/public/trmnl-component.js
|
314
|
-
- web/public/white-case.jpg
|
315
302
|
- web/views/index.erb
|
316
303
|
- web/views/render_html.erb
|
317
304
|
homepage: https://github.com/usetrmnl/trmnlp
|
data/templates/init/bin/dev
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
#! /bin/bash
|
2
|
-
|
3
|
-
if command -v trmnlp &> /dev/null
|
4
|
-
then
|
5
|
-
echo "Starting trmnlp..."
|
6
|
-
trmnlp serve
|
7
|
-
exit
|
8
|
-
fi
|
9
|
-
|
10
|
-
if command -v docker &> /dev/null
|
11
|
-
then
|
12
|
-
echo "Running trmnl/trmnlp container..."
|
13
|
-
docker run -p 4567:4567 -v .:/plugin trmnl/trmnlp
|
14
|
-
exit
|
15
|
-
fi
|
16
|
-
|
17
|
-
echo "Install the trmnl_preview gem:
|
18
|
-
|
19
|
-
gem install trmnl_preview
|
20
|
-
|
21
|
-
Or install Docker:
|
22
|
-
|
23
|
-
https://docs.docker.com/get-docker/"
|
24
|
-
|
25
|
-
exit 1
|
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
|