datalog-theme 0.10.1 → 0.11.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 +127 -0
- data/CITATION.cff +2 -2
- data/README.md +4 -2
- data/_data/i18n/en.yml +55 -3
- data/_data/i18n/es.yml +55 -3
- data/_data/i18n/pt.yml +55 -3
- data/_includes/analytics/dashboard.html +24 -9
- data/_includes/components/academic-dashboard.html +168 -133
- data/_includes/components/author-bio.html +7 -5
- data/_includes/components/author-list.html +6 -5
- data/_includes/components/citation-tools.html +2 -1
- data/_includes/components/content-provenance.html +19 -1
- data/_includes/components/correction-report.html +15 -8
- data/_includes/components/hero.html +22 -15
- data/_includes/components/package-index.html +121 -0
- data/_includes/components/package-install.html +27 -50
- data/_includes/components/post-list.html +14 -0
- data/_includes/components/responsive-image.html +15 -21
- data/_includes/footer.html +1 -1
- data/_includes/head.html +70 -11
- data/_includes/layouts/default/article.html +76 -46
- data/_includes/meta/dataset-json.html +171 -0
- data/_includes/meta/math-config.html +15 -1
- data/_includes/meta/package-json.html +69 -0
- data/_includes/meta/person-json.html +36 -13
- data/_includes/meta/publisher.html +15 -0
- data/_includes/meta/schema.html +108 -23
- data/_includes/meta/scholarly.html +10 -5
- data/_includes/post/related-posts.html +17 -43
- data/_includes/scripts.html +2 -2
- data/_includes/search/index-data.json +10 -1
- data/_includes/search/page.html +11 -6
- data/_layouts/archive.html +67 -0
- data/_layouts/dataset.html +2 -1
- data/_layouts/default.html +13 -11
- data/_layouts/docs.html +53 -0
- data/_layouts/home.html +30 -1
- data/_layouts/notebook.html +5 -1
- data/_layouts/package.html +38 -21
- data/_layouts/portfolio.html +1 -0
- data/_layouts/post.html +21 -62
- data/_layouts/project.html +2 -1
- data/_layouts/research.html +14 -2
- data/_plugins/analytics_dashboard.rb +4 -1
- data/_plugins/archive.rb +114 -0
- data/_plugins/authors.rb +50 -1
- data/_plugins/citation_exports.rb +12 -1
- data/_plugins/config_validator.rb +77 -2
- data/_plugins/correction_fallback.rb +110 -0
- data/_plugins/image_optimizer.rb +129 -22
- data/_plugins/math_preprocessor.rb +7 -48
- data/_plugins/notebook_converter.rb +44 -5
- data/_plugins/packages.rb +84 -0
- data/_plugins/page_dates.rb +37 -0
- data/_plugins/references.rb +16 -2
- data/_plugins/related_posts.rb +149 -0
- data/_plugins/search_sections.rb +110 -0
- data/_plugins/site_identity.rb +42 -0
- data/_plugins/social_cards.rb +17 -0
- data/_sass/_academic-dashboard.scss +18 -30
- data/_sass/_base.scss +2 -0
- data/_sass/_components.scss +21 -23
- data/_sass/_docs.scss +143 -0
- data/_sass/_features.scss +6 -0
- data/_sass/_layout.scss +65 -6
- data/_sass/_mathematical.scss +61 -19
- data/_sass/_notebooks.scss +1 -32
- data/_sass/_package-docs.scss +6 -9
- data/_sass/_post-components.scss +23 -23
- data/_sass/_print.scss +1 -1
- data/_sass/_search-page.scss +75 -0
- data/_sass/_search.scss +17 -26
- data/_sass/_syntax-highlighting.scss +23 -1
- data/_sass/_theme.scss +1 -0
- data/_sass/_utilities.scss +87 -40
- data/_sass/_variables.scss +31 -29
- data/assets/css/main.scss +2 -1
- data/assets/js/dist/academic.js +1 -1
- data/assets/js/dist/analytics-dashboard.js +1 -1
- data/assets/js/dist/chunks/chunk-TNVD6UAM.js +1 -0
- data/assets/js/dist/comments.js +1 -1
- data/assets/js/dist/contact.js +1 -1
- data/assets/js/dist/core.js +1 -1
- data/assets/js/dist/corrections.js +5 -1
- data/assets/js/dist/loader.js +1 -1
- data/assets/js/dist/math.js +2 -1
- data/assets/js/dist/moderation.js +1 -1
- data/assets/js/dist/reactions.js +1 -1
- data/assets/js/dist/search.js +1 -1
- data/assets/js/dist/sources.json +20 -16
- data/assets/js/dist/subscriptions.js +1 -1
- data/assets/js/loader.js +6 -2
- data/lib/datalog/audit/checks.rb +269 -0
- data/lib/datalog/audit/known_keys.rb +61 -0
- data/lib/datalog/audit/site_reader.rb +118 -0
- data/lib/datalog/audit/source_file.rb +85 -0
- data/lib/datalog/audit.rb +145 -0
- data/lib/datalog/citations/bibtex.rb +200 -0
- data/lib/datalog/citations/entry.rb +333 -0
- data/lib/datalog/citations/markup.rb +79 -0
- data/lib/datalog/cli.rb +164 -89
- data/lib/datalog/critical_css.rb +126 -21
- data/lib/datalog/latex_speech/words.json +88 -0
- data/lib/datalog/latex_speech.rb +355 -0
- data/lib/datalog/packages/command.rb +51 -0
- data/lib/datalog/packages/refresh.rb +187 -0
- data/lib/datalog/packages.rb +177 -0
- data/lib/datalog/plugin_system.rb +5 -0
- data/lib/datalog/plugins/citations.rb +325 -109
- data/lib/datalog/site_config.rb +26 -0
- data/lib/datalog/social_cards/font.rb +286 -0
- data/lib/datalog/social_cards/fonts/IBMPlexSans-Regular.ttf +0 -0
- data/lib/datalog/social_cards/fonts/IBMPlexSerif-SemiBold.ttf +0 -0
- data/lib/datalog/social_cards/fonts/OFL.txt +92 -0
- data/lib/datalog/social_cards/geometry.rb +51 -0
- data/lib/datalog/social_cards/outline.rb +54 -0
- data/lib/datalog/social_cards/plain_text.rb +258 -0
- data/lib/datalog/social_cards/template.rb +322 -0
- data/lib/datalog/social_cards/template.svg +17 -0
- data/lib/datalog/social_cards/typesetter.rb +178 -0
- data/lib/datalog/social_cards.rb +369 -0
- data/lib/datalog/theme/updater.rb +307 -0
- data/lib/datalog/theme/version.rb +1 -1
- metadata +44 -7
- data/_includes/components/enhanced-code-block.html +0 -212
- data/_includes/components/performance-monitor.html +0 -170
- data/_includes/components/viz-table-fallback.html +0 -19
- data/_plugins/datalog_bibliography.rb +0 -21
- data/assets/js/dist/chunks/chunk-WIRUK3OZ.js +0 -1
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "yaml"
|
|
8
|
+
require_relative "../packages"
|
|
9
|
+
require_relative "../site_config"
|
|
10
|
+
require_relative "../theme/version"
|
|
11
|
+
|
|
12
|
+
module Datalog
|
|
13
|
+
module Packages
|
|
14
|
+
# `datalog packages refresh`: reads the latest release of every package the
|
|
15
|
+
# site lists from each registry it names, and writes them to
|
|
16
|
+
# _data/package_releases.yml for the package layout to read. The build
|
|
17
|
+
# itself never goes to a registry, so it stays offline and two builds of
|
|
18
|
+
# one commit match. A registry that cannot be read stops the command before
|
|
19
|
+
# the file is touched. The file holds nothing but the releases, so it
|
|
20
|
+
# changes only when one does, and a scheduled run has nothing to propose
|
|
21
|
+
# otherwise.
|
|
22
|
+
class Refresh
|
|
23
|
+
class Error < StandardError; end
|
|
24
|
+
|
|
25
|
+
USER_AGENT = "datalog-theme/#{Datalog::Theme::VERSION} (+https://github.com/DiogoRibeiro7/analytics-blog-jekyll)".freeze
|
|
26
|
+
HEADER = "# The latest release of each package on each registry, written by\n" \
|
|
27
|
+
"# `bundle exec datalog packages refresh`. Run it again to update this file;\n" \
|
|
28
|
+
"# a package's registries are set in its front matter (`registry:`).\n"
|
|
29
|
+
|
|
30
|
+
attr_reader :root
|
|
31
|
+
|
|
32
|
+
# `fetch` takes a URL and returns [status, body]; the default asks the network.
|
|
33
|
+
def initialize(root:, fetch: nil)
|
|
34
|
+
@root = File.expand_path(root)
|
|
35
|
+
@fetch = fetch || method(:http_get)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def config
|
|
39
|
+
@config ||= Datalog::SiteConfig.load(File.join(root, "_config.yml"))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def data_file
|
|
43
|
+
File.join(root, config["data_dir"] || "_data", "package_releases.yml")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# {"pypi" => {"name" => release}}, read from every registry, or Error
|
|
47
|
+
# naming each registry that could not be read.
|
|
48
|
+
def releases
|
|
49
|
+
found = Hash.new { |hash, key| hash[key] = {} }
|
|
50
|
+
failures = []
|
|
51
|
+
packages.each do |file, data|
|
|
52
|
+
Packages.registries(data).each do |registry, name|
|
|
53
|
+
next unless ENDPOINTS.key?(registry)
|
|
54
|
+
|
|
55
|
+
found[registry][name] = read(registry, name)
|
|
56
|
+
rescue Error => e
|
|
57
|
+
failures << "#{file}: #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
raise Error, "No release was written, because #{failures.join('; ')}" unless failures.empty?
|
|
61
|
+
|
|
62
|
+
found.sort.to_h.transform_values { |names| names.sort.to_h }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Writes the file and returns the releases, or raises before writing.
|
|
66
|
+
# With no release to write, it leaves the file alone.
|
|
67
|
+
def run(dry_run: false)
|
|
68
|
+
data = releases
|
|
69
|
+
yaml = HEADER + YAML.dump(data).sub(/\A---\n/, "")
|
|
70
|
+
unless dry_run || data.empty?
|
|
71
|
+
FileUtils.mkdir_p(File.dirname(data_file))
|
|
72
|
+
File.write(data_file, yaml)
|
|
73
|
+
end
|
|
74
|
+
[data, yaml]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# [relative path, front matter] of each document in the packages collection.
|
|
78
|
+
def packages
|
|
79
|
+
directory = File.join(root, config["collections_dir"].to_s, "_packages")
|
|
80
|
+
Dir.glob(File.join(directory, "**", "*.{md,markdown,html}")).filter_map do |path|
|
|
81
|
+
text = File.read(path, encoding: "bom|utf-8")
|
|
82
|
+
front = text[/\A---\s*\n(.*?)^---\s*$/m, 1]
|
|
83
|
+
data = front && YAML.safe_load(front, permitted_classes: [Date, Time], aliases: true)
|
|
84
|
+
[path.delete_prefix("#{root}/"), data] if data.is_a?(Hash)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def read(registry, name)
|
|
89
|
+
url = format(ENDPOINTS.fetch(registry), URI.encode_www_form_component(name))
|
|
90
|
+
status, body = begin
|
|
91
|
+
@fetch.call(url)
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
raise Error, "could not reach #{REGISTRIES.dig(registry, :label)} at #{url} (#{e.message})"
|
|
94
|
+
end
|
|
95
|
+
if status == 404
|
|
96
|
+
raise Error, "#{REGISTRIES.dig(registry, :label)} has no package named #{name} (#{url})"
|
|
97
|
+
elsif !(200..299).cover?(status.to_i)
|
|
98
|
+
raise Error, "#{REGISTRIES.dig(registry, :label)} answered #{status} for #{name} (#{url})"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
found = Releases.public_send(registry, JSON.parse(body))
|
|
102
|
+
found.merge("prerelease" => Packages.prerelease?(found["version"], registry))
|
|
103
|
+
rescue JSON::ParserError, KeyError, NoMethodError, TypeError => e
|
|
104
|
+
raise Error,
|
|
105
|
+
"#{REGISTRIES.dig(registry, :label)} sent an answer for #{name} that is not a release (#{e.message})"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def http_get(url)
|
|
109
|
+
uri = URI(url)
|
|
110
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 10,
|
|
111
|
+
read_timeout: 20) do |http|
|
|
112
|
+
request = Net::HTTP::Get.new(uri)
|
|
113
|
+
request["User-Agent"] = USER_AGENT
|
|
114
|
+
request["Accept"] = "application/json"
|
|
115
|
+
response = http.request(request)
|
|
116
|
+
[response.code.to_i, response.body]
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# One registry's answer as one release: version, released (a date),
|
|
122
|
+
# license, the language it needs, and whether it is a pre-release or yanked.
|
|
123
|
+
module Releases
|
|
124
|
+
module_function
|
|
125
|
+
|
|
126
|
+
def pypi(json)
|
|
127
|
+
info = json.fetch("info")
|
|
128
|
+
version = info.fetch("version")
|
|
129
|
+
files = Array(json.dig("releases", version) || json["urls"])
|
|
130
|
+
license = info["license_expression"].to_s
|
|
131
|
+
license = info["license"].to_s if license.empty? && info["license"].to_s.length.between?(1, 40)
|
|
132
|
+
license = classifier_license(info["classifiers"]) if license.empty?
|
|
133
|
+
release(version, files.map { |file| file["upload_time_iso_8601"] || file["upload_time"] }.compact.min,
|
|
134
|
+
license, "requires_python" => info["requires_python"],
|
|
135
|
+
"yanked" => !files.empty? && files.all? { |file| file["yanked"] })
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def crates(json)
|
|
139
|
+
crate = json.fetch("crate")
|
|
140
|
+
version = crate["max_stable_version"] || crate["newest_version"] || crate.fetch("max_version")
|
|
141
|
+
found = Array(json["versions"]).find { |entry| entry["num"] == version } || {}
|
|
142
|
+
release(version, found["created_at"], found["license"],
|
|
143
|
+
"rust_version" => found["rust_version"], "yanked" => found["yanked"] == true)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def rubygems(json)
|
|
147
|
+
versions = Array(json)
|
|
148
|
+
found = versions.find { |entry| entry["prerelease"] != true } || versions.fetch(0)
|
|
149
|
+
release(found.fetch("number"), found["created_at"], Array(found["licenses"]).join(", "),
|
|
150
|
+
"required_ruby_version" => found["ruby_version"])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def cran(json)
|
|
154
|
+
depends = json["Depends"]
|
|
155
|
+
r_version = depends.is_a?(Hash) ? depends["R"] : depends.to_s[/\bR\s*\(([^)]+)\)/, 1]
|
|
156
|
+
release(json.fetch("Version"), json["Date/Publication"], json["License"], "r_version" => r_version)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def npm(json)
|
|
160
|
+
version = json.dig("dist-tags", "latest") || raise(KeyError, "no latest version")
|
|
161
|
+
found = json.dig("versions", version) || {}
|
|
162
|
+
license = found["license"].is_a?(Hash) ? found.dig("license", "type") : found["license"]
|
|
163
|
+
# A deprecated version still installs, with a warning: npm has no yank.
|
|
164
|
+
release(version, json.dig("time", version), license, "node_version" => found.dig("engines", "node"))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def release(version, released, license, extra = {})
|
|
168
|
+
date = released.to_s[/\A\d{4}-\d{2}-\d{2}/]
|
|
169
|
+
found = { "version" => version.to_s, "released" => date, "license" => blank(license),
|
|
170
|
+
"prerelease" => Packages.prerelease?(version) }
|
|
171
|
+
found.merge(extra.transform_values { |value| blank(value) })
|
|
172
|
+
.reject { |key, value| value.nil? || (key == "yanked" && !value) }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def blank(value)
|
|
176
|
+
return value if [true, false].include?(value)
|
|
177
|
+
|
|
178
|
+
text = value.to_s.strip
|
|
179
|
+
text.empty? ? nil : text
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def classifier_license(classifiers)
|
|
183
|
+
Array(classifiers).filter_map { |entry| entry[/\ALicense :: (?:OSI Approved :: )?(.+)\z/, 1] }.first.to_s
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Datalog
|
|
7
|
+
# Package pages that know their registries: where each registry lists a
|
|
8
|
+
# package, how to install it from there, and how to read its latest release
|
|
9
|
+
# from the registry's API. The layout and the install panel read the first
|
|
10
|
+
# two through _plugins/packages.rb; `datalog packages refresh` the third.
|
|
11
|
+
module Packages
|
|
12
|
+
# key => label, page, install commands as [console, template], and docs.
|
|
13
|
+
REGISTRIES = {
|
|
14
|
+
"pypi" => { label: "PyPI", tab: "pip", url: "https://pypi.org/project/%s/",
|
|
15
|
+
install: [["terminal", "pip install %s"]] },
|
|
16
|
+
"conda_forge" => { label: "conda-forge", tab: "conda", url: "https://anaconda.org/conda-forge/%s",
|
|
17
|
+
install: [["terminal", "conda install -c conda-forge %s"]] },
|
|
18
|
+
"cran" => { label: "CRAN", tab: "cran", url: "https://cran.r-project.org/package=%s",
|
|
19
|
+
install: [["r_console", "install.packages('%s')"]] },
|
|
20
|
+
"crates" => { label: "crates.io", tab: "cargo", url: "https://crates.io/crates/%s",
|
|
21
|
+
install: [["terminal", "cargo add %s"]], docs: "https://docs.rs/%s" },
|
|
22
|
+
"rubygems" => { label: "RubyGems", tab: "gem", url: "https://rubygems.org/gems/%s",
|
|
23
|
+
install: [["terminal", "gem install %s"], ["gemfile", "gem \"%s\""]] },
|
|
24
|
+
"npm" => { label: "npm", tab: "npm", url: "https://www.npmjs.com/package/%s",
|
|
25
|
+
install: [["terminal", "npm install %s"]] },
|
|
26
|
+
"julia" => { label: "JuliaHub", tab: "julia", url: "https://juliahub.com/ui/Packages/General/%s",
|
|
27
|
+
install: [["julia_repl", "using Pkg; Pkg.add(\"%s\")"]] }
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# A tab is named for the tool that installs from the registry.
|
|
31
|
+
TAB_LABELS = { "pip" => "pip", "conda" => "conda", "cran" => "CRAN", "cargo" => "cargo", "gem" => "gem",
|
|
32
|
+
"npm" => "npm", "julia" => "Pkg" }.freeze
|
|
33
|
+
|
|
34
|
+
# The registries `datalog packages refresh` reads, and the address of a
|
|
35
|
+
# package's latest release in each.
|
|
36
|
+
ENDPOINTS = {
|
|
37
|
+
"pypi" => "https://pypi.org/pypi/%s/json",
|
|
38
|
+
"crates" => "https://crates.io/api/v1/crates/%s",
|
|
39
|
+
"rubygems" => "https://rubygems.org/api/v1/versions/%s.json",
|
|
40
|
+
"cran" => "https://crandb.r-pkg.org/%s",
|
|
41
|
+
"npm" => "https://registry.npmjs.org/%s"
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
# A PEP 440 version, read leniently: an epoch, the release, then a
|
|
45
|
+
# pre-release, a post-release and a development release, each optional,
|
|
46
|
+
# then a local label. A pre-release or a development release makes it a
|
|
47
|
+
# pre-release (1.0a1, 2.0rc1, 1!1.0rc1, 1.0.post1.dev2); a post-release
|
|
48
|
+
# alone does not (1.0.post1).
|
|
49
|
+
PEP440 = /\A(?:\d+!)?\d+(?:\.\d+)*
|
|
50
|
+
(?<pre>[-_.]?(?:a|b|c|rc|alpha|beta|pre|preview)[-_.]?\d*)?
|
|
51
|
+
(?:[-_.]?(?:post|rev|r)[-_.]?\d*|-\d+)?
|
|
52
|
+
(?<dev>[-_.]?dev[-_.]?\d*)?
|
|
53
|
+
(?:\+[a-z0-9]+(?:[-_.][a-z0-9]+)*)?\z/ix
|
|
54
|
+
|
|
55
|
+
# Registries whose versions are semver, where a hyphen marks a pre-release
|
|
56
|
+
# (1.0.0-beta.2) and a plus only build metadata (1.0.0+build.5).
|
|
57
|
+
SEMVER = %w[crates npm julia].freeze
|
|
58
|
+
|
|
59
|
+
# The two addresses pages gave before `registry:`.
|
|
60
|
+
LEGACY = { "pypi" => "pypi_url", "cran" => "cran_url" }.freeze
|
|
61
|
+
|
|
62
|
+
module_function
|
|
63
|
+
|
|
64
|
+
# The registries a page names, in the order it names them: {"pypi" => "name"}.
|
|
65
|
+
# `pypi_url` and `cran_url` still count, for pages written before `registry:`.
|
|
66
|
+
def registries(data)
|
|
67
|
+
named = data["registry"].is_a?(Hash) ? data["registry"].transform_keys(&:to_s) : {}
|
|
68
|
+
named = named.select { |key, name| REGISTRIES.key?(key) && !name.to_s.strip.empty? }
|
|
69
|
+
LEGACY.each_key do |key|
|
|
70
|
+
name = legacy_name(key, legacy_url(data, key))
|
|
71
|
+
named[key] ||= name if name
|
|
72
|
+
end
|
|
73
|
+
named.transform_values { |name| name.to_s.strip }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def legacy_url(data, key)
|
|
77
|
+
url = data[LEGACY.fetch(key)].to_s.strip
|
|
78
|
+
url.empty? ? nil : url
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def legacy_name(key, url)
|
|
82
|
+
return nil unless url
|
|
83
|
+
|
|
84
|
+
case key
|
|
85
|
+
when "pypi" then url[%r{pypi\.(?:python\.)?org/(?:project|pypi)/([^/?#]+)}, 1]
|
|
86
|
+
when "cran" then url[/package=([\w.]+)/, 1] || url[%r{/web/packages/([\w.]+)}, 1]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The registry pages, and docs.rs for a crate: [{"registry", "label", "url"}].
|
|
91
|
+
# A `pypi_url` or `cran_url` is the link when it names the same package as
|
|
92
|
+
# the registry's entry, and still a link when it names none this can read.
|
|
93
|
+
def links(data)
|
|
94
|
+
named = registries(data)
|
|
95
|
+
found = named.flat_map do |key, name|
|
|
96
|
+
spec = REGISTRIES.fetch(key)
|
|
97
|
+
legacy = LEGACY.key?(key) ? legacy_url(data, key) : nil
|
|
98
|
+
legacy = nil unless legacy && legacy_name(key, legacy).to_s.casecmp?(name)
|
|
99
|
+
[{ "registry" => key, "label" => spec[:label], "url" => legacy || format(spec[:url], name) },
|
|
100
|
+
spec[:docs] && { "registry" => "docs_rs", "label" => "docs.rs", "url" => format(spec[:docs], name) }].compact
|
|
101
|
+
end
|
|
102
|
+
LEGACY.each_key do |key|
|
|
103
|
+
url = legacy_url(data, key)
|
|
104
|
+
found << { "registry" => key, "label" => REGISTRIES.dig(key, :label), "url" => url } if url && !named.key?(key)
|
|
105
|
+
end
|
|
106
|
+
found
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The install panel's tabs: one per registry, then Git. Each is
|
|
110
|
+
# {"id", "label", "commands" => [{"console", "command"}]}. A page that
|
|
111
|
+
# names no registry keeps the tabs its language always had: pip, conda
|
|
112
|
+
# and Git for Python, CRAN and GitHub for R. `overrides` replaces the
|
|
113
|
+
# command of a tab by its id, as the include's pip_command and siblings do.
|
|
114
|
+
def install_tabs(data, language: nil, git_url: nil, name: nil, overrides: {})
|
|
115
|
+
named = registries(data)
|
|
116
|
+
tabs = named.empty? ? language_tabs(language.to_s.downcase, name) : named.map { |key, package| tab(key, package) }
|
|
117
|
+
git = git_tab(language.to_s.downcase, git_url)
|
|
118
|
+
tabs << git if git
|
|
119
|
+
tabs.each do |item|
|
|
120
|
+
override = overrides[item["id"]].to_s.strip
|
|
121
|
+
item["commands"][0]["command"] = override unless override.empty?
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def tab(key, package)
|
|
126
|
+
spec = REGISTRIES.fetch(key)
|
|
127
|
+
commands = spec[:install].map do |console, template|
|
|
128
|
+
{ "console" => console, "command" => format(template, package) }
|
|
129
|
+
end
|
|
130
|
+
{ "id" => spec[:tab], "label" => TAB_LABELS.fetch(spec[:tab]), "commands" => commands }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def language_tabs(language, name)
|
|
134
|
+
case language
|
|
135
|
+
when "python" then [terminal_tab("pip", "pip install #{name}"), terminal_tab("conda", "conda install #{name}")]
|
|
136
|
+
when "r" then [tab("cran", name)]
|
|
137
|
+
else []
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def terminal_tab(id, command, console: "terminal", label: TAB_LABELS.fetch(id, id))
|
|
142
|
+
{ "id" => id, "label" => label, "commands" => [{ "console" => console, "command" => command }] }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def git_tab(language, git_url)
|
|
146
|
+
return nil if git_url.to_s.empty?
|
|
147
|
+
|
|
148
|
+
if language == "r"
|
|
149
|
+
repository = git_url.sub(%r{\Ahttps?://github\.com/}, "").sub(/\.git\z/, "")
|
|
150
|
+
return terminal_tab("github", "devtools::install_github(\"#{repository}\")", console: "r_console",
|
|
151
|
+
label: "GitHub")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The clone's directory is the repository's name, which need not be the package's.
|
|
155
|
+
lines = ["git clone #{git_url}", "cd #{File.basename(git_url.to_s.chomp('/'), '.git')}"]
|
|
156
|
+
lines << "pip install -e ." if language == "python"
|
|
157
|
+
terminal_tab("git", lines.join("\n"), label: "Git")
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Whether a version is a pre-release by the rules of its registry: any
|
|
161
|
+
# letter for RubyGems, a hyphen for semver, PEP 440 otherwise (and semver
|
|
162
|
+
# for a version PEP 440 cannot read). CRAN has no pre-releases.
|
|
163
|
+
def prerelease?(version, registry = nil)
|
|
164
|
+
text = version.to_s.strip
|
|
165
|
+
return false if text.empty? || registry == "cran"
|
|
166
|
+
return text.match?(/[a-z]/i) if registry == "rubygems"
|
|
167
|
+
return semver_prerelease?(text) if SEMVER.include?(registry)
|
|
168
|
+
|
|
169
|
+
match = PEP440.match(text)
|
|
170
|
+
match ? !(match[:pre] || match[:dev]).nil? : semver_prerelease?(text)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def semver_prerelease?(text)
|
|
174
|
+
text.split("+", 2).first.include?("-")
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
@@ -97,6 +97,9 @@ module Datalog
|
|
|
97
97
|
next unless plugin.respond_to?(hook)
|
|
98
98
|
|
|
99
99
|
plugin.public_send(hook, *args)
|
|
100
|
+
rescue Jekyll::Errors::FatalException
|
|
101
|
+
# A plugin's build error is the site's, as {% ref %}'s are: it stops the build.
|
|
102
|
+
raise
|
|
100
103
|
rescue StandardError => e
|
|
101
104
|
Jekyll.logger.warn("datalog plugin", "#{plugin.id} failed during #{hook}: #{e.message}")
|
|
102
105
|
end
|
|
@@ -112,6 +115,8 @@ module Datalog
|
|
|
112
115
|
next unless result.is_a?(Hash)
|
|
113
116
|
|
|
114
117
|
acc.replace(deep_merge(acc, result))
|
|
118
|
+
rescue Jekyll::Errors::FatalException
|
|
119
|
+
raise
|
|
115
120
|
rescue StandardError => e
|
|
116
121
|
Jekyll.logger.warn("datalog plugin", "#{plugin.id} failed during #{hook}: #{e.message}")
|
|
117
122
|
end
|