datalog-theme 0.8.0 → 0.9.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -0
  3. data/CITATION.cff +2 -2
  4. data/README.md +4 -2
  5. data/_data/i18n/en.yml +297 -0
  6. data/_data/i18n/es.yml +297 -0
  7. data/_data/i18n/pt.yml +297 -0
  8. data/_data/js_manifest.json +16 -0
  9. data/_includes/components/author-bio.html +21 -11
  10. data/_includes/components/author-list.html +32 -0
  11. data/_includes/components/citation-tools.html +33 -25
  12. data/_includes/components/comments-thread.html +90 -0
  13. data/_includes/components/contact-form.html +120 -0
  14. data/_includes/components/correction-report.html +82 -0
  15. data/_includes/components/enhanced-toc.html +6 -7
  16. data/_includes/components/license-link.html +13 -0
  17. data/_includes/components/license-notice.html +28 -0
  18. data/_includes/components/moderation-inbox.html +139 -0
  19. data/_includes/components/reactions.html +49 -0
  20. data/_includes/components/reading-list.html +31 -0
  21. data/_includes/components/reading-mode-toggle.html +65 -0
  22. data/_includes/components/reading-state-bookmark.html +27 -0
  23. data/_includes/components/reading-state-panel.html +71 -0
  24. data/_includes/components/reproducibility.html +61 -0
  25. data/_includes/components/responsive-image.html +3 -3
  26. data/_includes/components/revision-history.html +39 -0
  27. data/_includes/components/revision-notice.html +35 -0
  28. data/_includes/components/series-nav.html +64 -0
  29. data/_includes/components/subscribe-form.html +88 -0
  30. data/_includes/components/subscription-manage.html +68 -0
  31. data/_includes/components/webmentions.html +48 -0
  32. data/_includes/csp-meta.html +21 -1
  33. data/_includes/footer.html +3 -0
  34. data/_includes/head.html +51 -40
  35. data/_includes/layouts/default/article.html +4 -6
  36. data/_includes/meta/dynamic-services-config.html +14 -0
  37. data/_includes/meta/math-config.html +9 -5
  38. data/_includes/meta/person-json.html +27 -0
  39. data/_includes/meta/publisher.html +45 -0
  40. data/_includes/meta/schema.html +62 -28
  41. data/_includes/meta/scholarly.html +121 -0
  42. data/_includes/meta/scripts-loader.html +2 -0
  43. data/_includes/meta/webmention-discovery.html +14 -0
  44. data/_includes/scripts.html +57 -0
  45. data/_layouts/dataset.html +4 -3
  46. data/_layouts/default.html +1 -1
  47. data/_layouts/package.html +3 -2
  48. data/_layouts/page.html +15 -0
  49. data/_layouts/post.html +82 -18
  50. data/_layouts/project.html +2 -2
  51. data/_layouts/research.html +20 -8
  52. data/_plugins/authors.rb +133 -0
  53. data/_plugins/config_validator.rb +223 -20
  54. data/_plugins/critical_css_check.rb +42 -0
  55. data/_plugins/i18n.rb +6 -4
  56. data/_plugins/image_optimizer.rb +224 -172
  57. data/_plugins/licenses.rb +135 -0
  58. data/_plugins/math_preprocessor.rb +32 -10
  59. data/_plugins/references.rb +238 -0
  60. data/_plugins/reproducibility.rb +150 -0
  61. data/_plugins/revisions.rb +101 -0
  62. data/_plugins/scholarly.rb +50 -0
  63. data/_plugins/series.rb +104 -0
  64. data/_plugins/statements.rb +87 -0
  65. data/_sass/_base.scss +8 -1
  66. data/_sass/_comments-thread.scss +159 -0
  67. data/_sass/_layout.scss +384 -0
  68. data/_sass/_mathematical.scss +27 -0
  69. data/_sass/_moderation.scss +222 -0
  70. data/_sass/_post-components.scss +4 -2
  71. data/_sass/_print.scss +291 -0
  72. data/_sass/_reactions.scss +89 -0
  73. data/_sass/_reading-state.scss +290 -0
  74. data/_sass/_service-forms.scss +204 -0
  75. data/_sass/_subscriptions.scss +140 -0
  76. data/_sass/_syntax-highlighting.scss +2 -2
  77. data/_sass/_theme.scss +11 -0
  78. data/_sass/_typography.scss +116 -0
  79. data/_sass/_utilities.scss +5 -0
  80. data/_sass/_variables.scss +6 -0
  81. data/_sass/_webmentions.scss +125 -0
  82. data/assets/js/dist/academic.js +1 -1
  83. data/assets/js/dist/analytics-dashboard.js +1 -1
  84. data/assets/js/dist/chunks/chunk-2DYDWUFX.js +1 -0
  85. data/assets/js/dist/chunks/chunk-PATLC23F.js +1 -0
  86. data/assets/js/dist/chunks/chunk-V7734B2G.js +1 -0
  87. data/assets/js/dist/comments.js +2 -0
  88. data/assets/js/dist/contact.js +1 -0
  89. data/assets/js/dist/core.js +1 -1
  90. data/assets/js/dist/corrections.js +1 -0
  91. data/assets/js/dist/loader.js +1 -1
  92. data/assets/js/dist/math.js +1 -1
  93. data/assets/js/dist/moderation.js +1 -0
  94. data/assets/js/dist/notebook.js +1 -1
  95. data/assets/js/dist/reactions.js +1 -0
  96. data/assets/js/dist/reading-state.js +1 -0
  97. data/assets/js/dist/search.js +1 -1
  98. data/assets/js/dist/sources.json +52 -0
  99. data/assets/js/dist/subscriptions.js +1 -0
  100. data/assets/js/dist/webmentions.js +1 -0
  101. data/assets/js/loader.js +34 -0
  102. data/datalog-theme.gemspec +1 -2
  103. data/lib/datalog/cli.rb +9 -1
  104. data/lib/datalog/critical_css.rb +168 -0
  105. data/lib/datalog/plugins/comments.rb +33 -3
  106. data/lib/datalog/theme/installed_files.rb +113 -0
  107. data/lib/datalog/theme/repository_checkout.rb +6 -2
  108. data/lib/datalog/theme/version.rb +5 -1
  109. data/lib/datalog-theme.rb +1 -0
  110. metadata +57 -23
  111. data/assets/js/dist/chunks/chunk-225H5YXE.js +0 -1
data/assets/js/loader.js CHANGED
@@ -80,6 +80,40 @@ const FEATURE_CONFIG = [
80
80
  {
81
81
  name: "notebook",
82
82
  test: () => document.body?.dataset.featureNotebook === "true" || document.querySelector(".notebook-output")
83
+ },
84
+ {
85
+ name: "reading-state",
86
+ // A post with the reading-state panel, or a page listing the saved articles.
87
+ test: () => document.querySelector("[data-reading-state], [data-reading-list]")
88
+ },
89
+ {
90
+ name: "corrections",
91
+ test: () => document.querySelector("[data-correction-report]")
92
+ },
93
+ {
94
+ name: "contact",
95
+ test: () => document.querySelector("[data-contact-form]")
96
+ },
97
+ {
98
+ name: "comments",
99
+ test: () => document.querySelector("[data-comments-thread]")
100
+ },
101
+ {
102
+ name: "reactions",
103
+ test: () => document.querySelector("[data-reactions]")
104
+ },
105
+ {
106
+ name: "webmentions",
107
+ test: () => document.querySelector("[data-webmentions]")
108
+ },
109
+ {
110
+ name: "subscriptions",
111
+ // A subscribe form, or the page the newsletter's emails link to.
112
+ test: () => document.querySelector("[data-subscribe], [data-subscription-manage]")
113
+ },
114
+ {
115
+ name: "moderation",
116
+ test: () => document.querySelector("[data-moderation-inbox]")
83
117
  }
84
118
  ];
85
119
 
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
 
27
27
  # The sass-embedded and nokogiri releases the theme is tested with need Ruby
28
28
  # 3.2; the gemspec said 3.0.
29
- spec.required_ruby_version = ">= 3.2"
29
+ spec.required_ruby_version = Datalog::Theme::RUBY_REQUIREMENT
30
30
  spec.require_paths = ["lib"]
31
31
  spec.bindir = "bin"
32
32
  spec.executables = ["datalog"]
@@ -79,7 +79,6 @@ Gem::Specification.new do |spec|
79
79
  spec.add_runtime_dependency "fastimage", "~> 2.2"
80
80
  spec.add_runtime_dependency "loofah", "~> 2.19"
81
81
  spec.add_runtime_dependency "nokogiri", "~> 1.15"
82
- spec.add_runtime_dependency "mini_magick", ">= 4.12", "< 6"
83
82
  spec.add_runtime_dependency "logger", "~> 1.6"
84
83
  spec.add_runtime_dependency "thor", "~> 1.3"
85
84
  # googleauth, with the Google Cloud gems it brings, is needed only by the
data/lib/datalog/cli.rb CHANGED
@@ -422,6 +422,14 @@ module Datalog
422
422
  end
423
423
  end
424
424
 
425
+ desc "critical-css", "Write the critical CSS a production build inlines, into _includes/critical-css"
426
+ method_option :critical, type: :array,
427
+ desc: "Command that runs critical (default: node_modules/.bin/critical or npx critical@8)"
428
+ def critical_css
429
+ require_relative "critical_css"
430
+ CriticalCss.command(site_root, options[:critical], ->(*line) { say_status(*line) })
431
+ end
432
+
425
433
  register(New, "new", "new COMMAND", "Scaffold posts, notebooks, and portfolio projects")
426
434
 
427
435
  private
@@ -439,7 +447,7 @@ module Datalog
439
447
  end
440
448
 
441
449
  def ruby_supported?
442
- Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.0.0")
450
+ Gem::Requirement.new(Datalog::Theme::RUBY_REQUIREMENT).satisfied_by?(Gem::Version.new(RUBY_VERSION))
443
451
  end
444
452
 
445
453
  def gemfile_path(root)
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "fileutils"
5
+ require "jekyll"
6
+ require "open3"
7
+ require "tmpdir"
8
+ require "yaml"
9
+
10
+ module Datalog
11
+ # Writes the critical CSS a production build inlines. head.html inlines
12
+ # _includes/critical-css/<target>.html for a page's layout (home, post, or
13
+ # default for every other layout) and then loads main.css without blocking
14
+ # rendering. Critical CSS depends on a site's own pages and styles, so the
15
+ # theme ships those files empty and each site writes its own with
16
+ # `datalog critical-css`.
17
+ class CriticalCss
18
+ TARGETS = %w[home post default].freeze
19
+ DEFAULT_DIMENSIONS = [{ "width" => 1920, "height" => 1080 }, { "width" => 375, "height" => 667 }].freeze
20
+ PACKAGE = "critical@8"
21
+
22
+ class Error < StandardError; end
23
+
24
+ attr_reader :root, :settings
25
+
26
+ # `datalog critical-css`. status reports a line as the command's shell
27
+ # does; an error ends the command with exit status 1.
28
+ def self.command(root, critical, status)
29
+ config = YAML.safe_load_file(File.join(root, "_config.yml"), permitted_classes: [Date, Time]) || {}
30
+ log = ->(message) { status.call(:critical, message, :blue) }
31
+ written = new(root, config["critical_css"], critical: critical, log: log).run
32
+ written.each_value { |path| status.call(:write, path, :green) }
33
+ rescue Error, Jekyll::Errors::FatalException, SystemCallError => e
34
+ status.call(:error, e.message, :red)
35
+ exit 1
36
+ end
37
+
38
+ # critical: the command that runs critical, as a list of words. By default
39
+ # the site's own node_modules/.bin/critical, or `npx --yes critical@8`.
40
+ def initialize(root, settings, critical: nil, log: ->(_message) {})
41
+ @root = File.expand_path(root)
42
+ @settings = settings || {}
43
+ @critical = critical
44
+ @log = log
45
+ end
46
+
47
+ # Returns the files written, by target. A target with no page to extract
48
+ # from, such as post on a site without posts, is left as it is.
49
+ def run
50
+ unless settings["enabled"] == true
51
+ raise Error, "critical_css.enabled is not true in _config.yml, so no page would inline critical CSS"
52
+ end
53
+
54
+ Dir.mktmpdir("datalog-critical") do |site_dir|
55
+ @log.call("Building the site for production into a temporary directory")
56
+ build(site_dir)
57
+ pages_for(site_dir).each_with_object({}) do |(target, page), written|
58
+ if page.nil?
59
+ @log.call("No page uses the #{target} layout; _includes/critical-css/#{target}.html is left as it is")
60
+ next
61
+ end
62
+
63
+ @log.call("Extracting critical CSS for #{target} from #{page}")
64
+ written[target] = write(target, extract(site_dir, page))
65
+ end
66
+ end
67
+ end
68
+
69
+ # A production build, with critical CSS turned off for it: pages then link
70
+ # main.css as usual, and CSS inlined by an earlier run cannot skew the result.
71
+ def build(destination)
72
+ previous = ENV.fetch("JEKYLL_ENV", nil)
73
+ ENV["JEKYLL_ENV"] = "production"
74
+ config = Jekyll.configuration(
75
+ "source" => root, "destination" => destination, "quiet" => true,
76
+ "critical_css" => settings.merge("enabled" => false)
77
+ )
78
+ Jekyll::Site.new(config).process
79
+ ensure
80
+ ENV["JEKYLL_ENV"] = previous
81
+ end
82
+
83
+ # The page each target extracts from: the one critical_css.pages names, or
84
+ # the first built page with that layout, nearest the site root first.
85
+ def pages_for(site_dir)
86
+ configured = settings["pages"] || {}
87
+ found = TARGETS.to_h { |target| [target, configured[target] && page_file(site_dir, configured[target])] }
88
+ missing = TARGETS.reject { |target| found[target] }
89
+
90
+ html_files(site_dir).each do |page|
91
+ break if missing.empty?
92
+
93
+ target = target_of(File.read(File.join(site_dir, page), encoding: "utf-8"))
94
+ next unless missing.delete(target)
95
+
96
+ found[target] = page
97
+ end
98
+ found
99
+ end
100
+
101
+ # Mirrors head.html: the home and post layouts have their own file, and
102
+ # every other page the theme's default layout renders uses default. Pages
103
+ # kept out of search engines, such as the search page, are not typical.
104
+ def target_of(html)
105
+ classes = html[/<body\b[^>]*\bclass="([^"]*)"/, 1].to_s.split
106
+ return unless classes.include?("site-body")
107
+ return "home" if classes.include?("layout-home")
108
+ return "post" if classes.include?("layout-post")
109
+
110
+ "default" unless html.match?(/<meta name="robots" content="noindex/)
111
+ end
112
+
113
+ # Only the site's stylesheet: the Google Fonts stylesheet would inline
114
+ # @font-face rules for the font files headless Chrome is served (TrueType),
115
+ # and fetching it made each page take over a minute.
116
+ def extract(site_dir, page)
117
+ arguments = [page, "--base", site_dir, "--css", File.join(site_dir, "assets", "css", "main.css"),
118
+ *dimension_arguments, *penthouse_arguments]
119
+ output, errors, status = Open3.capture3(*critical_command, *arguments, chdir: site_dir)
120
+ unless status.success?
121
+ raise Error, "critical could not extract CSS from #{page}: #{errors.strip.lines.last(5).join.strip}"
122
+ end
123
+
124
+ css = output.strip
125
+ raise Error, "critical found no critical CSS in #{page}" if css.empty?
126
+
127
+ css
128
+ end
129
+
130
+ def write(target, css)
131
+ path = File.join(root, "_includes", "critical-css", "#{target}.html")
132
+ FileUtils.mkdir_p(File.dirname(path))
133
+ # The include is rendered as Liquid; raw keeps a "{{" in CSS from being read as a tag.
134
+ File.write(path, "{% raw %}\n#{css}\n{% endraw %}\n")
135
+ path
136
+ end
137
+
138
+ def critical_command
139
+ return @critical if @critical
140
+
141
+ local = File.join(root, "node_modules", ".bin", Gem.win_platform? ? "critical.cmd" : "critical")
142
+ File.file?(local) ? [local] : ["npx", "--yes", PACKAGE]
143
+ end
144
+
145
+ def dimension_arguments
146
+ dimensions = Array(settings["dimensions"]).grep(Hash)
147
+ dimensions = DEFAULT_DIMENSIONS if dimensions.empty?
148
+ dimensions.flat_map { |entry| ["--dimensions", "#{entry['width']}x#{entry['height']}"] }
149
+ end
150
+
151
+ def penthouse_arguments
152
+ options = settings["penthouse_options"]
153
+ return [] unless options.is_a?(Hash)
154
+
155
+ options.flat_map { |key, value| ["--penthouse-#{key}", value.to_s] }
156
+ end
157
+
158
+ def page_file(site_dir, url)
159
+ path = url.to_s.sub(%r{\A/+}, "")
160
+ path = File.join(path, "index.html") if path.empty? || path.end_with?("/")
161
+ path if File.file?(File.join(site_dir, path))
162
+ end
163
+
164
+ def html_files(site_dir)
165
+ Dir.glob("**/*.html", base: site_dir).sort_by { |page| [page.count("/"), page] }
166
+ end
167
+ end
168
+ end
@@ -19,16 +19,23 @@ module Datalog
19
19
  REQUIRED_FIELDS = {
20
20
  "giscus" => %w[repo repo_id category category_id],
21
21
  "utterances" => %w[repo],
22
- "disqus" => %w[shortname]
22
+ "disqus" => %w[shortname],
23
+ "api" => []
23
24
  }.freeze
24
25
 
26
+ # The api provider's thread (components/comments-thread.html) is a Liquid
27
+ # include, so it shares the service forms' labels and states.
28
+ API_INCLUDE = "{% include components/comments-thread.html %}"
29
+
25
30
  def initialize(site, config = {})
26
31
  super
27
32
  @defaults = {
28
33
  "provider" => "giscus",
29
34
  "mapping" => "pathname",
30
35
  "theme" => "light",
31
- "enabled_by_default" => false
36
+ "enabled_by_default" => false,
37
+ "replies" => true,
38
+ "moderation" => true
32
39
  }.merge(@config)
33
40
  @memo = {}
34
41
  end
@@ -67,9 +74,12 @@ module Datalog
67
74
  settings = @memo[key]
68
75
  return "" unless settings
69
76
 
77
+ embed = settings["embed"]
78
+ embed = render_api(context) if settings["provider"] == "api" && embed.to_s.empty?
79
+
70
80
  <<~HTML
71
81
  <div class="datalog-comments" aria-label="Comments">
72
- #{settings['embed']}
82
+ #{embed}
73
83
  </div>
74
84
  HTML
75
85
  end
@@ -109,6 +119,8 @@ module Datalog
109
119
  render_utterances(settings)
110
120
  when "disqus"
111
121
  render_disqus(document, settings)
122
+ when "api"
123
+ "" # the thread is rendered by the tag, in its own context
112
124
  else
113
125
  %(<p class="datalog-comments-unsupported">Comments provider "#{settings['provider']}" is not supported.</p>)
114
126
  end
@@ -140,6 +152,12 @@ module Datalog
140
152
  async></script>)
141
153
  end
142
154
 
155
+ # The thread of the site's own backend (#253), rendered in the tag's own
156
+ # context so the include sees the page and the site.
157
+ def render_api(context)
158
+ Liquid::Template.parse(API_INCLUDE).render(context)
159
+ end
160
+
143
161
  def render_disqus(document, settings)
144
162
  shortname = settings["shortname"] || ""
145
163
  nonce = nonce_attribute(document)
@@ -171,10 +189,22 @@ module Datalog
171
189
  end
172
190
 
173
191
  def missing_required_fields(provider, settings)
192
+ return api_missing(settings) if provider.to_s == "api"
193
+
174
194
  required = REQUIRED_FIELDS.fetch(provider.to_s, [])
175
195
  required.select { |key| settings[key].to_s.strip.empty? }
176
196
  end
177
197
 
198
+ # The api provider needs a backend: its own endpoint, or the site's
199
+ # dynamic services (docs/dynamic-services.md).
200
+ def api_missing(settings)
201
+ services = site.config["dynamic_services"]
202
+ base = services.is_a?(Hash) ? services["base_url"].to_s.strip : ""
203
+ return [] unless settings["endpoint"].to_s.strip.empty? && base.empty?
204
+
205
+ ["endpoint (or dynamic_services.base_url)"]
206
+ end
207
+
178
208
  def logger
179
209
  Jekyll.logger
180
210
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "jekyll"
6
+ require "json"
7
+ require_relative "package"
8
+ require_relative "repository_checkout"
9
+
10
+ module Datalog
11
+ module Theme
12
+ # Stops a build that would serve a mix of theme versions. A site that uses
13
+ # the theme from a Git submodule or another checkout reads every theme file
14
+ # from it, but the script bundles are build output and have to be built
15
+ # again whenever the checkout moves. Any site can also hold copies of files
16
+ # the theme provides, left from copying the theme in by hand; a site's file
17
+ # takes the place of the theme's, and the copies fall behind on the next
18
+ # update.
19
+ module InstalledFiles
20
+ module_function
21
+
22
+ BUNDLE_SOURCES = "assets/js/dist/sources.json"
23
+ # Theme data a site has no reason to replace: the script manifest names
24
+ # the bundles this version of the theme ships, and the integrity hashes
25
+ # belong to the CDN files its layouts load. Translations are left out,
26
+ # since a site may change them.
27
+ DATA_FILES = %w[_data/js_manifest.json _data/cdn-integrity.yml].freeze
28
+
29
+ # Jekyll prints an error on one line, so the messages are written as
30
+ # sentences rather than laid out in lines.
31
+ def check_bundles(site)
32
+ return unless RepositoryCheckout.applies_to?(site)
33
+
34
+ root = site.theme.root
35
+ reasons = stale_bundle_reasons(root)
36
+ return if reasons.empty?
37
+
38
+ listed = reasons.first(5).join("; ")
39
+ listed += "; and #{reasons.size - 5} more" if reasons.size > 5
40
+ raise Jekyll::Errors::FatalException,
41
+ "The script bundles in #{File.join(root, 'assets', 'js', 'dist')} were not built from the theme " \
42
+ "files beside them (#{listed}). Pages would run scripts from another version of the theme. Build " \
43
+ "them again by running `npm ci && npm run build:js` in #{root}."
44
+ end
45
+
46
+ # scripts/build_js.mjs records a SHA-256 of each file it builds from.
47
+ def stale_bundle_reasons(root)
48
+ record = File.join(root, BUNDLE_SOURCES)
49
+ unless File.file?(record)
50
+ return ["#{BUNDLE_SOURCES} is missing: the bundles were never built, or built before it was recorded"]
51
+ end
52
+
53
+ JSON.parse(File.read(record)).fetch("sources").filter_map do |path, digest|
54
+ file = File.join(root, path)
55
+ if !File.file?(file)
56
+ "#{path} is gone"
57
+ elsif Digest::SHA256.file(file).hexdigest != digest
58
+ "#{path} has changed"
59
+ end
60
+ end
61
+ end
62
+
63
+ def check_site_copies(site)
64
+ theme = site.theme
65
+ return unless theme && theme.name == "datalog-theme"
66
+
67
+ copies = stale_asset_copies(site, theme.root) + stale_data_copies(site, theme.root)
68
+ return if copies.empty?
69
+
70
+ raise Jekyll::Errors::FatalException,
71
+ "Your site has its own copies of files datalog-theme provides, and they differ from the theme's: " \
72
+ "#{copies.join(', ')}. A site's file takes the place of the theme's, so pages would load scripts, " \
73
+ "or check their integrity, for another version of the theme. Delete these files: the theme " \
74
+ "provides them."
75
+ end
76
+
77
+ # The site's static files that stand in for theme files pages load. Jekyll
78
+ # leaves the theme's file out when the site has one at the same path.
79
+ def stale_asset_copies(site, root)
80
+ source = File.join(site.source, "")
81
+ site.static_files.filter_map do |file|
82
+ next unless file.path.start_with?(source)
83
+
84
+ relative = file.relative_path.delete_prefix("/")
85
+ next unless Package::SCRIPTS_PAGES_LOAD.any? { |entry| Package.inside?(relative, entry) }
86
+
87
+ theme_file = File.join(root, relative)
88
+ relative if File.file?(theme_file) && !FileUtils.identical?(file.path, theme_file)
89
+ end
90
+ end
91
+
92
+ def stale_data_copies(site, root)
93
+ reader = Jekyll::DataReader.new(site)
94
+ own = reader.read(site.config["data_dir"])
95
+ DATA_FILES.filter_map do |entry|
96
+ key = File.basename(entry, ".*")
97
+ theme_file = File.join(root, entry)
98
+ next unless own.key?(key) && File.file?(theme_file)
99
+ next if own[key] == reader.read_data_file(theme_file)
100
+
101
+ Dir.glob("#{key}.*", base: site.in_source_dir(site.config["data_dir"])).map do |name|
102
+ File.join(site.config["data_dir"], name)
103
+ end.first
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ Jekyll::Hooks.register :site, :post_read do |site|
111
+ Datalog::Theme::InstalledFiles.check_bundles(site)
112
+ Datalog::Theme::InstalledFiles.check_site_copies(site)
113
+ end
@@ -54,9 +54,13 @@ module Datalog
54
54
  remove_demo_data(site)
55
55
  end
56
56
 
57
+ # A checkout also holds the script sources the bundles are built from,
58
+ # which no page loads. The site gets the theme assets the gem would give it.
57
59
  def remove_demo_assets(site)
58
- demo_dirs = Package::DEMO_ASSETS.map { |entry| File.join(site.theme.root, entry, "") }
59
- site.static_files.reject! { |file| demo_dirs.any? { |dir| file.path.start_with?(dir) } }
60
+ root = File.join(site.theme.root, "")
61
+ site.static_files.reject! do |file|
62
+ file.path.start_with?(root) && !Package.theme_file?(file.path.delete_prefix(root))
63
+ end
60
64
  end
61
65
 
62
66
  # site.data holds the theme's data with the site's merged over it. Each key
@@ -2,6 +2,10 @@
2
2
 
3
3
  module Datalog
4
4
  module Theme
5
- VERSION = "0.8.0" unless const_defined?(:VERSION)
5
+ VERSION = "0.9.0" unless const_defined?(:VERSION)
6
+ # The Ruby the gem installs on, for the gemspec and `datalog check`. The
7
+ # release workflow rewrites every quoted version assigned to a name ending
8
+ # in VERSION in this file, so this name must not end that way.
9
+ RUBY_REQUIREMENT = ">= 3.2" unless const_defined?(:RUBY_REQUIREMENT)
6
10
  end
7
11
  end
data/lib/datalog-theme.rb CHANGED
@@ -36,3 +36,4 @@ end
36
36
  Datalog::Theme.load_plugins
37
37
 
38
38
  require_relative "datalog/theme/repository_checkout"
39
+ require_relative "datalog/theme/installed_files"