gemstar 1.0.1 → 1.0.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 +9 -1
- data/lib/gemstar/cache.rb +3 -3
- data/lib/gemstar/cache_warmer.rb +6 -0
- data/lib/gemstar/change_log.rb +19 -19
- data/lib/gemstar/commands/server.rb +28 -4
- data/lib/gemstar/project.rb +23 -9
- data/lib/gemstar/remote_repository.rb +11 -2
- data/lib/gemstar/ruby_gems_metadata.rb +5 -5
- data/lib/gemstar/version.rb +1 -1
- data/lib/gemstar/web/app.rb +169 -47
- data/lib/gemstar/web/templates/app.css +71 -2
- data/lib/gemstar/web/templates/app.js.erb +83 -3
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b290f73ca813d6ef39789e23d883f3bd382fbdcc17f0803cd5fdc296d7f60582
|
|
4
|
+
data.tar.gz: '058a2d17b98461a62d463ba673d170b621471e8f9693ea294da403382da5a759'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fe408b9d9259ab0771e41522238b35c259e624759ac27ffe013d689c76ebe0164905c5ee67bb7868e8cc43ff361568f854f8dccf2e513468176685a395bb9786
|
|
7
|
+
data.tar.gz: 149884982e1e2d124f7ade0bf565604106a455db2bab04f6b3811b316fb0386f72ed3fdaa0969369c310e3811e7735d97d29c147f9263f32f62c6c2fcb0d7487
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 1.0.2
|
|
4
|
+
|
|
5
|
+
- General server performance improvements.
|
|
6
|
+
- Server: Required by / Requires / date added information now hidden in a "Details" section by default.
|
|
7
|
+
- Server: Improve color hilighting in gems list.
|
|
8
|
+
- Server: Improve up/down/left/right arrow key navigation.
|
|
9
|
+
- Fix problem not fetching new changelogs due to extraneous caching.
|
|
10
|
+
- Fix `nil.include?` error fetching changelogs for gems without either `homepage_uri` or `source_code_uri`.
|
|
11
|
+
- Add gem-release as a development dependency.
|
|
4
12
|
|
|
5
13
|
## 1.0.1
|
|
6
14
|
|
data/lib/gemstar/cache.rb
CHANGED
|
@@ -4,7 +4,7 @@ require "digest"
|
|
|
4
4
|
|
|
5
5
|
module Gemstar
|
|
6
6
|
class Cache
|
|
7
|
-
MAX_CACHE_AGE = 60 * 60 * 24
|
|
7
|
+
MAX_CACHE_AGE = 60 * 60 * 24 # 1 day
|
|
8
8
|
CACHE_DIR = File.join(Gemstar::Config.home_directory, "cache")
|
|
9
9
|
|
|
10
10
|
@@initialized = false
|
|
@@ -16,12 +16,12 @@ module Gemstar
|
|
|
16
16
|
@@initialized = true
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def self.fetch(key, &block)
|
|
19
|
+
def self.fetch(key, force: false, &block)
|
|
20
20
|
init
|
|
21
21
|
|
|
22
22
|
path = path_for(key)
|
|
23
23
|
|
|
24
|
-
if fresh?(path)
|
|
24
|
+
if !force && fresh?(path)
|
|
25
25
|
content = File.read(path)
|
|
26
26
|
return nil if content == "__404__"
|
|
27
27
|
return content
|
data/lib/gemstar/cache_warmer.rb
CHANGED
data/lib/gemstar/change_log.rb
CHANGED
|
@@ -10,21 +10,21 @@ module Gemstar
|
|
|
10
10
|
|
|
11
11
|
attr_reader :metadata
|
|
12
12
|
|
|
13
|
-
def content(cache_only: false)
|
|
13
|
+
def content(cache_only: false, force_refresh: false)
|
|
14
14
|
return @content if !cache_only && defined?(@content)
|
|
15
15
|
|
|
16
|
-
result = fetch_changelog_content(cache_only: cache_only)
|
|
16
|
+
result = fetch_changelog_content(cache_only: cache_only, force_refresh: force_refresh)
|
|
17
17
|
@content = result unless cache_only
|
|
18
18
|
result
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def sections(cache_only: false)
|
|
21
|
+
def sections(cache_only: false, force_refresh: false)
|
|
22
22
|
return @sections if !cache_only && defined?(@sections)
|
|
23
23
|
|
|
24
24
|
result = begin
|
|
25
|
-
s = parse_changelog_sections(cache_only: cache_only)
|
|
25
|
+
s = parse_changelog_sections(cache_only: cache_only, force_refresh: force_refresh)
|
|
26
26
|
if s.nil? || s.empty?
|
|
27
|
-
s = parse_github_release_sections(cache_only: cache_only)
|
|
27
|
+
s = parse_github_release_sections(cache_only: cache_only, force_refresh: force_refresh)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
pp @@candidates_found if Gemstar.debug? && !cache_only
|
|
@@ -74,10 +74,10 @@ module Gemstar
|
|
|
74
74
|
nil
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
def changelog_uri_candidates(cache_only: false)
|
|
77
|
+
def changelog_uri_candidates(cache_only: false, force_refresh: false)
|
|
78
78
|
candidates = []
|
|
79
79
|
|
|
80
|
-
repo_uri = @metadata.repo_uri(cache_only: cache_only)
|
|
80
|
+
repo_uri = @metadata.repo_uri(cache_only: cache_only, force_refresh: force_refresh)
|
|
81
81
|
return [] if repo_uri.nil? || repo_uri.empty?
|
|
82
82
|
|
|
83
83
|
if repo_uri =~ %r{https://github\.com/aws/aws-sdk-ruby}
|
|
@@ -100,14 +100,14 @@ module Gemstar
|
|
|
100
100
|
|
|
101
101
|
remote_repository = RemoteRepository.new(base)
|
|
102
102
|
|
|
103
|
-
branches = aws_style ? [""] : remote_repository.find_main_branch
|
|
103
|
+
branches = aws_style ? [""] : remote_repository.find_main_branch(cache_only: cache_only, force_refresh: force_refresh)
|
|
104
104
|
|
|
105
105
|
candidates += paths.product(branches).map do |file, branch|
|
|
106
106
|
uri = aws_style ? "#{base}/#{file}" : "#{base}/#{branch}/#{file}"
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
# Add the gem's changelog_uri last as it's usually not the most parsable:
|
|
110
|
-
meta = @metadata.meta(cache_only: cache_only)
|
|
110
|
+
meta = @metadata.meta(cache_only: cache_only, force_refresh: force_refresh)
|
|
111
111
|
candidates += [Gemstar::GitHub::github_blob_to_raw(meta["changelog_uri"])] if meta
|
|
112
112
|
|
|
113
113
|
candidates.flatten!
|
|
@@ -117,14 +117,14 @@ module Gemstar
|
|
|
117
117
|
candidates
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
-
def fetch_changelog_content(cache_only: false)
|
|
120
|
+
def fetch_changelog_content(cache_only: false, force_refresh: false)
|
|
121
121
|
content = nil
|
|
122
122
|
|
|
123
|
-
changelog_uri_candidates(cache_only: cache_only).find do |candidate|
|
|
123
|
+
changelog_uri_candidates(cache_only: cache_only, force_refresh: force_refresh).find do |candidate|
|
|
124
124
|
content = if cache_only
|
|
125
125
|
Cache.peek("changelog-#{candidate}")
|
|
126
126
|
else
|
|
127
|
-
Cache.fetch("changelog-#{candidate}") do
|
|
127
|
+
Cache.fetch("changelog-#{candidate}", force: force_refresh) do
|
|
128
128
|
URI.open(candidate, read_timeout: 8)&.read
|
|
129
129
|
rescue => e
|
|
130
130
|
puts "#{candidate}: #{e}" if Gemstar.debug?
|
|
@@ -152,11 +152,11 @@ module Gemstar
|
|
|
152
152
|
/^\s*(?:[-*]\s+)?(?:Version\s+)?v?(\d+\.\d+(?:\.\d+)?(?:[-.][A-Za-z0-9]+)*)(?![A-Za-z0-9])(?:\s*[-(].*)?/i
|
|
153
153
|
]
|
|
154
154
|
|
|
155
|
-
def parse_changelog_sections(cache_only: false)
|
|
155
|
+
def parse_changelog_sections(cache_only: false, force_refresh: false)
|
|
156
156
|
# If the fetched content looks like a GitHub Releases HTML page, return {}
|
|
157
157
|
# so that the GitHub releases scraper can handle it. This avoids
|
|
158
158
|
# accidentally parsing HTML from /releases pages as a markdown changelog.
|
|
159
|
-
c = content(cache_only: cache_only)
|
|
159
|
+
c = content(cache_only: cache_only, force_refresh: force_refresh)
|
|
160
160
|
return {} if c.nil? || c.strip.empty?
|
|
161
161
|
if (c.include?("<html") || c.include?("<!DOCTYPE html")) &&
|
|
162
162
|
(c.include?('data-test-selector="body-content"') || c.include?("/releases/tag/"))
|
|
@@ -212,14 +212,14 @@ module Gemstar
|
|
|
212
212
|
sections
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
-
def parse_github_release_sections(cache_only: false)
|
|
215
|
+
def parse_github_release_sections(cache_only: false, force_refresh: false)
|
|
216
216
|
begin
|
|
217
217
|
require "nokogiri"
|
|
218
218
|
rescue LoadError
|
|
219
219
|
return {}
|
|
220
220
|
end
|
|
221
221
|
|
|
222
|
-
repo_uri = @metadata&.repo_uri(cache_only: cache_only)
|
|
222
|
+
repo_uri = @metadata&.repo_uri(cache_only: cache_only, force_refresh: force_refresh)
|
|
223
223
|
return {} unless repo_uri&.include?("github.com")
|
|
224
224
|
|
|
225
225
|
url = github_releases_url(repo_uri)
|
|
@@ -228,9 +228,9 @@ module Gemstar
|
|
|
228
228
|
html = if cache_only
|
|
229
229
|
Cache.peek("releases-#{url}")
|
|
230
230
|
else
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
231
|
+
Cache.fetch("releases-#{url}", force: force_refresh) do
|
|
232
|
+
begin
|
|
233
|
+
URI.open(url, read_timeout: 8)&.read
|
|
234
234
|
rescue => e
|
|
235
235
|
puts "#{url}: #{e}" if Gemstar.debug?
|
|
236
236
|
nil
|
|
@@ -8,6 +8,7 @@ module Gemstar
|
|
|
8
8
|
DEFAULT_PORT = 2112
|
|
9
9
|
RELOAD_ENV_VAR = "GEMSTAR_RELOAD_ACTIVE"
|
|
10
10
|
RELOAD_GLOB = "{lib/**/*.rb,lib/gemstar/web/templates/**/*,bin/gemstar,README.md}"
|
|
11
|
+
RELOAD_DIRS = %w[lib bin].freeze
|
|
11
12
|
|
|
12
13
|
attr_reader :bind
|
|
13
14
|
attr_reader :port
|
|
@@ -71,7 +72,7 @@ module Gemstar
|
|
|
71
72
|
end
|
|
72
73
|
|
|
73
74
|
puts "Starting gemstar server in reload mode..."
|
|
74
|
-
puts "Watching
|
|
75
|
+
puts "Watching directories #{RELOAD_DIRS.join(", ")} with glob #{RELOAD_GLOB.inspect}"
|
|
75
76
|
|
|
76
77
|
env = ENV.to_h.merge(RELOAD_ENV_VAR => "1")
|
|
77
78
|
exec env, *rerun_command(rerun_executable)
|
|
@@ -86,12 +87,35 @@ module Gemstar
|
|
|
86
87
|
def rerun_command(rerun_executable)
|
|
87
88
|
[
|
|
88
89
|
rerun_executable,
|
|
90
|
+
"--dir",
|
|
91
|
+
RELOAD_DIRS.join(","),
|
|
89
92
|
"--pattern",
|
|
90
93
|
RELOAD_GLOB,
|
|
94
|
+
"--signal",
|
|
95
|
+
"INT,KILL",
|
|
96
|
+
"--wait",
|
|
97
|
+
"1",
|
|
98
|
+
"--name",
|
|
99
|
+
"Gemstar",
|
|
100
|
+
"--background",
|
|
91
101
|
"--",
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
]
|
|
102
|
+
*server_runner_command,
|
|
103
|
+
*server_arguments_without_reload
|
|
104
|
+
]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def server_runner_command
|
|
108
|
+
return %w[bundle exec gemstar] if ENV["BUNDLE_GEMFILE"]
|
|
109
|
+
|
|
110
|
+
repo_executable = File.expand_path("../../../bin/gemstar", __dir__)
|
|
111
|
+
return [repo_executable] if File.exist?(repo_executable)
|
|
112
|
+
|
|
113
|
+
gem_executable = Gem.bin_path("gemstar", "gemstar")
|
|
114
|
+
return [gem_executable] if gem_executable
|
|
115
|
+
|
|
116
|
+
["gemstar"]
|
|
117
|
+
rescue Gem::Exception
|
|
118
|
+
["gemstar"]
|
|
95
119
|
end
|
|
96
120
|
|
|
97
121
|
def server_arguments_without_reload
|
data/lib/gemstar/project.rb
CHANGED
|
@@ -26,6 +26,10 @@ module Gemstar
|
|
|
26
26
|
@directory = File.dirname(@gemfile_path)
|
|
27
27
|
@lockfile_path = File.join(@directory, "Gemfile.lock")
|
|
28
28
|
@name = File.basename(@directory)
|
|
29
|
+
@lockfile_cache = {}
|
|
30
|
+
@gem_states_cache = {}
|
|
31
|
+
@gem_added_on_cache = {}
|
|
32
|
+
@history_cache = {}
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
def git_repo
|
|
@@ -84,7 +88,9 @@ module Gemstar
|
|
|
84
88
|
end
|
|
85
89
|
|
|
86
90
|
def lockfile_for_revision(revision_id)
|
|
87
|
-
|
|
91
|
+
cache_key = revision_id || "worktree"
|
|
92
|
+
return @lockfile_cache[cache_key] if @lockfile_cache.key?(cache_key)
|
|
93
|
+
return @lockfile_cache[cache_key] = current_lockfile if revision_id.nil? || revision_id == "worktree"
|
|
88
94
|
return nil unless lockfile?
|
|
89
95
|
|
|
90
96
|
relative_lockfile_path = git_repo.relative_path(lockfile_path)
|
|
@@ -93,16 +99,19 @@ module Gemstar
|
|
|
93
99
|
content = git_repo.try_git_command(["show", "#{revision_id}:#{relative_lockfile_path}"])
|
|
94
100
|
return nil if content.nil? || content.empty?
|
|
95
101
|
|
|
96
|
-
Gemstar::LockFile.new(content: content)
|
|
102
|
+
@lockfile_cache[cache_key] = Gemstar::LockFile.new(content: content)
|
|
97
103
|
end
|
|
98
104
|
|
|
99
105
|
def gem_states(from_revision_id: default_from_revision_id, to_revision_id: "worktree")
|
|
106
|
+
cache_key = [from_revision_id, to_revision_id]
|
|
107
|
+
return @gem_states_cache[cache_key] if @gem_states_cache.key?(cache_key)
|
|
108
|
+
|
|
100
109
|
from_lockfile = lockfile_for_revision(from_revision_id)
|
|
101
110
|
to_lockfile = lockfile_for_revision(to_revision_id)
|
|
102
111
|
from_specs = from_lockfile&.specs || {}
|
|
103
112
|
to_specs = to_lockfile&.specs || {}
|
|
104
113
|
|
|
105
|
-
(from_specs.keys | to_specs.keys).map do |gem_name|
|
|
114
|
+
@gem_states_cache[cache_key] = (from_specs.keys | to_specs.keys).map do |gem_name|
|
|
106
115
|
old_version = from_specs[gem_name]
|
|
107
116
|
new_version = to_specs[gem_name]
|
|
108
117
|
bundle_origins = to_lockfile&.origins_for(gem_name) || []
|
|
@@ -120,23 +129,25 @@ module Gemstar
|
|
|
120
129
|
end
|
|
121
130
|
|
|
122
131
|
def gem_added_on(gem_name, revision_id: "worktree")
|
|
132
|
+
cache_key = [gem_name, revision_id]
|
|
133
|
+
return @gem_added_on_cache[cache_key] if @gem_added_on_cache.key?(cache_key)
|
|
123
134
|
return nil unless lockfile?
|
|
124
135
|
|
|
125
136
|
target_lockfile = lockfile_for_revision(revision_id)
|
|
126
|
-
return nil unless target_lockfile&.specs&.key?(gem_name)
|
|
137
|
+
return @gem_added_on_cache[cache_key] = nil unless target_lockfile&.specs&.key?(gem_name)
|
|
127
138
|
|
|
128
139
|
relative_path = git_repo.relative_path(lockfile_path)
|
|
129
|
-
return nil if relative_path.nil?
|
|
140
|
+
return @gem_added_on_cache[cache_key] = nil if relative_path.nil?
|
|
130
141
|
|
|
131
142
|
first_seen_revision = history_for_paths([relative_path], limit: nil, reverse: true).find do |revision|
|
|
132
143
|
lockfile = lockfile_for_revision(revision[:id])
|
|
133
144
|
lockfile&.specs&.key?(gem_name)
|
|
134
145
|
end
|
|
135
146
|
|
|
136
|
-
return worktree_added_on_info if first_seen_revision.nil? && revision_id == "worktree"
|
|
137
|
-
return nil unless first_seen_revision
|
|
147
|
+
return @gem_added_on_cache[cache_key] = worktree_added_on_info if first_seen_revision.nil? && revision_id == "worktree"
|
|
148
|
+
return @gem_added_on_cache[cache_key] = nil unless first_seen_revision
|
|
138
149
|
|
|
139
|
-
{
|
|
150
|
+
@gem_added_on_cache[cache_key] = {
|
|
140
151
|
project_name: name,
|
|
141
152
|
date: first_seen_revision[:authored_at].strftime("%Y-%m-%d"),
|
|
142
153
|
revision: first_seen_revision[:short_sha],
|
|
@@ -162,10 +173,13 @@ module Gemstar
|
|
|
162
173
|
return [] if git_root.nil? || git_root.empty?
|
|
163
174
|
return [] if paths.empty?
|
|
164
175
|
|
|
176
|
+
cache_key = [paths.sort, limit, reverse]
|
|
177
|
+
return @history_cache[cache_key] if @history_cache.key?(cache_key)
|
|
178
|
+
|
|
165
179
|
output = git_repo.log_for_paths(paths, limit: limit, reverse: reverse)
|
|
166
180
|
return [] if output.nil? || output.empty?
|
|
167
181
|
|
|
168
|
-
output.lines.filter_map do |line|
|
|
182
|
+
@history_cache[cache_key] = output.lines.filter_map do |line|
|
|
169
183
|
full_sha, short_sha, authored_at, subject = line.strip.split("\u001f", 4)
|
|
170
184
|
next if full_sha.nil?
|
|
171
185
|
|
|
@@ -6,11 +6,20 @@ module Gemstar
|
|
|
6
6
|
@repository_uri = repository_uri
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
def find_main_branch
|
|
9
|
+
def find_main_branch(cache_only: false, force_refresh: false)
|
|
10
10
|
# Attempt loading .gitignore (assumed to be present in all repos) from either
|
|
11
11
|
# main or master branch:
|
|
12
12
|
%w[main master].each do |branch|
|
|
13
|
-
|
|
13
|
+
cache_key = "gitignore-#{@repository_uri}-#{branch}"
|
|
14
|
+
|
|
15
|
+
if cache_only
|
|
16
|
+
content = Cache.peek(cache_key)
|
|
17
|
+
return [branch] unless content.nil?
|
|
18
|
+
|
|
19
|
+
next
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Cache.fetch(cache_key, force: force_refresh) do
|
|
14
23
|
content = begin
|
|
15
24
|
URI.open("#{@repository_uri}/#{branch}/.gitignore", read_timeout: 8)&.read
|
|
16
25
|
rescue
|
|
@@ -10,14 +10,14 @@ module Gemstar
|
|
|
10
10
|
|
|
11
11
|
attr_reader :gem_name
|
|
12
12
|
|
|
13
|
-
def meta(cache_only: false)
|
|
13
|
+
def meta(cache_only: false, force_refresh: false)
|
|
14
14
|
return @meta if !cache_only && defined?(@meta)
|
|
15
15
|
|
|
16
16
|
json = if cache_only
|
|
17
17
|
Cache.peek("rubygems-#{gem_name}")
|
|
18
18
|
else
|
|
19
19
|
url = "https://rubygems.org/api/v1/gems/#{URI.encode_www_form_component(gem_name)}.json"
|
|
20
|
-
Cache.fetch("rubygems-#{gem_name}") do
|
|
20
|
+
Cache.fetch("rubygems-#{gem_name}", force: force_refresh) do
|
|
21
21
|
URI.open(url).read
|
|
22
22
|
end
|
|
23
23
|
end
|
|
@@ -32,8 +32,8 @@ module Gemstar
|
|
|
32
32
|
parsed
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def repo_uri(cache_only: false)
|
|
36
|
-
resolved_meta = meta(cache_only: cache_only)
|
|
35
|
+
def repo_uri(cache_only: false, force_refresh: false)
|
|
36
|
+
resolved_meta = meta(cache_only: cache_only, force_refresh: force_refresh)
|
|
37
37
|
return nil unless resolved_meta
|
|
38
38
|
|
|
39
39
|
return @repo_uri if !cache_only && defined?(@repo_uri)
|
|
@@ -43,7 +43,7 @@ module Gemstar
|
|
|
43
43
|
|
|
44
44
|
if uri.nil?
|
|
45
45
|
uri = resolved_meta["homepage_uri"]
|
|
46
|
-
if uri
|
|
46
|
+
if uri&.include?("github.com")
|
|
47
47
|
uri = uri[%r{http[s?]://github\.com/[^/]+/[^/]+}]
|
|
48
48
|
end
|
|
49
49
|
end
|
data/lib/gemstar/version.rb
CHANGED
data/lib/gemstar/web/app.rb
CHANGED
|
@@ -12,12 +12,18 @@ end
|
|
|
12
12
|
module Gemstar
|
|
13
13
|
module Web
|
|
14
14
|
class App < Roda
|
|
15
|
+
MISSING_METADATA = Object.new
|
|
16
|
+
|
|
15
17
|
class << self
|
|
16
18
|
def build(projects:, config_home:, cache_warmer: nil)
|
|
17
19
|
Class.new(self) do
|
|
18
20
|
opts[:projects] = projects
|
|
19
21
|
opts[:config_home] = config_home
|
|
20
22
|
opts[:cache_warmer] = cache_warmer
|
|
23
|
+
opts[:change_sections_cache] = {}
|
|
24
|
+
opts[:detail_html_cache] = {}
|
|
25
|
+
opts[:detail_request_cache] = {}
|
|
26
|
+
opts[:metadata_cache] = {}
|
|
21
27
|
end.freeze.app
|
|
22
28
|
end
|
|
23
29
|
end
|
|
@@ -26,7 +32,7 @@ module Gemstar
|
|
|
26
32
|
@projects = self.class.opts.fetch(:projects)
|
|
27
33
|
@config_home = self.class.opts.fetch(:config_home)
|
|
28
34
|
@cache_warmer = self.class.opts[:cache_warmer]
|
|
29
|
-
@metadata_cache =
|
|
35
|
+
@metadata_cache = self.class.opts[:metadata_cache]
|
|
30
36
|
apply_no_cache_headers!
|
|
31
37
|
|
|
32
38
|
r.root do
|
|
@@ -39,9 +45,17 @@ module Gemstar
|
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
r.get "detail" do
|
|
48
|
+
request_cache_key = detail_request_cache_key(r.params)
|
|
49
|
+
request_cache = self.class.opts[:detail_request_cache]
|
|
50
|
+
if request_cache_key && request_cache.key?(request_cache_key)
|
|
51
|
+
next request_cache[request_cache_key]
|
|
52
|
+
end
|
|
53
|
+
|
|
42
54
|
load_state(r.params)
|
|
43
55
|
prioritize_selected_gem
|
|
44
|
-
render_detail
|
|
56
|
+
detail_html = render_detail
|
|
57
|
+
request_cache[request_cache_key] = detail_html if request_cache_key
|
|
58
|
+
detail_html
|
|
45
59
|
end
|
|
46
60
|
|
|
47
61
|
r.get "gemfile" do
|
|
@@ -67,6 +81,28 @@ module Gemstar
|
|
|
67
81
|
response["Expires"] = "0"
|
|
68
82
|
end
|
|
69
83
|
|
|
84
|
+
def detail_request_cache_key(params)
|
|
85
|
+
project_index = selected_project_index(params["project"])
|
|
86
|
+
project = @projects[project_index]
|
|
87
|
+
return nil unless project
|
|
88
|
+
|
|
89
|
+
lockfile_stamp =
|
|
90
|
+
if File.file?(project.lockfile_path)
|
|
91
|
+
File.mtime(project.lockfile_path).to_i
|
|
92
|
+
else
|
|
93
|
+
0
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
[
|
|
97
|
+
project_index,
|
|
98
|
+
params["from"],
|
|
99
|
+
params["to"],
|
|
100
|
+
params["filter"],
|
|
101
|
+
params["gem"],
|
|
102
|
+
lockfile_stamp
|
|
103
|
+
]
|
|
104
|
+
end
|
|
105
|
+
|
|
70
106
|
def page_title
|
|
71
107
|
return "Gemstar" unless @selected_project
|
|
72
108
|
|
|
@@ -385,21 +421,38 @@ module Gemstar
|
|
|
385
421
|
def render_detail
|
|
386
422
|
return empty_detail_html unless @selected_gem
|
|
387
423
|
|
|
388
|
-
|
|
389
|
-
|
|
424
|
+
cache_key = [
|
|
425
|
+
@selected_project_index,
|
|
426
|
+
@selected_from_revision_id,
|
|
427
|
+
@selected_to_revision_id,
|
|
428
|
+
@selected_filter,
|
|
429
|
+
@selected_gem[:name],
|
|
430
|
+
@selected_gem[:old_version],
|
|
431
|
+
@selected_gem[:new_version],
|
|
432
|
+
@selected_gem[:status]
|
|
433
|
+
]
|
|
434
|
+
detail_cache = self.class.opts[:detail_html_cache]
|
|
435
|
+
return detail_cache[cache_key] if detail_cache.key?(cache_key)
|
|
436
|
+
|
|
437
|
+
metadata = metadata_for(@selected_gem[:name], refresh_if_missing: true)
|
|
438
|
+
groups = grouped_change_sections(@selected_gem)
|
|
439
|
+
detail_pending = detail_pending?(@selected_gem[:name], metadata, groups)
|
|
390
440
|
|
|
391
|
-
<<~HTML
|
|
392
|
-
<section class="detail" data-detail-panel data-detail-pending="#{detail_pending}" data-detail-url="#{h(detail_query(project: @selected_project_index, from: @selected_from_revision_id, to: @selected_to_revision_id, filter: @selected_filter, gem: @selected_gem[:name]))}">
|
|
441
|
+
detail_html = <<~HTML
|
|
442
|
+
<section class="detail" data-detail-panel tabindex="0" data-detail-pending="#{detail_pending}" data-detail-url="#{h(detail_query(project: @selected_project_index, from: @selected_from_revision_id, to: @selected_to_revision_id, filter: @selected_filter, gem: @selected_gem[:name]))}">
|
|
393
443
|
#{render_detail_hero(metadata)}
|
|
394
444
|
#{render_detail_loading_notice if detail_pending}
|
|
395
|
-
#{render_detail_revision_panel}
|
|
445
|
+
#{render_detail_revision_panel(groups)}
|
|
396
446
|
</section>
|
|
397
447
|
HTML
|
|
448
|
+
|
|
449
|
+
detail_cache[cache_key] = detail_html unless detail_pending
|
|
450
|
+
detail_html
|
|
398
451
|
end
|
|
399
452
|
|
|
400
453
|
def empty_detail_html
|
|
401
454
|
<<~HTML
|
|
402
|
-
<section class="detail" data-detail-panel>
|
|
455
|
+
<section class="detail" data-detail-panel tabindex="0">
|
|
403
456
|
<div class="empty-panel">
|
|
404
457
|
<h2>No gem selected</h2>
|
|
405
458
|
<p>Choose a gem from the list to inspect its current version and changelog revisions.</p>
|
|
@@ -426,13 +479,13 @@ module Gemstar
|
|
|
426
479
|
<section class="detail-hero">
|
|
427
480
|
<div class="detail-hero-copy">
|
|
428
481
|
<div class="detail-title-row">
|
|
429
|
-
<
|
|
482
|
+
<div class="detail-title-lockup">
|
|
483
|
+
<h2>#{title_markup}#{bundled_version ? %(<span class="detail-title-version"> #{h(bundled_version)}</span>) : ""}</h2>
|
|
484
|
+
</div>
|
|
430
485
|
#{render_detail_links(metadata)}
|
|
431
486
|
</div>
|
|
432
487
|
<p class="detail-subtitle">#{description ? h(description) : "Metadata will appear here when RubyGems information is available."}</p>
|
|
433
|
-
#{
|
|
434
|
-
#{render_dependency_origins(bundle_origins)}
|
|
435
|
-
#{render_requirements(requirement_names)}
|
|
488
|
+
#{render_dependency_details(bundle_origins, requirement_names, added_on)}
|
|
436
489
|
</div>
|
|
437
490
|
</section>
|
|
438
491
|
HTML
|
|
@@ -449,12 +502,12 @@ module Gemstar
|
|
|
449
502
|
|
|
450
503
|
<<~HTML
|
|
451
504
|
<div class="detail-origin">
|
|
452
|
-
<p>Added to
|
|
505
|
+
<p>Added to project <strong>#{h(added_on[:project_name])}</strong> on #{h(added_on[:date])} (#{revision_markup}).</p>
|
|
453
506
|
</div>
|
|
454
507
|
HTML
|
|
455
508
|
end
|
|
456
509
|
|
|
457
|
-
def
|
|
510
|
+
def dependency_origin_items(bundle_origins)
|
|
458
511
|
origins = Array(bundle_origins).filter_map do |origin|
|
|
459
512
|
path = Array(origin[:path]).compact
|
|
460
513
|
display_path = path.dup
|
|
@@ -465,17 +518,6 @@ module Gemstar
|
|
|
465
518
|
linked_path = linked_gem_chain(["Gemfile", *display_path])
|
|
466
519
|
origin[:type] == :direct ? gemfile_link("Gemfile") : linked_path
|
|
467
520
|
end.uniq
|
|
468
|
-
return "" if origins.empty?
|
|
469
|
-
|
|
470
|
-
items = origins.map { |origin| "<li>#{origin}</li>" }.join
|
|
471
|
-
<<~HTML
|
|
472
|
-
<div class="detail-origin">
|
|
473
|
-
<strong>Required by</strong>
|
|
474
|
-
<ul class="detail-origin-list">
|
|
475
|
-
#{items}
|
|
476
|
-
</ul>
|
|
477
|
-
</div>
|
|
478
|
-
HTML
|
|
479
521
|
end
|
|
480
522
|
|
|
481
523
|
def render_detail_links(metadata)
|
|
@@ -495,18 +537,35 @@ module Gemstar
|
|
|
495
537
|
HTML
|
|
496
538
|
end
|
|
497
539
|
|
|
498
|
-
def
|
|
499
|
-
|
|
500
|
-
|
|
540
|
+
def render_dependency_details(bundle_origins, requirement_names, added_on)
|
|
541
|
+
required_by = dependency_origin_items(bundle_origins)
|
|
542
|
+
requires = Array(requirement_names).compact.uniq.map { |name| internal_gem_link(name) }
|
|
543
|
+
added_markup = render_added_on(added_on)
|
|
544
|
+
return "" if required_by.empty? && requires.empty? && added_markup.empty?
|
|
501
545
|
|
|
502
|
-
items = names.map { |name| "<li>#{internal_gem_link(name)}</li>" }.join
|
|
503
546
|
<<~HTML
|
|
504
|
-
<
|
|
505
|
-
<
|
|
547
|
+
<details class="detail-disclosure">
|
|
548
|
+
<summary><span class="detail-disclosure-caret" aria-hidden="true"></span><h3>Details</h3></summary>
|
|
549
|
+
<div class="detail-disclosure-panel">
|
|
550
|
+
#{added_markup}
|
|
551
|
+
#{render_dependency_popover_section("Required by", required_by)}
|
|
552
|
+
#{render_dependency_popover_section("Requires", requires)}
|
|
553
|
+
</div>
|
|
554
|
+
</details>
|
|
555
|
+
HTML
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def render_dependency_popover_section(title, items)
|
|
559
|
+
return "" if items.empty?
|
|
560
|
+
|
|
561
|
+
list_items = items.map { |item| "<li>#{item}</li>" }.join
|
|
562
|
+
<<~HTML
|
|
563
|
+
<section class="detail-info-section">
|
|
564
|
+
<strong>#{h(title)}</strong>
|
|
506
565
|
<ul class="detail-origin-list">
|
|
507
|
-
#{
|
|
566
|
+
#{list_items}
|
|
508
567
|
</ul>
|
|
509
|
-
</
|
|
568
|
+
</section>
|
|
510
569
|
HTML
|
|
511
570
|
end
|
|
512
571
|
|
|
@@ -554,14 +613,12 @@ module Gemstar
|
|
|
554
613
|
%(<a href="#{h(href)}" data-gem-link-inline="true">#{h(name)}</a>)
|
|
555
614
|
end
|
|
556
615
|
|
|
557
|
-
def render_detail_revision_panel
|
|
558
|
-
groups = grouped_change_sections(@selected_gem)
|
|
559
|
-
|
|
616
|
+
def render_detail_revision_panel(groups)
|
|
560
617
|
<<~HTML
|
|
561
618
|
<section class="revision-panel">
|
|
562
619
|
#{render_revision_group("Latest", groups[:latest], empty_message: nil) if groups[:latest].any?}
|
|
563
620
|
#{render_revision_group(current_section_title, groups[:current], empty_message: "No changelog entries in this revision range.")}
|
|
564
|
-
#{render_revision_group("
|
|
621
|
+
#{render_revision_group("Earlier changes", groups[:previous], empty_message: nil) if groups[:previous].any?}
|
|
565
622
|
</section>
|
|
566
623
|
HTML
|
|
567
624
|
end
|
|
@@ -637,11 +694,15 @@ module Gemstar
|
|
|
637
694
|
end
|
|
638
695
|
|
|
639
696
|
def change_sections(gem_state)
|
|
697
|
+
cache_key = [gem_state[:name], gem_state[:old_version], gem_state[:new_version], gem_state[:status]]
|
|
698
|
+
change_sections_cache = self.class.opts[:change_sections_cache]
|
|
699
|
+
return change_sections_cache[cache_key] if change_sections_cache.key?(cache_key)
|
|
700
|
+
|
|
640
701
|
return [] if gem_state[:new_version].nil? && gem_state[:old_version].nil?
|
|
641
702
|
|
|
642
703
|
metadata = Gemstar::RubyGemsMetadata.new(gem_state[:name])
|
|
643
|
-
sections =
|
|
644
|
-
return [] if sections.nil? || sections.empty?
|
|
704
|
+
sections = resolved_sections(metadata, gem_state)
|
|
705
|
+
return change_sections_cache[cache_key] = [] if sections.nil? || sections.empty?
|
|
645
706
|
|
|
646
707
|
current_version = gem_state[:new_version] || gem_state[:old_version]
|
|
647
708
|
previous_version = gem_state[:old_version]
|
|
@@ -660,11 +721,45 @@ module Gemstar
|
|
|
660
721
|
}
|
|
661
722
|
end
|
|
662
723
|
|
|
663
|
-
rendered_sections.sort_by { |section| section_sort_key(section) }
|
|
724
|
+
change_sections_cache[cache_key] = rendered_sections.sort_by { |section| section_sort_key(section) }
|
|
664
725
|
rescue StandardError
|
|
665
726
|
[]
|
|
666
727
|
end
|
|
667
728
|
|
|
729
|
+
def resolved_sections(metadata, gem_state)
|
|
730
|
+
changelog = Gemstar::ChangeLog.new(metadata)
|
|
731
|
+
cached_sections = changelog.sections(cache_only: true) || {}
|
|
732
|
+
return cached_sections unless selected_gem_requires_refresh?(gem_state, cached_sections)
|
|
733
|
+
|
|
734
|
+
@metadata_cache.delete(gem_state[:name])
|
|
735
|
+
metadata.meta(cache_only: false, force_refresh: true)
|
|
736
|
+
metadata.repo_uri(cache_only: false, force_refresh: true)
|
|
737
|
+
Gemstar::ChangeLog.new(metadata).sections(cache_only: false, force_refresh: true) || cached_sections
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
def selected_gem_requires_refresh?(gem_state, cached_sections)
|
|
741
|
+
return false unless @selected_gem && gem_state[:name] == @selected_gem[:name]
|
|
742
|
+
|
|
743
|
+
bundled_version = gem_state[:new_version] || gem_state[:old_version]
|
|
744
|
+
return false if bundled_version.nil?
|
|
745
|
+
metadata = metadata_for(gem_state[:name]) || {}
|
|
746
|
+
has_upstream_release_source =
|
|
747
|
+
!metadata["changelog_uri"].to_s.empty? ||
|
|
748
|
+
!metadata["source_code_uri"].to_s.empty? ||
|
|
749
|
+
!metadata["homepage_uri"].to_s.empty?
|
|
750
|
+
return false unless has_upstream_release_source
|
|
751
|
+
return true if cached_sections.nil? || cached_sections.empty?
|
|
752
|
+
|
|
753
|
+
cached_versions = cached_sections.keys
|
|
754
|
+
return true unless cached_versions.include?(bundled_version)
|
|
755
|
+
|
|
756
|
+
compare_versions(bundled_version, newest_version(cached_versions)) == 1
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def newest_version(versions)
|
|
760
|
+
Array(versions).max { |left, right| compare_versions(left, right) }
|
|
761
|
+
end
|
|
762
|
+
|
|
668
763
|
def section_kind(version, previous_version, current_version, status)
|
|
669
764
|
return :future if compare_versions(version, current_version) == 1
|
|
670
765
|
return :current if status == :added && compare_versions(version, current_version) <= 0
|
|
@@ -736,14 +831,25 @@ module Gemstar
|
|
|
736
831
|
left.to_s <=> right.to_s
|
|
737
832
|
end
|
|
738
833
|
|
|
739
|
-
def metadata_for(gem_name)
|
|
740
|
-
@metadata_cache[gem_name]
|
|
834
|
+
def metadata_for(gem_name, refresh_if_missing: false)
|
|
835
|
+
cached = @metadata_cache[gem_name]
|
|
836
|
+
return nil if cached.equal?(MISSING_METADATA)
|
|
837
|
+
return cached if cached
|
|
838
|
+
|
|
839
|
+
metadata = Gemstar::RubyGemsMetadata.new(gem_name).meta(cache_only: true)
|
|
840
|
+
if metadata.nil? && refresh_if_missing
|
|
841
|
+
metadata = Gemstar::RubyGemsMetadata.new(gem_name).meta(cache_only: false, force_refresh: true)
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
@metadata_cache[gem_name] = metadata || MISSING_METADATA
|
|
845
|
+
metadata
|
|
741
846
|
rescue StandardError
|
|
847
|
+
@metadata_cache[gem_name] = MISSING_METADATA
|
|
742
848
|
nil
|
|
743
849
|
end
|
|
744
850
|
|
|
745
|
-
def detail_pending?(gem_name, metadata)
|
|
746
|
-
|
|
851
|
+
def detail_pending?(gem_name, metadata, groups)
|
|
852
|
+
false
|
|
747
853
|
end
|
|
748
854
|
|
|
749
855
|
def icon_button(label, url, icon_type:)
|
|
@@ -762,6 +868,8 @@ module Gemstar
|
|
|
762
868
|
'<svg viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M8 .8 1.2 6.3v8.9h4.3V10h5v5.2h4.3V6.3L8 .8Zm5.2 13.3h-1.8V8.9H4.6v5.2H2.8V6.8L8 2.6l5.2 4.2v7.3Z"/></svg>'
|
|
763
869
|
when :rubygems
|
|
764
870
|
'<svg viewBox="0 0 16 16" aria-hidden="true"><rect width="16" height="16" rx="2.6" fill="#fff"/><path fill="#111" d="m8 2.35 4.55 2.63v5.24L8 12.85l-4.55-2.63V4.98L8 2.35Zm0 1.3L4.58 5.62v3.96L8 11.55l3.42-1.97V5.62L8 3.65Zm0 1.07 2.5 1.44v2.88L8 10.48 5.5 9.04V6.16L8 4.72Z"/></svg>'
|
|
871
|
+
when :info
|
|
872
|
+
'<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="6.5" fill="none" stroke="currentColor" stroke-width="1.4"/><circle cx="8" cy="4.5" r="0.9" fill="currentColor"/><path d="M8 7v4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>'
|
|
765
873
|
else
|
|
766
874
|
'<svg viewBox="0 0 16 16" aria-hidden="true"><rect width="16" height="16" rx="2.6" fill="#fff"/><path fill="#111" d="m8 2.35 4.55 2.63v5.24L8 12.85l-4.55-2.63V4.98L8 2.35Zm0 1.3L4.58 5.62v3.96L8 11.55l3.42-1.97V5.62L8 3.65Zm0 1.07 2.5 1.44v2.88L8 10.48 5.5 9.04V6.16L8 4.72Z"/></svg>'
|
|
767
875
|
end
|
|
@@ -812,11 +920,25 @@ module Gemstar
|
|
|
812
920
|
return nil if previous_sections.any? { |section| section[:version] == version }
|
|
813
921
|
return nil if latest_sections.any? { |section| section[:version] == version }
|
|
814
922
|
|
|
923
|
+
metadata = metadata_for(gem_state[:name]) || {}
|
|
815
924
|
repo_url = Gemstar::RubyGemsMetadata.new(gem_state[:name]).repo_uri(cache_only: true)
|
|
816
|
-
|
|
925
|
+
fallback_url =
|
|
926
|
+
if !repo_url.to_s.empty?
|
|
927
|
+
repo_url
|
|
928
|
+
elsif metadata["project_uri"]
|
|
929
|
+
metadata["project_uri"]
|
|
930
|
+
else
|
|
931
|
+
metadata["documentation_uri"]
|
|
932
|
+
end
|
|
933
|
+
fallback_label = if repo_url.to_s.empty?
|
|
934
|
+
metadata["project_uri"] ? "the RubyGems page" : "the gem documentation"
|
|
935
|
+
else
|
|
817
936
|
"the gem repository"
|
|
937
|
+
end
|
|
938
|
+
fallback_link = if fallback_url.to_s.empty?
|
|
939
|
+
fallback_label
|
|
818
940
|
else
|
|
819
|
-
%(<a href="#{h(
|
|
941
|
+
%(<a href="#{h(fallback_url)}" target="_blank" rel="noreferrer">#{h(fallback_label)}</a>)
|
|
820
942
|
end
|
|
821
943
|
|
|
822
944
|
{
|
|
@@ -824,7 +946,7 @@ module Gemstar
|
|
|
824
946
|
title: version,
|
|
825
947
|
kind: :current,
|
|
826
948
|
previous_version: fallback_previous_version_for(gem_state, previous_sections),
|
|
827
|
-
html: "<p>No release information available. Check #{
|
|
949
|
+
html: "<p>No release information available. Check #{fallback_link} for more information.</p>"
|
|
828
950
|
}
|
|
829
951
|
end
|
|
830
952
|
|
|
@@ -280,10 +280,12 @@
|
|
|
280
280
|
.gem-row[hidden] {
|
|
281
281
|
display: none;
|
|
282
282
|
}
|
|
283
|
-
.gem-row:hover
|
|
284
|
-
.gem-row.is-selected {
|
|
283
|
+
.gem-row:hover {
|
|
285
284
|
background: #faf8f3;
|
|
286
285
|
}
|
|
286
|
+
.gem-row.is-selected {
|
|
287
|
+
background: #f2ece0;
|
|
288
|
+
}
|
|
287
289
|
.gem-name {
|
|
288
290
|
font-weight: 700;
|
|
289
291
|
font-size: 0.9rem;
|
|
@@ -343,6 +345,10 @@
|
|
|
343
345
|
min-width: 0;
|
|
344
346
|
width: 100%;
|
|
345
347
|
}
|
|
348
|
+
.detail-title-lockup {
|
|
349
|
+
min-width: 0;
|
|
350
|
+
flex: 1 1 auto;
|
|
351
|
+
}
|
|
346
352
|
.detail-hero-copy h2 {
|
|
347
353
|
margin: 0;
|
|
348
354
|
font-size: 2.35rem;
|
|
@@ -360,6 +366,50 @@
|
|
|
360
366
|
align-self: start;
|
|
361
367
|
flex: 0 0 auto;
|
|
362
368
|
}
|
|
369
|
+
.detail-disclosure {
|
|
370
|
+
margin-top: 0.2rem;
|
|
371
|
+
border: 1px solid #ece8df;
|
|
372
|
+
border-radius: 0.3rem;
|
|
373
|
+
background: #fff;
|
|
374
|
+
padding: 0.45rem 0.55rem;
|
|
375
|
+
}
|
|
376
|
+
.detail-disclosure summary {
|
|
377
|
+
cursor: pointer;
|
|
378
|
+
user-select: none;
|
|
379
|
+
list-style: none;
|
|
380
|
+
display: flex;
|
|
381
|
+
align-items: center;
|
|
382
|
+
gap: 0.35rem;
|
|
383
|
+
}
|
|
384
|
+
.detail-disclosure summary::-webkit-details-marker {
|
|
385
|
+
display: none;
|
|
386
|
+
}
|
|
387
|
+
.detail-disclosure-caret {
|
|
388
|
+
width: 0;
|
|
389
|
+
height: 0;
|
|
390
|
+
border-top: 0.3rem solid transparent;
|
|
391
|
+
border-bottom: 0.3rem solid transparent;
|
|
392
|
+
border-left: 0.42rem solid var(--muted);
|
|
393
|
+
transition: transform 120ms ease;
|
|
394
|
+
flex: 0 0 auto;
|
|
395
|
+
}
|
|
396
|
+
.detail-disclosure[open] .detail-disclosure-caret {
|
|
397
|
+
transform: rotate(90deg);
|
|
398
|
+
}
|
|
399
|
+
.detail-disclosure summary h3 {
|
|
400
|
+
margin: 0;
|
|
401
|
+
font-size: 1.1rem;
|
|
402
|
+
line-height: 1.2;
|
|
403
|
+
color: var(--ink);
|
|
404
|
+
}
|
|
405
|
+
.detail-disclosure-panel {
|
|
406
|
+
margin-top: 0.45rem;
|
|
407
|
+
}
|
|
408
|
+
.detail-info-section + .detail-info-section {
|
|
409
|
+
margin-top: 0.45rem;
|
|
410
|
+
padding-top: 0.45rem;
|
|
411
|
+
border-top: 1px solid #f0ede6;
|
|
412
|
+
}
|
|
363
413
|
.panel-heading-meta {
|
|
364
414
|
color: var(--muted);
|
|
365
415
|
font-size: 0.76rem;
|
|
@@ -370,6 +420,24 @@
|
|
|
370
420
|
background: #fff;
|
|
371
421
|
padding: 0.5rem;
|
|
372
422
|
}
|
|
423
|
+
.detail-loading-shell {
|
|
424
|
+
min-height: 14rem;
|
|
425
|
+
display: grid;
|
|
426
|
+
place-items: center;
|
|
427
|
+
}
|
|
428
|
+
.detail-loading-spinner {
|
|
429
|
+
width: 2rem;
|
|
430
|
+
height: 2rem;
|
|
431
|
+
border-radius: 999px;
|
|
432
|
+
border: 0.18rem solid rgba(31, 27, 23, 0.12);
|
|
433
|
+
border-top-color: rgba(31, 27, 23, 0.62);
|
|
434
|
+
animation: detail-loading-spin 0.85s linear infinite;
|
|
435
|
+
}
|
|
436
|
+
@keyframes detail-loading-spin {
|
|
437
|
+
to {
|
|
438
|
+
transform: rotate(360deg);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
373
441
|
.link-strip {
|
|
374
442
|
display: flex;
|
|
375
443
|
gap: 0.3rem;
|
|
@@ -407,6 +475,7 @@
|
|
|
407
475
|
.detail-origin-list {
|
|
408
476
|
margin: 0.2rem 0 0 1.1rem;
|
|
409
477
|
padding: 0;
|
|
478
|
+
font-size: 0.88rem;
|
|
410
479
|
}
|
|
411
480
|
.detail-origin-list li + li {
|
|
412
481
|
margin-top: 0.12rem;
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
let detailPanel = document.querySelector("[data-detail-panel]");
|
|
10
10
|
const gemLinks = Array.from(document.querySelectorAll("[data-gem-link]"));
|
|
11
11
|
let detailPollTimer = null;
|
|
12
|
+
let detailLoadingTimer = null;
|
|
13
|
+
let detailRequestToken = 0;
|
|
14
|
+
let activeDetailUrl = detailPanel ? detailPanel.dataset.detailUrl : null;
|
|
12
15
|
let currentFilter = <%= selected_filter_json %>;
|
|
13
16
|
let currentSearch = "";
|
|
14
17
|
const emptyDetailHtml = <%= empty_detail_html_json %>;
|
|
@@ -21,6 +24,14 @@
|
|
|
21
24
|
});
|
|
22
25
|
};
|
|
23
26
|
const requestedGemName = () => new URL(window.location.href).searchParams.get("gem");
|
|
27
|
+
const isSidebarFocused = () => document.activeElement === sidebarPanel;
|
|
28
|
+
const isDetailFocused = () => detailPanel && document.activeElement === detailPanel;
|
|
29
|
+
const focusSidebar = () => {
|
|
30
|
+
if (sidebarPanel) sidebarPanel.focus({ preventScroll: true });
|
|
31
|
+
};
|
|
32
|
+
const focusDetail = () => {
|
|
33
|
+
if (detailPanel) detailPanel.focus({ preventScroll: true });
|
|
34
|
+
};
|
|
24
35
|
|
|
25
36
|
const applyGemFilter = (filter) => {
|
|
26
37
|
currentFilter = filter;
|
|
@@ -78,9 +89,14 @@
|
|
|
78
89
|
|
|
79
90
|
const replaceDetail = (html) => {
|
|
80
91
|
if (!detailPanel) return;
|
|
92
|
+
const shouldRestoreDetailFocus = isDetailFocused();
|
|
81
93
|
detailPanel.outerHTML = html;
|
|
82
94
|
detailPanel = document.querySelector("[data-detail-panel]");
|
|
83
95
|
if (detailPanel) detailPanel.scrollTop = 0;
|
|
96
|
+
activeDetailUrl = detailPanel ? detailPanel.dataset.detailUrl : null;
|
|
97
|
+
if (shouldRestoreDetailFocus) {
|
|
98
|
+
focusDetail();
|
|
99
|
+
}
|
|
84
100
|
scheduleDetailPoll();
|
|
85
101
|
};
|
|
86
102
|
|
|
@@ -91,10 +107,37 @@
|
|
|
91
107
|
}
|
|
92
108
|
};
|
|
93
109
|
|
|
110
|
+
const stopDetailLoading = () => {
|
|
111
|
+
if (detailLoadingTimer) {
|
|
112
|
+
clearTimeout(detailLoadingTimer);
|
|
113
|
+
detailLoadingTimer = null;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const loadingDetailHtml = (url) => `
|
|
118
|
+
<section class="detail" data-detail-panel tabindex="0" data-detail-pending="false" data-detail-url="${url}">
|
|
119
|
+
<div class="detail-loading-shell" aria-hidden="true">
|
|
120
|
+
<div class="detail-loading-spinner"></div>
|
|
121
|
+
</div>
|
|
122
|
+
</section>
|
|
123
|
+
`;
|
|
124
|
+
|
|
94
125
|
const fetchDetail = (url, pushHistory = true) => {
|
|
126
|
+
const normalizedUrl = new URL(url, window.location.origin).toString();
|
|
127
|
+
const requestToken = ++detailRequestToken;
|
|
128
|
+
activeDetailUrl = normalizedUrl;
|
|
129
|
+
stopDetailLoading();
|
|
130
|
+
detailLoadingTimer = setTimeout(() => {
|
|
131
|
+
if (requestToken !== detailRequestToken || normalizedUrl !== activeDetailUrl) return;
|
|
132
|
+
replaceDetail(loadingDetailHtml(normalizedUrl));
|
|
133
|
+
}, 1000);
|
|
134
|
+
|
|
95
135
|
fetch(url, { headers: { "X-Requested-With": "gemstar-detail" } })
|
|
96
136
|
.then((response) => response.text())
|
|
97
137
|
.then((html) => {
|
|
138
|
+
stopDetailLoading();
|
|
139
|
+
if (requestToken !== detailRequestToken || normalizedUrl !== activeDetailUrl) return;
|
|
140
|
+
|
|
98
141
|
replaceDetail(html);
|
|
99
142
|
if (pushHistory) {
|
|
100
143
|
const pageUrl = new URL(window.location.href);
|
|
@@ -105,24 +148,28 @@
|
|
|
105
148
|
}
|
|
106
149
|
const detailUrl = new URL(url, window.location.origin);
|
|
107
150
|
syncSidebarSelection(detailUrl.searchParams.get("gem"));
|
|
151
|
+
})
|
|
152
|
+
.catch(() => {
|
|
153
|
+
stopDetailLoading();
|
|
108
154
|
});
|
|
109
155
|
};
|
|
110
156
|
|
|
111
157
|
const activateGemLink = (link, pushHistory = true, keepVisible = false) => {
|
|
112
158
|
if (!link) return;
|
|
113
159
|
|
|
160
|
+
stopDetailPoll();
|
|
161
|
+
stopDetailLoading();
|
|
114
162
|
syncSidebarSelection(link.dataset.gemName, keepVisible);
|
|
115
163
|
fetchDetail(link.dataset.detailUrl || link.href, pushHistory);
|
|
116
164
|
|
|
117
|
-
|
|
118
|
-
sidebarPanel.focus({ preventScroll: true });
|
|
119
|
-
}
|
|
165
|
+
focusSidebar();
|
|
120
166
|
};
|
|
121
167
|
|
|
122
168
|
const scheduleDetailPoll = () => {
|
|
123
169
|
stopDetailPoll();
|
|
124
170
|
if (!detailPanel || detailPanel.dataset.detailPending !== "true") return;
|
|
125
171
|
detailPollTimer = setTimeout(() => {
|
|
172
|
+
if (!detailPanel || !detailPanel.dataset.detailUrl) return;
|
|
126
173
|
fetchDetail(detailPanel.dataset.detailUrl, false);
|
|
127
174
|
}, 1000);
|
|
128
175
|
};
|
|
@@ -204,6 +251,39 @@
|
|
|
204
251
|
document.addEventListener("keydown", (event) => {
|
|
205
252
|
const tagName = document.activeElement && document.activeElement.tagName;
|
|
206
253
|
if (tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT") return;
|
|
254
|
+
|
|
255
|
+
if (event.key === "ArrowRight" && isSidebarFocused()) {
|
|
256
|
+
event.preventDefault();
|
|
257
|
+
focusDetail();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (event.key === "ArrowLeft" && isDetailFocused()) {
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
focusSidebar();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (isDetailFocused()) {
|
|
268
|
+
if (event.key === "ArrowDown") {
|
|
269
|
+
event.preventDefault();
|
|
270
|
+
detailPanel.scrollBy({ top: 80, behavior: "auto" });
|
|
271
|
+
}
|
|
272
|
+
if (event.key === "ArrowUp") {
|
|
273
|
+
event.preventDefault();
|
|
274
|
+
detailPanel.scrollBy({ top: -80, behavior: "auto" });
|
|
275
|
+
}
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!isSidebarFocused()) {
|
|
280
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
281
|
+
event.preventDefault();
|
|
282
|
+
focusSidebar();
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
207
287
|
const links = visibleGemLinks();
|
|
208
288
|
if (!links.length) return;
|
|
209
289
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gemstar
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Florian Dejako
|
|
@@ -79,6 +79,20 @@ dependencies:
|
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '0.14'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: gem-release
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: 2.2.4
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: 2.2.4
|
|
82
96
|
- !ruby/object:Gem::Dependency
|
|
83
97
|
name: kramdown
|
|
84
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -267,7 +281,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
267
281
|
- !ruby/object:Gem::Version
|
|
268
282
|
version: '0'
|
|
269
283
|
requirements: []
|
|
270
|
-
rubygems_version:
|
|
284
|
+
rubygems_version: 4.0.6
|
|
271
285
|
specification_version: 4
|
|
272
286
|
summary: Making sense of gems.
|
|
273
287
|
test_files: []
|