trmnl_preview 0.14.1 → 0.15.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbe879abcadc7c6f7efceb9e54c88606aafd090a9538c9942f8cd3afef9596da
4
- data.tar.gz: d9a123366e021a55b8bdc5b55016e38f1f1b4107163bc570a5112d89c8084270
3
+ metadata.gz: de41fcea181f5a6a71aba1dcd87b4faa4c3d886df1f6581c3008a0f91e597bba
4
+ data.tar.gz: e9f1c5bb500da483e282ec826f812915b17dbfbbe5704e2b2be2c36de802e4cb
5
5
  SHA512:
6
- metadata.gz: 0cd3eb90f4a2231a28dfe8e08097a90bc0d70d93861f9decd1946708b77c0ddf4b5ba3e3dd1acbae0b1daac7238cb88ea74d9f504880adeb1ee7c6ac6402c774
7
- data.tar.gz: fbd799614df7d3c978ca2a67fb1b7916b446994bfbd15ce268f764ad0dbd62fed7a34f6c8e63621926490a3eabece74d44b089f3175bd0ad305de7958cf622a1
6
+ metadata.gz: 5cba8a2ebb0798770ac545866b2c6e882e857db554fc2bcfa41d7c1046e53aaf3a2d0f798b32c634e5f00c4353cbb6fdeb14a4e407e3770d6bc016c90e0717c2
7
+ data.tar.gz: d3941f34861a81f2a21f9d1907620fb7d90f2f7f5d0fcb2dda3bac43ce683fa5eaafc5825853358635025125904037cb1305a038be841522d020b1239f3b2b07
data/CHANGELOG.md CHANGED
@@ -1,6 +1,18 @@
1
1
 
2
2
  # Changelog
3
3
 
4
+ ## 0.15.0
5
+
6
+ - `trmnlp lint` names where each finding is: a rule ID such as `[no_opacity]`, then the file, line and column with the source line. Before, it printed only the message, so you had to search every view and Shared for the cause. `trmnlp lint --format json` writes the same report as one JSON object (`version`, `passed`, `issues`) for CI. Exit statuses are unchanged, and the values of unused custom fields in `.trmnlp.yml` are never printed. (#153)
7
+ - The inline-style check counts real CSS declarations in `style` attributes, still with a limit of six. It counted eight property names anywhere in the markup, so `font-size` in an HTML comment or a `<style>` block failed while seven `style="color:red"` passed. Comments, scripts, text and `<style>` blocks no longer count, and declarations inside Liquid `{% if %}` branches all count. (#152)
8
+
9
+ ## 0.14.2
10
+
11
+ - A failed transform prints its stderr once. The `transform failed:` line repeated the whole stack trace that the `transform stderr:` line had just printed; it now names only the exit code. The preview page still shows the full error. (#148)
12
+ - When a polling URL answers 401 and an OAuth account is connected, trmnlp refreshes the token and polls once more, as the hosted service does. Before, it refreshed only once the stored expiry had passed, so a token the provider revoked early kept failing until you reconnected. (#150)
13
+ - A polling response that is not a 200 still reaches the markup and the transform, as on the hosted service, with a warning for a status outside 2xx. trmnlp replaced it with `{}`, so a transform that follows a 202 "pending job" body (`job_id`, `poll`) got nothing locally. (#146)
14
+ - `trmnlp lint` reports bracketed Framework classes the Framework never generates, such as `w--[192px]` (sizes stop at 128px), `gap--[60px]` and `rounded--[51px]` (both stop at 50px), `h--[6cqw]` (height takes `cqh`) and, from 3.2.0, a screen-prefixed `md:gap--[20px]`. They match no CSS rule, so they do nothing: a flag sized `lg:w--[192px]` stayed at its smaller width on the X with no warning. The ranges are checked against every 3.x `plugins.css`, and plugins on Framework 2 or earlier are skipped, since v2 generated wider sizes. (#149)
15
+
4
16
  ## 0.14.1
5
17
 
6
18
  - `trmnlp lint` reports a `recipe_overview` under 100 words. Below that, TRMNL serves the public recipe page with `robots: noindex`, so the recipe never appears in Google or other search engines, and nothing said so: the page looked fine, and only its HTML gave it away. A blank overview still passes, since private plugins have none. (#144)
data/README.md CHANGED
@@ -99,6 +99,26 @@ trmnlp push # upload
99
99
 
100
100
  `trmnlp lint` exits non-zero when it finds issues, so you can gate CI on it. Run `trmnlp help` for all flags.
101
101
 
102
+ Lint findings include a stable snake_case rule ID, severity and source locations.
103
+ Locations use project-relative paths and one-based line/column numbers, followed
104
+ by a source excerpt (up to 240 characters). Aggregate checks show their contributing
105
+ locations; checks for a missing chart setting show the related Highcharts usage.
106
+ Unused project custom-field values are omitted from excerpts because they can
107
+ contain credentials.
108
+
109
+ For CI integrations, use `trmnlp lint --format json`. It writes one JSON object
110
+ with `version: 1`, `passed` and an `issues` array. Each issue has `rule_id`,
111
+ `severity`, `message`, `locations` and, when available, `learn_more`. Each location
112
+ has `path`, `line`, `column` and `snippet`. A clean report has `passed: true` and
113
+ `issues: []`. All existing checks retain severity `error` and the same exit status:
114
+ zero when clean, nonzero when findings exist. `--quiet` suppresses either format.
115
+
116
+ The inline-style check counts CSS declarations in actual HTML `style` attributes,
117
+ with a limit of six across the view templates and Shared. All property names count,
118
+ including custom properties. HTML/Liquid comments, script strings, text,
119
+ `data-*` attributes and `<style>` blocks are excluded from this inline check.
120
+ Liquid in declaration values is counted without rendering the plugin, every branch included.
121
+
102
122
  ## Building Static Files
103
123
 
104
124
  `trmnlp build` renders every view to a static file under `_build/` — handy for exporting a snapshot or feeding the output into another pipeline. Run it from inside a plugin project:
data/lib/trmnlp/cli.rb CHANGED
@@ -72,6 +72,8 @@ module TRMNLP
72
72
  end
73
73
 
74
74
  desc 'lint', 'Check plugin code against TRMNL best practices'
75
+ method_option :format, type: :string, default: 'text', enum: %w[text json],
76
+ desc: 'Report format (text or json)'
75
77
  def lint
76
78
  # Exit non-zero when issues are found so CI pipelines can gate on it.
77
79
  exit(1) unless Commands::Lint.run(options)
@@ -2,12 +2,18 @@
2
2
 
3
3
  require_relative 'base'
4
4
  require_relative '../lint'
5
+ require_relative '../lint/diagnostic'
6
+ require 'json'
5
7
 
6
8
  module TRMNLP
7
9
  module Commands
8
10
  # Runs the markup best-practice checks and reports their findings.
9
11
  class Lint < Base
10
- Options = Data.define(:dir, :quiet)
12
+ Options = Data.define(:dir, :quiet, :format) do
13
+ def initialize(dir:, quiet:, format: 'text')
14
+ super(dir:, quiet:, format: format || 'text')
15
+ end
16
+ end
11
17
 
12
18
  def call
13
19
  context.validate!
@@ -18,7 +24,10 @@ module TRMNLP
18
24
  private
19
25
 
20
26
  def issues
21
- @issues ||= TRMNLP::Lint::CHECKS.flat_map { |check| check.new(source).issues }.uniq
27
+ @issues ||= TRMNLP::Lint::CHECKS.flat_map do |type|
28
+ check = type.new(source)
29
+ check.issues.map { |finding| TRMNLP::Lint::Diagnostic.new(check, source, finding).to_h }
30
+ end.uniq
22
31
  end
23
32
 
24
33
  def source
@@ -26,6 +35,8 @@ module TRMNLP
26
35
  end
27
36
 
28
37
  def report
38
+ return reporter.info(JSON.generate(version: 1, passed: issues.empty?, issues:)) if options.format == 'json'
39
+
29
40
  return reporter.info(reporter.green('✓ All checks passed!')) if issues.empty?
30
41
 
31
42
  reporter.info(reporter.yellow("#{issues.size} issue#{'s' if issues.size > 1} found:\n"))
@@ -34,7 +45,11 @@ module TRMNLP
34
45
  end
35
46
 
36
47
  def report_issue(issue, index)
37
- reporter.info(" #{index + 1}. #{issue[:message]}")
48
+ reporter.info(" #{index + 1}. [#{issue[:rule_id]}] #{issue[:message]}")
49
+ issue[:locations].each do |location|
50
+ reporter.info(" #{location[:path]}:#{location[:line]}:#{location[:column]}")
51
+ reporter.info(" #{location[:snippet]}") unless location[:snippet].to_s.empty?
52
+ end
38
53
  reporter.info(" Learn more: #{issue[:learn_more]}") if issue[:learn_more]
39
54
  end
40
55
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../check'
4
+
5
+ module TRMNLP
6
+ module Lint
7
+ module Checks
8
+ # Reports bracketed Framework classes the Framework never generates, one
9
+ # finding per class. They match no rule, so they do nothing, and nothing
10
+ # says so: `lg:w--[192px]` leaves the element at its smaller width.
11
+ #
12
+ # The ranges mirror the loops in the Framework's utilities/_size.scss,
13
+ # _gap.scss and _rounded.scss, and hold for every 3.x release's
14
+ # plugins.css. v2 generated wider sizes, so earlier versions are skipped;
15
+ # prefixed gaps (`md:gap--[20px]`) existed until 3.2.0. There is no
16
+ # upstream file to sync from, so update this by hand when the Framework
17
+ # changes, the same way db/data/form_fields.yml is kept current.
18
+ class ArbitraryValuesInRange < Check
19
+ # Each family as written before the bracket: `w--[Npx]`, `w--max-[Npx]`.
20
+ SIZE_FAMILIES = %w[w-- h-- w--min- w--max- h--min- h--max-].freeze
21
+
22
+ # family => { unit => max }. Every range starts at 0 and steps by 1.
23
+ RANGES = SIZE_FAMILIES.to_h do |family|
24
+ [family, { 'px' => 128, (family.start_with?('w') ? 'cqw' : 'cqh') => 100 }]
25
+ end.merge('gap--' => { 'px' => 50 }, 'rounded--' => { 'px' => 50 }).freeze
26
+
27
+ # `gap--[Npx]` and `rounded--[Npx]` exist only without a screen prefix.
28
+ PREFIXABLE = SIZE_FAMILIES
29
+
30
+ FIRST_VERSION = Gem::Version.new('3.0.0')
31
+ UNPREFIXED_GAP_VERSION = Gem::Version.new('3.2.0')
32
+
33
+ PATTERN = /
34
+ (?<![\w:-])
35
+ (?<prefix>(?:[a-z0-9-]+:)*)
36
+ (?<family>w--min-|w--max-|h--min-|h--max-|w--|h--|gap--|rounded--)
37
+ \[(?<value>[\d.]+)(?<unit>[a-z]+)\]
38
+ /x
39
+
40
+ def issues
41
+ return [] if version < FIRST_VERSION
42
+
43
+ source.all_markup.scan(PATTERN).uniq.filter_map do |prefix, family, value, unit|
44
+ css_class = "#{prefix}#{family}[#{value}#{unit}]"
45
+ reason = problem(prefix, family, value, unit)
46
+ { message: "'#{css_class}' has no effect: #{reason}." } if reason
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def version = @version ||= Gem::Version.new(source.framework_version.number)
53
+
54
+ def problem(prefix, family, value, unit)
55
+ max = RANGES.dig(family, unit)
56
+ return "the Framework has no '#{unit}' values for '#{family}'" unless max
57
+ return "'#{family}[Npx]' takes no screen prefix" if prefix_unsupported?(prefix, family)
58
+ return nil if value.match?(/\A\d+\z/) && value.to_i <= max
59
+
60
+ "the Framework generates '#{family}[N#{unit}]' from 0 to #{max} in whole numbers"
61
+ end
62
+
63
+ def prefix_unsupported?(prefix, family)
64
+ !prefix.empty? && !PREFIXABLE.include?(family) &&
65
+ (family == 'rounded--' || version >= UNPREFIXED_GAP_VERSION)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -19,9 +19,14 @@ module TRMNLP
19
19
  UNREACHABLE = [SocketError, Net::OpenTimeout, Net::ReadTimeout,
20
20
  Errno::ECONNREFUSED, Errno::EHOSTUNREACH, OpenSSL::SSL::SSLError].freeze
21
21
 
22
+ attr_reader :unreachable_urls
23
+
22
24
  private
23
25
 
24
- def pass? = static_image_urls.all? { |url| reachable?(url) }
26
+ def pass?
27
+ @unreachable_urls = static_image_urls.reject { |url| reachable?(url) }
28
+ unreachable_urls.empty?
29
+ end
25
30
 
26
31
  def static_image_urls
27
32
  source.all_markup
@@ -1,25 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../check'
4
+ require 'crass'
5
+ require 'nokogiri'
4
6
 
5
7
  module TRMNLP
6
8
  module Lint
7
9
  module Checks
8
10
  class LimitedInlineStyles < Check
9
11
  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
12
  MESSAGE = 'Markup uses too many inline styles, add more native Framework classes.'
15
13
  LEARN_MORE = 'https://help.trmnl.com/en/articles/11395668-recipe-best-practices#h_3a3eab0712'
16
14
 
17
15
  private
18
16
 
19
17
  def pass?
20
- count = PROPERTIES.sum { |property| source.all_markup.scan(property).size }
18
+ count = Nokogiri::HTML.fragment(markup_without_liquid_comments).css('[style]').sum do |element|
19
+ # Count Liquid output as a CSS value and drop Liquid tags, so every
20
+ # branch's declarations count without evaluating the template.
21
+ css = element['style'].gsub(/\{\{.*?\}\}/m, 'var(--trmnlp-liquid)').gsub(/\{%.*?%\}/m, '')
22
+ Crass.parse_properties(css).count { |token| token[:node] == :property }
23
+ end
21
24
  count <= MAX_INLINE_STYLES
22
25
  end
26
+
27
+ def markup_without_liquid_comments
28
+ source.all_markup.gsub(/\{%-?\s*comment\s*-?%\}.*?\{%-?\s*endcomment\s*-?%\}/m, '')
29
+ end
23
30
  end
24
31
  end
25
32
  end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRMNLP
4
+ module Lint
5
+ # Adds reporting metadata without changing a check's existing #issues API.
6
+ # Rule IDs are the snake_case check names and are part of the report contract.
7
+ class Diagnostic
8
+ SETTINGS_KEYS = {
9
+ 'title_casing' => 'name', 'title_length' => 'name',
10
+ 'description_length' => 'description', 'recipe_overview_length' => 'recipe_overview'
11
+ }.freeze
12
+ MARKUP_PATTERNS = {
13
+ 'no_async_functions' => /async function/i,
14
+ 'no_opacity' => Checks::NoOpacity::PATTERN,
15
+ 'no_size_classes' => Checks::NoSizeClasses::PATTERN,
16
+ 'waits_for_dom_load' => Regexp.new(Regexp.union(Checks::WaitsForDomLoad::FORBIDDEN).source, Regexp::IGNORECASE),
17
+ 'limited_inline_styles' => /\sstyle\s*=/i,
18
+ 'highcharts_animations_disabled' => /highcharts/i,
19
+ 'highcharts_elements_unique' => /highcharts/i
20
+ }.freeze
21
+
22
+ def initialize(check, source, finding)
23
+ @check = check
24
+ @source = source
25
+ @finding = finding
26
+ end
27
+
28
+ def to_h
29
+ finding.merge(rule_id:, severity: 'error', locations: locations.uniq)
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :check, :source, :finding
35
+
36
+ def rule_id
37
+ check.class.name.split('::').last.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
38
+ end
39
+
40
+ def locations
41
+ return source.yaml_location('src/settings.yml', SETTINGS_KEYS[rule_id]) if SETTINGS_KEYS.key?(rule_id)
42
+ return source.locations(MARKUP_PATTERNS[rule_id]) if MARKUP_PATTERNS.key?(rule_id)
43
+
44
+ case rule_id
45
+ when 'arbitrary_values_in_range' then class_locations
46
+ when 'layouts_have_content' then empty_view_locations
47
+ when 'form_fields_valid' then form_field_locations
48
+ when 'custom_fields_used' then project_field_locations
49
+ when 'image_links_reachable' then source.locations(Regexp.union(check.unreachable_urls))
50
+ else []
51
+ end
52
+ end
53
+
54
+ def class_locations
55
+ css_class = finding[:message][/\A'([^']+)'/, 1]
56
+ css_class ? source.locations(Regexp.new(Regexp.escape(css_class))) : []
57
+ end
58
+
59
+ def empty_view_locations
60
+ return [] if source.shared_markup.length >= Checks::LayoutsHaveContent::MIN_CONTENT_LENGTH
61
+
62
+ source.view_markup.filter_map do |view, markup|
63
+ if markup.length < Checks::LayoutsHaveContent::MIN_CONTENT_LENGTH
64
+ { path: "src/#{view}.liquid", line: 1, column: 1, snippet: markup }
65
+ end
66
+ end
67
+ end
68
+
69
+ def form_field_locations
70
+ warning = finding[:message].split(' — ', 2).last
71
+ source.custom_field_definitions.each_with_index.flat_map do |field, index|
72
+ if FormField.validate(field).include?(warning)
73
+ source.yaml_location('src/settings.yml', 'custom_fields', index)
74
+ else
75
+ []
76
+ end
77
+ end
78
+ end
79
+
80
+ def project_field_locations
81
+ key = finding[:message][/Custom field '([^']+)'/, 1]
82
+ return [] unless key
83
+
84
+ # Project custom-field values can contain credentials. Show the key,
85
+ # but never echo its value into terminal output or CI artifacts.
86
+ source.yaml_location('.trmnlp.yml', 'custom_fields', key).map do |location|
87
+ location.merge(snippet: "#{key}: [value omitted]")
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'yaml'
4
+
3
5
  module TRMNLP
4
6
  module Lint
5
7
  # The plugin data every lint check examines: markup per view, shared
@@ -18,6 +20,7 @@ module TRMNLP
18
20
  def plugin_name = settings['name'].to_s
19
21
  def plugin_description = settings['description'].to_s
20
22
  def recipe_overview = settings['recipe_overview'].to_s
23
+ def framework_version = config.plugin.framework_version
21
24
  def settings = config.plugin.settings
22
25
  def custom_field_values = config.project.custom_fields
23
26
  def custom_field_definitions = config.plugin.custom_field_definitions
@@ -43,10 +46,55 @@ module TRMNLP
43
46
  end
44
47
  end
45
48
 
49
+ # Keep the original whitespace here: stripped templates cannot provide
50
+ # accurate line/column numbers. All paths in reports are project-relative.
51
+ def markup_files
52
+ @markup_files ||= (VIEWS + ['shared']).to_h do |view|
53
+ ["src/#{view}.liquid", raw_read(paths.template(view))]
54
+ end
55
+ end
56
+
57
+ def locations(pattern, files: markup_files)
58
+ files.flat_map do |path, contents|
59
+ contents.to_enum(:scan, pattern).map do
60
+ offset = Regexp.last_match.begin(0)
61
+ prefix = contents[0...offset]
62
+ line = prefix.count("\n") + 1
63
+ { path:, line:, column: offset - (prefix.rindex("\n") || -1),
64
+ snippet: contents.lines[line - 1].to_s.chomp[0, 240] }
65
+ end
66
+ end.uniq
67
+ end
68
+
69
+ def yaml_location(file, *keys)
70
+ path = file == '.trmnlp.yml' ? paths.trmnlp_config : paths.plugin_config
71
+ contents = raw_read(path)
72
+ node = Psych.parse_stream(contents).children.first&.root
73
+ keys.each do |key|
74
+ node = if key.is_a?(Integer) && node.is_a?(Psych::Nodes::Sequence)
75
+ node.children[key]
76
+ else
77
+ mapping_value(node, key)
78
+ end
79
+ end
80
+ return [] unless node
81
+
82
+ [{ path: file, line: node.start_line + 1, column: node.start_column + 1,
83
+ snippet: contents.lines[node.start_line].to_s.chomp[0, 240] }]
84
+ end
85
+
46
86
  private
47
87
 
48
88
  attr_reader :config, :paths
49
89
 
90
+ def raw_read(path) = path.exist? ? path.read : ''
91
+
92
+ def mapping_value(node, key)
93
+ return unless node.is_a?(Psych::Nodes::Mapping)
94
+
95
+ node.children.each_slice(2).to_a.rfind { |name, _| name.value == key.to_s }&.last
96
+ end
97
+
50
98
  def read(path) = path.exist? ? path.read.strip : ''
51
99
  end
52
100
  end
data/lib/trmnlp/lint.rb CHANGED
@@ -11,6 +11,7 @@ require_relative 'lint/checks/no_async_functions'
11
11
  require_relative 'lint/checks/waits_for_dom_load'
12
12
  require_relative 'lint/checks/limited_inline_styles'
13
13
  require_relative 'lint/checks/no_size_classes'
14
+ require_relative 'lint/checks/arbitrary_values_in_range'
14
15
  require_relative 'lint/checks/no_opacity'
15
16
  require_relative 'lint/checks/highcharts_animations_disabled'
16
17
  require_relative 'lint/checks/highcharts_elements_unique'
@@ -32,6 +33,7 @@ module TRMNLP
32
33
  Checks::WaitsForDomLoad,
33
34
  Checks::LimitedInlineStyles,
34
35
  Checks::NoSizeClasses,
36
+ Checks::ArbitraryValuesInRange,
35
37
  Checks::NoOpacity,
36
38
  Checks::HighchartsAnimationsDisabled,
37
39
  Checks::HighchartsElementsUnique,
@@ -45,6 +45,12 @@ module TRMNLP
45
45
 
46
46
  def disconnect = token_store.clear
47
47
 
48
+ # For a token the provider rejected before its expiry. nil when there is nothing to refresh.
49
+ def force_refresh!
50
+ stored = token_store.read
51
+ refreshed(stored) if stored&.access_token && stored.refresh_token
52
+ end
53
+
48
54
  private
49
55
 
50
56
  attr_reader :provider, :token_store, :client
data/lib/trmnlp/poller.rb CHANGED
@@ -43,21 +43,28 @@ module TRMNLP
43
43
  attr_reader :config, :paths, :oauth_session, :reporter
44
44
 
45
45
  def aggregate_responses
46
- # Resolve once per poll (it refreshes as a side effect), then share across
47
- # every URL, header, and body render.
48
- oauth_variables = oauth_session.liquid_variables
49
- urls = config.plugin.polling_urls(extra_variables: oauth_variables)
50
- responses = urls.map { |url| fetch_one(url, oauth_variables) }
46
+ fetched = fetch_all
47
+ # Like the hosted service: a token rejected before its expiry gets one refresh and retry.
48
+ fetched = fetch_all if fetched.any? { |_url, response| response.status == 401 } && oauth_session.force_refresh!
49
+ responses = fetched.map { |url, response| parse_response(response, url) }
51
50
  return responses.first if responses.size == 1
52
51
 
53
52
  responses.each_with_index.with_object({}) { |(r, i), h| h["IDX_#{i}"] = r }
54
53
  end
55
54
 
55
+ def fetch_all
56
+ # Resolve once per attempt (it refreshes as a side effect), then share across
57
+ # every URL, header, and body render.
58
+ oauth_variables = oauth_session.liquid_variables
59
+ urls = config.plugin.polling_urls(extra_variables: oauth_variables)
60
+ urls.map { |url| [url, fetch_one(url, oauth_variables)] }
61
+ end
62
+
56
63
  def fetch_one(url, oauth_variables)
57
64
  verb = config.plugin.polling_verb.upcase
58
65
  response = perform_request(url, verb, oauth_variables)
59
66
  reporter.info("#{verb} #{url} — received #{response.body.length} bytes (#{response.status} status)")
60
- parse_response(response)
67
+ response
61
68
  end
62
69
 
63
70
  def perform_request(url, verb, oauth_variables)
@@ -69,17 +76,13 @@ module TRMNLP
69
76
  end
70
77
  end
71
78
 
72
- def parse_response(response)
73
- return parse_failure(response.body) unless response.status == 200
74
-
79
+ # Like the hosted service: an error status is reported, but its body still reaches the data.
80
+ def parse_response(response, url)
81
+ success = (200..299).cover?(response.status)
82
+ reporter.info(reporter.yellow("warning: HTTP #{response.status} from #{url}")) unless success
75
83
  parse_body(response.body, response.headers['content-type'])
76
84
  end
77
85
 
78
- def parse_failure(body)
79
- reporter.info(body)
80
- {}
81
- end
82
-
83
86
  def parse_body(body, content_type_header)
84
87
  content_type = content_type_header&.split(';')&.first&.strip
85
88
  case content_type
@@ -55,7 +55,7 @@ module TRMNLP
55
55
 
56
56
  def record_failure(result, fallback)
57
57
  @error = result.error || "transform exited #{result.exit_code}: #{result.stderr.strip}"
58
- reporter.info("transform failed: #{@error}")
58
+ reporter.info("transform failed: #{result.error || "exited #{result.exit_code}"}")
59
59
  fallback
60
60
  end
61
61
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRMNLP
4
- VERSION = '0.14.1'
4
+ VERSION = '0.15.0'
5
5
  end
@@ -47,6 +47,8 @@ Gem::Specification.new do |spec|
47
47
 
48
48
  # HTML rendering
49
49
  spec.add_dependency 'activesupport', '~> 8.0'
50
+ spec.add_dependency 'crass', '~> 1.0'
51
+ spec.add_dependency 'nokogiri', '~> 1.18'
50
52
  spec.add_dependency 'trmnl-liquid', '~> 0.8.2'
51
53
 
52
54
  # PNG rendering
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.14.1
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rockwell Schrock
@@ -65,6 +65,34 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '8.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: crass
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '1.0'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: nokogiri
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '1.18'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '1.18'
68
96
  - !ruby/object:Gem::Dependency
69
97
  name: trmnl-liquid
70
98
  requirement: !ruby/object:Gem::Requirement
@@ -304,6 +332,7 @@ files:
304
332
  - lib/trmnlp/image_quantizer.rb
305
333
  - lib/trmnlp/lint.rb
306
334
  - lib/trmnlp/lint/check.rb
335
+ - lib/trmnlp/lint/checks/arbitrary_values_in_range.rb
307
336
  - lib/trmnlp/lint/checks/custom_fields_used.rb
308
337
  - lib/trmnlp/lint/checks/description_length.rb
309
338
  - lib/trmnlp/lint/checks/form_fields_valid.rb
@@ -319,6 +348,7 @@ files:
319
348
  - lib/trmnlp/lint/checks/title_casing.rb
320
349
  - lib/trmnlp/lint/checks/title_length.rb
321
350
  - lib/trmnlp/lint/checks/waits_for_dom_load.rb
351
+ - lib/trmnlp/lint/diagnostic.rb
322
352
  - lib/trmnlp/lint/source.rb
323
353
  - lib/trmnlp/oauth.rb
324
354
  - lib/trmnlp/oauth/client.rb