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
data/lib/trmnlp/app.rb
CHANGED
|
@@ -1,103 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
1
2
|
|
|
2
|
-
require 'faye/websocket'
|
|
3
3
|
require 'sinatra'
|
|
4
4
|
require 'sinatra/base'
|
|
5
5
|
|
|
6
6
|
require_relative 'context'
|
|
7
7
|
require_relative 'screen_generator'
|
|
8
|
+
require_relative 'screenshot'
|
|
8
9
|
|
|
9
10
|
module TRMNLP
|
|
10
11
|
class App < Sinatra::Base
|
|
11
12
|
# Sinatra settings
|
|
12
13
|
set :views, File.join(File.dirname(__FILE__), '..', '..', 'web', 'views')
|
|
13
14
|
set :public_folder, File.join(File.dirname(__FILE__), '..', '..', 'web', 'public')
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
helpers do
|
|
17
|
+
def format_bytes(bytes)
|
|
18
|
+
bytes < 1024 ? "#{bytes} bytes" : format('%.1f KB', bytes / 1024.0)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# NOTE: render_html.erb's layout yields raw plugin HTML through `<%= yield %>`,
|
|
22
|
+
# so a global `escape_html` setting would corrupt the render. Escape per-value.
|
|
23
|
+
def h(text)
|
|
24
|
+
Rack::Utils.escape_html(text.to_s)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
15
28
|
def initialize(*args)
|
|
16
29
|
super
|
|
17
30
|
|
|
18
31
|
@context = settings.context
|
|
32
|
+
@poller = @context.poller
|
|
33
|
+
@renderer = @context.renderer
|
|
34
|
+
@user_data_assembler = @context.user_data_assembler
|
|
35
|
+
@transform_pipeline = @context.transform_pipeline
|
|
36
|
+
@watcher = @context.watcher
|
|
37
|
+
@screenshot = Screenshot.new(pool: settings.browser_pool)
|
|
19
38
|
|
|
20
|
-
@
|
|
39
|
+
@poller.poll_data
|
|
21
40
|
|
|
22
|
-
@
|
|
41
|
+
@watcher.start if @context.config.project.live_render?
|
|
23
42
|
|
|
24
43
|
@live_reload_clients = []
|
|
25
|
-
@
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
ws.send(payload.to_json)
|
|
34
|
-
end
|
|
44
|
+
@watcher.on_change do |view, user_data|
|
|
45
|
+
payload = {
|
|
46
|
+
'type' => 'reload',
|
|
47
|
+
'view' => view,
|
|
48
|
+
'user_data' => user_data
|
|
49
|
+
}
|
|
50
|
+
message = "data: #{payload.to_json}\n\n"
|
|
51
|
+
@live_reload_clients.each { |queue| queue << message }
|
|
35
52
|
end
|
|
36
53
|
end
|
|
37
54
|
|
|
38
55
|
post '/webhook' do
|
|
39
|
-
@
|
|
40
|
-
|
|
56
|
+
@poller.put_webhook(request.body.read)
|
|
57
|
+
'OK'
|
|
41
58
|
end
|
|
42
|
-
|
|
59
|
+
|
|
43
60
|
get '/' do
|
|
44
61
|
redirect '/full'
|
|
45
62
|
end
|
|
46
63
|
|
|
47
64
|
get '/data' do
|
|
48
65
|
content_type :json
|
|
49
|
-
|
|
66
|
+
device = @user_data_assembler.device_from_params(params)
|
|
67
|
+
JSON.pretty_generate(@user_data_assembler.call(device:))
|
|
50
68
|
end
|
|
51
69
|
|
|
70
|
+
# Live reload is a one-directional server->browser push, so it uses
|
|
71
|
+
# server-sent events rather than a websocket. Each client gets a blocking
|
|
72
|
+
# queue; the stream thread parks on queue.pop until the watcher broadcasts.
|
|
52
73
|
get '/live_reload' do
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
ws.on(:close) do |event|
|
|
60
|
-
@live_reload_clients.delete(ws)
|
|
74
|
+
content_type 'text/event-stream'
|
|
75
|
+
queue = Thread::Queue.new
|
|
76
|
+
@live_reload_clients << queue
|
|
77
|
+
stream(:keep_open) do |out|
|
|
78
|
+
out.callback { @live_reload_clients.delete(queue) }
|
|
79
|
+
out << queue.pop until out.closed?
|
|
61
80
|
end
|
|
62
|
-
|
|
63
|
-
ws.rack_response
|
|
64
81
|
end
|
|
65
82
|
|
|
66
83
|
get '/poll' do
|
|
67
|
-
@
|
|
84
|
+
@poller.poll_data
|
|
68
85
|
redirect back
|
|
69
86
|
end
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
|
|
88
|
+
Screen.all.each do |screen|
|
|
89
|
+
view = screen.name
|
|
72
90
|
get "/#{view}" do
|
|
73
91
|
@view = view
|
|
74
|
-
|
|
92
|
+
device = @user_data_assembler.device_from_params(params)
|
|
93
|
+
user_data = @user_data_assembler.call(device:)
|
|
94
|
+
@user_data = JSON.pretty_generate(user_data)
|
|
95
|
+
# Measured on compact JSON, the way the hosted service sizes merge variables.
|
|
96
|
+
@payload_size = JSON.generate(user_data).bytesize
|
|
75
97
|
@live_reload = @context.config.project.live_render?
|
|
98
|
+
@transform_error = @transform_pipeline.error
|
|
76
99
|
|
|
77
100
|
erb :index
|
|
78
101
|
end
|
|
79
102
|
|
|
80
103
|
get "/render/#{view}.html" do
|
|
81
|
-
@
|
|
104
|
+
@renderer.render_full_page(view, params)
|
|
82
105
|
end
|
|
83
|
-
|
|
106
|
+
|
|
84
107
|
get "/render/#{view}.png" do
|
|
85
108
|
@view = view
|
|
86
|
-
html = @
|
|
109
|
+
html = @renderer.render_full_page(view, params)
|
|
110
|
+
temp_image = render_png(html, params)
|
|
87
111
|
|
|
88
|
-
# Parse optional rendering params (sent by the web UI for PNG output)
|
|
89
|
-
width = params[:width] && params[:width].to_i
|
|
90
|
-
height = params[:height] && params[:height].to_i
|
|
91
|
-
color_depth = params[:color_depth] && params[:color_depth].to_i
|
|
92
|
-
|
|
93
|
-
generator = ScreenGenerator.new(html, image: true, width: width, height: height, color_depth: color_depth)
|
|
94
|
-
temp_image = generator.process
|
|
95
|
-
|
|
96
112
|
send_file temp_image.path, type: 'image/png', disposition: 'inline'
|
|
97
113
|
|
|
98
114
|
temp_image.close
|
|
99
115
|
temp_image.unlink
|
|
100
116
|
end
|
|
101
117
|
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# ScreenGenerator is request-scoped — it carries the per-request width,
|
|
122
|
+
# height, and color_depth — so it is built here rather than on the shared
|
|
123
|
+
# Context graph. Screenshots are a serve-only concern and would not belong
|
|
124
|
+
# on a Context shared by every command (build, lint, push, ...).
|
|
125
|
+
def render_png(html, params)
|
|
126
|
+
ScreenGenerator.new(html, screenshot: @screenshot,
|
|
127
|
+
width: params[:width]&.to_i,
|
|
128
|
+
height: params[:height]&.to_i,
|
|
129
|
+
color_depth: params[:color_depth]&.to_i).process
|
|
130
|
+
end
|
|
102
131
|
end
|
|
103
|
-
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRMNLP
|
|
4
|
+
class BrowserPool
|
|
5
|
+
def initialize(driver_factory:, max_size: 2)
|
|
6
|
+
@driver_factory = driver_factory
|
|
7
|
+
@max_size = max_size
|
|
8
|
+
@drivers = []
|
|
9
|
+
@available = Queue.new
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
@shutdown = false
|
|
12
|
+
|
|
13
|
+
at_exit { shutdown }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def with_driver
|
|
17
|
+
driver = checkout
|
|
18
|
+
yield driver
|
|
19
|
+
ensure
|
|
20
|
+
checkin(driver) if driver
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def shutdown
|
|
24
|
+
@mutex.synchronize do
|
|
25
|
+
return if @shutdown
|
|
26
|
+
|
|
27
|
+
@shutdown = true
|
|
28
|
+
@drivers.each do |driver|
|
|
29
|
+
driver.quit
|
|
30
|
+
rescue StandardError
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
@drivers.clear
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def checkout
|
|
40
|
+
driver = acquire
|
|
41
|
+
healthy?(driver) ? driver : recycle(driver)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def acquire
|
|
45
|
+
pop_available || build_new || @available.pop
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def pop_available
|
|
49
|
+
@available.pop(true)
|
|
50
|
+
rescue StandardError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def build_new
|
|
55
|
+
@mutex.synchronize do
|
|
56
|
+
return nil if @drivers.size >= @max_size
|
|
57
|
+
|
|
58
|
+
@driver_factory.call.tap { |d| @drivers << d }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def healthy?(driver)
|
|
63
|
+
driver.title
|
|
64
|
+
true
|
|
65
|
+
rescue StandardError
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def recycle(driver)
|
|
70
|
+
@mutex.synchronize do
|
|
71
|
+
@drivers.delete(driver)
|
|
72
|
+
@driver_factory.call.tap { |d| @drivers << d }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def checkin(driver)
|
|
77
|
+
return if @shutdown
|
|
78
|
+
|
|
79
|
+
@available.push(driver)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/trmnlp/cli.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'thor'
|
|
2
4
|
|
|
3
5
|
require_relative '../trmnlp'
|
|
@@ -8,7 +10,7 @@ module TRMNLP
|
|
|
8
10
|
package_name 'trmnlp'
|
|
9
11
|
|
|
10
12
|
class_option :dir, type: :string, default: Dir.pwd, aliases: '-d',
|
|
11
|
-
|
|
13
|
+
desc: 'Plugin project directory'
|
|
12
14
|
|
|
13
15
|
class_option :quiet, type: :boolean, default: false, desc: 'Suppress output', aliases: '-q'
|
|
14
16
|
|
|
@@ -18,46 +20,57 @@ module TRMNLP
|
|
|
18
20
|
|
|
19
21
|
desc 'build', 'Generate static HTML files'
|
|
20
22
|
def build
|
|
21
|
-
Commands::Build.
|
|
23
|
+
Commands::Build.run(options)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
desc 'login', 'Authenticate with TRMNL server'
|
|
25
27
|
def login
|
|
26
|
-
Commands::Login.
|
|
28
|
+
Commands::Login.run(options)
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
desc 'init NAME', 'Start a new plugin project'
|
|
30
32
|
method_option :skip_liquid, type: :boolean, default: false, desc: 'Skip generating liquid templates'
|
|
31
33
|
def init(name)
|
|
32
|
-
Commands::Init.
|
|
34
|
+
Commands::Init.run(options, name)
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
desc 'clone NAME ID', 'Copy a plugin project from TRMNL server'
|
|
36
38
|
def clone(name, id)
|
|
37
|
-
Commands::Clone.
|
|
39
|
+
Commands::Clone.run(options, name, id)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc 'list', 'List private plugins from TRMNL server'
|
|
43
|
+
def list
|
|
44
|
+
Commands::List.run(options)
|
|
38
45
|
end
|
|
39
46
|
|
|
40
47
|
desc 'pull', 'Download latest plugin settings from TRMNL server'
|
|
41
48
|
method_option :force, type: :boolean, default: false, aliases: '-f',
|
|
42
|
-
|
|
49
|
+
desc: 'Skip confirmation prompts'
|
|
43
50
|
method_option :id, type: :string, aliases: '-i', desc: 'Plugin settings ID'
|
|
44
51
|
def pull
|
|
45
|
-
Commands::Pull.
|
|
52
|
+
Commands::Pull.run(options)
|
|
46
53
|
end
|
|
47
54
|
|
|
48
55
|
desc 'push', 'Upload latest plugin settings to TRMNL server'
|
|
49
56
|
method_option :force, type: :boolean, default: false, aliases: '-f',
|
|
50
|
-
|
|
57
|
+
desc: 'Skip confirmation prompts'
|
|
51
58
|
method_option :id, type: :string, aliases: '-i', desc: 'Plugin settings ID'
|
|
52
59
|
def push
|
|
53
|
-
Commands::Push.
|
|
60
|
+
Commands::Push.run(options)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
desc 'lint', 'Check plugin code against TRMNL best practices'
|
|
64
|
+
def lint
|
|
65
|
+
# Exit non-zero when issues are found so CI pipelines can gate on it.
|
|
66
|
+
exit(1) unless Commands::Lint.run(options)
|
|
54
67
|
end
|
|
55
68
|
|
|
56
69
|
desc 'serve', 'Start a local dev server'
|
|
57
70
|
method_option :bind, type: :string, default: default_bind, aliases: '-b', desc: 'Bind address'
|
|
58
71
|
method_option :port, type: :numeric, default: 4567, aliases: '-p', desc: 'Port number'
|
|
59
72
|
def serve
|
|
60
|
-
Commands::Serve.
|
|
73
|
+
Commands::Serve.run(options)
|
|
61
74
|
end
|
|
62
75
|
|
|
63
76
|
desc 'version', 'Show version'
|
|
@@ -65,4 +78,4 @@ module TRMNLP
|
|
|
65
78
|
puts VERSION
|
|
66
79
|
end
|
|
67
80
|
end
|
|
68
|
-
end
|
|
81
|
+
end
|
data/lib/trmnlp/commands/base.rb
CHANGED
|
@@ -1,15 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative '../context'
|
|
4
|
+
require_relative '../form_field'
|
|
5
|
+
require_relative '../reporter'
|
|
4
6
|
|
|
5
7
|
module TRMNLP
|
|
6
8
|
module Commands
|
|
7
9
|
class Base
|
|
8
|
-
|
|
10
|
+
def self.run(input, *)
|
|
11
|
+
options = options_from(input)
|
|
12
|
+
reporter = Reporter.new(quiet: options.quiet)
|
|
13
|
+
new(context: Context.new(options.dir, reporter:), options:, reporter:).call(*)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# NOTE: Thor only includes flags the user actually passed, but Data.define
|
|
17
|
+
# requires every member. We pad missing members with nil so partial Thor
|
|
18
|
+
# hashes round-trip into a fully-populated typed Options struct.
|
|
19
|
+
def self.options_from(input)
|
|
20
|
+
return input if input.is_a?(self::Options)
|
|
21
|
+
|
|
22
|
+
hash = input.to_h.transform_keys(&:to_sym)
|
|
23
|
+
self::Options.new(**self::Options.members.to_h { [it, hash[it]] })
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(context:, options:, reporter: nil)
|
|
27
|
+
raise ArgumentError, "options must be a #{self.class}::Options" unless options.is_a?(self.class::Options)
|
|
9
28
|
|
|
10
|
-
|
|
11
|
-
@options =
|
|
12
|
-
@
|
|
29
|
+
@context = context
|
|
30
|
+
@options = options
|
|
31
|
+
@reporter = reporter || Reporter.new(quiet: options.quiet)
|
|
13
32
|
end
|
|
14
33
|
|
|
15
34
|
def call
|
|
@@ -18,17 +37,21 @@ module TRMNLP
|
|
|
18
37
|
|
|
19
38
|
protected
|
|
20
39
|
|
|
21
|
-
attr_accessor :options, :context
|
|
40
|
+
attr_accessor :options, :context, :reporter
|
|
22
41
|
|
|
23
42
|
def config = context.config
|
|
24
43
|
def paths = context.paths
|
|
25
44
|
|
|
26
45
|
def authenticate!
|
|
27
|
-
raise
|
|
46
|
+
raise NotLoggedIn, 'please run `trmnlp login`' unless config.app.logged_in?
|
|
28
47
|
end
|
|
29
48
|
|
|
30
|
-
|
|
31
|
-
|
|
49
|
+
# Non-blocking: warn about malformed settings.yml custom_fields so a
|
|
50
|
+
# plugin author notices before the field misbehaves in production.
|
|
51
|
+
def report_form_field_warnings
|
|
52
|
+
FormField.validate_all(config.plugin.custom_field_definitions).each do |warning|
|
|
53
|
+
reporter.info("warning: settings.yml custom_fields — #{warning}")
|
|
54
|
+
end
|
|
32
55
|
end
|
|
33
56
|
|
|
34
57
|
def prompt(message)
|
|
@@ -37,4 +60,4 @@ module TRMNLP
|
|
|
37
60
|
end
|
|
38
61
|
end
|
|
39
62
|
end
|
|
40
|
-
end
|
|
63
|
+
end
|
|
@@ -1,21 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'base'
|
|
2
4
|
|
|
3
5
|
module TRMNLP
|
|
4
6
|
module Commands
|
|
5
7
|
class Build < Base
|
|
8
|
+
Options = Data.define(:dir, :quiet)
|
|
9
|
+
|
|
6
10
|
def call
|
|
7
11
|
context.validate!
|
|
8
|
-
|
|
12
|
+
report_form_field_warnings
|
|
13
|
+
context.poller.poll_data
|
|
9
14
|
context.paths.create_build_dir
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
output_path = context.paths.build_dir.join("#{
|
|
13
|
-
|
|
14
|
-
output_path.write(context.render_full_page(
|
|
16
|
+
Screen.all.each do |screen|
|
|
17
|
+
output_path = context.paths.build_dir.join("#{screen.name}.html")
|
|
18
|
+
reporter.info "Writing #{output_path}..."
|
|
19
|
+
output_path.write(context.renderer.render_full_page(screen.name))
|
|
15
20
|
end
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
|
|
22
|
+
reporter.info 'Done!'
|
|
18
23
|
end
|
|
19
24
|
end
|
|
20
25
|
end
|
|
21
|
-
end
|
|
26
|
+
end
|
|
@@ -1,24 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require_relative 'base'
|
|
4
|
+
require_relative 'init'
|
|
2
5
|
require_relative 'pull'
|
|
3
6
|
|
|
4
7
|
module TRMNLP
|
|
5
8
|
module Commands
|
|
6
9
|
class Clone < Base
|
|
10
|
+
Options = Data.define(:dir, :quiet)
|
|
11
|
+
|
|
7
12
|
def call(directory_name, id)
|
|
8
13
|
authenticate!
|
|
9
14
|
|
|
10
15
|
destination_path = Pathname.new(options.dir).join(directory_name)
|
|
11
|
-
raise
|
|
16
|
+
raise DirectoryExists, "directory #{destination_path} already exists, aborting" if destination_path.exist?
|
|
12
17
|
|
|
13
|
-
Init.
|
|
18
|
+
Init.run({ dir: options.dir, skip_liquid: true, quiet: true }, directory_name)
|
|
14
19
|
|
|
15
|
-
Pull.
|
|
20
|
+
Pull.run({ dir: destination_path.to_s, force: true, id: id })
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
reporter.info <<~HEREDOC
|
|
18
23
|
|
|
19
|
-
|
|
24
|
+
To start the local server:
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
cd #{Pathname.new(destination_path).relative_path_from(Dir.pwd)} && trmnlp serve
|
|
22
27
|
HEREDOC
|
|
23
28
|
end
|
|
24
29
|
|
|
@@ -27,4 +32,4 @@ module TRMNLP
|
|
|
27
32
|
def template_dir = paths.templates_dir.join('init')
|
|
28
33
|
end
|
|
29
34
|
end
|
|
30
|
-
end
|
|
35
|
+
end
|
data/lib/trmnlp/commands/init.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'fileutils'
|
|
2
4
|
|
|
3
5
|
require_relative 'base'
|
|
@@ -5,11 +7,13 @@ require_relative 'base'
|
|
|
5
7
|
module TRMNLP
|
|
6
8
|
module Commands
|
|
7
9
|
class Init < Base
|
|
10
|
+
Options = Data.define(:dir, :quiet, :skip_liquid)
|
|
11
|
+
|
|
8
12
|
def call(name)
|
|
9
13
|
destination_dir = Pathname.new(options.dir).join(name)
|
|
10
14
|
|
|
11
15
|
unless destination_dir.exist?
|
|
12
|
-
|
|
16
|
+
reporter.info "Creating #{destination_dir}"
|
|
13
17
|
destination_dir.mkpath
|
|
14
18
|
end
|
|
15
19
|
|
|
@@ -20,36 +24,36 @@ module TRMNLP
|
|
|
20
24
|
relative_pathname = source_pathname.relative_path_from(template_dir)
|
|
21
25
|
destination_pathname = destination_dir.join(relative_pathname)
|
|
22
26
|
destination_pathname.dirname.mkpath
|
|
23
|
-
|
|
27
|
+
|
|
24
28
|
if destination_pathname.exist?
|
|
25
29
|
answer = prompt("#{destination_pathname} already exists. Overwrite? (y/n): ").downcase
|
|
26
30
|
if answer != 'y'
|
|
27
|
-
|
|
31
|
+
reporter.info "Skipping #{destination_pathname}"
|
|
28
32
|
next
|
|
29
33
|
end
|
|
30
34
|
end
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
reporter.info "Creating #{destination_pathname}"
|
|
33
37
|
FileUtils.cp(source_pathname, destination_pathname)
|
|
34
38
|
end
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
reporter.info <<~HEREDOC
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
To start the local server:
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
cd #{Pathname.new(destination_dir).relative_path_from(Dir.pwd)}
|
|
45
|
+
trmnlp serve
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
To publish the plugin:
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
trmnlp login
|
|
50
|
+
trmnlp push
|
|
47
51
|
HEREDOC
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
private
|
|
51
55
|
|
|
52
|
-
def template_dir = paths.templates_dir.join('init')
|
|
56
|
+
def template_dir = paths.templates_dir.join('init')
|
|
53
57
|
end
|
|
54
58
|
end
|
|
55
|
-
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../lint'
|
|
5
|
+
|
|
6
|
+
module TRMNLP
|
|
7
|
+
module Commands
|
|
8
|
+
# Runs the markup best-practice checks and reports their findings.
|
|
9
|
+
class Lint < Base
|
|
10
|
+
Options = Data.define(:dir, :quiet)
|
|
11
|
+
|
|
12
|
+
def call
|
|
13
|
+
context.validate!
|
|
14
|
+
report
|
|
15
|
+
issues.empty?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def issues
|
|
21
|
+
@issues ||= TRMNLP::Lint::CHECKS.flat_map { |check| check.new(source).issues }.uniq
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def source
|
|
25
|
+
@source ||= TRMNLP::Lint::Source.new(config:, paths:)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def report
|
|
29
|
+
return reporter.info(reporter.green('✓ All checks passed!')) if issues.empty?
|
|
30
|
+
|
|
31
|
+
reporter.info(reporter.yellow("#{issues.size} issue#{'s' if issues.size > 1} found:\n"))
|
|
32
|
+
issues.each_with_index { |issue, index| report_issue(issue, index) }
|
|
33
|
+
reporter.info('')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def report_issue(issue, index)
|
|
37
|
+
reporter.info(" #{index + 1}. #{issue[:message]}")
|
|
38
|
+
reporter.info(" Learn more: #{issue[:learn_more]}") if issue[:learn_more]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../api_client'
|
|
5
|
+
|
|
6
|
+
module TRMNLP
|
|
7
|
+
module Commands
|
|
8
|
+
class List < Base
|
|
9
|
+
Options = Data.define(:dir, :quiet)
|
|
10
|
+
|
|
11
|
+
PRIVATE_PLUGIN_ID = 37
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
authenticate!
|
|
15
|
+
|
|
16
|
+
api = APIClient.new(config)
|
|
17
|
+
response = api.get_plugin_settings
|
|
18
|
+
plugins = (response || [])
|
|
19
|
+
.select { |p| p['plugin_id'] == PRIVATE_PLUGIN_ID }
|
|
20
|
+
.sort_by { |p| (p['name'] || '').downcase }
|
|
21
|
+
|
|
22
|
+
if plugins.empty?
|
|
23
|
+
reporter.info 'No plugins found.'
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
reporter.info "Your plugins:\n\n"
|
|
28
|
+
reporter.info ' ID NAME'
|
|
29
|
+
reporter.info " #{'-' * 50}"
|
|
30
|
+
|
|
31
|
+
plugins.each do |plugin|
|
|
32
|
+
reporter.info format(' %-8s %s', plugin['id'], plugin['name'])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
reporter.info "\nTo clone a plugin:"
|
|
36
|
+
reporter.info ' trmnlp clone [folder_name] [id]'
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|