trmnl_preview 0.25.0 → 0.27.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 +14 -0
- data/README.md +24 -18
- data/db/data/framework_prefixes.yml +1258 -0
- data/lib/trmnlp/cli.rb +2 -0
- data/lib/trmnlp/lint/checks/custom_field_links_embedded.rb +35 -0
- data/lib/trmnlp/lint/checks/files_under_upload_limit.rb +22 -0
- data/lib/trmnlp/lint/checks/framework_classes_exist.rb +19 -3
- data/lib/trmnlp/lint/checks/framework_prefixes_exist.rb +80 -0
- data/lib/trmnlp/lint/checks/webhook_url_shown.rb +24 -0
- data/lib/trmnlp/lint/diagnostic.rb +10 -2
- data/lib/trmnlp/lint/source.rb +6 -0
- data/lib/trmnlp/lint.rb +9 -1
- data/lib/trmnlp/version.rb +1 -1
- data/templates/init/bin/trmnlp +3 -2
- data/templates/run_in_docker +1 -0
- metadata +6 -1
data/lib/trmnlp/cli.rb
CHANGED
|
@@ -117,6 +117,8 @@ module TRMNLP
|
|
|
117
117
|
puts VERSION
|
|
118
118
|
# Keep the version first when stdout is a pipe and stderr is not.
|
|
119
119
|
$stdout.flush
|
|
120
|
+
# On stderr, so a script that reads the version still gets the number alone.
|
|
121
|
+
warn "Running in Docker (#{ENV.fetch('TRMNLP_IMAGE', 'image unknown')})" if self.class.in_container?
|
|
120
122
|
check_for_update(fresh: true)
|
|
121
123
|
end
|
|
122
124
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../check'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
module Lint
|
|
7
|
+
module Checks
|
|
8
|
+
# Recipe review flags a plain https:// link in a custom field's description. An
|
|
9
|
+
# embedded link is shorter, opens in a new tab and is plainly clickable. Review
|
|
10
|
+
# skips a link without https://, which is how an example stays unlinked.
|
|
11
|
+
class CustomFieldLinksEmbedded < Check
|
|
12
|
+
LEARN_MORE = 'https://help.trmnl.com/en/articles/11395668-recipe-best-practices#h_d981e5e6c6'
|
|
13
|
+
DESCRIPTION_KEY = /\Adescription(?:-[\w-]+)?\z/
|
|
14
|
+
ANCHOR = %r{<a\b[^>]*>.*?</a>}im
|
|
15
|
+
PLAIN_LINK = %r{https://[^\s<>"']*[^\s<>"'.,;:!?)]}
|
|
16
|
+
|
|
17
|
+
def issues
|
|
18
|
+
source.custom_field_definitions.flat_map do |field|
|
|
19
|
+
field.select { |key, _| key.match?(DESCRIPTION_KEY) }.flat_map do |key, text|
|
|
20
|
+
text.to_s.gsub(ANCHOR, '').scan(PLAIN_LINK).uniq.map { issue_for(field['keyname'], key, it) }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def issue_for(keyname, key, url)
|
|
28
|
+
{ message: "Custom field '#{keyname}' #{key} has a plain link, #{url}. Embed it as " \
|
|
29
|
+
"<a href=\"#{url}\" class=\"underline\">, or remove https:// if it is only an example.",
|
|
30
|
+
learn_more: LEARN_MORE }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../check'
|
|
4
|
+
|
|
5
|
+
module TRMNLP
|
|
6
|
+
module Lint
|
|
7
|
+
module Checks
|
|
8
|
+
# TRMNL refuses to import a settings.yml, view or transform over 100 KB, so `trmnlp push` fails.
|
|
9
|
+
class FilesUnderUploadLimit < Check
|
|
10
|
+
MAX_BYTES = 100 * 1024
|
|
11
|
+
|
|
12
|
+
def issues
|
|
13
|
+
source.upload_files.filter_map do |path|
|
|
14
|
+
next if path.size <= MAX_BYTES
|
|
15
|
+
|
|
16
|
+
{ message: "src/#{path.basename} is #{path.size} bytes; TRMNL refuses to import a file over 100 KB." }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -12,14 +12,22 @@ module TRMNLP
|
|
|
12
12
|
class FrameworkClassesExist < Check
|
|
13
13
|
FAMILY_PATTERN = /\A(?:value|label|title|description|text|content)--/
|
|
14
14
|
DATA_PATH = File.expand_path('../../../../db/data/framework_classes.yml', __dir__)
|
|
15
|
+
LEARN_MORE = 'https://trmnl.com/framework/docs/element_sizes'
|
|
16
|
+
# Smallest to largest. Each family has its own subset, and the same name is a different size in each.
|
|
17
|
+
# No family has `medium`, but it is the name people reach for between small and large.
|
|
18
|
+
SIZES = %w[xxsmall xsmall small base medium large xlarge xxlarge xxxlarge mega giga tera peta].freeze
|
|
15
19
|
|
|
16
20
|
# Release number => the family classes its plugins.css defines, from that release until the next one listed.
|
|
17
21
|
CLASSES_BY_RELEASE = YAML.load_file(DATA_PATH).transform_keys { Gem::Version.new(it) }.sort.freeze
|
|
18
22
|
|
|
19
23
|
def issues
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
classes = release_classes
|
|
25
|
+
(used_classes - classes).map do |name|
|
|
26
|
+
missing = "'#{name}' is not a class in Framework v#{source.framework_version}, so it has no effect."
|
|
27
|
+
sizes = sizes_of(name, classes)
|
|
28
|
+
next { message: "#{missing} Check the name in the Framework docs." } if sizes.empty?
|
|
29
|
+
|
|
30
|
+
{ message: "#{missing} Its sizes are #{sizes.join(', ')}.", learn_more: LEARN_MORE }
|
|
23
31
|
end
|
|
24
32
|
end
|
|
25
33
|
|
|
@@ -36,6 +44,14 @@ module TRMNLP
|
|
|
36
44
|
version = Gem::Version.new(source.framework_version.number)
|
|
37
45
|
CLASSES_BY_RELEASE.select { |release, _| release <= version }.last.last
|
|
38
46
|
end
|
|
47
|
+
|
|
48
|
+
# The family's sizes in this release, when the class names a size; empty otherwise.
|
|
49
|
+
def sizes_of(name, classes)
|
|
50
|
+
family, size = name.split('--', 2)
|
|
51
|
+
return [] unless SIZES.include?(size)
|
|
52
|
+
|
|
53
|
+
SIZES.map { "#{family}--#{it}" } & classes
|
|
54
|
+
end
|
|
39
55
|
end
|
|
40
56
|
end
|
|
41
57
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
require_relative '../check'
|
|
6
|
+
require_relative 'framework_classes_exist'
|
|
7
|
+
|
|
8
|
+
module TRMNLP
|
|
9
|
+
module Lint
|
|
10
|
+
module Checks
|
|
11
|
+
# Reports a Framework class behind screen prefixes that the plugin's release does not
|
|
12
|
+
# define for it, such as `xl:value--large` or `dark:value--large`. The class matches no
|
|
13
|
+
# rule, so it does nothing. Classes the release lacks entirely are left to FrameworkClassesExist.
|
|
14
|
+
class FrameworkPrefixesExist < Check
|
|
15
|
+
PREFIXED_PATTERN = /\A(?<prefix>.+):(?<name>(?:value|label|title|description|text|content)--.+)\z/
|
|
16
|
+
DATA_PATH = File.expand_path('../../../../db/data/framework_prefixes.yml', __dir__)
|
|
17
|
+
LEARN_MORE = 'https://trmnl.com/framework/docs/responsive'
|
|
18
|
+
|
|
19
|
+
# Release number => the prefixes its plugins.css defines and the classes each covers,
|
|
20
|
+
# from that release until the next one listed.
|
|
21
|
+
VARIANTS_BY_RELEASE = YAML.load_file(DATA_PATH).transform_keys { Gem::Version.new(it) }.sort.freeze
|
|
22
|
+
|
|
23
|
+
def issues
|
|
24
|
+
prefixed_classes.filter_map do |full_name, prefix, name|
|
|
25
|
+
next unless release_classes.include?(name)
|
|
26
|
+
|
|
27
|
+
next if covers?(prefix, name)
|
|
28
|
+
|
|
29
|
+
{ message: "'#{full_name}' has no effect: #{reason(prefix, name)}", learn_more: LEARN_MORE }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# A Liquid tag splits into several words, so any word with a brace or percent sign is skipped.
|
|
36
|
+
def prefixed_classes
|
|
37
|
+
source.html_fragments.values.flat_map { it.css('[class]').flat_map { it['class'].split } }
|
|
38
|
+
.grep_v(/[{}%]/)
|
|
39
|
+
.uniq
|
|
40
|
+
.filter_map { |full_name| (match = full_name.match(PREFIXED_PATTERN)) && [full_name, *match.captures] }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reason(prefix, name)
|
|
44
|
+
version = "Framework v#{source.framework_version}"
|
|
45
|
+
return "#{name} has no '#{prefix}:' variant in #{version}." if families_by_prefix.key?(prefix)
|
|
46
|
+
|
|
47
|
+
reordered = families_by_prefix.keys.find do |known|
|
|
48
|
+
known.split(':').sort == prefix.split(':').sort && covers?(known, name)
|
|
49
|
+
end
|
|
50
|
+
return "#{version} has no '#{prefix}:' prefix. Write it as '#{reordered}:#{name}'." if reordered
|
|
51
|
+
|
|
52
|
+
"#{version} has no '#{prefix}:' prefix."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A family is covered as `all` its classes, all `except` some, or `only` some.
|
|
56
|
+
def covers?(prefix, name)
|
|
57
|
+
coverage = families_by_prefix.dig(prefix, name.split('--').first)
|
|
58
|
+
case coverage
|
|
59
|
+
when 'all' then true
|
|
60
|
+
when Hash then coverage.key?('except') ? !coverage['except'].include?(name) : coverage['only'].include?(name)
|
|
61
|
+
else false
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def families_by_prefix
|
|
66
|
+
@families_by_prefix ||= in_release(VARIANTS_BY_RELEASE).each_with_object({}) do |variant, prefixes|
|
|
67
|
+
variant['prefixes'].each { prefixes[it] = variant['families'] }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def release_classes = @release_classes ||= in_release(FrameworkClassesExist::CLASSES_BY_RELEASE)
|
|
72
|
+
|
|
73
|
+
def in_release(lists)
|
|
74
|
+
version = Gem::Version.new(source.framework_version.number)
|
|
75
|
+
lists.select { |release, _| release <= version }.last.last
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
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
|
+
# Recipe review asks a webhook recipe to show each install its own
|
|
9
|
+
# webhook URL, so the user can copy it into the service that pushes data.
|
|
10
|
+
class WebhookUrlShown < Check
|
|
11
|
+
MESSAGE = 'Add a custom field with field_type copyable_webhook_url, so people who install this webhook ' \
|
|
12
|
+
'recipe can copy their webhook URL. TRMNL fills it in with the URL of each install.'
|
|
13
|
+
LEARN_MORE = 'https://help.trmnl.com/en/articles/10513740-custom-plugin-form-builder#h_431c22552a'
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def pass?
|
|
18
|
+
source.settings['strategy'] != 'webhook' ||
|
|
19
|
+
source.custom_field_definitions.any? { it['field_type'] == 'copyable_webhook_url' }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -7,7 +7,8 @@ module TRMNLP
|
|
|
7
7
|
class Diagnostic
|
|
8
8
|
SETTINGS_KEYS = {
|
|
9
9
|
'title_casing' => 'name', 'title_length' => 'name',
|
|
10
|
-
'description_length' => 'description', 'recipe_overview_length' => 'recipe_overview'
|
|
10
|
+
'description_length' => 'description', 'recipe_overview_length' => 'recipe_overview',
|
|
11
|
+
'webhook_url_shown' => 'strategy'
|
|
11
12
|
}.freeze
|
|
12
13
|
MARKUP_PATTERNS = {
|
|
13
14
|
'no_async_functions' => /async function/i,
|
|
@@ -41,9 +42,10 @@ module TRMNLP
|
|
|
41
42
|
return source.locations(MARKUP_PATTERNS[rule_id]) if MARKUP_PATTERNS.key?(rule_id)
|
|
42
43
|
|
|
43
44
|
case rule_id
|
|
44
|
-
when 'arbitrary_values_in_range', 'framework_classes_exist' then class_locations
|
|
45
|
+
when 'arbitrary_values_in_range', 'framework_classes_exist', 'framework_prefixes_exist' then class_locations
|
|
45
46
|
when 'layouts_have_content' then empty_view_locations
|
|
46
47
|
when 'form_fields_valid' then form_field_locations
|
|
48
|
+
when 'custom_field_links_embedded' then custom_field_description_locations
|
|
47
49
|
when 'custom_fields_used' then project_field_locations
|
|
48
50
|
when 'no_custom_filters', 'no_unknown_filters' then filter_locations
|
|
49
51
|
when 'title_bar_outside_layout', 'no_nested_layouts'
|
|
@@ -83,6 +85,12 @@ module TRMNLP
|
|
|
83
85
|
end
|
|
84
86
|
end
|
|
85
87
|
|
|
88
|
+
def custom_field_description_locations
|
|
89
|
+
keyname, key = finding[:message].match(/\ACustom field '([^']*)' (\S+) has/)&.captures
|
|
90
|
+
index = source.custom_field_definitions.index { it['keyname'].to_s == keyname }
|
|
91
|
+
index ? source.yaml_location('src/settings.yml', 'custom_fields', index, key) : []
|
|
92
|
+
end
|
|
93
|
+
|
|
86
94
|
def project_field_locations
|
|
87
95
|
key = finding[:message][/Custom field '([^']+)'/, 1]
|
|
88
96
|
return [] unless key
|
data/lib/trmnlp/lint/source.rb
CHANGED
|
@@ -45,6 +45,12 @@ module TRMNLP
|
|
|
45
45
|
@all_markup ||= view_markup.values.join + shared_markup
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# The files TRMNL reads from a `trmnlp push` archive.
|
|
49
|
+
def upload_files
|
|
50
|
+
[paths.plugin_config, *(VIEWS + ['shared']).map { paths.template(it) }, paths.transform_file.first]
|
|
51
|
+
.compact.select(&:exist?)
|
|
52
|
+
end
|
|
53
|
+
|
|
48
54
|
# The serverless transform (src/transform.{py,rb,php,js}), or '' without one.
|
|
49
55
|
# It reads custom fields from its input, so a field may be used only there.
|
|
50
56
|
def transform_code
|
data/lib/trmnlp/lint.rb
CHANGED
|
@@ -26,7 +26,11 @@ require_relative 'lint/checks/author_contact_method'
|
|
|
26
26
|
require_relative 'lint/checks/title_bar_outside_layout'
|
|
27
27
|
require_relative 'lint/checks/no_nested_layouts'
|
|
28
28
|
require_relative 'lint/checks/framework_classes_exist'
|
|
29
|
+
require_relative 'lint/checks/framework_prefixes_exist'
|
|
29
30
|
require_relative 'lint/checks/no_external_qr_codes'
|
|
31
|
+
require_relative 'lint/checks/custom_field_links_embedded'
|
|
32
|
+
require_relative 'lint/checks/files_under_upload_limit'
|
|
33
|
+
require_relative 'lint/checks/webhook_url_shown'
|
|
30
34
|
|
|
31
35
|
module TRMNLP
|
|
32
36
|
# Markup best-practice checks behind `trmnlp lint`.
|
|
@@ -57,7 +61,11 @@ module TRMNLP
|
|
|
57
61
|
Checks::TitleBarOutsideLayout,
|
|
58
62
|
Checks::NoNestedLayouts,
|
|
59
63
|
Checks::FrameworkClassesExist,
|
|
60
|
-
Checks::
|
|
64
|
+
Checks::FrameworkPrefixesExist,
|
|
65
|
+
Checks::NoExternalQrCodes,
|
|
66
|
+
Checks::CustomFieldLinksEmbedded,
|
|
67
|
+
Checks::FilesUnderUploadLimit,
|
|
68
|
+
Checks::WebhookUrlShown
|
|
61
69
|
].freeze
|
|
62
70
|
|
|
63
71
|
def self.rule_id(check_type) = check_type.name.split('::').last.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
data/lib/trmnlp/version.rb
CHANGED
data/templates/init/bin/trmnlp
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
# Add a tag to stay on one release, v0.22.0 or later: trmnl/trmnlp:v0.22.0
|
|
8
8
|
IMAGE=trmnl/trmnlp
|
|
9
9
|
|
|
10
|
-
# Saved on the PATH as `trmnlp`, this script is what `command -v trmnlp` finds: not a gem.
|
|
10
|
+
# Saved on the PATH as `trmnlp`, this script is what `command -v trmnlp` finds: not a gem. Nor is
|
|
11
|
+
# another copy of it there (it names run_in_docker): that one would run its own image, not the one above.
|
|
11
12
|
GEM="$(command -v trmnlp)"
|
|
12
|
-
if [ -n "$GEM" ] && ! [ "$GEM" -ef "$0" ]
|
|
13
|
+
if [ -n "$GEM" ] && ! [ "$GEM" -ef "$0" ] && ! grep -qs run_in_docker "$GEM"
|
|
13
14
|
then
|
|
14
15
|
exec "$GEM" "$@"
|
|
15
16
|
fi
|
data/templates/run_in_docker
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: trmnl_preview
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.27.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rockwell Schrock
|
|
@@ -361,6 +361,7 @@ files:
|
|
|
361
361
|
- db/data/and_x_more.yml
|
|
362
362
|
- db/data/form_fields.yml
|
|
363
363
|
- db/data/framework_classes.yml
|
|
364
|
+
- db/data/framework_prefixes.yml
|
|
364
365
|
- db/data/framework_versions.yml
|
|
365
366
|
- db/data/locales/cs.yml
|
|
366
367
|
- db/data/locales/da.yml
|
|
@@ -424,10 +425,13 @@ files:
|
|
|
424
425
|
- lib/trmnlp/lint/checks/arbitrary_values_in_range.rb
|
|
425
426
|
- lib/trmnlp/lint/checks/author_contact_method.rb
|
|
426
427
|
- lib/trmnlp/lint/checks/coordinates_use_lat_lon.rb
|
|
428
|
+
- lib/trmnlp/lint/checks/custom_field_links_embedded.rb
|
|
427
429
|
- lib/trmnlp/lint/checks/custom_fields_used.rb
|
|
428
430
|
- lib/trmnlp/lint/checks/description_length.rb
|
|
431
|
+
- lib/trmnlp/lint/checks/files_under_upload_limit.rb
|
|
429
432
|
- lib/trmnlp/lint/checks/form_fields_valid.rb
|
|
430
433
|
- lib/trmnlp/lint/checks/framework_classes_exist.rb
|
|
434
|
+
- lib/trmnlp/lint/checks/framework_prefixes_exist.rb
|
|
431
435
|
- lib/trmnlp/lint/checks/highcharts_animations_disabled.rb
|
|
432
436
|
- lib/trmnlp/lint/checks/highcharts_elements_unique.rb
|
|
433
437
|
- lib/trmnlp/lint/checks/image_links_reachable.rb
|
|
@@ -445,6 +449,7 @@ files:
|
|
|
445
449
|
- lib/trmnlp/lint/checks/title_casing.rb
|
|
446
450
|
- lib/trmnlp/lint/checks/title_length.rb
|
|
447
451
|
- lib/trmnlp/lint/checks/waits_for_dom_load.rb
|
|
452
|
+
- lib/trmnlp/lint/checks/webhook_url_shown.rb
|
|
448
453
|
- lib/trmnlp/lint/checks/yes_no_selects_are_boolean.rb
|
|
449
454
|
- lib/trmnlp/lint/diagnostic.rb
|
|
450
455
|
- lib/trmnlp/lint/source.rb
|