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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -0
  3. data/CITATION.cff +2 -2
  4. data/README.md +4 -2
  5. data/_data/i18n/en.yml +55 -3
  6. data/_data/i18n/es.yml +55 -3
  7. data/_data/i18n/pt.yml +55 -3
  8. data/_includes/analytics/dashboard.html +24 -9
  9. data/_includes/components/academic-dashboard.html +168 -133
  10. data/_includes/components/author-bio.html +7 -5
  11. data/_includes/components/author-list.html +6 -5
  12. data/_includes/components/citation-tools.html +2 -1
  13. data/_includes/components/content-provenance.html +19 -1
  14. data/_includes/components/correction-report.html +15 -8
  15. data/_includes/components/hero.html +22 -15
  16. data/_includes/components/package-index.html +121 -0
  17. data/_includes/components/package-install.html +27 -50
  18. data/_includes/components/post-list.html +14 -0
  19. data/_includes/components/responsive-image.html +15 -21
  20. data/_includes/footer.html +1 -1
  21. data/_includes/head.html +70 -11
  22. data/_includes/layouts/default/article.html +76 -46
  23. data/_includes/meta/dataset-json.html +171 -0
  24. data/_includes/meta/math-config.html +15 -1
  25. data/_includes/meta/package-json.html +69 -0
  26. data/_includes/meta/person-json.html +36 -13
  27. data/_includes/meta/publisher.html +15 -0
  28. data/_includes/meta/schema.html +108 -23
  29. data/_includes/meta/scholarly.html +10 -5
  30. data/_includes/post/related-posts.html +17 -43
  31. data/_includes/scripts.html +2 -2
  32. data/_includes/search/index-data.json +10 -1
  33. data/_includes/search/page.html +11 -6
  34. data/_layouts/archive.html +67 -0
  35. data/_layouts/dataset.html +2 -1
  36. data/_layouts/default.html +13 -11
  37. data/_layouts/docs.html +53 -0
  38. data/_layouts/home.html +30 -1
  39. data/_layouts/notebook.html +5 -1
  40. data/_layouts/package.html +38 -21
  41. data/_layouts/portfolio.html +1 -0
  42. data/_layouts/post.html +21 -62
  43. data/_layouts/project.html +2 -1
  44. data/_layouts/research.html +14 -2
  45. data/_plugins/analytics_dashboard.rb +4 -1
  46. data/_plugins/archive.rb +114 -0
  47. data/_plugins/authors.rb +50 -1
  48. data/_plugins/citation_exports.rb +12 -1
  49. data/_plugins/config_validator.rb +77 -2
  50. data/_plugins/correction_fallback.rb +110 -0
  51. data/_plugins/image_optimizer.rb +129 -22
  52. data/_plugins/math_preprocessor.rb +7 -48
  53. data/_plugins/notebook_converter.rb +44 -5
  54. data/_plugins/packages.rb +84 -0
  55. data/_plugins/page_dates.rb +37 -0
  56. data/_plugins/references.rb +16 -2
  57. data/_plugins/related_posts.rb +149 -0
  58. data/_plugins/search_sections.rb +110 -0
  59. data/_plugins/site_identity.rb +42 -0
  60. data/_plugins/social_cards.rb +17 -0
  61. data/_sass/_academic-dashboard.scss +18 -30
  62. data/_sass/_base.scss +2 -0
  63. data/_sass/_components.scss +21 -23
  64. data/_sass/_docs.scss +143 -0
  65. data/_sass/_features.scss +6 -0
  66. data/_sass/_layout.scss +65 -6
  67. data/_sass/_mathematical.scss +61 -19
  68. data/_sass/_notebooks.scss +1 -32
  69. data/_sass/_package-docs.scss +6 -9
  70. data/_sass/_post-components.scss +23 -23
  71. data/_sass/_print.scss +1 -1
  72. data/_sass/_search-page.scss +75 -0
  73. data/_sass/_search.scss +17 -26
  74. data/_sass/_syntax-highlighting.scss +23 -1
  75. data/_sass/_theme.scss +1 -0
  76. data/_sass/_utilities.scss +87 -40
  77. data/_sass/_variables.scss +31 -29
  78. data/assets/css/main.scss +2 -1
  79. data/assets/js/dist/academic.js +1 -1
  80. data/assets/js/dist/analytics-dashboard.js +1 -1
  81. data/assets/js/dist/chunks/chunk-TNVD6UAM.js +1 -0
  82. data/assets/js/dist/comments.js +1 -1
  83. data/assets/js/dist/contact.js +1 -1
  84. data/assets/js/dist/core.js +1 -1
  85. data/assets/js/dist/corrections.js +5 -1
  86. data/assets/js/dist/loader.js +1 -1
  87. data/assets/js/dist/math.js +2 -1
  88. data/assets/js/dist/moderation.js +1 -1
  89. data/assets/js/dist/reactions.js +1 -1
  90. data/assets/js/dist/search.js +1 -1
  91. data/assets/js/dist/sources.json +20 -16
  92. data/assets/js/dist/subscriptions.js +1 -1
  93. data/assets/js/loader.js +6 -2
  94. data/lib/datalog/audit/checks.rb +269 -0
  95. data/lib/datalog/audit/known_keys.rb +61 -0
  96. data/lib/datalog/audit/site_reader.rb +118 -0
  97. data/lib/datalog/audit/source_file.rb +85 -0
  98. data/lib/datalog/audit.rb +145 -0
  99. data/lib/datalog/citations/bibtex.rb +200 -0
  100. data/lib/datalog/citations/entry.rb +333 -0
  101. data/lib/datalog/citations/markup.rb +79 -0
  102. data/lib/datalog/cli.rb +164 -89
  103. data/lib/datalog/critical_css.rb +126 -21
  104. data/lib/datalog/latex_speech/words.json +88 -0
  105. data/lib/datalog/latex_speech.rb +355 -0
  106. data/lib/datalog/packages/command.rb +51 -0
  107. data/lib/datalog/packages/refresh.rb +187 -0
  108. data/lib/datalog/packages.rb +177 -0
  109. data/lib/datalog/plugin_system.rb +5 -0
  110. data/lib/datalog/plugins/citations.rb +325 -109
  111. data/lib/datalog/site_config.rb +26 -0
  112. data/lib/datalog/social_cards/font.rb +286 -0
  113. data/lib/datalog/social_cards/fonts/IBMPlexSans-Regular.ttf +0 -0
  114. data/lib/datalog/social_cards/fonts/IBMPlexSerif-SemiBold.ttf +0 -0
  115. data/lib/datalog/social_cards/fonts/OFL.txt +92 -0
  116. data/lib/datalog/social_cards/geometry.rb +51 -0
  117. data/lib/datalog/social_cards/outline.rb +54 -0
  118. data/lib/datalog/social_cards/plain_text.rb +258 -0
  119. data/lib/datalog/social_cards/template.rb +322 -0
  120. data/lib/datalog/social_cards/template.svg +17 -0
  121. data/lib/datalog/social_cards/typesetter.rb +178 -0
  122. data/lib/datalog/social_cards.rb +369 -0
  123. data/lib/datalog/theme/updater.rb +307 -0
  124. data/lib/datalog/theme/version.rb +1 -1
  125. metadata +44 -7
  126. data/_includes/components/enhanced-code-block.html +0 -212
  127. data/_includes/components/performance-monitor.html +0 -170
  128. data/_includes/components/viz-table-fallback.html +0 -19
  129. data/_plugins/datalog_bibliography.rb +0 -21
  130. 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