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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +162 -0
- data/CITATION.cff +29 -0
- data/LICENSE +21 -0
- data/README.md +257 -0
- data/_data/academic.yml +217 -0
- data/_data/cdn-integrity.yml +51 -0
- data/_data/config/author.yml +121 -0
- data/_data/config/features.yml +262 -0
- data/_data/config/site.yml +42 -0
- data/_data/config/theme.yml +181 -0
- data/_data/datasets.yml +28 -0
- data/_data/i18n/en.yml +438 -0
- data/_data/i18n/es.yml +438 -0
- data/_data/i18n/pt.yml +438 -0
- data/_data/js_manifest.json +22 -0
- data/_data/js_meta.json +371 -0
- data/_data/navigation.yml +145 -0
- data/_data/projects.yml +41 -0
- data/_data/publications.yml +28 -0
- data/_data/social.yml +73 -0
- data/_data/visualizations.yml +51 -0
- data/_includes/analytics/dashboard.html +149 -0
- data/_includes/analytics.html +9 -0
- data/_includes/components/academic-dashboard.html +347 -0
- data/_includes/components/advanced-search.html +682 -0
- data/_includes/components/api-function.html +94 -0
- data/_includes/components/author-bio.html +166 -0
- data/_includes/components/bookmark-system.html +96 -0
- data/_includes/components/breadcrumbs.html +89 -0
- data/_includes/components/citation-tools.html +94 -0
- data/_includes/components/comments.html +244 -0
- data/_includes/components/content-provenance.html +43 -0
- data/_includes/components/content-recommendations.html +228 -0
- data/_includes/components/difficulty-badge.html +84 -0
- data/_includes/components/email-preferences.html +200 -0
- data/_includes/components/enhanced-code-block.html +212 -0
- data/_includes/components/enhanced-metadata.html +228 -0
- data/_includes/components/enhanced-toc.html +209 -0
- data/_includes/components/hero.html +15 -0
- data/_includes/components/language-switcher.html +396 -0
- data/_includes/components/math-fallback.html +19 -0
- data/_includes/components/navigation-enhancements.html +454 -0
- data/_includes/components/newsletter-signup.html +178 -0
- data/_includes/components/open-science-badges.html +16 -0
- data/_includes/components/package-install.html +194 -0
- data/_includes/components/performance-monitor.html +170 -0
- data/_includes/components/popular-posts.html +233 -0
- data/_includes/components/post-hero.html +62 -0
- data/_includes/components/reading-progress.html +133 -0
- data/_includes/components/reading-time.html +121 -0
- data/_includes/components/responsive-image.html +63 -0
- data/_includes/components/search-facets.html +307 -0
- data/_includes/components/series-navigation.html +124 -0
- data/_includes/components/social-proof.html +34 -0
- data/_includes/components/social-share.html +119 -0
- data/_includes/components/user-preferences.html +566 -0
- data/_includes/components/visualization-card.html +24 -0
- data/_includes/components/viz-table-fallback.html +19 -0
- data/_includes/critical-css/default.html +0 -0
- data/_includes/critical-css/home.html +0 -0
- data/_includes/critical-css/post.html +0 -0
- data/_includes/csp-meta.html +17 -0
- data/_includes/footer/nav-column.html +31 -0
- data/_includes/footer.html +76 -0
- data/_includes/head.html +337 -0
- data/_includes/header/navigation.html +48 -0
- data/_includes/header.html +70 -0
- data/_includes/helpers/array-to-sentence.html +21 -0
- data/_includes/helpers/date-format.html +16 -0
- data/_includes/helpers/link-with-icon.html +26 -0
- data/_includes/layouts/default/article.html +67 -0
- data/_includes/meta/language-attributes.html +25 -0
- data/_includes/meta/math-config.html +21 -0
- data/_includes/meta/schema.html +153 -0
- data/_includes/meta/scripts-loader.html +52 -0
- data/_includes/post/related-posts.html +63 -0
- data/_includes/scripts.html +72 -0
- data/_includes/skip-link.html +1 -0
- data/_includes/toc.html +2 -0
- data/_layouts/archive.html +282 -0
- data/_layouts/dataset.html +52 -0
- data/_layouts/default.html +33 -0
- data/_layouts/home.html +48 -0
- data/_layouts/notebook.html +173 -0
- data/_layouts/package.html +238 -0
- data/_layouts/page.html +16 -0
- data/_layouts/portfolio.html +41 -0
- data/_layouts/post-sidebar.html +183 -0
- data/_layouts/post.html +338 -0
- data/_layouts/project.html +252 -0
- data/_layouts/research.html +504 -0
- data/_plugins/analytics_dashboard.rb +315 -0
- data/_plugins/config_validator.rb +397 -0
- data/_plugins/csp_generator.rb +115 -0
- data/_plugins/datalog_bibliography.rb +19 -0
- data/_plugins/datalog_comments.rb +18 -0
- data/_plugins/datalog_slides.rb +20 -0
- data/_plugins/front_matter_compat.rb +109 -0
- data/_plugins/i18n.rb +160 -0
- data/_plugins/image_optimizer.rb +445 -0
- data/_plugins/math_preprocessor.rb +185 -0
- data/_plugins/notebook_converter.rb +1107 -0
- data/_plugins/plugin_loader.rb +185 -0
- data/_plugins/publications_generator.rb +234 -0
- data/_plugins/reading_time.rb +15 -0
- data/_plugins/search_normalizer.rb +74 -0
- data/_plugins/sri_filter.rb +18 -0
- data/_plugins/toc_filter.rb +68 -0
- data/_plugins/warning_filter.rb +3 -0
- data/_sass/_base.scss +142 -0
- data/_sass/_components.scss +2380 -0
- data/_sass/_header.scss +470 -0
- data/_sass/_interactive.scss +344 -0
- data/_sass/_layout.scss +1760 -0
- data/_sass/_mathematical.scss +368 -0
- data/_sass/_mixins.scss +99 -0
- data/_sass/_package-docs.scss +715 -0
- data/_sass/_phase1-enhancements.scss +721 -0
- data/_sass/_phase3-enhancements.scss +874 -0
- data/_sass/_phase4-enhancements.scss +1214 -0
- data/_sass/_phase5-enhancements.scss +414 -0
- data/_sass/_search.scss +654 -0
- data/_sass/_syntax-highlighting.scss +128 -0
- data/_sass/_theme.scss +79 -0
- data/_sass/_typography.scss +148 -0
- data/_sass/_utilities.scss +334 -0
- data/_sass/_variables.scss +153 -0
- data/_sass/_visualizations.scss +242 -0
- data/assets/css/main.scss +4 -0
- data/assets/img/20220607123041_detail.001.png +0 -0
- data/assets/img/favicons/android-chrome-192x192.png +0 -0
- data/assets/img/favicons/android-chrome-512x512.png +0 -0
- data/assets/img/favicons/apple-touch-icon.png +0 -0
- data/assets/img/favicons/favicon-16x16.png +0 -0
- data/assets/img/favicons/favicon-32x32.png +0 -0
- data/assets/img/favicons/favicon.ico +0 -0
- data/assets/img/favicons/site.webmanifest +21 -0
- data/assets/img/portfolio-placeholder.svg +20 -0
- data/assets/js/academic.js +262 -0
- data/assets/js/analytics-dashboard.js +382 -0
- data/assets/js/core/dark-mode.js +79 -0
- data/assets/js/core/github-cards.js +123 -0
- data/assets/js/core/language-filter.js +69 -0
- data/assets/js/core/navigation.js +184 -0
- data/assets/js/core/scroll-progress.js +45 -0
- data/assets/js/core/search-hotkeys.js +62 -0
- data/assets/js/core/skip-links.js +62 -0
- data/assets/js/loader.js +163 -0
- data/assets/js/main.js +23 -0
- data/assets/js/math.js +818 -0
- data/assets/js/notebook.js +158 -0
- data/assets/js/search/analytics.js +91 -0
- data/assets/js/search/app.js +271 -0
- data/assets/js/search/autocomplete.js +120 -0
- data/assets/js/search/engine.js +260 -0
- data/assets/js/search/filters.js +38 -0
- data/assets/js/search/render.js +217 -0
- data/assets/js/search/utils.js +99 -0
- data/assets/js/search.js +354 -0
- data/assets/js/visualizations.js +816 -0
- data/assets/publications/datalog-publications.bib +8 -0
- data/assets/publications/datalog-publications.ris +9 -0
- data/assets/publications/publications.bib +30 -0
- data/assets/templates/diogo-ribeiro-cv.md +31 -0
- data/assets/templates/diogo-ribeiro-cv.tex +32 -0
- data/bin/datalog +26 -0
- data/datalog-theme.gemspec +72 -0
- data/lib/datalog/cli.rb +563 -0
- data/lib/datalog/plugin_system/dependency_resolver.rb +223 -0
- data/lib/datalog/plugin_system.rb +237 -0
- data/lib/datalog/plugins/citations.rb +182 -0
- data/lib/datalog/plugins/comments.rb +183 -0
- data/lib/datalog/plugins/search.rb +45 -0
- data/lib/datalog/plugins/slides.rb +121 -0
- data/lib/datalog/theme/theme.rb +18 -0
- data/lib/datalog/theme/version.rb +7 -0
- data/lib/datalog/warning_filter.rb +28 -0
- data/lib/datalog-theme.rb +17 -0
- metadata +555 -0
data/lib/datalog/cli.rb
ADDED
|
@@ -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
|