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.
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
- (used_classes - release_classes).map do |name|
21
- { message: "'#{name}' is not a class in Framework v#{source.framework_version}, so it has no effect. " \
22
- 'Check the name in the Framework docs.' }
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
@@ -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::NoExternalQrCodes
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRMNLP
4
- VERSION = '0.25.0'
4
+ VERSION = '0.27.0'
5
5
  end
@@ -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
@@ -60,6 +60,7 @@ exec docker run \
60
60
  "${ARGS[@]}" \
61
61
  --env CI \
62
62
  --env TRMNL_API_KEY \
63
+ --env TRMNLP_IMAGE="$IMAGE" \
63
64
  --volume "$CONFIG_DIR:$CONTAINER_HOME/.config/trmnlp" \
64
65
  --volume "$CACHE_DIR:$CONTAINER_HOME/.cache/trmnl" \
65
66
  --volume "$(pwd):/plugin" \
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.25.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