datalog-theme 0.6.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 (180) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +162 -0
  3. data/CITATION.cff +29 -0
  4. data/LICENSE +21 -0
  5. data/README.md +257 -0
  6. data/_data/academic.yml +217 -0
  7. data/_data/cdn-integrity.yml +51 -0
  8. data/_data/config/author.yml +121 -0
  9. data/_data/config/features.yml +262 -0
  10. data/_data/config/site.yml +42 -0
  11. data/_data/config/theme.yml +181 -0
  12. data/_data/datasets.yml +28 -0
  13. data/_data/i18n/en.yml +438 -0
  14. data/_data/i18n/es.yml +438 -0
  15. data/_data/i18n/pt.yml +438 -0
  16. data/_data/js_manifest.json +22 -0
  17. data/_data/js_meta.json +371 -0
  18. data/_data/navigation.yml +145 -0
  19. data/_data/projects.yml +41 -0
  20. data/_data/publications.yml +28 -0
  21. data/_data/social.yml +73 -0
  22. data/_data/visualizations.yml +51 -0
  23. data/_includes/analytics/dashboard.html +149 -0
  24. data/_includes/analytics.html +9 -0
  25. data/_includes/components/academic-dashboard.html +347 -0
  26. data/_includes/components/advanced-search.html +682 -0
  27. data/_includes/components/api-function.html +94 -0
  28. data/_includes/components/author-bio.html +166 -0
  29. data/_includes/components/bookmark-system.html +96 -0
  30. data/_includes/components/breadcrumbs.html +89 -0
  31. data/_includes/components/citation-tools.html +94 -0
  32. data/_includes/components/comments.html +244 -0
  33. data/_includes/components/content-provenance.html +43 -0
  34. data/_includes/components/content-recommendations.html +228 -0
  35. data/_includes/components/difficulty-badge.html +84 -0
  36. data/_includes/components/email-preferences.html +200 -0
  37. data/_includes/components/enhanced-code-block.html +212 -0
  38. data/_includes/components/enhanced-metadata.html +228 -0
  39. data/_includes/components/enhanced-toc.html +209 -0
  40. data/_includes/components/hero.html +15 -0
  41. data/_includes/components/language-switcher.html +396 -0
  42. data/_includes/components/math-fallback.html +19 -0
  43. data/_includes/components/navigation-enhancements.html +454 -0
  44. data/_includes/components/newsletter-signup.html +178 -0
  45. data/_includes/components/open-science-badges.html +16 -0
  46. data/_includes/components/package-install.html +194 -0
  47. data/_includes/components/performance-monitor.html +170 -0
  48. data/_includes/components/popular-posts.html +233 -0
  49. data/_includes/components/post-hero.html +62 -0
  50. data/_includes/components/reading-progress.html +133 -0
  51. data/_includes/components/reading-time.html +121 -0
  52. data/_includes/components/responsive-image.html +63 -0
  53. data/_includes/components/search-facets.html +307 -0
  54. data/_includes/components/series-navigation.html +124 -0
  55. data/_includes/components/social-proof.html +34 -0
  56. data/_includes/components/social-share.html +119 -0
  57. data/_includes/components/user-preferences.html +566 -0
  58. data/_includes/components/visualization-card.html +24 -0
  59. data/_includes/components/viz-table-fallback.html +19 -0
  60. data/_includes/critical-css/default.html +0 -0
  61. data/_includes/critical-css/home.html +0 -0
  62. data/_includes/critical-css/post.html +0 -0
  63. data/_includes/csp-meta.html +17 -0
  64. data/_includes/footer/nav-column.html +31 -0
  65. data/_includes/footer.html +76 -0
  66. data/_includes/head.html +337 -0
  67. data/_includes/header/navigation.html +48 -0
  68. data/_includes/header.html +70 -0
  69. data/_includes/helpers/array-to-sentence.html +21 -0
  70. data/_includes/helpers/date-format.html +16 -0
  71. data/_includes/helpers/link-with-icon.html +26 -0
  72. data/_includes/layouts/default/article.html +67 -0
  73. data/_includes/meta/language-attributes.html +25 -0
  74. data/_includes/meta/math-config.html +21 -0
  75. data/_includes/meta/schema.html +153 -0
  76. data/_includes/meta/scripts-loader.html +52 -0
  77. data/_includes/post/related-posts.html +63 -0
  78. data/_includes/scripts.html +72 -0
  79. data/_includes/skip-link.html +1 -0
  80. data/_includes/toc.html +2 -0
  81. data/_layouts/archive.html +282 -0
  82. data/_layouts/dataset.html +52 -0
  83. data/_layouts/default.html +33 -0
  84. data/_layouts/home.html +48 -0
  85. data/_layouts/notebook.html +173 -0
  86. data/_layouts/package.html +238 -0
  87. data/_layouts/page.html +16 -0
  88. data/_layouts/portfolio.html +41 -0
  89. data/_layouts/post-sidebar.html +183 -0
  90. data/_layouts/post.html +338 -0
  91. data/_layouts/project.html +252 -0
  92. data/_layouts/research.html +504 -0
  93. data/_plugins/analytics_dashboard.rb +315 -0
  94. data/_plugins/config_validator.rb +397 -0
  95. data/_plugins/csp_generator.rb +115 -0
  96. data/_plugins/datalog_bibliography.rb +19 -0
  97. data/_plugins/datalog_comments.rb +18 -0
  98. data/_plugins/datalog_slides.rb +20 -0
  99. data/_plugins/front_matter_compat.rb +109 -0
  100. data/_plugins/i18n.rb +160 -0
  101. data/_plugins/image_optimizer.rb +445 -0
  102. data/_plugins/math_preprocessor.rb +185 -0
  103. data/_plugins/notebook_converter.rb +1107 -0
  104. data/_plugins/plugin_loader.rb +185 -0
  105. data/_plugins/publications_generator.rb +234 -0
  106. data/_plugins/reading_time.rb +15 -0
  107. data/_plugins/search_normalizer.rb +74 -0
  108. data/_plugins/sri_filter.rb +18 -0
  109. data/_plugins/toc_filter.rb +68 -0
  110. data/_plugins/warning_filter.rb +3 -0
  111. data/_sass/_base.scss +142 -0
  112. data/_sass/_components.scss +2380 -0
  113. data/_sass/_header.scss +470 -0
  114. data/_sass/_interactive.scss +344 -0
  115. data/_sass/_layout.scss +1760 -0
  116. data/_sass/_mathematical.scss +368 -0
  117. data/_sass/_mixins.scss +99 -0
  118. data/_sass/_package-docs.scss +715 -0
  119. data/_sass/_phase1-enhancements.scss +721 -0
  120. data/_sass/_phase3-enhancements.scss +874 -0
  121. data/_sass/_phase4-enhancements.scss +1214 -0
  122. data/_sass/_phase5-enhancements.scss +414 -0
  123. data/_sass/_search.scss +654 -0
  124. data/_sass/_syntax-highlighting.scss +128 -0
  125. data/_sass/_theme.scss +79 -0
  126. data/_sass/_typography.scss +148 -0
  127. data/_sass/_utilities.scss +334 -0
  128. data/_sass/_variables.scss +153 -0
  129. data/_sass/_visualizations.scss +242 -0
  130. data/assets/css/main.scss +4 -0
  131. data/assets/img/20220607123041_detail.001.png +0 -0
  132. data/assets/img/favicons/android-chrome-192x192.png +0 -0
  133. data/assets/img/favicons/android-chrome-512x512.png +0 -0
  134. data/assets/img/favicons/apple-touch-icon.png +0 -0
  135. data/assets/img/favicons/favicon-16x16.png +0 -0
  136. data/assets/img/favicons/favicon-32x32.png +0 -0
  137. data/assets/img/favicons/favicon.ico +0 -0
  138. data/assets/img/favicons/site.webmanifest +21 -0
  139. data/assets/img/portfolio-placeholder.svg +20 -0
  140. data/assets/js/academic.js +262 -0
  141. data/assets/js/analytics-dashboard.js +382 -0
  142. data/assets/js/core/dark-mode.js +79 -0
  143. data/assets/js/core/github-cards.js +123 -0
  144. data/assets/js/core/language-filter.js +69 -0
  145. data/assets/js/core/navigation.js +184 -0
  146. data/assets/js/core/scroll-progress.js +45 -0
  147. data/assets/js/core/search-hotkeys.js +62 -0
  148. data/assets/js/core/skip-links.js +62 -0
  149. data/assets/js/loader.js +163 -0
  150. data/assets/js/main.js +23 -0
  151. data/assets/js/math.js +818 -0
  152. data/assets/js/notebook.js +158 -0
  153. data/assets/js/search/analytics.js +91 -0
  154. data/assets/js/search/app.js +271 -0
  155. data/assets/js/search/autocomplete.js +120 -0
  156. data/assets/js/search/engine.js +260 -0
  157. data/assets/js/search/filters.js +38 -0
  158. data/assets/js/search/render.js +217 -0
  159. data/assets/js/search/utils.js +99 -0
  160. data/assets/js/search.js +354 -0
  161. data/assets/js/visualizations.js +816 -0
  162. data/assets/publications/datalog-publications.bib +8 -0
  163. data/assets/publications/datalog-publications.ris +9 -0
  164. data/assets/publications/publications.bib +30 -0
  165. data/assets/templates/diogo-ribeiro-cv.md +31 -0
  166. data/assets/templates/diogo-ribeiro-cv.tex +32 -0
  167. data/bin/datalog +26 -0
  168. data/datalog-theme.gemspec +72 -0
  169. data/lib/datalog/cli.rb +563 -0
  170. data/lib/datalog/plugin_system/dependency_resolver.rb +223 -0
  171. data/lib/datalog/plugin_system.rb +237 -0
  172. data/lib/datalog/plugins/citations.rb +182 -0
  173. data/lib/datalog/plugins/comments.rb +183 -0
  174. data/lib/datalog/plugins/search.rb +45 -0
  175. data/lib/datalog/plugins/slides.rb +121 -0
  176. data/lib/datalog/theme/theme.rb +18 -0
  177. data/lib/datalog/theme/version.rb +7 -0
  178. data/lib/datalog/warning_filter.rb +28 -0
  179. data/lib/datalog-theme.rb +17 -0
  180. metadata +555 -0
@@ -0,0 +1,563 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+ require "date"
5
+ require "fileutils"
6
+ require "json"
7
+ require "pathname"
8
+ require "tmpdir"
9
+ require "yaml"
10
+ require "thor"
11
+
12
+ require_relative "theme/version"
13
+
14
+ module Datalog
15
+ # Command line interface for automating common theme workflows.
16
+ class CLI < Thor
17
+ include Thor::Actions
18
+
19
+ class_option :root,
20
+ type: :string,
21
+ default: Dir.pwd,
22
+ aliases: "-r",
23
+ desc: "Path to the Jekyll site using the DataLog theme"
24
+
25
+ map %w[--version -v] => :version
26
+
27
+ desc "version", "Print the installed DataLog theme version"
28
+ def version
29
+ say "DataLog theme version #{Datalog::Theme::VERSION}"
30
+ end
31
+
32
+ desc "check", "Verify required runtimes, dependencies, and configuration"
33
+ long_desc <<~DESC
34
+ Runs a diagnostic pass to make sure the current site directory has all
35
+ dependencies installed, required configuration values in _config.yml, and
36
+ optional tooling such as Node.js and Python for notebooks and visualizations.
37
+ The command exits with a non-zero status code when a critical check fails.
38
+ DESC
39
+ def check
40
+ root = site_root
41
+ say_status :check, "Scanning #{root}" unless options[:quiet]
42
+
43
+ critical_failures = []
44
+ warnings = []
45
+
46
+ critical_failures << :ruby unless ruby_supported?
47
+ critical_failures << :bundler unless command_available?("bundle")
48
+ critical_failures << :jekyll unless gem_available?(root, "jekyll")
49
+ warnings << :node unless command_available?("node")
50
+ warnings << :python unless command_available?("python3") || command_available?("python")
51
+ warnings << :git unless command_available?("git")
52
+
53
+ if config_valid?(root)
54
+ warnings.concat(configuration_warnings(root))
55
+ else
56
+ critical_failures << :config
57
+ end
58
+
59
+ summarize_checks(critical_failures, warnings)
60
+
61
+ exit 1 unless critical_failures.empty?
62
+ end
63
+
64
+ desc "publish", "Build the site and deploy the _site artifacts to GitHub Pages"
65
+ method_option :branch,
66
+ type: :string,
67
+ default: "gh-pages",
68
+ aliases: "-b",
69
+ desc: "Target branch that serves the built site"
70
+ method_option :message,
71
+ type: :string,
72
+ aliases: "-m",
73
+ desc: "Custom commit message for the deployment"
74
+ long_desc <<~DESC
75
+ Builds the Jekyll site using the active configuration and pushes the
76
+ generated _site folder to the chosen branch. The command uses a temporary
77
+ git worktree to keep the deployment history isolated.
78
+ DESC
79
+ def publish
80
+ ensure_inside_git_repository!
81
+
82
+ root = site_root
83
+ branch = options[:branch]
84
+ message = options[:message] || "Publish DataLog site on #{Time.now.utc.strftime('%Y-%m-%d %H:%M UTC')}"
85
+
86
+ say_status :build, "bundle exec jekyll build", :blue
87
+ unless system({ "BUNDLE_GEMFILE" => gemfile_path(root) }, "bundle exec jekyll build", chdir: root)
88
+ say_error "Jekyll build failed. Resolve the error above and try again."
89
+ exit 1
90
+ end
91
+
92
+ Dir.mktmpdir("datalog-publish") do |tmp|
93
+ worktree_path = File.join(tmp, "deploy")
94
+ prepare_worktree(root, branch, worktree_path)
95
+ copy_site_output(root, worktree_path)
96
+ commit_and_push(worktree_path, branch, message)
97
+ cleanup_worktree(root, worktree_path)
98
+ end
99
+ end
100
+
101
+ desc "update", "Update the DataLog theme and related assets to the latest version"
102
+ long_desc <<~DESC
103
+ Runs Bundler to update the datalog-theme dependency to the latest compatible
104
+ release and refreshes npm packages if a package.json file is present.
105
+ DESC
106
+ def update
107
+ root = site_root
108
+ gemfile = gemfile_path(root)
109
+
110
+ if File.exist?(gemfile)
111
+ say_status :bundle, "bundle update datalog-theme", :blue
112
+ unless system({ "BUNDLE_GEMFILE" => gemfile }, "bundle update datalog-theme", chdir: root)
113
+ say_error "Bundler could not update datalog-theme."
114
+ exit 1
115
+ end
116
+ else
117
+ say_status :skip, "No Gemfile detected—skipping Bundler update", :yellow
118
+ end
119
+
120
+ package_json = File.join(root, "package.json")
121
+ if File.exist?(package_json) && command_available?("npm")
122
+ say_status :npm, "npm install", :blue
123
+ system("npm install", chdir: root)
124
+ elsif File.exist?(package_json)
125
+ say_status :warn, "Node.js tooling not available—skipping npm install", :yellow
126
+ end
127
+
128
+ say "Theme dependencies are up to date!"
129
+ end
130
+
131
+ class New < Thor
132
+ include Thor::Actions
133
+
134
+ class_option :root,
135
+ type: :string,
136
+ default: Dir.pwd,
137
+ aliases: "-r",
138
+ desc: "Path to the Jekyll site using the DataLog theme"
139
+
140
+ no_commands do
141
+ def site_root
142
+ File.expand_path(options[:root])
143
+ end
144
+
145
+ def ensure_directory!(relative_path)
146
+ absolute = File.join(site_root, relative_path)
147
+ FileUtils.mkdir_p(absolute)
148
+ absolute
149
+ end
150
+
151
+ def slugify(text)
152
+ text.downcase.strip.gsub(/[^a-z0-9]+/, "-").gsub(/^-|-$/, "")
153
+ end
154
+
155
+ def ask_with_default(prompt, default)
156
+ response = ask("#{prompt} [#{default}]")
157
+ response = response.strip
158
+ response.empty? ? default : response
159
+ end
160
+ end
161
+
162
+ desc "post", "Interactively scaffold a blog post with recommended front matter"
163
+ method_option :title, type: :string, aliases: "-t", desc: "Title for the post"
164
+ method_option :date, type: :string, aliases: "-d", desc: "Publication date (YYYY-MM-DD)"
165
+ method_option :tags, type: :array, aliases: "-g", desc: "List of tags"
166
+ method_option :difficulty, type: :string, aliases: "-l", desc: "Difficulty label"
167
+ def post
168
+ title = options[:title] || ask("Title?")
169
+ date_input = options[:date] || ask_with_default("Publication date", Date.today.strftime("%Y-%m-%d"))
170
+ date = parse_date(date_input)
171
+ slug = slugify(title)
172
+ summary = ask_with_default("One-sentence summary", "Describe what readers will learn.")
173
+ tags = options[:tags] || ask("Tags (comma separated)?").split(",").map(&:strip).reject(&:empty?)
174
+ difficulty = (options[:difficulty] || ask_with_default("Difficulty", "Intermediate")).capitalize
175
+ author = ask_with_default("Author", ENV.fetch("GIT_AUTHOR_NAME", "DataLog Team"))
176
+
177
+ posts_dir = ensure_directory!("_posts")
178
+ filename = File.join(posts_dir, format("%<date>s-%<slug>s.md", date: date.strftime("%Y-%m-%d"), slug: slug))
179
+
180
+ if File.exist?(filename)
181
+ say_status :error, "Post already exists: #{filename}", :red
182
+ exit 1
183
+ end
184
+
185
+ tags_yaml = if tags.empty?
186
+ "[]"
187
+ else
188
+ "\n" + tags.map { |tag| " - #{tag}" }.join("\n")
189
+ end
190
+ front_matter = <<~YAML
191
+ ---
192
+ layout: post
193
+ title: "#{title}"
194
+ description: "#{summary}"
195
+ author: "#{author}"
196
+ date: #{date.strftime('%Y-%m-%d')}
197
+ tags:#{tags_yaml}
198
+ difficulty: "#{difficulty}"
199
+ hero:
200
+ image: /assets/images/#{slug}.jpg
201
+ alt: "Hero image for #{title}"
202
+ callouts:
203
+ - label: Key takeaway
204
+ content: Highlight the most important insight from the post.
205
+ ---
206
+ YAML
207
+
208
+ body = <<~MARKDOWN
209
+ #{front_matter}
210
+ ## Overview
211
+
212
+ Introduce the topic and set expectations for the walkthrough.
213
+
214
+ ```python
215
+ # Drop in runnable code snippets with syntax highlighting
216
+ import pandas as pd
217
+ ```
218
+
219
+ > Use alerts, figures, and inline math like $\\alpha_t$ to demonstrate the theme.
220
+
221
+ ### Next steps
222
+
223
+ - Summarize the insights readers gained.
224
+ - Link to supporting datasets or notebooks.
225
+ - Invite feedback or reproduction reports.
226
+ MARKDOWN
227
+
228
+ create_file(filename, body)
229
+ say_status :create, relative_to_root(filename), :green
230
+ end
231
+
232
+ desc "notebook", "Generate a notebook landing page and starter .ipynb file"
233
+ method_option :title, type: :string, aliases: "-t", desc: "Title for the notebook"
234
+ method_option :language, type: :string, aliases: "-l", default: "python", desc: "Primary notebook language"
235
+ def notebook
236
+ site_root
237
+ title = options[:title] || ask("Title?")
238
+ language = options[:language]&.strip&.downcase || "python"
239
+ slug = slugify(title)
240
+ summary = ask_with_default("Summary", "Explain what the notebook demonstrates.")
241
+ binder = ask("Binder URL (optional)?").strip
242
+ colab = ask("Colab URL (optional)?").strip
243
+ tags = ask("Tags (comma separated)?").split(",").map(&:strip).reject(&:empty?)
244
+
245
+ build_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
246
+
247
+ notebooks_dir = ensure_directory!("_notebooks")
248
+ assets_dir = ensure_directory!("notebooks")
249
+ download_dir = ensure_directory!("assets/notebooks")
250
+
251
+ metadata = {
252
+ "title" => title,
253
+ "summary" => summary,
254
+ "tags" => tags,
255
+ "date" => Date.today.strftime("%Y-%m-%d"),
256
+ "notebook" => {
257
+ "source" => "notebooks/#{slug}.ipynb",
258
+ "download" => "/assets/notebooks/#{slug}.ipynb",
259
+ "executed_at" => Time.now.strftime("%Y-%m-%d %H:%M"),
260
+ "code_cells" => 0,
261
+ "markdown_cells" => 0,
262
+ "error_cells" => 0,
263
+ "kernelspec" => {
264
+ "display_name" => language.capitalize,
265
+ "language" => language
266
+ }
267
+ }
268
+ }
269
+
270
+ integrations = {}
271
+ integrations["binder"] = binder unless binder.empty?
272
+ integrations["colab"] = colab unless colab.empty?
273
+ metadata["notebook"]["integrations"] = integrations unless integrations.empty?
274
+
275
+ ipynb_path = File.join(assets_dir, "#{slug}.ipynb")
276
+ create_file(ipynb_path, JSON.pretty_generate(blank_notebook(language)))
277
+
278
+ download_path = File.join(download_dir, "#{slug}.ipynb")
279
+ if File.exist?(download_path)
280
+ say_status :skip, "Download already exists at #{relative_to_root(download_path)}", :yellow
281
+ else
282
+ FileUtils.cp(ipynb_path, download_path)
283
+ say_status :copy, relative_to_root(download_path), :blue
284
+ end
285
+
286
+ elapsed_seconds = Process.clock_gettime(Process::CLOCK_MONOTONIC) - build_started_at
287
+ metadata["notebook"]["duration"] = elapsed_seconds.positive? ? elapsed_seconds.round(2) : 0
288
+
289
+ metadata_yaml = metadata.to_yaml(line_width: -1)
290
+ markdown_path = File.join(notebooks_dir, "#{slug}.md")
291
+ create_file(markdown_path, <<~MARKDOWN)
292
+ ---
293
+ #{metadata_yaml.split("---\n").last.strip}
294
+ ---
295
+
296
+ This notebook demonstrates how to weave executable analysis into your narrative.
297
+
298
+ ```#{language}
299
+ print("Hello, DataLog!")
300
+ ```
301
+ MARKDOWN
302
+
303
+ say_status :create, relative_to_root(markdown_path), :green
304
+ say_status :create, relative_to_root(ipynb_path), :green
305
+ end
306
+
307
+ desc "project", "Scaffold a portfolio project entry"
308
+ method_option :title, type: :string, aliases: "-t", desc: "Title for the project"
309
+ def project
310
+ site_root
311
+ title = options[:title] || ask("Title?")
312
+ slug = slugify(title)
313
+ summary = ask_with_default("Summary", "Explain the project's impact and results.")
314
+ role = ask_with_default("Role", "Lead Analyst")
315
+ client = ask("Client or stakeholder? (optional)").strip
316
+ timeline = ask_with_default("Timeline", "#{Date.today.strftime('%b %Y')} – Present")
317
+ tags = ask("Tags (comma separated)?").split(",").map(&:strip).reject(&:empty?)
318
+
319
+ portfolio_dir = ensure_directory!("_portfolio")
320
+ path = File.join(portfolio_dir, "#{slug}.md")
321
+
322
+ metadata = {
323
+ "layout" => "portfolio",
324
+ "title" => title,
325
+ "summary" => summary,
326
+ "role" => role,
327
+ "timeline" => timeline,
328
+ "tags" => tags,
329
+ "links" => [
330
+ { "label" => "Repository", "url" => "https://github.com/#{ENV.fetch('GITHUB_USER', 'DiogoRibeiro7')}/#{slug}" }
331
+ ]
332
+ }
333
+ metadata["client"] = client unless client.empty?
334
+
335
+ create_file(path, <<~MARKDOWN)
336
+ ---
337
+ #{metadata.to_yaml.split("---\n").last.strip}
338
+ ---
339
+
340
+ ## Problem
341
+
342
+ Describe the problem space, constraints, and success criteria.
343
+
344
+ ## Approach
345
+
346
+ Outline the methodology, tooling, and collaboration model you used.
347
+
348
+ ## Outcomes
349
+
350
+ Highlight measurable impact, visuals, and lessons learned.
351
+ MARKDOWN
352
+
353
+ say_status :create, relative_to_root(path), :green
354
+ end
355
+
356
+ private
357
+
358
+ def parse_date(input)
359
+ Date.parse(input)
360
+ rescue ArgumentError
361
+ say_status :warn, "Invalid date provided. Using today's date.", :yellow
362
+ Date.today
363
+ end
364
+
365
+ def blank_notebook(language)
366
+ {
367
+ "cells" => [
368
+ {
369
+ "cell_type" => "markdown",
370
+ "metadata" => {},
371
+ "source" => ["# #{options[:title] || 'New Notebook'}\n", "Describe the objective of this analysis."]
372
+ },
373
+ {
374
+ "cell_type" => "code",
375
+ "execution_count" => nil,
376
+ "metadata" => {},
377
+ "outputs" => [],
378
+ "source" => [example_snippet(language)]
379
+ }
380
+ ],
381
+ "metadata" => {
382
+ "kernelspec" => {
383
+ "display_name" => language.capitalize,
384
+ "language" => language,
385
+ "name" => language
386
+ },
387
+ "language_info" => {
388
+ "name" => language
389
+ }
390
+ },
391
+ "nbformat" => 4,
392
+ "nbformat_minor" => 5
393
+ }
394
+ end
395
+
396
+ def example_snippet(language)
397
+ case language
398
+ when "python"
399
+ "print('Hello, DataLog!')\n"
400
+ when "r"
401
+ "print('Hello, DataLog!')\n"
402
+ when "julia"
403
+ "println('Hello, DataLog!')\n"
404
+ else
405
+ "# Add your #{language} code here\n"
406
+ end
407
+ end
408
+
409
+ def relative_to_root(path)
410
+ Pathname.new(path).relative_path_from(Pathname.new(site_root)).to_s
411
+ end
412
+ end
413
+
414
+ register(New, "new", "new COMMAND", "Scaffold posts, notebooks, and portfolio projects")
415
+
416
+ private
417
+
418
+ def site_root
419
+ File.expand_path(options[:root])
420
+ end
421
+
422
+ def ensure_inside_git_repository!
423
+ return if command_available?("git") && system("git", "rev-parse", "--is-inside-work-tree", out: File::NULL,
424
+ err: File::NULL, chdir: site_root)
425
+
426
+ say_error "The publish command must be run inside a git repository."
427
+ exit 1
428
+ end
429
+
430
+ def ruby_supported?
431
+ Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("3.0.0")
432
+ end
433
+
434
+ def gemfile_path(root)
435
+ File.join(root, "Gemfile")
436
+ end
437
+
438
+ def gem_available?(root, gem_name)
439
+ gemfile = gemfile_path(root)
440
+ return false unless File.exist?(gemfile)
441
+
442
+ lockfile = File.join(root, "Gemfile.lock")
443
+ return false unless File.exist?(lockfile)
444
+
445
+ locked = Bundler::LockfileParser.new(Bundler.read_file(lockfile))
446
+ locked.specs.any? { |spec| spec.name == gem_name }
447
+ rescue Bundler::BundlerError
448
+ false
449
+ end
450
+
451
+ def command_available?(command)
452
+ system("command", "-v", command, out: File::NULL, err: File::NULL)
453
+ end
454
+
455
+ def config_valid?(root)
456
+ config_path = File.join(root, "_config.yml")
457
+ return false unless File.exist?(config_path)
458
+
459
+ config = load_config(config_path)
460
+ config["theme"] == "datalog" || Array(config["plugins"]).include?("datalog-theme")
461
+ rescue Psych::SyntaxError
462
+ false
463
+ end
464
+
465
+ def configuration_warnings(root)
466
+ config_path = File.join(root, "_config.yml")
467
+ return [] unless File.exist?(config_path)
468
+
469
+ config = load_config(config_path)
470
+ warnings = []
471
+ warnings << :collections unless config.key?("collections")
472
+ warnings << :url unless config.key?("url")
473
+ warnings
474
+ rescue Psych::SyntaxError
475
+ []
476
+ end
477
+
478
+ def load_config(path)
479
+ YAML.safe_load_file(path, permitted_classes: [Date, Time]) || {}
480
+ end
481
+
482
+ def summarize_checks(critical, warnings)
483
+ if critical.empty?
484
+ say_status :ok, "All critical checks passed", :green
485
+ else
486
+ critical.each do |item|
487
+ say_status :fail, "Missing required dependency: #{item}", :red
488
+ end
489
+ end
490
+
491
+ warnings.each do |item|
492
+ message = case item
493
+ when :collections
494
+ "Optional configuration missing: define collections in _config.yml to enable datasets and portfolio content"
495
+ when :url
496
+ "Set site url in _config.yml for accurate SEO metadata"
497
+ else
498
+ "Optional tooling not available: #{item}"
499
+ end
500
+ say_status :warn, message, :yellow
501
+ end
502
+ end
503
+
504
+ def remote_branch?(root, branch)
505
+ system("git", "ls-remote", "--exit-code", "origin", "refs/heads/#{branch}", out: File::NULL, err: File::NULL,
506
+ chdir: root)
507
+ end
508
+
509
+ def prepare_worktree(root, branch, worktree_path)
510
+ unless system("git", "show-ref", "--verify", "--quiet", "refs/heads/#{branch}", chdir: root)
511
+ say_status :info, "Creating #{branch} branch", :blue
512
+ if remote_branch?(root, branch)
513
+ system("git", "branch", branch, "origin/#{branch}", chdir: root)
514
+ else
515
+ system("git", "branch", branch, chdir: root)
516
+ end
517
+ end
518
+
519
+ say_status :git, "git worktree add --force #{worktree_path} #{branch}", :blue
520
+ return if system("git", "worktree", "add", "--force", worktree_path, branch, chdir: root)
521
+
522
+ say_error "Unable to create git worktree for #{branch}."
523
+ exit 1
524
+ end
525
+
526
+ def copy_site_output(root, worktree_path)
527
+ say_status :sync, "Copying _site to #{worktree_path}", :blue
528
+ FileUtils.rm_rf(Dir.glob(File.join(worktree_path, "*"),
529
+ File::FNM_DOTMATCH) - [File.join(worktree_path, "."), File.join(worktree_path, ".."),
530
+ File.join(worktree_path, ".git")])
531
+ Dir.glob(File.join(root, "_site", "*"), File::FNM_DOTMATCH).each do |entry|
532
+ next if [".", ".."].include?(File.basename(entry))
533
+
534
+ FileUtils.cp_r(entry, worktree_path, preserve: true)
535
+ end
536
+ end
537
+
538
+ def commit_and_push(worktree_path, branch, message)
539
+ Dir.chdir(worktree_path) do
540
+ system("git", "add", "--all")
541
+ if system("git", "diff", "--cached", "--quiet")
542
+ say_status :skip, "No changes to publish", :yellow
543
+ return
544
+ end
545
+
546
+ system("git", "commit", "-m", message)
547
+ say_status :git, "git push origin #{branch}", :blue
548
+ system("git", "push", "origin", branch)
549
+ end
550
+ end
551
+
552
+ def cleanup_worktree(root, worktree_path)
553
+ say_status :git, "git worktree remove --force #{worktree_path}", :blue
554
+ system("git", "worktree", "remove", "--force", worktree_path, chdir: root)
555
+ ensure
556
+ FileUtils.rm_rf(File.dirname(worktree_path)) if worktree_path && File.directory?(File.dirname(worktree_path))
557
+ end
558
+
559
+ def say_error(message)
560
+ say message, :red
561
+ end
562
+ end
563
+ end