trmnl_preview 0.14.0 → 0.14.2
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 +11 -0
- data/README.md +1 -1
- data/lib/trmnlp/lint/checks/arbitrary_values_in_range.rb +70 -0
- data/lib/trmnlp/lint/checks/recipe_overview_length.rb +26 -0
- data/lib/trmnlp/lint/source.rb +2 -0
- data/lib/trmnlp/lint.rb +4 -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
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2dabcf8ddd9b11d8ce8fc76e389820c7cd4f2d3b48db7aa7848896f9dfb6ec59
|
|
4
|
+
data.tar.gz: 467322fc910fdfed1ed5c37bb9773a7256d3d8f1f44683e2c9e4f3882b4ab7df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75747b93b8aa45ac964b1ae87ef0dd50134c836bf5bc4bef19e47f166132b1dfca6edf8744f967702be6ffdc4c80c7b9b725646443053708f2d74df37a8537e9
|
|
7
|
+
data.tar.gz: b6fa8f755ab31f3dae9a3c2773ced039f646c7363288ae27512d0e3a95e72143093ca2869cb26a402084dd1b66c6c2dfc29a022ef8fed84bbb6e18fa5ffed347
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
|
|
2
2
|
# Changelog
|
|
3
3
|
|
|
4
|
+
## 0.14.2
|
|
5
|
+
|
|
6
|
+
- 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)
|
|
7
|
+
- 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)
|
|
8
|
+
- 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)
|
|
9
|
+
- `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)
|
|
10
|
+
|
|
11
|
+
## 0.14.1
|
|
12
|
+
|
|
13
|
+
- `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)
|
|
14
|
+
|
|
4
15
|
## 0.14.0
|
|
5
16
|
|
|
6
17
|
- `trmnlp serve` and `build` print what the transform writes to stdout and stderr (`console.log`, `print`, `puts`), one `transform stdout:` / `transform stderr:` line each, so a transform can be debugged without returning its logs in the data. (#128)
|
data/README.md
CHANGED
|
@@ -434,7 +434,7 @@ The `settings.yml` file is part of the plugin definition, and is uploaded and do
|
|
|
434
434
|
|
|
435
435
|
`description:` is an optional one-line summary of the plugin, up to 35 characters. `trmnlp lint` reports anything longer, and `trmnlp list` shows it next to each plugin name.
|
|
436
436
|
|
|
437
|
-
`recipe_overview:` is optional free text for a published recipe, with no length limit. TRMNL shows it on the public recipe page with its line breaks kept, and its first 120 characters feed the page's link preview. An overview of at least 100 words lets the recipe appear in Google and other search engines. Put credits and links in an About This Plugin field instead. Leave the key out to keep the overview already on TRMNL; an empty value clears it.
|
|
437
|
+
`recipe_overview:` is optional free text for a published recipe, with no length limit. TRMNL shows it on the public recipe page with its line breaks kept, and its first 120 characters feed the page's link preview. An overview of at least 100 words lets the recipe appear in Google and other search engines; `trmnlp lint` reports one that is shorter. Put credits and links in an About This Plugin field instead. Leave the key out to keep the overview already on TRMNL; an empty value clears it.
|
|
438
438
|
|
|
439
439
|
See [TRMNL documentation](https://help.trmnl.com/en/articles/10542599-importing-and-exporting-private-plugins#h_581fb988f0) for details on this file's contents.
|
|
440
440
|
|
|
@@ -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
|
|
@@ -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
|
+
# Below this many words the hosted service serves the public recipe page
|
|
9
|
+
# with `robots: noindex`, so search engines drop it. The overview is
|
|
10
|
+
# optional (private plugins never show one), so a blank value passes.
|
|
11
|
+
# Words are counted on whitespace; there is no upstream count to match.
|
|
12
|
+
class RecipeOverviewLength < Check
|
|
13
|
+
MIN_WORDS = 100
|
|
14
|
+
MESSAGE = "Recipe overview should be at least #{MIN_WORDS} words long " \
|
|
15
|
+
'for the recipe page to appear in Google and other search engines.'.freeze
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def pass?
|
|
20
|
+
words = source.recipe_overview.split.size
|
|
21
|
+
words.zero? || words >= MIN_WORDS
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/trmnlp/lint/source.rb
CHANGED
|
@@ -17,6 +17,8 @@ module TRMNLP
|
|
|
17
17
|
|
|
18
18
|
def plugin_name = settings['name'].to_s
|
|
19
19
|
def plugin_description = settings['description'].to_s
|
|
20
|
+
def recipe_overview = settings['recipe_overview'].to_s
|
|
21
|
+
def framework_version = config.plugin.framework_version
|
|
20
22
|
def settings = config.plugin.settings
|
|
21
23
|
def custom_field_values = config.project.custom_fields
|
|
22
24
|
def custom_field_definitions = config.plugin.custom_field_definitions
|
data/lib/trmnlp/lint.rb
CHANGED
|
@@ -5,11 +5,13 @@ require_relative 'lint/source'
|
|
|
5
5
|
require_relative 'lint/checks/title_casing'
|
|
6
6
|
require_relative 'lint/checks/title_length'
|
|
7
7
|
require_relative 'lint/checks/description_length'
|
|
8
|
+
require_relative 'lint/checks/recipe_overview_length'
|
|
8
9
|
require_relative 'lint/checks/layouts_have_content'
|
|
9
10
|
require_relative 'lint/checks/no_async_functions'
|
|
10
11
|
require_relative 'lint/checks/waits_for_dom_load'
|
|
11
12
|
require_relative 'lint/checks/limited_inline_styles'
|
|
12
13
|
require_relative 'lint/checks/no_size_classes'
|
|
14
|
+
require_relative 'lint/checks/arbitrary_values_in_range'
|
|
13
15
|
require_relative 'lint/checks/no_opacity'
|
|
14
16
|
require_relative 'lint/checks/highcharts_animations_disabled'
|
|
15
17
|
require_relative 'lint/checks/highcharts_elements_unique'
|
|
@@ -25,11 +27,13 @@ module TRMNLP
|
|
|
25
27
|
Checks::TitleCasing,
|
|
26
28
|
Checks::TitleLength,
|
|
27
29
|
Checks::DescriptionLength,
|
|
30
|
+
Checks::RecipeOverviewLength,
|
|
28
31
|
Checks::LayoutsHaveContent,
|
|
29
32
|
Checks::NoAsyncFunctions,
|
|
30
33
|
Checks::WaitsForDomLoad,
|
|
31
34
|
Checks::LimitedInlineStyles,
|
|
32
35
|
Checks::NoSizeClasses,
|
|
36
|
+
Checks::ArbitraryValuesInRange,
|
|
33
37
|
Checks::NoOpacity,
|
|
34
38
|
Checks::HighchartsAnimationsDisabled,
|
|
35
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
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.
|
|
4
|
+
version: 0.14.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rockwell Schrock
|
|
@@ -304,6 +304,7 @@ files:
|
|
|
304
304
|
- lib/trmnlp/image_quantizer.rb
|
|
305
305
|
- lib/trmnlp/lint.rb
|
|
306
306
|
- lib/trmnlp/lint/check.rb
|
|
307
|
+
- lib/trmnlp/lint/checks/arbitrary_values_in_range.rb
|
|
307
308
|
- lib/trmnlp/lint/checks/custom_fields_used.rb
|
|
308
309
|
- lib/trmnlp/lint/checks/description_length.rb
|
|
309
310
|
- lib/trmnlp/lint/checks/form_fields_valid.rb
|
|
@@ -315,6 +316,7 @@ files:
|
|
|
315
316
|
- lib/trmnlp/lint/checks/no_async_functions.rb
|
|
316
317
|
- lib/trmnlp/lint/checks/no_opacity.rb
|
|
317
318
|
- lib/trmnlp/lint/checks/no_size_classes.rb
|
|
319
|
+
- lib/trmnlp/lint/checks/recipe_overview_length.rb
|
|
318
320
|
- lib/trmnlp/lint/checks/title_casing.rb
|
|
319
321
|
- lib/trmnlp/lint/checks/title_length.rb
|
|
320
322
|
- lib/trmnlp/lint/checks/waits_for_dom_load.rb
|