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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c67d7a534ea6adba08ab5b46d40f60ec8e505123136916ba98e43991c6877f58
4
- data.tar.gz: 5a22288cb32a76b2b8cb8d8163a38c964736e6fd7c067da6f76f9fe8c7401ec1
3
+ metadata.gz: 16a333009aab09891c12d6fe7bf3b8662912cd6e376f31c1a4997de5a6c93314
4
+ data.tar.gz: cf57e05fa4ba41059e55fb7281d7644ee07772459b63503ebe4a4023fae14085
5
5
  SHA512:
6
- metadata.gz: 9df016907e67d1cdc0b771299162b769c83b2bb2c7d9ab4b0e5bd77cf8db3f6a8f15db93ad4a00810e0fd224cb2e3dbee1876c9fe416854f26f239704a28537a
7
- data.tar.gz: bc4e3d5c46fc64d25a3791c77c0e43af5a84875c2ad2853742e3ba31cf7a5bbdef5a548d92a4bab4b27b048e089eae4e712cd99eec43ad1ee0902f27e39a364c
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 BMP images (slower, but more accurate).
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
- ### Via RubyGems
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 BMP rendering (optional):
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
- ### Via Docker (`trmnlp serve` only)
73
+ ### Installing via Docker
70
74
 
71
75
  ```sh
72
76
  docker run \
73
- -p 4567:4567 \
74
- -v /path/to/plugin/on/host:/plugin \
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")
@@ -18,20 +18,24 @@ module TRMNLP
18
18
  temp_file.write(response.body)
19
19
  temp_file.rewind
20
20
 
21
- # return the path to the temp file
22
- Pathname.new(temp_file.path)
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: Faraday::Multipart::FilePart.new(path, 'application/zip')
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}.bmp" do
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
- img_path = generator.process
94
- send_file img_path, type: 'image/png', disposition: 'inline'
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: '127.0.0.1', aliases: '-b', desc: 'Bind address'
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
@@ -19,11 +19,11 @@ module TRMNLP
19
19
  end
20
20
 
21
21
  api = APIClient.new(config)
22
- temp_path = api.get_plugin_setting_archive(plugin_settings_id)
22
+ tempfile = api.get_plugin_setting_archive(plugin_settings_id)
23
23
  size = 0
24
24
 
25
25
  begin
26
- Zip::File.open(temp_path) do |zip_file|
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(temp_path)
34
+ size = File.size(tempfile.path)
35
35
  ensure
36
- temp_path.delete
36
+ tempfile.close
37
37
  end
38
38
 
39
39
  puts "Downloaded plugin (#{size} bytes)"
@@ -29,18 +29,18 @@ module TRMNLP
29
29
 
30
30
  size = 0
31
31
 
32
- Tempfile.create(binmode: true) do |temp_file|
33
- Zip::File.open(temp_file.path, Zip::File::CREATE) do |zip_file|
34
- paths.src_files.each do |file|
35
- zip_file.add(File.basename(file), file)
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)
@@ -3,9 +3,9 @@ require 'yaml'
3
3
  module TRMNLP
4
4
  class Config
5
5
  class Plugin
6
- def initialize(paths, trmnlp_config)
6
+ def initialize(paths, project_config)
7
7
  @paths = paths
8
- @trmnlp_config = trmnlp_config
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
- return [] if @config['polling_url'].nil? || @config['polling_url'].empty?
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
- urls = @config['polling_url'].split("\n").map(&:strip)
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, :trmnlp_config
60
+ attr_reader :paths, :project_config
60
61
 
61
- def with_custom_fields(value) = trmnlp_config.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['custom_fields'] || {}
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.path
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
- # context = browser_instance.create_incognito_browser_context
35
- page = firefox_browser.new_page
36
- page.viewport = Puppeteer::Viewport.new(width: width, height: height)
37
- # NOTE: Use below for chromium
38
- # page.set_content(input, wait_until: ['networkidle0', 'domcontentloaded'])
39
- # Note: Use below for firefox
40
- page.set_content(input, timeout: 10000)
41
- page.evaluate(<<~JAVASCRIPT)
42
- () => {
43
- document.getElementsByTagName('html')[0].style.overflow = "hidden";
44
- document.getElementsByTagName('body')[0].style.overflow = "hidden";
45
- }
46
- JAVASCRIPT
47
- self.output = Tempfile.new
48
- page.screenshot(path: output.path, type: 'png')
49
- firefox_browser.close
50
- end
51
- rescue Puppeteer::TimeoutError, Puppeteer::FrameManager::NavigationError => e
52
- retry_count += 1
53
- firefox_browser.close
54
- if retry_count <= 1
55
- @browser = nil
56
- retry
57
- else
58
- puts "ERROR -> Converter::Html#convert_to_image_by_firefox -> #{e.message}"
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 << ('bmp3:' << img.path)
158
+ m << img.path
104
159
  end
105
160
  end
106
161
 
107
162
  def mono_image(img)
108
- # Changelog:
109
- # ImageMagick 6.XX used to convert the png to bitmap with dithering while maintaining the channel to 1
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
- m.depth(color_depth) # Should be set to 1 for 1-bit output
118
- m.strip # Remove any additional metadata
119
- m << ('bmp3:' << img.path) # Converts to Bitmap.
120
- end
121
- MiniMagick::Tool::Convert.new do |m|
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRMNLP
4
- VERSION = "0.5.3".freeze
4
+ VERSION = "0.5.5".freeze
5
5
  end
@@ -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
+
@@ -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
- # BMP rendering
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;")
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: #ddd;
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="840" height="520"
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="bmp">BMP</option>
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.3
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-05-22 00:00:00.000000000 Z
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/dev
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/black-case.jpg
310
- - web/public/clear-case.jpg
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
@@ -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
Binary file
Binary file
Binary file