ratatui_ruby-devtools 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -1
- data/README.md +17 -0
- data/lib/ratatui_ruby/devtools/site_tasks/build.rake +184 -0
- data/lib/ratatui_ruby/devtools/site_tasks/deploy.rake +18 -0
- data/lib/ratatui_ruby/devtools/site_tasks/serve.rake +14 -0
- data/lib/ratatui_ruby/devtools/tasks/resources/index.html.erb +129 -0
- data/lib/ratatui_ruby/devtools/tasks/website/index_page.rb +54 -0
- data/lib/ratatui_ruby/devtools/tasks/website/version.rb +173 -0
- data/lib/ratatui_ruby/devtools/tasks/website/version_menu.rb +85 -0
- data/lib/ratatui_ruby/devtools/tasks/website/versioned_documentation.rb +106 -0
- data/lib/ratatui_ruby/devtools/tasks/website/website.rb +78 -0
- data/lib/ratatui_ruby/devtools/tasks/website.rake +37 -0
- data/lib/ratatui_ruby/devtools/version.rb +1 -1
- data/lib/ratatui_ruby/devtools.rb +36 -7
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 13d0c21c772a1be88c23b0345200140bc7d8befcbaf1c20d25a88230847188be
|
|
4
|
+
data.tar.gz: 54de2f06a6849dbe99f182967834c5f3ca6a24963fc98ee929cd5fc59529fbb2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28ce8dacd601f0fc068c34665d5e455523ff78fb5d43db7601341304d35c825d21fdb528ff1af09db13e35137894a3d9c0742f45b728672dce44e6da084f359f
|
|
7
|
+
data.tar.gz: 8dbf94f55d3259e14735e7b55bb57d3d4ad57d7d0423699a012b73ce820393622220a15f9fab02eb0fea47fb1db2d3a290ceda2a51cce5a696aab6bd5e7ca9b5
|
data/CHANGELOG.md
CHANGED
|
@@ -15,9 +15,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
15
|
|
|
16
16
|
### Added
|
|
17
17
|
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
### Removed
|
|
23
|
+
|
|
24
|
+
## [0.1.1] - 2026-01-26
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Multi-version documentation website builder: `install_website_tasks!` generates
|
|
29
|
+
documentation portals with version dropdown menus. Discovers versions from git
|
|
30
|
+
tags, builds RDoc for each in isolation, and creates a landing page with
|
|
31
|
+
branding support.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
39
|
+
## [0.1.0] - 2026-01-08
|
|
40
|
+
|
|
41
|
+
### Added
|
|
42
|
+
|
|
18
43
|
- Initial release of `ratatui_ruby-devtools`
|
|
19
44
|
- Rake tasks: `lint`, `lint:fix`, `reuse`, `license`, `bump`
|
|
20
45
|
- Executables: `agent_rake`, `consolidate_md`, `hbs`, `announce`
|
|
21
46
|
- Templates for scaffolding new ecosystem gems
|
|
22
47
|
- Auto-discovery of gem configuration from `*.gemspec`
|
|
23
|
-
- Conditional Cargo/Rust task support for gems with native extensions
|
|
48
|
+
- Conditional Cargo/Rust task support for gems with native extensions
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
### Removed
|
|
57
|
+
|
|
58
|
+
[Unreleased]: https://git.sr.ht/~kerrick/ratatui_ruby-devtools/refs/HEAD
|
|
59
|
+
[0.1.1]: https://git.sr.ht/~kerrick/ratatui_ruby-devtools/refs/v0.1.1
|
|
60
|
+
[0.1.0]: https://git.sr.ht/~kerrick/ratatui_ruby-devtools/refs/v0.1.0
|
data/README.md
CHANGED
|
@@ -17,6 +17,23 @@ Mailing List: Development](https://img.shields.io/badge/mailing_list-development
|
|
|
17
17
|
|
|
18
18
|
This is a **non-runtime** gem. It provides build tooling—not application features. Add it to your `:development` group only.
|
|
19
19
|
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Quick Links
|
|
23
|
+
|
|
24
|
+
### The Ecosystem
|
|
25
|
+
|
|
26
|
+
**RatatuiRuby:** [Core engine](https://git.sr.ht/~kerrick/ratatui_ruby) • **Tea:** [MVU architecture](https://git.sr.ht/~kerrick/ratatui_ruby-tea) • **Kit:** [Component architecture](https://git.sr.ht/~kerrick/ratatui_ruby-kit) (Planned) • **DSL:** [Glimmer syntax](https://sr.ht/~kerrick/ratatui_ruby/#chapter-4-the-syntax) (Planned) • **Framework:** [Omakase framework](https://git.sr.ht/~kerrick/ratatui_ruby-framework) (Planned) • **UI:** [Polished widgets](https://git.sr.ht/~kerrick/ratatui_ruby-ui) (Planned) • **UI Pro:** [More polished widgets](https://sr.ht/~kerrick/ratatui_ruby#chapter-6-licensing) (Planned)
|
|
27
|
+
|
|
28
|
+
### For App Developers
|
|
29
|
+
|
|
30
|
+
**Get Started:** [Quickstart](https://git.sr.ht/~kerrick/ratatui_ruby/tree/stable/item/doc/getting_started/quickstart.md) • [Examples](https://git.sr.ht/~kerrick/ratatui_ruby/tree/stable/item/examples) ⸺ **Stay Informed:** [Announce List](https://lists.sr.ht/~kerrick/ratatui_ruby-announce) • [FAQ](https://man.sr.ht/~kerrick/ratatui_ruby/troubleshooting.md) ⸺ **Reach Out:** [Discuss List](https://lists.sr.ht/~kerrick/ratatui_ruby-discuss) • [Bug Tracker](https://todo.sr.ht/~kerrick/ratatui_ruby)
|
|
31
|
+
|
|
32
|
+
### For Contributors
|
|
33
|
+
|
|
34
|
+
**Get Started:** [Contributing Guide](https://man.sr.ht/~kerrick/ratatui_ruby/contributing.md) • [Code of Conduct](https://man.sr.ht/~kerrick/ratatui_ruby/code_of_conduct.md) ⸺ **Stay Informed:** [Announce List](https://lists.sr.ht/~kerrick/ratatui_ruby-announce) • [Project History](https://man.sr.ht/~kerrick/ratatui_ruby/history/index.md) ⸺ **Reach Out:** [Development List](https://lists.sr.ht/~kerrick/ratatui_ruby-devel) • [Bug Tracker](https://todo.sr.ht/~kerrick/ratatui_ruby)
|
|
35
|
+
|
|
36
|
+
---
|
|
20
37
|
|
|
21
38
|
## Installation
|
|
22
39
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "fileutils"
|
|
9
|
+
require "digest/md5"
|
|
10
|
+
|
|
11
|
+
# Website build tasks for ecosystem documentation sites.
|
|
12
|
+
#
|
|
13
|
+
# These tasks sync documentation from a source gem repository, update
|
|
14
|
+
# HTML metadata, and manage deployments.
|
|
15
|
+
|
|
16
|
+
SOURCE_GEM_DIR = RatatuiRuby::Devtools.source_gem_dir
|
|
17
|
+
PUBLIC_DOCS_DIR = File.expand_path("public/docs", Dir.pwd)
|
|
18
|
+
INDEX_HTML = File.expand_path("public/index.html", Dir.pwd)
|
|
19
|
+
BUILT_SHA_FILE = File.join(PUBLIC_DOCS_DIR, ".built_sha")
|
|
20
|
+
|
|
21
|
+
# Shared helper to load version metadata from source gem
|
|
22
|
+
def load_source_gem_metadata
|
|
23
|
+
# Find the version.rb file dynamically
|
|
24
|
+
version_files = Dir.glob(File.join(SOURCE_GEM_DIR, "lib/**/version.rb"))
|
|
25
|
+
raise "No version.rb found in #{SOURCE_GEM_DIR}" if version_files.empty?
|
|
26
|
+
|
|
27
|
+
version_file = version_files.first
|
|
28
|
+
load version_file
|
|
29
|
+
|
|
30
|
+
# Find the module that defines VERSION
|
|
31
|
+
# Convention: the namespace matches the gem name
|
|
32
|
+
gemspec_files = Dir.glob(File.join(SOURCE_GEM_DIR, "*.gemspec"))
|
|
33
|
+
raise "No gemspec found in #{SOURCE_GEM_DIR}" if gemspec_files.empty?
|
|
34
|
+
|
|
35
|
+
gem_name = File.basename(gemspec_files.first, ".gemspec")
|
|
36
|
+
|
|
37
|
+
# Convert gem_name to module name (e.g., ratatui_ruby -> RatatuiRuby)
|
|
38
|
+
module_name = gem_name.split("_").map(&:capitalize).join
|
|
39
|
+
.split("-").map(&:capitalize).join("::")
|
|
40
|
+
|
|
41
|
+
full_version = Object.const_get(module_name)::VERSION
|
|
42
|
+
raise "Could not load VERSION from #{version_file}" unless full_version
|
|
43
|
+
|
|
44
|
+
# Compute docs version (major.minor only) using Gem::Version
|
|
45
|
+
segments = Gem::Version.new(full_version).segments
|
|
46
|
+
docs_version = segments.first(2).join(".")
|
|
47
|
+
|
|
48
|
+
# Read summary from gemspec
|
|
49
|
+
gemspec_file = gemspec_files.first
|
|
50
|
+
gemspec_content = File.read(gemspec_file)
|
|
51
|
+
summary = gemspec_content[/spec\.summary\s*=\s*"([^"]+)"/, 1]
|
|
52
|
+
raise "Could not parse summary from #{gemspec_file}" unless summary
|
|
53
|
+
|
|
54
|
+
# Get datePublished from git tag's tagger date
|
|
55
|
+
tag_date = Dir.chdir(SOURCE_GEM_DIR) do
|
|
56
|
+
`git tag -l --format='%(creatordate:short)' v#{full_version} 2>/dev/null`.strip
|
|
57
|
+
end
|
|
58
|
+
tag_date = nil if tag_date.empty?
|
|
59
|
+
|
|
60
|
+
# Get copyrightYear from this version's release (each version is its own work)
|
|
61
|
+
copyright_year = tag_date&.split("-")&.first
|
|
62
|
+
|
|
63
|
+
{ full_version:, docs_version:, summary:, tag_date:, copyright_year:, gem_name: }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def current_source_sha
|
|
67
|
+
Dir.chdir(SOURCE_GEM_DIR) { `git rev-parse trunk`.strip }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def docs_content_hash
|
|
71
|
+
return nil unless File.directory?(PUBLIC_DOCS_DIR)
|
|
72
|
+
|
|
73
|
+
trunk_dir = File.join(PUBLIC_DOCS_DIR, "trunk")
|
|
74
|
+
return nil unless File.directory?(trunk_dir)
|
|
75
|
+
|
|
76
|
+
digest = Digest::MD5.new
|
|
77
|
+
Dir.glob("#{PUBLIC_DOCS_DIR}/**/*", File::FNM_DOTMATCH).sort.each do |path|
|
|
78
|
+
next if File.directory?(path)
|
|
79
|
+
next if path == BUILT_SHA_FILE
|
|
80
|
+
digest.update(path)
|
|
81
|
+
digest.update(File.binread(path))
|
|
82
|
+
end
|
|
83
|
+
digest.hexdigest
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def docs_up_to_date?
|
|
87
|
+
return false unless File.exist?(BUILT_SHA_FILE)
|
|
88
|
+
|
|
89
|
+
stored = File.read(BUILT_SHA_FILE).strip.split("\n")
|
|
90
|
+
stored_sha = stored[0]
|
|
91
|
+
stored_hash = stored[1]
|
|
92
|
+
|
|
93
|
+
return false unless stored_sha == current_source_sha
|
|
94
|
+
return false unless stored_hash == docs_content_hash
|
|
95
|
+
|
|
96
|
+
true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def write_build_stamp
|
|
100
|
+
File.write(BUILT_SHA_FILE, "#{current_source_sha}\n#{docs_content_hash}")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
namespace :build do
|
|
104
|
+
desc "Build RDoc from source gem and copy to public/docs/ (FORCE=1 to rebuild)"
|
|
105
|
+
task :docs do
|
|
106
|
+
if docs_up_to_date? && !ENV["FORCE"]
|
|
107
|
+
puts "Docs already built for #{current_source_sha[0, 8]}, skipping (use FORCE=1 to rebuild)"
|
|
108
|
+
next
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
Bundler.with_unbundled_env do
|
|
112
|
+
Dir.chdir(SOURCE_GEM_DIR) do
|
|
113
|
+
sh "bundle install --quiet"
|
|
114
|
+
sh "bundle exec rake website:build"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
www_source = File.join(SOURCE_GEM_DIR, "www")
|
|
119
|
+
|
|
120
|
+
FileUtils.rm_rf(PUBLIC_DOCS_DIR)
|
|
121
|
+
FileUtils.mkdir_p(PUBLIC_DOCS_DIR)
|
|
122
|
+
|
|
123
|
+
# Copy contents, not the directory itself, to avoid cp_r's quirky behavior
|
|
124
|
+
Dir.glob("#{www_source}/*", File::FNM_DOTMATCH).each do |entry|
|
|
125
|
+
next if entry.end_with?("/.", "/..")
|
|
126
|
+
FileUtils.cp_r(entry, PUBLIC_DOCS_DIR)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Failsafe: if files ended up in public/docs/www/, move them up
|
|
130
|
+
nested_www = File.join(PUBLIC_DOCS_DIR, "www")
|
|
131
|
+
if File.directory?(nested_www)
|
|
132
|
+
warn "WARNING: Detected nested www/ directory, moving contents up..."
|
|
133
|
+
Dir.glob("#{nested_www}/*", File::FNM_DOTMATCH).each do |entry|
|
|
134
|
+
next if entry.end_with?("/.", "/..")
|
|
135
|
+
FileUtils.mv(entry, PUBLIC_DOCS_DIR)
|
|
136
|
+
end
|
|
137
|
+
FileUtils.rmdir(nested_www)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Write SHA + content hash after successful build
|
|
141
|
+
write_build_stamp
|
|
142
|
+
|
|
143
|
+
puts "Copied docs to #{PUBLIC_DOCS_DIR} (#{current_source_sha[0, 8]})"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
desc "Update index.html with version metadata from source gem"
|
|
147
|
+
task :html do
|
|
148
|
+
meta = load_source_gem_metadata
|
|
149
|
+
|
|
150
|
+
html = File.read(INDEX_HTML)
|
|
151
|
+
|
|
152
|
+
if html =~ /"softwareVersion":\s*"([^"]+)"/
|
|
153
|
+
old_version = $1
|
|
154
|
+
old_docs = old_version.split(".").first(2).join(".")
|
|
155
|
+
|
|
156
|
+
# Plain string replacement for all occurrences
|
|
157
|
+
# Bare version catches both "v1.1.0" and "gem_name-1.1.0.gem"
|
|
158
|
+
html.gsub!(old_version, meta[:full_version])
|
|
159
|
+
html.gsub!("v#{old_docs}/", "v#{meta[:docs_version]}/")
|
|
160
|
+
|
|
161
|
+
# Update description from gemspec summary
|
|
162
|
+
html.gsub!(/"description":\s*"[^"]+"/, %{"description": "#{meta[:summary]}"})
|
|
163
|
+
|
|
164
|
+
# Update datePublished from git tag date
|
|
165
|
+
if meta[:tag_date]
|
|
166
|
+
html.gsub!(/"datePublished":\s*"[^"]+"/, %{"datePublished": "#{meta[:tag_date]}"})
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Update copyrightYear from first release
|
|
170
|
+
if meta[:copyright_year]
|
|
171
|
+
html.gsub!(/"copyrightYear":\s*\d+/, %{"copyrightYear": #{meta[:copyright_year]}})
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
File.write(INDEX_HTML, html)
|
|
175
|
+
else
|
|
176
|
+
warn "WARNING: No softwareVersion found in index.html JSON-LD, skipping version update"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
puts "Updated index.html to version v#{meta[:full_version]} (docs: v#{meta[:docs_version]})"
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
desc "Build docs and update index.html"
|
|
184
|
+
task build: ["build:docs", "build:html"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
# Uses PUBLIC_DOCS_DIR from build.rake
|
|
9
|
+
|
|
10
|
+
desc "Deploy to production (builds first, validates docs)"
|
|
11
|
+
task deploy: :build do
|
|
12
|
+
trunk_dir = File.join(PUBLIC_DOCS_DIR, "trunk")
|
|
13
|
+
unless File.directory?(trunk_dir)
|
|
14
|
+
abort "ERROR: #{trunk_dir} does not exist. Run `rake build:docs` first."
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sh "kamal deploy"
|
|
18
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
desc "Serve public/ locally on http://localhost:8000"
|
|
9
|
+
task :serve do
|
|
10
|
+
require "webrick"
|
|
11
|
+
server = WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: File.expand_path("public", Dir.pwd))
|
|
12
|
+
trap("INT") { server.shutdown }
|
|
13
|
+
server.start
|
|
14
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
-->
|
|
6
|
+
<%
|
|
7
|
+
# Branding configuration with sensible defaults
|
|
8
|
+
logo_small = branding[:logo_small] || "../logo-small.png"
|
|
9
|
+
wordmark = branding[:wordmark] || project_name
|
|
10
|
+
tagline = branding[:tagline] || "API documentation and guides"
|
|
11
|
+
|
|
12
|
+
# Navigation links (array of {text:, href:} hashes)
|
|
13
|
+
nav_links = branding[:nav_links] || []
|
|
14
|
+
|
|
15
|
+
# Footer columns (array of {heading:, links: [{text:, href:, tag:, tag_muted:}]} hashes)
|
|
16
|
+
footer_columns = branding[:footer_columns] || []
|
|
17
|
+
|
|
18
|
+
# Footer bottom bar
|
|
19
|
+
footer_meta_links = branding[:footer_meta_links] || []
|
|
20
|
+
copyright = branding[:copyright] || "© #{Time.now.year}"
|
|
21
|
+
license_text = branding[:license_text] || ""
|
|
22
|
+
|
|
23
|
+
# CSS/font customization
|
|
24
|
+
stylesheet = branding[:stylesheet] || "../styles.css"
|
|
25
|
+
google_fonts_url = branding[:google_fonts_url]
|
|
26
|
+
|
|
27
|
+
# Find latest version for default links
|
|
28
|
+
latest = versions.find { |v| v.respond_to?(:latest?) && v.latest? }
|
|
29
|
+
latest_slug = latest&.slug || "trunk"
|
|
30
|
+
%>
|
|
31
|
+
<!DOCTYPE html>
|
|
32
|
+
<html lang="en">
|
|
33
|
+
<head>
|
|
34
|
+
<meta charset="UTF-8">
|
|
35
|
+
<title><%= project_name %> documentation</title>
|
|
36
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
37
|
+
<meta name="description" content="<%= tagline %> for <%= project_name %>, organized by version.">
|
|
38
|
+
<link rel="stylesheet" href="<%= stylesheet %>">
|
|
39
|
+
<% if google_fonts_url %>
|
|
40
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
41
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
42
|
+
<link href="<%= google_fonts_url %>" rel="stylesheet">
|
|
43
|
+
<% end %>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<header class="header">
|
|
47
|
+
<div class="header__inner">
|
|
48
|
+
<a href="/" class="header__brand">
|
|
49
|
+
<img src="<%= logo_small %>" alt="<%= project_name %> Logo" class="header__logo" width="48" height="48">
|
|
50
|
+
<span class="header__wordmark"><%= wordmark %></span>
|
|
51
|
+
</a>
|
|
52
|
+
<% unless nav_links.empty? %>
|
|
53
|
+
<nav class="header__nav" aria-label="Primary">
|
|
54
|
+
<% nav_links.each do |link| %>
|
|
55
|
+
<a href="<%= link[:href] %>"<%= ' class="header__nav-external"' if link[:external] %>><%= link[:text] %></a>
|
|
56
|
+
<% end %>
|
|
57
|
+
</nav>
|
|
58
|
+
<% end %>
|
|
59
|
+
</div>
|
|
60
|
+
</header>
|
|
61
|
+
|
|
62
|
+
<main>
|
|
63
|
+
<section class="section">
|
|
64
|
+
<div class="section__inner">
|
|
65
|
+
<header class="section__header">
|
|
66
|
+
<span class="section__eyebrow">Reference</span>
|
|
67
|
+
<h1 class="section__title">Documentation</h1>
|
|
68
|
+
<p class="section__intro"><%= tagline %> for current and past versions of <%= project_name %>.</p>
|
|
69
|
+
</header>
|
|
70
|
+
|
|
71
|
+
<ul class="version-list">
|
|
72
|
+
<% versions.each do |version| %>
|
|
73
|
+
<li class="version-list__item<%= ' version-list__item--latest' if version.latest? %>">
|
|
74
|
+
<a href="<%= version.slug %>/index.html" class="version-list__link">
|
|
75
|
+
<span class="version-list__name"><%= version.name %></span>
|
|
76
|
+
<span class="version-list__meta">
|
|
77
|
+
<% if version.latest? %>
|
|
78
|
+
Latest release via RubyGems
|
|
79
|
+
<% elsif version.edge? %>
|
|
80
|
+
Pre-release via git
|
|
81
|
+
<% else %>
|
|
82
|
+
Historical
|
|
83
|
+
<% end %>
|
|
84
|
+
</span>
|
|
85
|
+
</a>
|
|
86
|
+
</li>
|
|
87
|
+
<% end %>
|
|
88
|
+
</ul>
|
|
89
|
+
</div>
|
|
90
|
+
</section>
|
|
91
|
+
</main>
|
|
92
|
+
|
|
93
|
+
<footer class="footer">
|
|
94
|
+
<div class="footer__inner">
|
|
95
|
+
<% unless footer_columns.empty? %>
|
|
96
|
+
<!-- Megafooter columns -->
|
|
97
|
+
<div class="footer__columns">
|
|
98
|
+
<% footer_columns.each do |column| %>
|
|
99
|
+
<div class="footer__column">
|
|
100
|
+
<h3 class="footer__heading"><%= column[:heading] %></h3>
|
|
101
|
+
<ul class="footer__list">
|
|
102
|
+
<% column[:links].each do |link| %>
|
|
103
|
+
<li><a href="<%= link[:href] %>"><%= link[:text] %><% if link[:tag] %> <span class="footer__tag<%= ' footer__tag--muted' if link[:tag_muted] %>"><%= link[:tag] %></span><% end %></a></li>
|
|
104
|
+
<% end %>
|
|
105
|
+
</ul>
|
|
106
|
+
</div>
|
|
107
|
+
<% end %>
|
|
108
|
+
</div>
|
|
109
|
+
<% end %>
|
|
110
|
+
|
|
111
|
+
<!-- Bottom bar -->
|
|
112
|
+
<div class="footer__bottom">
|
|
113
|
+
<div class="footer__brand">
|
|
114
|
+
<img src="<%= logo_small %>" alt="" class="footer__logo" width="32" height="32" aria-hidden="true">
|
|
115
|
+
<span class="footer__wordmark"><%= wordmark %></span>
|
|
116
|
+
</div>
|
|
117
|
+
<% unless footer_meta_links.empty? %>
|
|
118
|
+
<nav class="footer__meta" aria-label="Meta links">
|
|
119
|
+
<% footer_meta_links.each do |link| %>
|
|
120
|
+
<a href="<%= link[:href] %>"><%= link[:text] %></a>
|
|
121
|
+
<% end %>
|
|
122
|
+
</nav>
|
|
123
|
+
<% end %>
|
|
124
|
+
<p class="footer__copy"><%= copyright %><% if license_text && !license_text.empty? %> · <%= license_text %><% end %></p>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</footer>
|
|
128
|
+
</body>
|
|
129
|
+
</html>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "erb"
|
|
9
|
+
|
|
10
|
+
# Landing page for multi-version documentation portals.
|
|
11
|
+
#
|
|
12
|
+
# Documentation websites need a version picker. Users land on the portal
|
|
13
|
+
# and choose their version. Without a landing page, they'd need to guess
|
|
14
|
+
# the URL.
|
|
15
|
+
#
|
|
16
|
+
# This class generates an index page with version links and optional
|
|
17
|
+
# branding. It marks the newest tagged release as "(latest)".
|
|
18
|
+
#
|
|
19
|
+
# Use it to build the root <tt>index.html</tt> for documentation portals.
|
|
20
|
+
class IndexPage
|
|
21
|
+
# Creates an IndexPage.
|
|
22
|
+
#
|
|
23
|
+
# Marks the newest Tagged version as latest for display.
|
|
24
|
+
#
|
|
25
|
+
# [versions] Array of Version objects.
|
|
26
|
+
# [branding] Optional hash of branding overrides for the template.
|
|
27
|
+
def initialize(versions, branding: {})
|
|
28
|
+
@versions = versions
|
|
29
|
+
@branding = branding
|
|
30
|
+
|
|
31
|
+
latest_version = @versions.find { |v| v.is_a?(Tagged) }
|
|
32
|
+
latest_version.is_latest = true if latest_version
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Renders the landing page HTML to a file.
|
|
36
|
+
#
|
|
37
|
+
# Uses an ERB template with version links and branding.
|
|
38
|
+
#
|
|
39
|
+
# [path] Output file path.
|
|
40
|
+
# [project_name] Project name for page title.
|
|
41
|
+
def publish_to(path, project_name:)
|
|
42
|
+
puts "Generating index page..."
|
|
43
|
+
|
|
44
|
+
template_path = File.expand_path("../resources/index.html.erb", __dir__)
|
|
45
|
+
template = File.read(template_path)
|
|
46
|
+
|
|
47
|
+
versions = @versions
|
|
48
|
+
branding = @branding
|
|
49
|
+
# project_name is used in the ERB
|
|
50
|
+
html_content = ERB.new(template).result(binding)
|
|
51
|
+
|
|
52
|
+
File.write(path, html_content)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "rubygems"
|
|
9
|
+
require "fileutils"
|
|
10
|
+
|
|
11
|
+
# A documentation version for multi-version websites.
|
|
12
|
+
#
|
|
13
|
+
# Documentation websites need to serve multiple versions. Users browse docs
|
|
14
|
+
# for their installed release. Maintainers preview trunk changes. Manually
|
|
15
|
+
# tracking git tags and branches is tedious.
|
|
16
|
+
#
|
|
17
|
+
# This class discovers versions from git tags. It extracts source files at
|
|
18
|
+
# each ref. It provides metadata for version menus.
|
|
19
|
+
#
|
|
20
|
+
# Use it to build multi-version documentation portals.
|
|
21
|
+
class Version
|
|
22
|
+
# Discovers all available versions.
|
|
23
|
+
#
|
|
24
|
+
# Returns Edge (trunk) plus the latest patch for each minor version,
|
|
25
|
+
# sorted newest first.
|
|
26
|
+
def self.all
|
|
27
|
+
tags = `git tag`.split.grep(/^v\d/)
|
|
28
|
+
sorted_versions = tags.map { |t| Tagged.new(t) }
|
|
29
|
+
.sort_by(&:semver)
|
|
30
|
+
.reverse
|
|
31
|
+
|
|
32
|
+
# Keep only the latest patch for each minor version
|
|
33
|
+
# e.g., if we have v0.6.0, v0.6.1, v0.6.2, only keep v0.6.2
|
|
34
|
+
latest_per_minor = sorted_versions
|
|
35
|
+
.group_by { |v| v.semver.segments[0..1] } # group by [major, minor]
|
|
36
|
+
.values
|
|
37
|
+
.map(&:first) # take the first (highest patch) from each group
|
|
38
|
+
|
|
39
|
+
[Edge.new] + latest_per_minor
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# URL-safe identifier for this version.
|
|
43
|
+
def slug
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Human-readable name for version menus.
|
|
48
|
+
def name
|
|
49
|
+
raise NotImplementedError
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Version type: <tt>:edge</tt> or <tt>:version</tt>.
|
|
53
|
+
def type
|
|
54
|
+
raise NotImplementedError
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Git ref (branch or tag) for checkout.
|
|
58
|
+
def ref
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Yields a temporary directory containing this version's source.
|
|
63
|
+
#
|
|
64
|
+
# Exports the git archive at the specified ref. Removes the native
|
|
65
|
+
# extension directory to avoid compilation issues.
|
|
66
|
+
#
|
|
67
|
+
# [globs] File patterns (unused, for API compatibility).
|
|
68
|
+
def checkout(globs:, &block)
|
|
69
|
+
Dir.mktmpdir do |path|
|
|
70
|
+
# Use git archive to export the version at the specified ref
|
|
71
|
+
# Pipe to tar to extract into the temporary directory
|
|
72
|
+
system("git archive #{ref} | tar -x -C #{path}")
|
|
73
|
+
|
|
74
|
+
# Remove the native extension directory as we don't need it for builds
|
|
75
|
+
# and it can cause issues if not meant to be compiled in this context
|
|
76
|
+
FileUtils.rm_rf("#{path}/ext")
|
|
77
|
+
|
|
78
|
+
yield path
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Returns <tt>true</tt> if this is the latest stable release.
|
|
83
|
+
def latest?
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns <tt>true</tt> if this is the edge (trunk) version.
|
|
88
|
+
def edge?
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The trunk branch version for unreleased changes.
|
|
94
|
+
#
|
|
95
|
+
# Developers preview upcoming features before release. The edge version
|
|
96
|
+
# builds documentation from trunk.
|
|
97
|
+
class Edge < Version
|
|
98
|
+
# Stable URL path for bookmarks. Trunk docs live at <tt>/trunk/</tt>.
|
|
99
|
+
def slug
|
|
100
|
+
"trunk"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Appears in version menus as-is, without version numbering.
|
|
104
|
+
def name
|
|
105
|
+
"trunk"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Distinguishes unreleased docs from tagged releases in conditionals.
|
|
109
|
+
def type
|
|
110
|
+
:edge
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Git branch name for archive extraction.
|
|
114
|
+
def ref
|
|
115
|
+
"trunk"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Identifies this as unreleased for "(dev)" labels in menus.
|
|
119
|
+
def edge?
|
|
120
|
+
true
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# A Git release tag version.
|
|
125
|
+
#
|
|
126
|
+
# Each release gets a tag like <tt>v0.6.0</tt>. The documentation website
|
|
127
|
+
# shows only the latest patch for each minor version.
|
|
128
|
+
class Tagged < Version
|
|
129
|
+
# The raw Git tag for archive extraction.
|
|
130
|
+
attr_reader :tag
|
|
131
|
+
|
|
132
|
+
# Creates a Tagged version.
|
|
133
|
+
#
|
|
134
|
+
# [tag] Git tag string.
|
|
135
|
+
def initialize(tag)
|
|
136
|
+
@tag = tag
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Groups patch releases under one URL. Docs for <tt>v0.6.0</tt> and
|
|
140
|
+
# <tt>v0.6.1</tt> both live at <tt>/v0.6/</tt>.
|
|
141
|
+
def slug
|
|
142
|
+
segments = semver.segments
|
|
143
|
+
"v#{segments[0]}.#{segments[1]}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Shows exact version in menus. Users know which patch they're viewing.
|
|
147
|
+
def name
|
|
148
|
+
@tag
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Distinguishes released docs from trunk in conditionals.
|
|
152
|
+
def type
|
|
153
|
+
:version
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Git tag for archive extraction.
|
|
157
|
+
def ref
|
|
158
|
+
@tag
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Enables sorting and grouping by semantic version rules.
|
|
162
|
+
def semver
|
|
163
|
+
Gem::Version.new(@tag.sub(/^v/, ""))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Set by IndexPage to enable "(latest)" label in menus.
|
|
167
|
+
attr_accessor :is_latest
|
|
168
|
+
|
|
169
|
+
# Identifies this for "(latest)" labels in menus.
|
|
170
|
+
def latest?
|
|
171
|
+
@is_latest
|
|
172
|
+
end
|
|
173
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
# Version switcher for generated documentation pages.
|
|
9
|
+
#
|
|
10
|
+
# Multi-version docs need navigation between versions. Users viewing
|
|
11
|
+
# <tt>v0.5</tt> docs want to switch to <tt>v0.6</tt>. Without a switcher,
|
|
12
|
+
# they'd need to navigate back to the portal.
|
|
13
|
+
#
|
|
14
|
+
# This class injects a dropdown menu into every generated HTML page.
|
|
15
|
+
# It calculates relative paths so links work from any nesting depth.
|
|
16
|
+
#
|
|
17
|
+
# Use it after generating all version documentation.
|
|
18
|
+
class VersionMenu
|
|
19
|
+
# Creates a VersionMenu.
|
|
20
|
+
#
|
|
21
|
+
# [root] Root directory of the generated site.
|
|
22
|
+
# [versions] Array of Version objects for the dropdown.
|
|
23
|
+
def initialize(root:, versions:)
|
|
24
|
+
@root = root
|
|
25
|
+
@versions = versions
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Injects the version dropdown into all HTML files.
|
|
29
|
+
#
|
|
30
|
+
# Finds the theme toggle button and inserts the menu before it.
|
|
31
|
+
def run
|
|
32
|
+
puts "Injecting version menu into generated HTML..."
|
|
33
|
+
|
|
34
|
+
# Process all HTML files in the output directory
|
|
35
|
+
Dir.glob(File.join(@root, "**/*.html")).each do |file|
|
|
36
|
+
inject_menu(file)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private def inject_menu(file)
|
|
41
|
+
content = File.read(file)
|
|
42
|
+
|
|
43
|
+
# Find the injection point (before the theme toggle button)
|
|
44
|
+
pattern = /(<button[^>]*id="theme-toggle"[^>]*>)/mi
|
|
45
|
+
|
|
46
|
+
unless content.match?(pattern)
|
|
47
|
+
# warn "Could not find theme-toggle in #{file}"
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Calculate relative path to root from this file
|
|
52
|
+
file_dir = File.dirname(file)
|
|
53
|
+
relative_path_to_root = Pathname.new(@root).relative_path_from(Pathname.new(file_dir)).to_s
|
|
54
|
+
relative_path_to_root += "/" unless relative_path_to_root.end_with?("/")
|
|
55
|
+
|
|
56
|
+
# Determine current version from file path
|
|
57
|
+
relative_path_from_root = Pathname.new(file).relative_path_from(Pathname.new(@root)).to_s
|
|
58
|
+
current_version_slug = relative_path_from_root.split("/").first
|
|
59
|
+
|
|
60
|
+
# Build options
|
|
61
|
+
options = @versions.map do |version|
|
|
62
|
+
value = "#{relative_path_to_root}#{version.slug}/index.html"
|
|
63
|
+
selected = (version.slug == current_version_slug) ? "selected" : ""
|
|
64
|
+
display_name = version.name
|
|
65
|
+
display_name += " (latest)" if version.respond_to?(:latest?) && version.latest?
|
|
66
|
+
display_name += " (dev)" if version.edge?
|
|
67
|
+
|
|
68
|
+
%Q{<option value="#{value}" #{selected}>#{display_name}</option>}
|
|
69
|
+
end.join("\n")
|
|
70
|
+
|
|
71
|
+
# margin-left: auto pushes it to the right
|
|
72
|
+
# margin-right: 1rem spacing from the theme toggle
|
|
73
|
+
switcher_html = <<~HTML
|
|
74
|
+
<select class="version-menu" onchange="window.location.href=this.value" style="margin-left: auto; padding: 0.25rem; border-radius: 4px; border: 1px solid #ccc; margin-right: 1rem;">
|
|
75
|
+
#{options}
|
|
76
|
+
<option value="#{relative_path_to_root}index.html">All Versions</option>
|
|
77
|
+
</select>
|
|
78
|
+
HTML
|
|
79
|
+
|
|
80
|
+
# Inject before the button
|
|
81
|
+
new_content = content.sub(pattern, "#{switcher_html}\n\\1")
|
|
82
|
+
|
|
83
|
+
File.write(file, new_content)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require_relative "../rdoc_config"
|
|
9
|
+
|
|
10
|
+
# Builds documentation for a single version.
|
|
11
|
+
#
|
|
12
|
+
# Multi-version sites need docs generated from each git ref. Building
|
|
13
|
+
# locally would pollute the working directory. Using git checkout would
|
|
14
|
+
# disrupt work in progress.
|
|
15
|
+
#
|
|
16
|
+
# This class extracts source files to a temp directory and runs RDoc.
|
|
17
|
+
# It copies the output to the target path. It preserves the current
|
|
18
|
+
# working directory.
|
|
19
|
+
#
|
|
20
|
+
# Use it to build one version's documentation within a multi-version site.
|
|
21
|
+
class VersionedDocumentation
|
|
22
|
+
# Creates a VersionedDocumentation builder.
|
|
23
|
+
#
|
|
24
|
+
# [version] Version object to build docs for.
|
|
25
|
+
def initialize(version)
|
|
26
|
+
@version = version
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Generates and publishes documentation for this version.
|
|
30
|
+
#
|
|
31
|
+
# Extracts source to a temp directory, runs RDoc, and copies output.
|
|
32
|
+
# Falls back to direct RDoc if rake fails.
|
|
33
|
+
#
|
|
34
|
+
# [path] Output directory for generated docs.
|
|
35
|
+
# [project_name] Project name for RDoc title.
|
|
36
|
+
# [globs] File patterns to document.
|
|
37
|
+
# [assets] Optional directories to copy into output.
|
|
38
|
+
def publish_to(path, project_name:, globs:, assets: [])
|
|
39
|
+
puts "Building documentation for #{@version.name}..."
|
|
40
|
+
|
|
41
|
+
absolute_path = File.absolute_path(path)
|
|
42
|
+
gemfile_path = File.absolute_path("Gemfile")
|
|
43
|
+
custom_css_path = File.absolute_path("doc/custom.css")
|
|
44
|
+
rakefile_path = File.absolute_path("Rakefile")
|
|
45
|
+
tasks_dir_path = File.absolute_path("tasks")
|
|
46
|
+
|
|
47
|
+
@version.checkout(globs:) do |source_path|
|
|
48
|
+
# Copy current Rakefile and tasks into the temp directory
|
|
49
|
+
# This ensures all versions use the latest example generation logic
|
|
50
|
+
FileUtils.cp(rakefile_path, File.join(source_path, "Rakefile"))
|
|
51
|
+
FileUtils.cp_r(tasks_dir_path, File.join(source_path, "tasks"))
|
|
52
|
+
|
|
53
|
+
Dir.chdir(source_path) do
|
|
54
|
+
title = "#{project_name} #{@version.name}"
|
|
55
|
+
title = "#{project_name} (trunk)" if @version.edge?
|
|
56
|
+
|
|
57
|
+
# Use rake rerdoc to ensure copy_examples runs
|
|
58
|
+
# Set environment variables to override rdoc settings
|
|
59
|
+
success = system(
|
|
60
|
+
{
|
|
61
|
+
"BUNDLE_GEMFILE" => gemfile_path,
|
|
62
|
+
"RDOC_OUTPUT" => absolute_path,
|
|
63
|
+
"RDOC_TITLE" => title,
|
|
64
|
+
"RDOC_CUSTOM_CSS" => custom_css_path,
|
|
65
|
+
},
|
|
66
|
+
"bundle exec rake rerdoc"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Fall back to direct rdoc call if rake fails for any reason
|
|
70
|
+
unless success
|
|
71
|
+
puts " Rake task failed, falling back to direct rdoc call..."
|
|
72
|
+
files = globs.flat_map { |glob| Dir[glob] }.uniq
|
|
73
|
+
system(
|
|
74
|
+
{ "BUNDLE_GEMFILE" => gemfile_path },
|
|
75
|
+
"bundle exec rdoc -o #{absolute_path} --main #{RDocConfig::MAIN} --title '#{title}' --template-stylesheets \"#{custom_css_path}\" #{files.join(' ')}"
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Copy generated documentation to target path if it was generated elsewhere
|
|
80
|
+
# This handles cases where RDOC_OUTPUT wasn't respected (evaluated at load time)
|
|
81
|
+
temp_output_paths = ["tmp/rdoc", "doc"]
|
|
82
|
+
temp_output_paths.each do |temp_path|
|
|
83
|
+
# Check if this looks like generated rdoc (has index.html)
|
|
84
|
+
if Dir.exist?(temp_path) && !Dir.empty?(temp_path) && File.exist?(File.join(temp_path, "index.html"))
|
|
85
|
+
puts " Copying generated docs from #{temp_path} to #{absolute_path}..."
|
|
86
|
+
FileUtils.mkdir_p(absolute_path)
|
|
87
|
+
FileUtils.cp_r Dir["#{temp_path}/*"], absolute_path
|
|
88
|
+
break
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
copy_assets_to(absolute_path, assets)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private def copy_assets_to(path, assets)
|
|
98
|
+
assets.each do |asset_dir|
|
|
99
|
+
if Dir.exist?(asset_dir)
|
|
100
|
+
destination = File.join(path, asset_dir)
|
|
101
|
+
FileUtils.mkdir_p(destination)
|
|
102
|
+
FileUtils.cp_r Dir["#{asset_dir}/*"], destination
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require_relative "version"
|
|
9
|
+
require_relative "versioned_documentation"
|
|
10
|
+
require_relative "index_page"
|
|
11
|
+
require_relative "version_menu"
|
|
12
|
+
require "fileutils"
|
|
13
|
+
|
|
14
|
+
# Multi-version documentation website builder.
|
|
15
|
+
#
|
|
16
|
+
# Projects need documentation for multiple versions. Users on v0.5 read
|
|
17
|
+
# v0.5 docs. Users on trunk preview upcoming changes. Building this
|
|
18
|
+
# manually is tedious and error-prone.
|
|
19
|
+
#
|
|
20
|
+
# This class orchestrates the entire build: discovers versions, generates
|
|
21
|
+
# docs for each, creates the landing page, and injects version menus.
|
|
22
|
+
#
|
|
23
|
+
# Use it to build a complete documentation portal.
|
|
24
|
+
class Website
|
|
25
|
+
# Creates a Website builder.
|
|
26
|
+
#
|
|
27
|
+
# [at] Output directory (default: <tt>www</tt>).
|
|
28
|
+
# [project_name] Project name for page titles.
|
|
29
|
+
# [globs] File patterns to document.
|
|
30
|
+
# [assets] Optional directories to copy into each version's output.
|
|
31
|
+
# [branding] Optional hash of branding overrides for the index template.
|
|
32
|
+
def initialize(at: "www", project_name:, globs:, assets: [], branding: {})
|
|
33
|
+
@destination = at
|
|
34
|
+
@project_name = project_name
|
|
35
|
+
@globs = globs
|
|
36
|
+
@assets = assets
|
|
37
|
+
@branding = branding
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Builds the complete documentation website.
|
|
41
|
+
#
|
|
42
|
+
# Cleans the output directory, generates docs for all versions,
|
|
43
|
+
# creates the landing page, and injects version menus.
|
|
44
|
+
def build
|
|
45
|
+
clean
|
|
46
|
+
|
|
47
|
+
versions.each do |version|
|
|
48
|
+
VersionedDocumentation.new(version).publish_to(
|
|
49
|
+
join(version.slug),
|
|
50
|
+
project_name: @project_name,
|
|
51
|
+
globs: @globs,
|
|
52
|
+
assets: @assets
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
IndexPage.new(versions, branding: @branding).publish_to(join("index.html"), project_name: @project_name)
|
|
57
|
+
|
|
58
|
+
VersionMenu.new(root: @destination, versions:).run
|
|
59
|
+
|
|
60
|
+
puts "Website built in #{@destination}/"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Discovered versions for this build.
|
|
64
|
+
#
|
|
65
|
+
# Cached after first call. Includes Edge plus latest patch per minor.
|
|
66
|
+
def versions
|
|
67
|
+
@versions ||= Version.all
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private def join(path)
|
|
71
|
+
File.join(@destination, path)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private def clean
|
|
75
|
+
FileUtils.rm_rf(@destination)
|
|
76
|
+
FileUtils.mkdir_p(@destination)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#--
|
|
4
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
|
|
5
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
6
|
+
#++
|
|
7
|
+
|
|
8
|
+
require "erb"
|
|
9
|
+
require "fileutils"
|
|
10
|
+
require "tmpdir"
|
|
11
|
+
require_relative "rdoc_config"
|
|
12
|
+
|
|
13
|
+
namespace :website do
|
|
14
|
+
desc "Build documentation for trunk (current dir) and all git tags"
|
|
15
|
+
task :build do
|
|
16
|
+
require_relative "website/website"
|
|
17
|
+
|
|
18
|
+
spec = Gem::Specification.load(Dir["*.gemspec"].first)
|
|
19
|
+
globs = RDocConfig::RDOC_FILES + ["*.gemspec", "doc/images/**/*", "examples/**/*"]
|
|
20
|
+
|
|
21
|
+
# Allow projects to customize via website_config.rb in their tasks/ directory
|
|
22
|
+
config = if File.exist?("tasks/website_config.rb")
|
|
23
|
+
load File.expand_path("tasks/website_config.rb", Dir.pwd)
|
|
24
|
+
WebsiteConfig::CONFIG
|
|
25
|
+
else
|
|
26
|
+
{}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Website.new(
|
|
30
|
+
at: config[:output_dir] || "www",
|
|
31
|
+
project_name: spec.name,
|
|
32
|
+
globs:,
|
|
33
|
+
assets: config[:assets] || ["doc/images"],
|
|
34
|
+
branding: config[:branding] || {}
|
|
35
|
+
).build
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -40,19 +40,48 @@ module RatatuiRuby
|
|
|
40
40
|
# Configuration accessors - auto-discovered if not set
|
|
41
41
|
attr_writer :gem_name, :gemspec_file, :version_file
|
|
42
42
|
|
|
43
|
-
# Loads
|
|
43
|
+
# Loads gem development Rake tasks (test, lint, bump, license, etc.)
|
|
44
44
|
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
# once in your Rakefile to import all devtools tasks.
|
|
45
|
+
# Use this in gem/library Rakefiles. These tasks help with version
|
|
46
|
+
# management, testing, linting, and releasing gems.
|
|
48
47
|
#
|
|
49
48
|
# === Example
|
|
50
49
|
#
|
|
51
50
|
# require "ratatui_ruby/devtools"
|
|
52
|
-
# RatatuiRuby::Devtools.
|
|
51
|
+
# RatatuiRuby::Devtools.install_gem_tasks!
|
|
53
52
|
#
|
|
54
|
-
def
|
|
55
|
-
|
|
53
|
+
def install_gem_tasks!
|
|
54
|
+
load_tasks_from("tasks")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Loads website hosting Rake tasks (build, serve, deploy).
|
|
58
|
+
#
|
|
59
|
+
# Use this in website repository Rakefiles. These tasks help sync docs
|
|
60
|
+
# from a source gem, update HTML metadata, and deploy.
|
|
61
|
+
#
|
|
62
|
+
# @param source_gem_dir [String] Path to the source gem directory
|
|
63
|
+
# (e.g., "~/Developer/ratatui_ruby")
|
|
64
|
+
#
|
|
65
|
+
# === Example
|
|
66
|
+
#
|
|
67
|
+
# require "ratatui_ruby/devtools"
|
|
68
|
+
# RatatuiRuby::Devtools.install_website_tasks!(
|
|
69
|
+
# source_gem_dir: "~/Developer/ratatui_ruby"
|
|
70
|
+
# )
|
|
71
|
+
#
|
|
72
|
+
def install_website_tasks!(source_gem_dir:)
|
|
73
|
+
@source_gem_dir = File.expand_path(source_gem_dir)
|
|
74
|
+
load_tasks_from("site_tasks")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Returns the source gem directory for website tasks.
|
|
78
|
+
attr_reader :source_gem_dir
|
|
79
|
+
|
|
80
|
+
# Backwards-compatible alias for install_gem_tasks!
|
|
81
|
+
alias install! install_gem_tasks!
|
|
82
|
+
|
|
83
|
+
private def load_tasks_from(subdir)
|
|
84
|
+
tasks_dir = File.expand_path("devtools/#{subdir}", __dir__)
|
|
56
85
|
Dir.glob("#{tasks_dir}/*.rake").each do |task_file|
|
|
57
86
|
Rake.application.add_import(task_file)
|
|
58
87
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ratatui_ruby-devtools
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kerrick Long
|
|
@@ -162,6 +162,9 @@ files:
|
|
|
162
162
|
- exe/hbs
|
|
163
163
|
- exe/scaffold
|
|
164
164
|
- lib/ratatui_ruby/devtools.rb
|
|
165
|
+
- lib/ratatui_ruby/devtools/site_tasks/build.rake
|
|
166
|
+
- lib/ratatui_ruby/devtools/site_tasks/deploy.rake
|
|
167
|
+
- lib/ratatui_ruby/devtools/site_tasks/serve.rake
|
|
165
168
|
- lib/ratatui_ruby/devtools/tasks/autodoc.rake
|
|
166
169
|
- lib/ratatui_ruby/devtools/tasks/autodoc/examples.rb
|
|
167
170
|
- lib/ratatui_ruby/devtools/tasks/autodoc/member.rb
|
|
@@ -188,10 +191,17 @@ files:
|
|
|
188
191
|
- lib/ratatui_ruby/devtools/tasks/lint.rake
|
|
189
192
|
- lib/ratatui_ruby/devtools/tasks/rdoc_config.rb
|
|
190
193
|
- lib/ratatui_ruby/devtools/tasks/resources/build.yml.erb
|
|
194
|
+
- lib/ratatui_ruby/devtools/tasks/resources/index.html.erb
|
|
191
195
|
- lib/ratatui_ruby/devtools/tasks/resources/rubies.yml
|
|
192
196
|
- lib/ratatui_ruby/devtools/tasks/reuse.rake
|
|
193
197
|
- lib/ratatui_ruby/devtools/tasks/sourcehut.rake
|
|
194
198
|
- lib/ratatui_ruby/devtools/tasks/test.rake
|
|
199
|
+
- lib/ratatui_ruby/devtools/tasks/website.rake
|
|
200
|
+
- lib/ratatui_ruby/devtools/tasks/website/index_page.rb
|
|
201
|
+
- lib/ratatui_ruby/devtools/tasks/website/version.rb
|
|
202
|
+
- lib/ratatui_ruby/devtools/tasks/website/version_menu.rb
|
|
203
|
+
- lib/ratatui_ruby/devtools/tasks/website/versioned_documentation.rb
|
|
204
|
+
- lib/ratatui_ruby/devtools/tasks/website/website.rb
|
|
195
205
|
- lib/ratatui_ruby/devtools/templates/.builds/ruby.yml.erb
|
|
196
206
|
- lib/ratatui_ruby/devtools/templates/.gitignore.erb
|
|
197
207
|
- lib/ratatui_ruby/devtools/templates/.pre-commit-config.yaml.erb
|