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,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../check'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
module Lint
|
|
7
|
+
module Checks
|
|
8
|
+
class NoAsyncFunctions < Check
|
|
9
|
+
MESSAGE = 'Async JavaScript functions are not allowed due to browser ' \
|
|
10
|
+
'timeout settings for generating screenshots.'
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def pass? = !source.all_markup.downcase.include?('async function')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../check'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
module Lint
|
|
7
|
+
module Checks
|
|
8
|
+
class NoOpacity < Check
|
|
9
|
+
MESSAGE = 'Opacity should not be used, use the "--gray--##" Framework classes instead.'
|
|
10
|
+
LEARN_MORE = 'https://trmnl.com/framework/docs/text_color'
|
|
11
|
+
PATTERN = /opacity:\s*[\d.]+/
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def pass? = !source.all_markup.match?(PATTERN)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../check'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
module Lint
|
|
7
|
+
module Checks
|
|
8
|
+
class NoSizeClasses < Check
|
|
9
|
+
PATTERN = /\b(view(--|__)(full|half_horizontal|half_vertical|quadrant))\b("|')>/
|
|
10
|
+
MESSAGE = "We already apply the 'full', 'half_horizontal', 'half_vertical', and " \
|
|
11
|
+
"'quadrant' classes to each view, please remove them."
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def pass? = !source.all_markup.match?(PATTERN)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../check'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
module Lint
|
|
7
|
+
module Checks
|
|
8
|
+
class TitleCasing < Check
|
|
9
|
+
MESSAGE = 'Title should begin with a capital letter.'
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def pass?
|
|
14
|
+
name = source.plugin_name
|
|
15
|
+
name.empty? || name[0] == name[0].upcase
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../check'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
module Lint
|
|
7
|
+
module Checks
|
|
8
|
+
class TitleLength < Check
|
|
9
|
+
MAX_LENGTH = 50
|
|
10
|
+
MESSAGE = "Title should be <= #{MAX_LENGTH} characters long.".freeze
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def pass? = source.plugin_name.length <= MAX_LENGTH
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../check'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
module Lint
|
|
7
|
+
module Checks
|
|
8
|
+
class WaitsForDomLoad < Check
|
|
9
|
+
MESSAGE = 'JavaScript should listen for the DOMContentLoaded event, not window.onLoad()'
|
|
10
|
+
LEARN_MORE = 'https://help.trmnl.com/en/articles/9510536-private-plugins#h_db7030f8b8'
|
|
11
|
+
FORBIDDEN = ['window.onload', 'window.addeventlistener("load")',
|
|
12
|
+
"window.addeventlistener('load')"].freeze
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def pass?
|
|
17
|
+
markup = source.all_markup.downcase
|
|
18
|
+
FORBIDDEN.none? { |token| markup.include?(token) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRMNLP
|
|
4
|
+
module Lint
|
|
5
|
+
# The plugin data every lint check examines: markup per view, shared
|
|
6
|
+
# markup, the combined markup string, and the raw settings.yml. Built
|
|
7
|
+
# once and shared across all checks. Settings come straight from
|
|
8
|
+
# Config::Plugin (a single parse); checks read the raw `{{ }}` templates,
|
|
9
|
+
# which Config::Plugin's semantic readers render away.
|
|
10
|
+
class Source
|
|
11
|
+
VIEWS = %w[full half_horizontal half_vertical quadrant].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(config:, paths:)
|
|
14
|
+
@config = config
|
|
15
|
+
@paths = paths
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def plugin_name = settings['name'].to_s
|
|
19
|
+
def settings = config.plugin.settings
|
|
20
|
+
def custom_field_values = config.project.custom_fields
|
|
21
|
+
def custom_field_definitions = config.plugin.custom_field_definitions
|
|
22
|
+
|
|
23
|
+
def view_markup
|
|
24
|
+
@view_markup ||= VIEWS.to_h { |view| [view, read(paths.template(view))] }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def shared_markup
|
|
28
|
+
@shared_markup ||= read(paths.shared_template)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def all_markup
|
|
32
|
+
@all_markup ||= view_markup.values.join + shared_markup
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :config, :paths
|
|
38
|
+
|
|
39
|
+
def read(path) = path.exist? ? path.read.strip : ''
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/trmnlp/lint.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lint/check'
|
|
4
|
+
require_relative 'lint/source'
|
|
5
|
+
require_relative 'lint/checks/title_casing'
|
|
6
|
+
require_relative 'lint/checks/title_length'
|
|
7
|
+
require_relative 'lint/checks/layouts_have_content'
|
|
8
|
+
require_relative 'lint/checks/no_async_functions'
|
|
9
|
+
require_relative 'lint/checks/waits_for_dom_load'
|
|
10
|
+
require_relative 'lint/checks/limited_inline_styles'
|
|
11
|
+
require_relative 'lint/checks/no_size_classes'
|
|
12
|
+
require_relative 'lint/checks/no_opacity'
|
|
13
|
+
require_relative 'lint/checks/highcharts_animations_disabled'
|
|
14
|
+
require_relative 'lint/checks/highcharts_elements_unique'
|
|
15
|
+
require_relative 'lint/checks/image_links_reachable'
|
|
16
|
+
require_relative 'lint/checks/custom_fields_used'
|
|
17
|
+
require_relative 'lint/checks/form_fields_valid'
|
|
18
|
+
|
|
19
|
+
module TRMNLP
|
|
20
|
+
# Markup best-practice checks behind `trmnlp lint`.
|
|
21
|
+
module Lint
|
|
22
|
+
# Every check the lint command runs, in report order.
|
|
23
|
+
CHECKS = [
|
|
24
|
+
Checks::TitleCasing,
|
|
25
|
+
Checks::TitleLength,
|
|
26
|
+
Checks::LayoutsHaveContent,
|
|
27
|
+
Checks::NoAsyncFunctions,
|
|
28
|
+
Checks::WaitsForDomLoad,
|
|
29
|
+
Checks::LimitedInlineStyles,
|
|
30
|
+
Checks::NoSizeClasses,
|
|
31
|
+
Checks::NoOpacity,
|
|
32
|
+
Checks::HighchartsAnimationsDisabled,
|
|
33
|
+
Checks::HighchartsElementsUnique,
|
|
34
|
+
Checks::ImageLinksReachable,
|
|
35
|
+
Checks::CustomFieldsUsed,
|
|
36
|
+
Checks::FormFieldsValid
|
|
37
|
+
].freeze
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/trmnlp/paths.rb
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'xdg'
|
|
2
4
|
|
|
3
5
|
module TRMNLP
|
|
4
6
|
class Paths
|
|
5
7
|
attr_reader :root_dir
|
|
6
|
-
|
|
8
|
+
|
|
7
9
|
def initialize(root_dir)
|
|
8
10
|
@root_dir = Pathname.new(root_dir)
|
|
9
11
|
@xdg = XDG.new
|
|
10
12
|
end
|
|
11
|
-
|
|
13
|
+
|
|
12
14
|
# --- trmnlp library ---
|
|
13
|
-
|
|
15
|
+
|
|
14
16
|
def gem_dir = Pathname.new(__dir__).join('..', '..').expand_path
|
|
15
17
|
|
|
16
18
|
def templates_dir = gem_dir.join('templates')
|
|
17
19
|
|
|
18
20
|
# --- directories ---
|
|
19
|
-
|
|
21
|
+
|
|
20
22
|
def src_dir = root_dir.join('src')
|
|
21
23
|
|
|
22
24
|
def build_dir = root_dir.join('_build')
|
|
@@ -34,19 +36,37 @@ module TRMNLP
|
|
|
34
36
|
def trmnlp_config = root_dir.join('.trmnlp.yml')
|
|
35
37
|
|
|
36
38
|
def plugin_config = src_dir.join('settings.yml')
|
|
37
|
-
|
|
39
|
+
|
|
38
40
|
def template(view) = src_dir.join("#{view}.liquid")
|
|
39
41
|
|
|
40
42
|
def shared_template = template('shared')
|
|
41
|
-
|
|
43
|
+
|
|
42
44
|
def app_config = app_config_dir.join('config.yml')
|
|
43
45
|
|
|
44
46
|
def user_data = cache_dir.join('data.json')
|
|
45
47
|
|
|
46
48
|
def render_template = Pathname.new(__dir__).join('..', '..', 'web', 'views', 'render_html.erb')
|
|
47
|
-
|
|
49
|
+
|
|
48
50
|
def src_files = src_dir.glob('*').select(&:file?)
|
|
49
51
|
|
|
52
|
+
# File extension → transform language identifier.
|
|
53
|
+
TRANSFORM_EXTENSIONS = {
|
|
54
|
+
'.py' => 'python',
|
|
55
|
+
'.rb' => 'ruby',
|
|
56
|
+
'.php' => 'php',
|
|
57
|
+
'.js' => 'node'
|
|
58
|
+
}.freeze
|
|
59
|
+
|
|
60
|
+
# Locate src/transform.{py,rb,php,js}. Returns [Pathname, language]
|
|
61
|
+
# or [nil, nil] if no transform file exists.
|
|
62
|
+
def transform_file
|
|
63
|
+
TRANSFORM_EXTENSIONS.each do |ext, language|
|
|
64
|
+
candidate = src_dir.join("transform#{ext}")
|
|
65
|
+
return [candidate, language] if candidate.exist?
|
|
66
|
+
end
|
|
67
|
+
[nil, nil]
|
|
68
|
+
end
|
|
69
|
+
|
|
50
70
|
# --- utilities ---
|
|
51
71
|
|
|
52
72
|
def expand(path) = Pathname.new(path).expand_path(root_dir)
|
|
@@ -55,4 +75,4 @@ module TRMNLP
|
|
|
55
75
|
|
|
56
76
|
attr_reader :xdg
|
|
57
77
|
end
|
|
58
|
-
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/hash/conversions'
|
|
4
|
+
require 'faraday'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
require_relative 'reporter'
|
|
8
|
+
|
|
9
|
+
module TRMNLP
|
|
10
|
+
class Poller
|
|
11
|
+
def initialize(config:, paths:, reporter: Reporter.new)
|
|
12
|
+
@config = config
|
|
13
|
+
@paths = paths
|
|
14
|
+
@reporter = reporter
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def poll_data
|
|
18
|
+
return unless config.plugin.polling?
|
|
19
|
+
raise InvalidConfig, 'config must specify polling_url or polling_urls' if config.plugin.polling_urls.empty?
|
|
20
|
+
|
|
21
|
+
data = aggregate_responses
|
|
22
|
+
write_user_data(data)
|
|
23
|
+
data
|
|
24
|
+
# NOTE: trmnlp is a dev tool — a flaky upstream API should surface a warning
|
|
25
|
+
# and keep the preview server alive, not crash the user's session. We
|
|
26
|
+
# deliberately swallow here and return {} so the renderer keeps rendering.
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
reporter.info("warning: #{e.message}")
|
|
29
|
+
{}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def put_webhook(payload)
|
|
33
|
+
write_user_data(wrap_array(JSON.parse(payload)))
|
|
34
|
+
# NOTE: Same rationale as #poll_data — a bad webhook payload shouldn't take
|
|
35
|
+
# down the dev server. Report a warning and keep serving.
|
|
36
|
+
rescue StandardError => e
|
|
37
|
+
reporter.info("webhook warning: #{e.message}")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :config, :paths, :reporter
|
|
43
|
+
|
|
44
|
+
def aggregate_responses
|
|
45
|
+
responses = config.plugin.polling_urls.map { |url| fetch_one(url) }
|
|
46
|
+
return responses.first if responses.size == 1
|
|
47
|
+
|
|
48
|
+
responses.each_with_index.with_object({}) { |(r, i), h| h["IDX_#{i}"] = r }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def fetch_one(url)
|
|
52
|
+
verb = config.plugin.polling_verb.upcase
|
|
53
|
+
response = perform_request(url, verb)
|
|
54
|
+
reporter.info("#{verb} #{url} — received #{response.body.length} bytes (#{response.status} status)")
|
|
55
|
+
parse_response(response)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def perform_request(url, verb)
|
|
59
|
+
conn = Faraday.new(url:, headers: config.plugin.polling_headers)
|
|
60
|
+
verb == 'POST' ? conn.post { |req| req.body = config.plugin.polling_body } : conn.get
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parse_response(response)
|
|
64
|
+
return parse_failure(response.body) unless response.status == 200
|
|
65
|
+
|
|
66
|
+
parse_body(response.body, response.headers['content-type'])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_failure(body)
|
|
70
|
+
reporter.info(body)
|
|
71
|
+
{}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def parse_body(body, content_type_header)
|
|
75
|
+
content_type = content_type_header&.split(';')&.first&.strip
|
|
76
|
+
case content_type
|
|
77
|
+
when 'application/json', %r{^application/.+\+json} then wrap_array(JSON.parse(body))
|
|
78
|
+
when 'text/xml', 'application/xml', %r{^application/.+\+xml} then wrap_array(Hash.from_xml(body))
|
|
79
|
+
when 'text/html', 'text/plain' then sniff_json(body) || { 'text' => body }
|
|
80
|
+
else log_unknown_type(content_type_header)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def log_unknown_type(header)
|
|
85
|
+
reporter.info("unknown content type received: #{header}")
|
|
86
|
+
{}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def wrap_array(json) = json.is_a?(Array) ? { data: json } : json
|
|
90
|
+
|
|
91
|
+
def sniff_json(body)
|
|
92
|
+
trimmed = body.to_s.strip
|
|
93
|
+
return nil unless trimmed.start_with?('{', '[')
|
|
94
|
+
|
|
95
|
+
wrap_array(JSON.parse(trimmed))
|
|
96
|
+
rescue JSON::ParserError
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def write_user_data(data)
|
|
101
|
+
paths.create_cache_dir
|
|
102
|
+
paths.user_data.write(JSON.generate(data))
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'trmnl/liquid'
|
|
5
|
+
|
|
6
|
+
require_relative 'screen'
|
|
7
|
+
|
|
8
|
+
module TRMNLP
|
|
9
|
+
class Renderer
|
|
10
|
+
def initialize(config:, paths:, user_data_assembler:)
|
|
11
|
+
@config = config
|
|
12
|
+
@paths = paths
|
|
13
|
+
@user_data_assembler = user_data_assembler
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render_full_page(view, params = {})
|
|
17
|
+
device = user_data_assembler.device_from_params(params)
|
|
18
|
+
binding_obj = TemplateBinding.new(self, view, params)
|
|
19
|
+
ERB.new(paths.render_template.read).result(
|
|
20
|
+
binding_obj.get_binding { render_or_error(view, device:) }
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def framework = config.plugin.framework_version
|
|
25
|
+
|
|
26
|
+
def screen_classes(classes = 'screen')
|
|
27
|
+
classes ||= 'screen' # an explicit nil (omitted screen_classes param) still needs a base
|
|
28
|
+
classes += ' screen--no-bleed' if config.plugin.no_screen_padding == 'yes'
|
|
29
|
+
classes
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :config, :paths, :user_data_assembler
|
|
35
|
+
|
|
36
|
+
# NOTE: a missing template or Liquid syntax error is a user-facing
|
|
37
|
+
# signal — the plugin author needs to *see* what broke. We surface
|
|
38
|
+
# those as RenderError, then #render_or_error embeds the message
|
|
39
|
+
# inside the preview frame so the dev server keeps serving instead
|
|
40
|
+
# of 500-ing. Anything that's not a RenderError bubbles up as a bug.
|
|
41
|
+
def render_liquid_template(view, device: {})
|
|
42
|
+
template_path = paths.template(view)
|
|
43
|
+
raise RenderError, "Missing template: #{template_path}" unless template_path.exist?
|
|
44
|
+
|
|
45
|
+
parse_and_render(template_path, device:)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parse_and_render(template_path, device:)
|
|
49
|
+
Liquid::Template.parse(full_markup(template_path), environment: liquid_environment)
|
|
50
|
+
.render(user_data_assembler.call(device:))
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
raise RenderError, e.message
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def render_or_error(view, device:)
|
|
56
|
+
render_liquid_template(view, device:)
|
|
57
|
+
rescue RenderError => e
|
|
58
|
+
e.message
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def full_markup(template_path)
|
|
62
|
+
shared = paths.shared_template
|
|
63
|
+
shared.exist? ? shared.read + template_path.read : template_path.read
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def liquid_environment
|
|
67
|
+
@liquid_environment ||= TRMNL::Liquid.new do |env|
|
|
68
|
+
config.project.user_filters.each do |module_name, relative_path|
|
|
69
|
+
require paths.root_dir.join(relative_path)
|
|
70
|
+
env.register_filter(Object.const_get(module_name))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# bindings must match the `GET /render/{view}.html` route in app.rb
|
|
76
|
+
class TemplateBinding
|
|
77
|
+
def initialize(renderer, view, params)
|
|
78
|
+
@view = view
|
|
79
|
+
@screen_classes = renderer.screen_classes(params[:screen_classes])
|
|
80
|
+
@framework = renderer.framework
|
|
81
|
+
@mashup_classes = Screen.find(view)&.mashup_classes
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def get_binding = binding
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRMNLP
|
|
4
|
+
# Single sink for user-facing command output. Records every message so
|
|
5
|
+
# specs can assert on what a command would have said, and writes to the
|
|
6
|
+
# underlying stream unless quiet:.
|
|
7
|
+
class Reporter
|
|
8
|
+
attr_reader :messages
|
|
9
|
+
|
|
10
|
+
def initialize(quiet: false, stream: $stdout)
|
|
11
|
+
@quiet = quiet
|
|
12
|
+
@stream = stream
|
|
13
|
+
@messages = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def info(message)
|
|
17
|
+
@messages << message
|
|
18
|
+
@stream.puts(message) unless @quiet
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def green(text) = colorize(text, 32)
|
|
22
|
+
def yellow(text) = colorize(text, 33)
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def colorize(text, code) = "\e[#{code}m#{text}\e[0m"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRMNLP
|
|
4
|
+
class Screen < Data.define(:name, :mashup_classes)
|
|
5
|
+
FULL = new(name: 'full', mashup_classes: nil)
|
|
6
|
+
HALF_HORIZONTAL = new(name: 'half_horizontal', mashup_classes: 'mashup mashup--1Tx1B')
|
|
7
|
+
HALF_VERTICAL = new(name: 'half_vertical', mashup_classes: 'mashup mashup--1Lx1R')
|
|
8
|
+
QUADRANT = new(name: 'quadrant', mashup_classes: 'mashup mashup--2x2')
|
|
9
|
+
|
|
10
|
+
ALL = [FULL, HALF_HORIZONTAL, HALF_VERTICAL, QUADRANT].freeze
|
|
11
|
+
|
|
12
|
+
def self.all = ALL
|
|
13
|
+
def self.find(name) = ALL.find { it.name == name }
|
|
14
|
+
def self.names = ALL.map(&:name)
|
|
15
|
+
end
|
|
16
|
+
end
|