trmnl_preview 0.7.2 → 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 +192 -0
- data/LICENSE.txt +21 -0
- data/README.md +319 -0
- data/bin/rake +27 -0
- data/bin/trmnlp +13 -14
- data/db/data/form_fields.yml +24 -0
- data/db/data/framework_versions.yml +72 -0
- data/lib/trmnlp/api_client.rb +99 -0
- data/lib/trmnlp/app.rb +132 -0
- data/lib/trmnlp/browser_pool.rb +82 -0
- data/lib/trmnlp/cli.rb +81 -0
- data/lib/trmnlp/commands/base.rb +63 -0
- data/lib/trmnlp/commands/build.rb +26 -0
- data/lib/trmnlp/commands/clone.rb +35 -0
- data/lib/trmnlp/commands/init.rb +59 -0
- data/lib/trmnlp/commands/lint.rb +42 -0
- data/lib/trmnlp/commands/list.rb +40 -0
- data/lib/trmnlp/commands/login.rb +42 -0
- data/lib/trmnlp/commands/pull.rb +51 -0
- data/lib/trmnlp/commands/push.rb +77 -0
- data/lib/trmnlp/commands/serve.rb +54 -0
- data/lib/trmnlp/commands.rb +3 -0
- data/lib/trmnlp/config/app.rb +50 -0
- data/lib/trmnlp/config/plugin.rb +117 -0
- data/lib/trmnlp/config/project.rb +106 -0
- data/lib/trmnlp/config.rb +17 -0
- data/lib/trmnlp/context.rb +35 -0
- 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 +78 -0
- 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 +34 -0
- 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 +5 -0
- data/lib/trmnlp/watcher.rb +60 -0
- data/lib/trmnlp.rb +10 -0
- data/templates/init/.trmnlp.yml +14 -0
- data/templates/init/bin/trmnlp +44 -0
- data/templates/init/src/full.liquid +1 -0
- data/templates/init/src/half_horizontal.liquid +1 -0
- data/templates/init/src/half_vertical.liquid +1 -0
- data/templates/init/src/quadrant.liquid +1 -0
- data/templates/init/src/settings.yml +16 -0
- data/templates/init/src/shared.liquid +1 -0
- data/templates/init/src/transform.py.example +14 -0
- data/trmnl_preview.gemspec +68 -0
- data/web/public/highlight/highlight.min.js +315 -0
- data/web/public/highlight/styles/atom-one-dark.min.css +1 -0
- data/web/public/index.css +119 -0
- data/web/public/index.js +96 -0
- data/web/public/trmnl-picker.js +656 -0
- data/web/views/index.erb +73 -0
- data/web/views/render_html.erb +25 -0
- metadata +324 -15
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
# Represents a TRMNL design-system framework version, so plugins can
|
|
7
|
+
# pin (or float to "latest") the CSS/JS bundle they render against,
|
|
8
|
+
# both in production and locally.
|
|
9
|
+
class FrameworkVersion
|
|
10
|
+
include Comparable
|
|
11
|
+
|
|
12
|
+
DEFAULT_ASSET_HOST = 'https://trmnl.com'
|
|
13
|
+
DATA_PATH = File.expand_path('../../db/data/framework_versions.yml', __dir__)
|
|
14
|
+
|
|
15
|
+
attr_reader :number
|
|
16
|
+
|
|
17
|
+
def self.config = @config ||= YAML.load_file(DATA_PATH).freeze
|
|
18
|
+
|
|
19
|
+
def self.version_numbers = config.fetch('versions').map { |v| v['number'] }.freeze
|
|
20
|
+
|
|
21
|
+
def self.latest = new(config.fetch('latest'))
|
|
22
|
+
|
|
23
|
+
# Suitable for showing in a `select` form field. Pinnable versions are
|
|
24
|
+
# ordered newest-first by semantic version — never by manifest order.
|
|
25
|
+
def self.options
|
|
26
|
+
newest_first = version_numbers.sort_by { |number| Gem::Version.new(number) }.reverse
|
|
27
|
+
[{ "Always track latest (currently v#{latest.number})" => 'latest' }] +
|
|
28
|
+
newest_first.map { |number| { "v#{number}" => number } }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(number, asset_host: DEFAULT_ASSET_HOST)
|
|
32
|
+
@asset_host = asset_host
|
|
33
|
+
|
|
34
|
+
if number.nil? || number == 'latest'
|
|
35
|
+
@number = self.class.config.fetch('latest')
|
|
36
|
+
@pinned = false
|
|
37
|
+
elsif self.class.version_numbers.include?(number)
|
|
38
|
+
@number = number
|
|
39
|
+
@pinned = true
|
|
40
|
+
else
|
|
41
|
+
raise ArgumentError, "unknown framework version: #{number}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def pinned? = @pinned
|
|
46
|
+
|
|
47
|
+
def css_url = "#{@asset_host}/css/#{path_segment}/plugins.css"
|
|
48
|
+
|
|
49
|
+
def js_url = "#{@asset_host}/js/#{path_segment}/plugins.js"
|
|
50
|
+
|
|
51
|
+
def ==(other) = other.is_a?(self.class) && number == other.number
|
|
52
|
+
|
|
53
|
+
def <=>(other)
|
|
54
|
+
return nil unless other.is_a?(self.class)
|
|
55
|
+
|
|
56
|
+
Gem::Version.new(number) <=> Gem::Version.new(other.number)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def as_json(*) = number
|
|
60
|
+
|
|
61
|
+
def to_s = number
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
# When pinned, requests assets at /css/<version>/plugins.css to lock
|
|
66
|
+
# behavior; otherwise hit /css/latest/ for live updates.
|
|
67
|
+
def path_segment = pinned? ? @number : 'latest'
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mini_magick'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
|
|
7
|
+
module TRMNLP
|
|
8
|
+
class ImageQuantizer
|
|
9
|
+
MIN_DEPTH = 1
|
|
10
|
+
MAX_DEPTH = 8
|
|
11
|
+
|
|
12
|
+
def initialize(depth:)
|
|
13
|
+
@depth = clamp(depth)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(path)
|
|
17
|
+
tmp = Tempfile.new(['mono', '.png'])
|
|
18
|
+
tmp.close
|
|
19
|
+
quantize(path, tmp.path)
|
|
20
|
+
FileUtils.mv(tmp.path, path, force: true)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def quantize(src, dst)
|
|
26
|
+
MiniMagick.convert do |m|
|
|
27
|
+
m << src
|
|
28
|
+
m.colorspace 'Gray'
|
|
29
|
+
m.dither 'FloydSteinberg'
|
|
30
|
+
apply_depth(m)
|
|
31
|
+
m.depth @depth
|
|
32
|
+
m.define "png:bit-depth=#{@depth}"
|
|
33
|
+
m.strip
|
|
34
|
+
m << dst
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def apply_depth(m)
|
|
39
|
+
if @depth == 1
|
|
40
|
+
# NOTE: 1-bit only has black/white, so a halftone remap simulates gray
|
|
41
|
+
# via dithering patterns. Skipping this collapses photos to pure
|
|
42
|
+
# silhouettes.
|
|
43
|
+
m.remap 'pattern:gray50'
|
|
44
|
+
m.posterize 2
|
|
45
|
+
m.colors 2
|
|
46
|
+
m.type 'Bilevel'
|
|
47
|
+
else
|
|
48
|
+
levels = 2**@depth
|
|
49
|
+
m.posterize levels
|
|
50
|
+
m.colors levels
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def clamp(depth)
|
|
55
|
+
[[depth.to_i, MIN_DEPTH].max, MAX_DEPTH].min
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRMNLP
|
|
4
|
+
module Lint
|
|
5
|
+
# Base class for a single best-practice check. A subclass declares a
|
|
6
|
+
# MESSAGE constant (and optionally LEARN_MORE) and implements #pass?.
|
|
7
|
+
# Checks that surface a variable number of findings override #issues.
|
|
8
|
+
class Check
|
|
9
|
+
def initialize(source)
|
|
10
|
+
@source = source
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def issues
|
|
14
|
+
pass? ? [] : [issue]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
attr_reader :source
|
|
20
|
+
|
|
21
|
+
def pass?
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def issue
|
|
26
|
+
learn_more = self.class.const_defined?(:LEARN_MORE) ? self.class::LEARN_MORE : nil
|
|
27
|
+
{ message: self.class::MESSAGE, learn_more: }.compact
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../check'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
module Lint
|
|
7
|
+
module Checks
|
|
8
|
+
# Reports custom fields declared in .trmnlp.yml that never appear in the
|
|
9
|
+
# polling settings or the markup — one finding per unused field.
|
|
10
|
+
class CustomFieldsUsed < Check
|
|
11
|
+
SETTINGS_KEYS = %w[polling_url polling_headers polling_body].freeze
|
|
12
|
+
|
|
13
|
+
def issues
|
|
14
|
+
source.custom_field_values.keys.reject { |keyname| used?(keyname) }.map do |keyname|
|
|
15
|
+
{ message: "Custom field '#{keyname}' is not used in form fields or markup." }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def used?(keyname)
|
|
22
|
+
pattern = /#{Regexp.escape(keyname)}/
|
|
23
|
+
searchable_settings.match?(pattern) || source.all_markup.match?(pattern)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def searchable_settings
|
|
27
|
+
@searchable_settings ||= SETTINGS_KEYS.filter_map { |key| source.settings[key] }.join(' ')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../check'
|
|
4
|
+
require_relative '../../form_field'
|
|
5
|
+
|
|
6
|
+
module TRMNLP
|
|
7
|
+
module Lint
|
|
8
|
+
module Checks
|
|
9
|
+
# Validates settings.yml custom_fields against the FormField schema —
|
|
10
|
+
# the same source serve/build use for their startup warnings.
|
|
11
|
+
class FormFieldsValid < Check
|
|
12
|
+
def issues
|
|
13
|
+
FormField.validate_all(source.custom_field_definitions).map do |warning|
|
|
14
|
+
{ message: "settings.yml custom_fields — #{warning}" }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
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 HighchartsAnimationsDisabled < Check
|
|
9
|
+
MESSAGE = 'Highcharts should have animations disabled.'
|
|
10
|
+
LEARN_MORE = 'https://trmnl.com/framework/chart'
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def pass?
|
|
15
|
+
markup = source.all_markup
|
|
16
|
+
return true unless markup.downcase.include?('highcharts')
|
|
17
|
+
|
|
18
|
+
markup.match?(/animation:\s{0,6}false/)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../check'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
module Lint
|
|
7
|
+
module Checks
|
|
8
|
+
class HighchartsElementsUnique < Check
|
|
9
|
+
MESSAGE = 'To avoid variable shadowing across charts in multiple layouts, ' \
|
|
10
|
+
'use the append_random filter for your Highcharts elements.'
|
|
11
|
+
LEARN_MORE = 'https://help.trmnl.com/en/articles/10347358-custom-plugin-filters'
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def pass?
|
|
16
|
+
markup = source.all_markup
|
|
17
|
+
return true unless markup.downcase.include?('highcharts')
|
|
18
|
+
|
|
19
|
+
markup.match?(/append_random/)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
require_relative '../check'
|
|
8
|
+
|
|
9
|
+
module TRMNLP
|
|
10
|
+
module Lint
|
|
11
|
+
module Checks
|
|
12
|
+
# Flags static <img> URLs that answer with a non-success status. A
|
|
13
|
+
# network failure (offline, DNS, timeout) is NOT a plugin defect, so
|
|
14
|
+
# those are skipped rather than reported as one.
|
|
15
|
+
class ImageLinksReachable < Check
|
|
16
|
+
MESSAGE = 'One or more <img> tags has a static "src" URL that does not ' \
|
|
17
|
+
'respond to HTTP GET requests with a success code.'
|
|
18
|
+
TIMEOUT = 5
|
|
19
|
+
UNREACHABLE = [SocketError, Net::OpenTimeout, Net::ReadTimeout,
|
|
20
|
+
Errno::ECONNREFUSED, Errno::EHOSTUNREACH, OpenSSL::SSL::SSLError].freeze
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def pass? = static_image_urls.all? { |url| reachable?(url) }
|
|
25
|
+
|
|
26
|
+
def static_image_urls
|
|
27
|
+
source.view_markup.values
|
|
28
|
+
.flat_map { |html| html.scan(/<img[^>]+src\s*=\s*["']([^"']+)["']/i).flatten }
|
|
29
|
+
.map(&:strip)
|
|
30
|
+
.reject { |src| src.empty? || src.include?('{{') || src.start_with?('data:') }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def reachable?(url)
|
|
34
|
+
return false unless url.match?(%r{\Ahttps?://})
|
|
35
|
+
|
|
36
|
+
response = fetch(URI.parse(url))
|
|
37
|
+
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
|
|
38
|
+
rescue *UNREACHABLE
|
|
39
|
+
true
|
|
40
|
+
rescue StandardError
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def fetch(uri)
|
|
45
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https',
|
|
46
|
+
open_timeout: TIMEOUT, read_timeout: TIMEOUT) do |http|
|
|
47
|
+
http.get(uri.request_uri)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../check'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
module Lint
|
|
7
|
+
module Checks
|
|
8
|
+
class LayoutsHaveContent < Check
|
|
9
|
+
MIN_CONTENT_LENGTH = 10
|
|
10
|
+
MESSAGE = 'Some markup layouts are empty, please provide basic treatment.'
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# A view passes when either its own markup or the shared markup
|
|
15
|
+
# carries real content — mirrors the hosted behaviour.
|
|
16
|
+
def pass?
|
|
17
|
+
source.view_markup.values.all? do |markup|
|
|
18
|
+
[markup.length, source.shared_markup.length].max >= MIN_CONTENT_LENGTH
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../check'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
module Lint
|
|
7
|
+
module Checks
|
|
8
|
+
class LimitedInlineStyles < Check
|
|
9
|
+
MAX_INLINE_STYLES = 6
|
|
10
|
+
PROPERTIES = %w[
|
|
11
|
+
justify-content padding margin background-color
|
|
12
|
+
border-radius text-align object-fit font-size
|
|
13
|
+
].freeze
|
|
14
|
+
MESSAGE = 'Markup uses too many inline styles, add more native Framework classes.'
|
|
15
|
+
LEARN_MORE = 'https://help.trmnl.com/en/articles/11395668-recipe-best-practices#h_3a3eab0712'
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def pass?
|
|
20
|
+
count = PROPERTIES.sum { |property| source.all_markup.scan(property).size }
|
|
21
|
+
count <= MAX_INLINE_STYLES
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
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 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
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'xdg'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
class Paths
|
|
7
|
+
attr_reader :root_dir
|
|
8
|
+
|
|
9
|
+
def initialize(root_dir)
|
|
10
|
+
@root_dir = Pathname.new(root_dir)
|
|
11
|
+
@xdg = XDG.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# --- trmnlp library ---
|
|
15
|
+
|
|
16
|
+
def gem_dir = Pathname.new(__dir__).join('..', '..').expand_path
|
|
17
|
+
|
|
18
|
+
def templates_dir = gem_dir.join('templates')
|
|
19
|
+
|
|
20
|
+
# --- directories ---
|
|
21
|
+
|
|
22
|
+
def src_dir = root_dir.join('src')
|
|
23
|
+
|
|
24
|
+
def build_dir = root_dir.join('_build')
|
|
25
|
+
def create_build_dir = build_dir.mkpath
|
|
26
|
+
|
|
27
|
+
def app_config_dir = xdg.config_home.join('trmnlp')
|
|
28
|
+
|
|
29
|
+
def cache_dir = xdg.cache_home.join('trmnl')
|
|
30
|
+
def create_cache_dir = cache_dir.mkpath
|
|
31
|
+
|
|
32
|
+
def valid? = trmnlp_config.exist?
|
|
33
|
+
|
|
34
|
+
# --- files ---
|
|
35
|
+
|
|
36
|
+
def trmnlp_config = root_dir.join('.trmnlp.yml')
|
|
37
|
+
|
|
38
|
+
def plugin_config = src_dir.join('settings.yml')
|
|
39
|
+
|
|
40
|
+
def template(view) = src_dir.join("#{view}.liquid")
|
|
41
|
+
|
|
42
|
+
def shared_template = template('shared')
|
|
43
|
+
|
|
44
|
+
def app_config = app_config_dir.join('config.yml')
|
|
45
|
+
|
|
46
|
+
def user_data = cache_dir.join('data.json')
|
|
47
|
+
|
|
48
|
+
def render_template = Pathname.new(__dir__).join('..', '..', 'web', 'views', 'render_html.erb')
|
|
49
|
+
|
|
50
|
+
def src_files = src_dir.glob('*').select(&:file?)
|
|
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
|
+
|
|
70
|
+
# --- utilities ---
|
|
71
|
+
|
|
72
|
+
def expand(path) = Pathname.new(path).expand_path(root_dir)
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
attr_reader :xdg
|
|
77
|
+
end
|
|
78
|
+
end
|