trmnl_preview 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -1
- data/README.md +180 -5
- data/bin/rake +6 -6
- data/bin/trmnlp +1 -0
- data/db/data/form_fields.yml +24 -0
- data/db/data/framework_versions.yml +72 -0
- data/lib/trmnlp/api_client.rb +41 -28
- data/lib/trmnlp/app.rb +73 -44
- data/lib/trmnlp/browser_pool.rb +82 -0
- data/lib/trmnlp/cli.rb +24 -11
- data/lib/trmnlp/commands/base.rb +33 -10
- data/lib/trmnlp/commands/build.rb +13 -8
- data/lib/trmnlp/commands/clone.rb +12 -7
- data/lib/trmnlp/commands/init.rb +17 -13
- data/lib/trmnlp/commands/lint.rb +42 -0
- data/lib/trmnlp/commands/list.rb +40 -0
- data/lib/trmnlp/commands/login.rb +28 -13
- data/lib/trmnlp/commands/pull.rb +14 -6
- data/lib/trmnlp/commands/push.rb +29 -19
- data/lib/trmnlp/commands/serve.rb +32 -3
- data/lib/trmnlp/commands.rb +3 -1
- data/lib/trmnlp/config/app.rb +6 -3
- data/lib/trmnlp/config/plugin.rb +56 -14
- data/lib/trmnlp/config/project.rb +59 -7
- data/lib/trmnlp/config.rb +3 -1
- data/lib/trmnlp/context.rb +21 -224
- data/lib/trmnlp/errors.rb +15 -0
- data/lib/trmnlp/form_field.rb +42 -0
- data/lib/trmnlp/framework_version.rb +69 -0
- data/lib/trmnlp/image_quantizer.rb +58 -0
- data/lib/trmnlp/lint/check.rb +31 -0
- data/lib/trmnlp/lint/checks/custom_fields_used.rb +32 -0
- data/lib/trmnlp/lint/checks/form_fields_valid.rb +20 -0
- data/lib/trmnlp/lint/checks/highcharts_animations_disabled.rb +23 -0
- data/lib/trmnlp/lint/checks/highcharts_elements_unique.rb +24 -0
- data/lib/trmnlp/lint/checks/image_links_reachable.rb +53 -0
- data/lib/trmnlp/lint/checks/layouts_have_content.rb +24 -0
- data/lib/trmnlp/lint/checks/limited_inline_styles.rb +26 -0
- data/lib/trmnlp/lint/checks/no_async_functions.rb +18 -0
- data/lib/trmnlp/lint/checks/no_opacity.rb +19 -0
- data/lib/trmnlp/lint/checks/no_size_classes.rb +19 -0
- data/lib/trmnlp/lint/checks/title_casing.rb +20 -0
- data/lib/trmnlp/lint/checks/title_length.rb +18 -0
- data/lib/trmnlp/lint/checks/waits_for_dom_load.rb +23 -0
- data/lib/trmnlp/lint/source.rb +42 -0
- data/lib/trmnlp/lint.rb +39 -0
- data/lib/trmnlp/paths.rb +28 -8
- data/lib/trmnlp/poller.rb +105 -0
- data/lib/trmnlp/renderer.rb +87 -0
- data/lib/trmnlp/reporter.rb +28 -0
- data/lib/trmnlp/screen.rb +16 -0
- data/lib/trmnlp/screen_generator.rb +11 -217
- data/lib/trmnlp/screenshot.rb +96 -0
- data/lib/trmnlp/transform_backend/http.rb +107 -0
- data/lib/trmnlp/transform_backend/subprocess.rb +130 -0
- data/lib/trmnlp/transform_backend/wrapper.rb +113 -0
- data/lib/trmnlp/transform_client.rb +47 -0
- data/lib/trmnlp/transform_pipeline.rb +65 -0
- data/lib/trmnlp/user_data_assembler.rb +96 -0
- data/lib/trmnlp/version.rb +1 -1
- data/lib/trmnlp/watcher.rb +60 -0
- data/lib/trmnlp.rb +6 -10
- data/templates/init/bin/trmnlp +1 -1
- data/templates/init/src/settings.yml +1 -0
- data/templates/init/src/transform.py.example +14 -0
- data/trmnl_preview.gemspec +34 -34
- data/web/public/index.css +6 -0
- data/web/public/index.js +31 -18
- data/web/views/index.erb +6 -1
- data/web/views/render_html.erb +4 -2
- metadata +81 -56
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRMNLP
|
|
4
|
+
# Strategy host for serverless transform execution. Local plugin
|
|
5
|
+
# development hits the Subprocess backend by default — transforms
|
|
6
|
+
# run in trmnlp's own image alongside Ruby, no daemon required.
|
|
7
|
+
# Setting `serverless_daemon_url` in .trmnlp.yml swaps in the Http
|
|
8
|
+
# backend so plugin authors can target a real remote transform
|
|
9
|
+
# daemon (production parity testing, shared team daemons, etc.).
|
|
10
|
+
class TransformClient
|
|
11
|
+
# Mirrors the remote daemon's ExecResponse. `output` is the canonical
|
|
12
|
+
# JSON result; `stdout` carries user prints.
|
|
13
|
+
Result = Data.define(:stdout, :stderr, :output, :exit_code, :duration_ms, :error) do
|
|
14
|
+
def success? = error.nil? && exit_code.zero?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :backend
|
|
18
|
+
|
|
19
|
+
# Returns nil when serverless is disabled in .trmnlp.yml so the
|
|
20
|
+
# pipeline can short-circuit without a per-request check.
|
|
21
|
+
def self.from_config(project_config)
|
|
22
|
+
runtime = project_config.transform_runtime
|
|
23
|
+
return nil if runtime.nil? || runtime.to_s == 'disabled'
|
|
24
|
+
|
|
25
|
+
new(backend: backend_for(project_config))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.backend_for(project_config)
|
|
29
|
+
if (url = project_config.serverless_daemon_url)
|
|
30
|
+
TransformBackend::Http.new(url: url, api_key: project_config.serverless_daemon_api_key)
|
|
31
|
+
else
|
|
32
|
+
TransformBackend::Subprocess.new
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(backend:)
|
|
37
|
+
@backend = backend
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def execute(code:, language:, stdin: '', timeout_seconds: 30)
|
|
41
|
+
backend.execute(code:, language:, stdin:, timeout_seconds:)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
require_relative 'transform_backend/subprocess'
|
|
47
|
+
require_relative 'transform_backend/http'
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
require_relative 'reporter'
|
|
6
|
+
require_relative 'transform_client'
|
|
7
|
+
|
|
8
|
+
module TRMNLP
|
|
9
|
+
# Pipes assembled merge_variables through src/transform.{py,rb,php,js}
|
|
10
|
+
# when a serverless runtime is configured. Mirrors the hosted
|
|
11
|
+
# transform behavior: the transform receives the data (including the
|
|
12
|
+
# trmnl namespace) on stdin and its stdout JSON replaces the data.
|
|
13
|
+
# Failure modes surface via #error (rendered in the preview UI), not
|
|
14
|
+
# raised.
|
|
15
|
+
class TransformPipeline
|
|
16
|
+
attr_reader :error
|
|
17
|
+
|
|
18
|
+
def initialize(config:, paths:, reporter: Reporter.new)
|
|
19
|
+
@config = config
|
|
20
|
+
@paths = paths
|
|
21
|
+
@reporter = reporter
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call(data)
|
|
25
|
+
@error = nil
|
|
26
|
+
transform_path, inferred_language = paths.transform_file
|
|
27
|
+
return data unless transform_path && client
|
|
28
|
+
|
|
29
|
+
run(transform_path, inferred_language, data)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def reset! = @client = nil
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
attr_reader :config, :paths, :reporter
|
|
37
|
+
|
|
38
|
+
def client = @client ||= TransformClient.from_config(config.project)
|
|
39
|
+
|
|
40
|
+
def run(path, inferred_language, data)
|
|
41
|
+
language = config.plugin.serverless_language || inferred_language
|
|
42
|
+
result = client.execute(code: path.read, stdin: JSON.generate(data), language:)
|
|
43
|
+
return record_failure(result, data) unless result.success?
|
|
44
|
+
|
|
45
|
+
parse_output(result.output, data)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def record_failure(result, fallback)
|
|
49
|
+
@error = result.error || "transform exited #{result.exit_code}: #{result.stderr.strip}"
|
|
50
|
+
reporter.info("transform failed: #{@error}")
|
|
51
|
+
fallback
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse_output(output, fallback)
|
|
55
|
+
transformed = JSON.parse(output)
|
|
56
|
+
transformed.is_a?(Hash) ? transformed : wrap_array(transformed)
|
|
57
|
+
rescue JSON::ParserError => e
|
|
58
|
+
@error = "transform produced non-JSON output: #{e.message}"
|
|
59
|
+
reporter.info(@error)
|
|
60
|
+
fallback
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def wrap_array(json) = json.is_a?(Array) ? { data: json } : json
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support'
|
|
4
|
+
require 'active_support/time'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module TRMNLP
|
|
8
|
+
class UserDataAssembler
|
|
9
|
+
DEFAULT_DEVICE_WIDTH = 800
|
|
10
|
+
DEFAULT_DEVICE_HEIGHT = 480
|
|
11
|
+
|
|
12
|
+
def initialize(config:, paths:, transform_pipeline:)
|
|
13
|
+
@config = config
|
|
14
|
+
@paths = paths
|
|
15
|
+
@transform_pipeline = transform_pipeline
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Assembles the merged data hash. The trmnl namespace is built first,
|
|
19
|
+
# layered with static_data / cached polled data / user_data_overrides,
|
|
20
|
+
# then piped through the transform. The trmnl namespace is re-applied
|
|
21
|
+
# after the transform so device, user, and plugin_settings survive
|
|
22
|
+
# even when the transform doesn't pass them through.
|
|
23
|
+
def call(device: {})
|
|
24
|
+
namespace = base_trmnl_data(device:)
|
|
25
|
+
merged = assemble(namespace)
|
|
26
|
+
result = transform_pipeline.call(merged)
|
|
27
|
+
result['trmnl'] = namespace['trmnl']
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def device_from_params(params)
|
|
32
|
+
{ 'width' => params[:width]&.to_i, 'height' => params[:height]&.to_i }.compact
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :config, :paths, :transform_pipeline
|
|
38
|
+
|
|
39
|
+
def assemble(namespace)
|
|
40
|
+
data = namespace.dup
|
|
41
|
+
merge_source_data!(data)
|
|
42
|
+
data.deep_merge!(config.project.user_data_overrides)
|
|
43
|
+
data
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def merge_source_data!(data)
|
|
47
|
+
if config.plugin.static?
|
|
48
|
+
data.merge!(config.plugin.static_data)
|
|
49
|
+
elsif paths.user_data.exist?
|
|
50
|
+
data.merge!(JSON.parse(paths.user_data.read))
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def base_trmnl_data(device:)
|
|
55
|
+
{ 'trmnl' => trmnl_namespace(device:) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def trmnl_namespace(device:)
|
|
59
|
+
{
|
|
60
|
+
'user' => user_namespace,
|
|
61
|
+
'device' => device_namespace(device),
|
|
62
|
+
'system' => { 'timestamp_utc' => Time.now.utc.to_i },
|
|
63
|
+
'plugin_settings' => plugin_settings_namespace
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def user_namespace
|
|
68
|
+
tz = ActiveSupport::TimeZone.find_tzinfo(config.project.time_zone)
|
|
69
|
+
iana = tz.name
|
|
70
|
+
{
|
|
71
|
+
'name' => 'name', 'first_name' => 'first_name', 'last_name' => 'last_name',
|
|
72
|
+
'locale' => 'en', 'time_zone' => ActiveSupport::TimeZone::MAPPING.invert[iana] || iana,
|
|
73
|
+
'time_zone_iana' => iana, 'utc_offset' => tz.utc_offset
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def device_namespace(device)
|
|
78
|
+
{
|
|
79
|
+
'friendly_id' => 'ABC123', 'percent_charged' => 85.0, 'wifi_strength' => 90,
|
|
80
|
+
'height' => device['height'] || DEFAULT_DEVICE_HEIGHT,
|
|
81
|
+
'width' => device['width'] || DEFAULT_DEVICE_WIDTH
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def plugin_settings_namespace
|
|
86
|
+
{
|
|
87
|
+
'instance_name' => 'instance_name',
|
|
88
|
+
'strategy' => config.plugin.strategy,
|
|
89
|
+
'dark_mode' => config.plugin.dark_mode,
|
|
90
|
+
'polling_headers' => config.plugin.polling_headers_encoded,
|
|
91
|
+
'polling_url' => config.plugin.polling_url_text,
|
|
92
|
+
'custom_fields_values' => config.project.custom_fields
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
data/lib/trmnlp/version.rb
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'filewatcher'
|
|
4
|
+
|
|
5
|
+
require_relative 'reporter'
|
|
6
|
+
|
|
7
|
+
module TRMNLP
|
|
8
|
+
class Watcher
|
|
9
|
+
def initialize(config:, user_data_assembler:, transform_pipeline:, reporter: Reporter.new)
|
|
10
|
+
@config = config
|
|
11
|
+
@user_data_assembler = user_data_assembler
|
|
12
|
+
@transform_pipeline = transform_pipeline
|
|
13
|
+
@reporter = reporter
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def start
|
|
17
|
+
@start ||= Thread.new { run }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def on_change(&block)
|
|
21
|
+
@view_change_callback = block
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
attr_reader :config, :user_data_assembler, :transform_pipeline, :reporter
|
|
27
|
+
|
|
28
|
+
def run
|
|
29
|
+
loop do
|
|
30
|
+
watch_cycle
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
reporter.info("error during live render: #{e}")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def watch_cycle
|
|
37
|
+
Filewatcher.new(config.project.watch_paths).watch do |changes|
|
|
38
|
+
reload_config!
|
|
39
|
+
notify(changes)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reload_config!
|
|
44
|
+
config.project.reload!
|
|
45
|
+
config.plugin.reload!
|
|
46
|
+
transform_pipeline.reset! # config may have changed runtime config
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# NOTE: transform.* changes don't trigger a re-poll — the transform
|
|
50
|
+
# runs inside user_data on every render against the cached polled
|
|
51
|
+
# response (or static_data), so editing the transform updates the
|
|
52
|
+
# preview without re-fetching the API.
|
|
53
|
+
def notify(changes)
|
|
54
|
+
data = user_data_assembler.call
|
|
55
|
+
return unless @view_change_callback
|
|
56
|
+
|
|
57
|
+
changes.each_key { |path| @view_change_callback.call(File.basename(path, '.liquid'), data) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/trmnlp.rb
CHANGED
|
@@ -2,13 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module TRMNLP; end
|
|
4
4
|
require 'oj'
|
|
5
|
-
Oj.mimic_JSON
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
8
|
-
require_relative
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
VIEWS = %w{full half_horizontal half_vertical quadrant}
|
|
12
|
-
|
|
13
|
-
class Error < StandardError; end
|
|
14
|
-
end
|
|
5
|
+
Oj.mimic_JSON
|
|
6
|
+
require_relative 'trmnlp/errors'
|
|
7
|
+
require_relative 'trmnlp/config'
|
|
8
|
+
require_relative 'trmnlp/context'
|
|
9
|
+
require_relative 'trmnlp/screen'
|
|
10
|
+
require_relative 'trmnlp/version'
|
data/templates/init/bin/trmnlp
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Optional serverless transform.
|
|
2
|
+
# Rename this file to `transform.py` (or transform.rb / transform.php / transform.js)
|
|
3
|
+
# and it runs automatically — serverless transforms are enabled by default.
|
|
4
|
+
# (Set `transform_runtime: disabled` in .trmnlp.yml to turn them off.)
|
|
5
|
+
#
|
|
6
|
+
# Define a `run(input)` function that takes the polled response (parsed
|
|
7
|
+
# JSON, including the trmnl namespace) and returns any JSON-serialisable
|
|
8
|
+
# value. The bundled transform-runtime wraps this and handles stdin/stdout.
|
|
9
|
+
# This matches the hosted serverless contract — the same code runs in production.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(input):
|
|
13
|
+
# Reshape `input` however you like, then return.
|
|
14
|
+
return input
|
data/trmnl_preview.gemspec
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative 'lib/trmnlp/version'
|
|
4
4
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name =
|
|
6
|
+
spec.name = 'trmnl_preview'
|
|
7
7
|
spec.version = TRMNLP::VERSION
|
|
8
|
-
spec.authors = [
|
|
9
|
-
spec.email = [
|
|
8
|
+
spec.authors = ['Rockwell Schrock']
|
|
9
|
+
spec.email = ['rockwell@schrock.me']
|
|
10
10
|
|
|
11
|
-
spec.summary =
|
|
12
|
-
spec.description =
|
|
13
|
-
spec.homepage =
|
|
14
|
-
spec.license =
|
|
15
|
-
spec.required_ruby_version =
|
|
11
|
+
spec.summary = 'Local web server to preview TRMNL plugins'
|
|
12
|
+
spec.description = 'Automatically rebuild and preview TRNML plugins in multiple views'
|
|
13
|
+
spec.homepage = 'https://github.com/usetrmnl/trmnlp'
|
|
14
|
+
spec.license = 'MIT'
|
|
15
|
+
spec.required_ruby_version = '>= 3.4'
|
|
16
16
|
|
|
17
|
-
spec.metadata[
|
|
17
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
|
18
18
|
|
|
19
|
-
spec.metadata[
|
|
20
|
-
spec.metadata[
|
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
|
20
|
+
spec.metadata['source_code_uri'] = 'https://github.com/usetrmnl/trmnlp'
|
|
21
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
|
21
22
|
|
|
22
23
|
spec.files = Dir.chdir(__dir__) do
|
|
23
24
|
[
|
|
24
25
|
'bin/**/*',
|
|
26
|
+
'db/**/*',
|
|
25
27
|
'lib/**/*',
|
|
26
28
|
'templates/**/{*,.*}',
|
|
27
29
|
'web/**/*',
|
|
@@ -31,37 +33,35 @@ Gem::Specification.new do |spec|
|
|
|
31
33
|
'trmnl_preview.gemspec'
|
|
32
34
|
].flat_map { |glob| Dir[glob] }
|
|
33
35
|
end
|
|
34
|
-
spec.bindir =
|
|
35
|
-
spec.executables = [
|
|
36
|
-
spec.require_paths = [
|
|
37
|
-
|
|
36
|
+
spec.bindir = 'bin'
|
|
37
|
+
spec.executables = ['trmnlp']
|
|
38
|
+
spec.require_paths = ['lib']
|
|
38
39
|
|
|
39
40
|
# Web server
|
|
40
|
-
spec.add_dependency
|
|
41
|
-
spec.add_dependency
|
|
42
|
-
spec.add_dependency
|
|
43
|
-
spec.add_dependency "faye-websocket", "~> 0.11.3"
|
|
41
|
+
spec.add_dependency 'puma', '~> 8.0'
|
|
42
|
+
spec.add_dependency 'rackup', '~> 2.2'
|
|
43
|
+
spec.add_dependency 'sinatra', '~> 4.1'
|
|
44
44
|
|
|
45
45
|
# HTML rendering
|
|
46
|
-
spec.add_dependency
|
|
47
|
-
spec.add_dependency
|
|
46
|
+
spec.add_dependency 'activesupport', '~> 8.0'
|
|
47
|
+
spec.add_dependency 'trmnl-liquid', '~> 0.7.0'
|
|
48
48
|
|
|
49
49
|
# PNG rendering
|
|
50
50
|
# spec.add_dependency 'puppeteer-ruby', '~> 0.45.6'
|
|
51
|
-
spec.add_dependency '
|
|
52
|
-
spec.add_dependency '
|
|
51
|
+
spec.add_dependency 'mini_magick', '~> 5.3'
|
|
52
|
+
spec.add_dependency 'selenium-webdriver', '~> 4.44'
|
|
53
53
|
|
|
54
54
|
# Utilities
|
|
55
|
-
spec.add_dependency
|
|
56
|
-
spec.add_dependency
|
|
57
|
-
spec.add_dependency
|
|
58
|
-
spec.add_dependency
|
|
59
|
-
spec.add_dependency
|
|
60
|
-
spec.add_dependency
|
|
61
|
-
spec.add_dependency
|
|
62
|
-
spec.add_dependency
|
|
63
|
-
spec.add_dependency
|
|
64
|
-
spec.add_dependency
|
|
55
|
+
spec.add_dependency 'cgi', '~> 0.5'
|
|
56
|
+
spec.add_dependency 'faraday', '~> 2.1'
|
|
57
|
+
spec.add_dependency 'faraday-multipart', '~> 1.1'
|
|
58
|
+
spec.add_dependency 'filewatcher', '~> 3.0'
|
|
59
|
+
spec.add_dependency 'oj', '~> 3.17'
|
|
60
|
+
spec.add_dependency 'rexml', '~> 3.4'
|
|
61
|
+
spec.add_dependency 'rubyzip', '~> 3.3'
|
|
62
|
+
spec.add_dependency 'thor', '~> 1.3'
|
|
63
|
+
spec.add_dependency 'tzinfo-data', '~> 1.2025'
|
|
64
|
+
spec.add_dependency 'xdg', '~> 10.2'
|
|
65
65
|
|
|
66
66
|
# For more information and examples about making a new gem, check out our
|
|
67
67
|
# guide at: https://bundler.io/guides/creating_gem.html
|
data/web/public/index.css
CHANGED
data/web/public/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
const trmnlp = {};
|
|
2
2
|
|
|
3
3
|
trmnlp.connectLiveRender = function () {
|
|
4
|
-
|
|
4
|
+
// EventSource reconnects automatically, so no manual retry loop is needed.
|
|
5
|
+
const source = new EventSource("/live_reload");
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
console.log("Connected to live reload
|
|
7
|
+
source.onopen = function () {
|
|
8
|
+
console.log("Connected to live reload stream");
|
|
8
9
|
};
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
const payload = JSON.parse(
|
|
11
|
+
source.onmessage = function (event) {
|
|
12
|
+
const payload = JSON.parse(event.data);
|
|
12
13
|
|
|
13
14
|
if (payload.type === "reload") {
|
|
14
15
|
trmnlp.fetchPreview();
|
|
@@ -16,37 +17,48 @@ trmnlp.connectLiveRender = function () {
|
|
|
16
17
|
hljs.highlightAll();
|
|
17
18
|
}
|
|
18
19
|
};
|
|
19
|
-
|
|
20
|
-
ws.onclose = function () {
|
|
21
|
-
console.log("Reconnecting to live reload socket...");
|
|
22
|
-
setTimeout(trmnlp.connectLiveRender, 1000);
|
|
23
|
-
};
|
|
24
20
|
};
|
|
25
21
|
|
|
26
22
|
|
|
27
23
|
trmnlp.fetchPreview = function (pickerState) {
|
|
28
|
-
const
|
|
24
|
+
const state = pickerState || trmnlp.picker?.state;
|
|
25
|
+
const screenClasses = (state?.screenClasses || []).join(" ");
|
|
29
26
|
const encodedScreenClasses = encodeURIComponent(screenClasses);
|
|
30
27
|
let src = `/render/${trmnlp.view}.${trmnlp.formatSelect.value}?screen_classes=${encodedScreenClasses}`;
|
|
31
28
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
// Pass dimensions for both HTML and PNG renders so trmnl.device.{width,height}
|
|
30
|
+
// in the Liquid context tracks the picker model selection.
|
|
31
|
+
if (state) {
|
|
35
32
|
const width = encodeURIComponent(state.width);
|
|
36
33
|
const height = encodeURIComponent(state.height);
|
|
37
|
-
|
|
34
|
+
src += `&width=${width}&height=${height}`;
|
|
35
|
+
}
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
// PNG-only: dark mode + color depth from palette
|
|
38
|
+
if (trmnlp.formatSelect.value === 'png' && state) {
|
|
39
|
+
const isDarkMode = state.isDarkMode ? 1 : 0;
|
|
40
40
|
const grays = state.palette.grays || 2;
|
|
41
41
|
const colorDepth = Math.ceil(Math.log2(grays));
|
|
42
|
-
|
|
43
|
-
src += `&width=${width}&height=${height}&color_depth=${colorDepth}`;
|
|
42
|
+
src += `&color_depth=${colorDepth}`;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
trmnlp.spinner.style.display = "inline-block";
|
|
47
46
|
trmnlp.iframe.src = src;
|
|
48
47
|
};
|
|
49
48
|
|
|
49
|
+
trmnlp.refreshUserData = async function (state) {
|
|
50
|
+
if (!state) return;
|
|
51
|
+
const params = new URLSearchParams({ width: state.width, height: state.height });
|
|
52
|
+
try {
|
|
53
|
+
const response = await fetch(`/data?${params}`);
|
|
54
|
+
if (!response.ok) return;
|
|
55
|
+
trmnlp.userData.textContent = await response.text();
|
|
56
|
+
hljs.highlightAll();
|
|
57
|
+
} catch (e) {
|
|
58
|
+
console.warn("Failed to refresh user-data:", e);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
50
62
|
document.addEventListener("DOMContentLoaded", async function () {
|
|
51
63
|
trmnlp.view = document.querySelector("meta[name='trmnl-view']").content;
|
|
52
64
|
trmnlp.iframe = document.querySelector("iframe");
|
|
@@ -77,6 +89,7 @@ document.addEventListener("DOMContentLoaded", async function () {
|
|
|
77
89
|
trmnlp.iframe.style.height = `${event.detail.height}px`;
|
|
78
90
|
|
|
79
91
|
trmnlp.fetchPreview(event.detail);
|
|
92
|
+
trmnlp.refreshUserData(event.detail);
|
|
80
93
|
});
|
|
81
94
|
|
|
82
95
|
trmnlp.picker = await TRMNLPicker.create('picker-form', { localStorageKey: 'trmnlp-picker' });
|
data/web/views/index.erb
CHANGED
|
@@ -60,9 +60,14 @@
|
|
|
60
60
|
</div>
|
|
61
61
|
</menu>
|
|
62
62
|
|
|
63
|
+
<% if @transform_error %>
|
|
64
|
+
<div class="transform-error" style="padding: 0.5em 0.75em; margin: 0.5em 0; background: #fee; border: 1px solid #c33; border-radius: 4px; color: #900; font-family: monospace; white-space: pre-wrap;">Transform error: <%= h @transform_error %></div>
|
|
65
|
+
<% end %>
|
|
66
|
+
|
|
63
67
|
<iframe style="width: 800px; height: 480px; border: 1px solid black; background: white;">Rendering…</iframe>
|
|
64
68
|
|
|
65
|
-
<
|
|
69
|
+
<div class="payload-size">Payload: <%= format_bytes(@payload_size) %></div>
|
|
70
|
+
<pre id="user-data"><code><%= h @user_data %></code></pre>
|
|
66
71
|
</main>
|
|
67
72
|
</body>
|
|
68
73
|
</html>
|
data/web/views/render_html.erb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
|
-
<link rel="stylesheet" href="
|
|
5
|
-
<script src="
|
|
4
|
+
<link rel="stylesheet" href="<%= @framework.css_url %>" />
|
|
5
|
+
<script src="<%= @framework.js_url %>"></script>
|
|
6
|
+
<meta name="trmnl-framework-version" content="<%= @framework.number %>" />
|
|
7
|
+
<meta name="trmnl-framework-pinned" content="<%= @framework.pinned? %>" />
|
|
6
8
|
|
|
7
9
|
<!-- Begin Inter font -->
|
|
8
10
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|