trmnl_preview 0.30.2 → 0.30.4

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: 685a69fdb40d83171f2f8cb17d89b6954fae9c97f9f551d4210d228acbdb0690
4
- data.tar.gz: e666f0a63344dc33447eb70cabf3202bab6c6030f967716e3b50954583e222e7
3
+ metadata.gz: 19d57b76b4b9fac2e897d2b4f18459bce312329e9ed647cb950781e5834387e6
4
+ data.tar.gz: 3f12fe83acc40963e8a36a218275e42713aa5211ea80761cb46a44d72889de01
5
5
  SHA512:
6
- metadata.gz: a53e8130820062a99d94d25b2a140506baefccb9e2747c30c46d2cc7795a96c423c4c537c520514c3706a9aafacd1cb215957fdd3af6048b5145632ddb24058a
7
- data.tar.gz: ebbaa2038fb76c066394dd98218a5305741d2b274763ddc2d41ad4151fc096763ec4593338f364d71807f032687264341e0585f17cf630684276101d757c6f78
6
+ metadata.gz: ec019e1a423c47855cd9a96b1ef39578135e99aa9fb8b2b807e67f919b08884d360a180bc4619dcbdace7bf99095eeea4d1a69d672f6821a66ea3d33a78ee9c7
7
+ data.tar.gz: 4bb9acf58ffd364b56a265a16e4a4d2cafa48847cc763c27f7d74d0fd0b261cc69fca465e6af327cfbdd7fb8e116cff46f9fff372e636b9733679af875834758
data/CHANGELOG.md CHANGED
@@ -1,6 +1,15 @@
1
1
 
2
2
  # Changelog
3
3
 
4
+ ## 0.30.4
5
+
6
+ - `trmnlp lint` reports a margin, padding, gap or position class with a leading minus (`no_negative_spacing`), such as `-mt--2` or `lg:-top--1`. Tailwind writes negative values this way, but no Framework release defines a class that starts with a minus, so TRMNL ignores it and says nothing: an icon nudged up with `-mt--1` stays where it was.
7
+
8
+ ## 0.30.3
9
+
10
+ - `trmnlp test` serves the Framework's stylesheets, scripts and fonts from a cache in `~/.cache/trmnl/assets`, downloaded from trmnl.com once and kept as long as trmnl.com says. A run no longer fails when trmnl.com is slow or down, as long as the files were fetched before; when a file is due for a refresh and trmnl.com cannot be reached, the copy on disk is used.
11
+ - `trmnlp test` also serves the Google Fonts stylesheet the Framework links for Inter, and its font files, from the same cache, asked for as Firefox asks so Google sends the same fonts. A test page now loads nothing from outside unless the recipe itself does.
12
+
4
13
  ## 0.30.2
5
14
 
6
15
  - `trmnlp serve`'s HTML preview of a device TRMNL scales runs the page's scripts at the CSS size too, then shows the page at the device's size and stops its timers, as the PNG and the device do. Before, a table showed 8 of 20 rows on a TRMNL X where the device shows 15.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../check'
4
+
5
+ module TRMNLP
6
+ module Lint
7
+ module Checks
8
+ # Reports margin, padding, gap and position classes written with a leading
9
+ # minus, such as `-mt--2` or `lg:-top--1`. Tailwind spells negative values
10
+ # this way, but no Framework release, from 1.0.0 to 3.4.2, defines a class
11
+ # that starts with a minus, so the class does nothing and nothing says so:
12
+ # an icon nudged up with `-mt--1` stays where it was.
13
+ class NoNegativeSpacing < Check
14
+ LEARN_MORE = 'https://trmnl.com/framework/docs/spacing'
15
+
16
+ PATTERN = /
17
+ (?<![\w:-])
18
+ (?<prefix>(?:[a-z0-9]+:)*)
19
+ -(?<family>[mp][trblxy]?|gap|top|right|bottom|left|inset)
20
+ --(?<value>[\w.]+)
21
+ (?![\w-])
22
+ /x
23
+
24
+ def issues
25
+ source.all_markup.scan(PATTERN).uniq.map do |prefix, family, value|
26
+ { message: "'#{prefix}-#{family}--#{value}' has no effect: " \
27
+ "the Framework has no negative '#{family}--' classes.",
28
+ learn_more: LEARN_MORE }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -43,7 +43,8 @@ module TRMNLP
43
43
  return source.locations(MARKUP_PATTERNS[rule_id]) if MARKUP_PATTERNS.key?(rule_id)
44
44
 
45
45
  case rule_id
46
- when 'arbitrary_values_in_range', 'framework_classes_exist', 'framework_prefixes_exist' then class_locations
46
+ when 'arbitrary_values_in_range', 'no_negative_spacing', 'framework_classes_exist', 'framework_prefixes_exist'
47
+ class_locations
47
48
  when 'layouts_have_content' then empty_view_locations
48
49
  when 'form_fields_valid' then form_field_locations
49
50
  when 'custom_field_links_embedded' then custom_field_description_locations
data/lib/trmnlp/lint.rb CHANGED
@@ -13,6 +13,7 @@ require_relative 'lint/checks/limited_inline_styles'
13
13
  require_relative 'lint/checks/no_style_restyling_framework'
14
14
  require_relative 'lint/checks/no_size_classes'
15
15
  require_relative 'lint/checks/arbitrary_values_in_range'
16
+ require_relative 'lint/checks/no_negative_spacing'
16
17
  require_relative 'lint/checks/no_opacity'
17
18
  require_relative 'lint/checks/gray_text_has_one_bit_class'
18
19
  require_relative 'lint/checks/highcharts_animations_disabled'
@@ -52,6 +53,7 @@ module TRMNLP
52
53
  Checks::NoStyleRestylingFramework,
53
54
  Checks::NoSizeClasses,
54
55
  Checks::ArbitraryValuesInRange,
56
+ Checks::NoNegativeSpacing,
55
57
  Checks::NoOpacity,
56
58
  Checks::GrayTextHasOneBitClass,
57
59
  Checks::HighchartsAnimationsDisabled,
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'net/http'
7
+ require 'socket'
8
+
9
+ require_relative '../framework_version'
10
+
11
+ module TRMNLP
12
+ module Testing
13
+ # Serves the Framework's files and Google Fonts to test pages from disk, for as long as their hosts say.
14
+ class AssetCache
15
+ HOST = FrameworkVersion::DEFAULT_ASSET_HOST
16
+ FRAMEWORK_LINK = %r{#{Regexp.escape(HOST)}(?=/(?:css|js)/)}
17
+ GOOGLE_FONTS_LINK = %r{https://(fonts\.(?:googleapis|gstatic)\.com)/}
18
+ SAFE_PATH = %r{\A/(?:[\w.-]+/)*[\w.-]+\z}
19
+ DOWNLOAD_TIMEOUT_SECONDS = 10
20
+ BAD_GATEWAY = "HTTP/1.1 502 Bad Gateway\r\ncontent-length: 0\r\nconnection: close\r\n\r\n"
21
+
22
+ def initialize(dir:, clock: -> { Time.now })
23
+ @dir = dir.to_s
24
+ @clock = clock
25
+ end
26
+
27
+ def start
28
+ @server = TCPServer.new('127.0.0.1', 0)
29
+ @thread = Thread.new { loop { Thread.new(@server.accept) { serve(it) } } }
30
+ self
31
+ end
32
+
33
+ def stop
34
+ @thread&.kill
35
+ @server&.close
36
+ end
37
+
38
+ def url = "http://127.0.0.1:#{@server.addr[1]}"
39
+
40
+ def point_framework_links_at_cache(html) = point_google_fonts_at_cache(html.gsub(FRAMEWORK_LINK, url))
41
+
42
+ private
43
+
44
+ def point_google_fonts_at_cache(text) = text.gsub(GOOGLE_FONTS_LINK) { "#{url}/#{::Regexp.last_match(1)}/" }
45
+
46
+ def serve(socket)
47
+ path, query = socket.gets.to_s.split[1].to_s.split('?', 2)
48
+ user_agent = nil
49
+ while (line = socket.gets) && !line.strip.empty?
50
+ user_agent = line.split(':', 2).last.strip if line.downcase.start_with?('user-agent:')
51
+ end
52
+ asset = !path.to_s.include?('..') && path.to_s.match?(SAFE_PATH) && asset(path, query, user_agent)
53
+ socket.write(asset ? response(asset) : BAD_GATEWAY)
54
+ rescue IOError, SystemCallError
55
+ nil
56
+ ensure
57
+ socket.close unless socket.closed?
58
+ end
59
+
60
+ def asset(path, query, user_agent)
61
+ key = query ? "#{path}.#{Digest::SHA256.hexdigest(query)[0, 16]}" : path
62
+ cached = cached(key)
63
+ return cached if cached && cached['expires_at'] > @clock.call.to_i
64
+
65
+ download(key, URI("#{upstream(path)}#{"?#{query}" if query}"), user_agent) || cached
66
+ end
67
+
68
+ # A Google Fonts path starts with its host, /fonts.gstatic.com/s/...; any other path is trmnl.com's.
69
+ def upstream(path)
70
+ host, rest = path.delete_prefix('/').split('/', 2)
71
+ host.match?(/\Afonts\.(googleapis|gstatic)\.com\z/) ? "https://#{host}/#{rest}" : "#{HOST}#{path}"
72
+ end
73
+
74
+ def cached(key)
75
+ meta = JSON.parse(File.read(file(key, '.json')))
76
+ meta.merge('body' => File.binread(file(key)))
77
+ rescue Errno::ENOENT, JSON::ParserError
78
+ nil
79
+ end
80
+
81
+ # trmnl.com sends its CORS header only to a request with an Origin; Google picks the font format by User-Agent.
82
+ def download(key, uri, user_agent)
83
+ headers = { 'Origin' => url, 'User-Agent' => user_agent }.compact
84
+ timeouts = { open_timeout: DOWNLOAD_TIMEOUT_SECONDS, read_timeout: DOWNLOAD_TIMEOUT_SECONDS }
85
+ answer = Net::HTTP.start(uri.host, uri.port, use_ssl: true, **timeouts) { it.get(uri.request_uri, headers) }
86
+ answer.is_a?(Net::HTTPSuccess) ? cache_download(key, answer) : nil
87
+ rescue StandardError
88
+ nil
89
+ end
90
+
91
+ def cache_download(path, answer)
92
+ meta = { 'content_type' => answer['content-type'], 'cache_control' => answer['cache-control'],
93
+ 'allow_origin' => answer['access-control-allow-origin'],
94
+ 'expires_at' => @clock.call.to_i + answer['cache-control'].to_s[/max-age=(\d+)/, 1].to_i }
95
+ write_to_disk(path, answer.body, meta)
96
+ meta.merge('body' => answer.body)
97
+ end
98
+
99
+ def write_to_disk(path, body, meta)
100
+ write(file(path), body)
101
+ write(file(path, '.json'), JSON.generate(meta))
102
+ rescue SystemCallError
103
+ nil
104
+ end
105
+
106
+ # Written beside the file and renamed over it, so another run reading the cache sees a whole file.
107
+ def write(path, content)
108
+ FileUtils.mkdir_p(File.dirname(path))
109
+ temporary = "#{path}.#{Process.pid}.#{Thread.current.object_id}"
110
+ File.binwrite(temporary, content)
111
+ File.rename(temporary, path)
112
+ end
113
+
114
+ def file(path, extension = '') = File.join(@dir, "#{path}#{extension}")
115
+
116
+ # A stylesheet's Google font files are pointed at the cache as it is served, since the cache's port changes.
117
+ def response(asset)
118
+ stylesheet = asset['content_type'].to_s.start_with?('text/css')
119
+ body = stylesheet ? point_google_fonts_at_cache(asset['body']) : asset['body']
120
+ headers = { 'content-type' => asset['content_type'], 'cache-control' => asset['cache_control'],
121
+ 'access-control-allow-origin' => asset['allow_origin'],
122
+ 'content-length' => body.bytesize, 'connection' => 'close' }.compact
123
+ "HTTP/1.1 200 OK\r\n#{headers.map { "#{it[0]}: #{it[1]}\r\n" }.join}\r\n#{body}"
124
+ end
125
+ end
126
+ end
127
+ end
@@ -13,9 +13,10 @@ module TRMNLP
13
13
  # The Firefox one example renders in, checked out of the pool on first use and back in by #release.
14
14
  # Pages are drawn through ScreenGenerator and Screenshot like `trmnlp build --png`'s, but opened from page_server.
15
15
  class Browser
16
- def initialize(pool:, page_server:)
16
+ def initialize(pool:, page_server:, asset_cache:)
17
17
  @pool = pool
18
18
  @page_server = page_server
19
+ @asset_cache = asset_cache
19
20
  @screenshot = Screenshot.new(pool:)
20
21
  @pages = {}.compare_by_identity
21
22
  @fresh_browsers = []
@@ -24,13 +25,15 @@ module TRMNLP
24
25
  # A Firefox of its own, with nothing cached, closed when this browser is released.
25
26
  def fresh
26
27
  pool = BrowserPool.new(driver_factory: FirefoxDriver.method(:build), max_size: 1)
27
- self.class.new(pool:, page_server: @page_server).tap { @fresh_browsers << [it, pool] }
28
+ browser = self.class.new(pool:, page_server: @page_server, asset_cache: @asset_cache)
29
+ browser.tap { @fresh_browsers << [it, pool] }
28
30
  end
29
31
 
30
32
  # Loads the screen's page; its picture is only taken when a test asks for it.
31
33
  def load(screen)
32
34
  generator = generator(screen)
33
- @pages[screen] = [generator.html, generator.width, generator.height, @page_server.add(generator.html)]
35
+ html = @asset_cache.point_framework_links_at_cache(generator.html)
36
+ @pages[screen] = [html, generator.width, generator.height, @page_server.add(html)]
34
37
  show(screen)
35
38
  end
36
39
 
@@ -8,6 +8,7 @@ require 'capybara/rspec/matchers' # after rspec/expectations, or its matchers lo
8
8
  require_relative '../../trmnlp'
9
9
  require_relative '../browser_pool'
10
10
  require_relative '../firefox_driver'
11
+ require_relative 'asset_cache'
11
12
  require_relative 'browser'
12
13
  require_relative 'certificate_authority'
13
14
  require_relative 'ink_count'
@@ -33,6 +34,11 @@ module TRMNLP
33
34
  @browser_pool ||= BrowserPool.new(driver_factory: FirefoxDriver.method(:build), max_size: 1)
34
35
  end
35
36
 
37
+ def self.asset_cache
38
+ @asset_cache ||= AssetCache.new(dir: Paths.new(plugin_dir).cache_dir.join('assets')).start
39
+ .tap { |cache| at_exit { cache.stop } }
40
+ end
41
+
36
42
  def self.page_server
37
43
  @page_server ||= PageServer.new.start.tap { |server| at_exit { server.stop } }
38
44
  end
@@ -41,7 +47,8 @@ module TRMNLP
41
47
  def trmnl = @trmnl ||= Plugin.new(Testing.plugin_dir, browser: trmnl_browser, authority: Testing.authority)
42
48
 
43
49
  def trmnl_browser
44
- @trmnl_browser ||= Browser.new(pool: Testing.browser_pool, page_server: Testing.page_server)
50
+ @trmnl_browser ||= Browser.new(pool: Testing.browser_pool, page_server: Testing.page_server,
51
+ asset_cache: Testing.asset_cache)
45
52
  end
46
53
  end
47
54
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRMNLP
4
- VERSION = '0.30.2'
4
+ VERSION = '0.30.4'
5
5
  end
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.30.2
4
+ version: 0.30.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rockwell Schrock
@@ -441,6 +441,7 @@ files:
441
441
  - lib/trmnlp/lint/checks/no_async_functions.rb
442
442
  - lib/trmnlp/lint/checks/no_custom_filters.rb
443
443
  - lib/trmnlp/lint/checks/no_external_qr_codes.rb
444
+ - lib/trmnlp/lint/checks/no_negative_spacing.rb
444
445
  - lib/trmnlp/lint/checks/no_nested_layouts.rb
445
446
  - lib/trmnlp/lint/checks/no_opacity.rb
446
447
  - lib/trmnlp/lint/checks/no_refresh_rate_field.rb
@@ -476,6 +477,7 @@ files:
476
477
  - lib/trmnlp/screen_generator.rb
477
478
  - lib/trmnlp/screenshot.rb
478
479
  - lib/trmnlp/select_field_choices.rb
480
+ - lib/trmnlp/testing/asset_cache.rb
479
481
  - lib/trmnlp/testing/browser.rb
480
482
  - lib/trmnlp/testing/certificate_authority.rb
481
483
  - lib/trmnlp/testing/device_models.rb