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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +20 -0
- data/lib/trmnlp/cli.rb +2 -0
- data/lib/trmnlp/commands/lint.rb +18 -3
- data/lib/trmnlp/lint/checks/arbitrary_values_in_range.rb +70 -0
- data/lib/trmnlp/lint/checks/image_links_reachable.rb +6 -1
- data/lib/trmnlp/lint/checks/limited_inline_styles.rb +12 -5
- data/lib/trmnlp/lint/diagnostic.rb +92 -0
- data/lib/trmnlp/lint/source.rb +48 -0
- data/lib/trmnlp/lint.rb +2 -0
- data/lib/trmnlp/oauth/session.rb +6 -0
- data/lib/trmnlp/poller.rb +17 -14
- data/lib/trmnlp/transform_pipeline.rb +1 -1
- data/lib/trmnlp/version.rb +1 -1
- data/trmnl_preview.gemspec +2 -0
- metadata +31 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de41fcea181f5a6a71aba1dcd87b4faa4c3d886df1f6581c3008a0f91e597bba
|
|
4
|
+
data.tar.gz: e9f1c5bb500da483e282ec826f812915b17dbfbbe5704e2b2be2c36de802e4cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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)
|
data/lib/trmnlp/commands/lint.rb
CHANGED
|
@@ -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
|
|
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?
|
|
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 =
|
|
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
|
data/lib/trmnlp/lint/source.rb
CHANGED
|
@@ -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,
|
data/lib/trmnlp/oauth/session.rb
CHANGED
|
@@ -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
|
-
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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: #{
|
|
58
|
+
reporter.info("transform failed: #{result.error || "exited #{result.exit_code}"}")
|
|
59
59
|
fallback
|
|
60
60
|
end
|
|
61
61
|
|
data/lib/trmnlp/version.rb
CHANGED
data/trmnl_preview.gemspec
CHANGED
|
@@ -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.
|
|
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
|