jackdaw 1.0.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/.rubocop.yml +32 -0
- data/CHANGELOG.md +82 -0
- data/README.md +467 -0
- data/Rakefile +8 -0
- data/TODO.md +167 -0
- data/benchmark.rb +235 -0
- data/exe/jackdaw +6 -0
- data/lib/jackdaw/builder.rb +206 -0
- data/lib/jackdaw/cli.rb +49 -0
- data/lib/jackdaw/cli_helpers.rb +33 -0
- data/lib/jackdaw/commands/build.rb +66 -0
- data/lib/jackdaw/commands/create.rb +111 -0
- data/lib/jackdaw/commands/new.rb +162 -0
- data/lib/jackdaw/commands/serve.rb +28 -0
- data/lib/jackdaw/commands/template.rb +60 -0
- data/lib/jackdaw/feed_generator.rb +113 -0
- data/lib/jackdaw/file_types.rb +171 -0
- data/lib/jackdaw/project.rb +80 -0
- data/lib/jackdaw/renderer.rb +154 -0
- data/lib/jackdaw/scanner.rb +44 -0
- data/lib/jackdaw/seo_helpers.rb +65 -0
- data/lib/jackdaw/server.rb +214 -0
- data/lib/jackdaw/sitemap_generator.rb +72 -0
- data/lib/jackdaw/version.rb +5 -0
- data/lib/jackdaw/watcher.rb +40 -0
- data/lib/jackdaw.rb +54 -0
- data/sig/jackdaw.rbs +4 -0
- metadata +185 -0
data/TODO.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Jackdaw - Static Site Generator TODO
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
A fast, incremental static site generator with folder-based organization using Thor for CLI.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Phase 1: Project Setup
|
|
9
|
+
- [x] Add Thor gem dependency to gemspec
|
|
10
|
+
- [x] Add development dependencies (rspec, rubocop, etc.)
|
|
11
|
+
- [x] Create basic CLI structure in exe/jackdaw
|
|
12
|
+
- [x] Set up Thor CLI class structure
|
|
13
|
+
- [x] Add any additional dependencies (for markdown parsing, file watching, etc.)
|
|
14
|
+
|
|
15
|
+
## Phase 2: Core File System Structure
|
|
16
|
+
- [x] Define standard project structure (site/ directory)
|
|
17
|
+
- [x] Create file watcher/tracker for incremental builds
|
|
18
|
+
- [x] Implement directory scanner
|
|
19
|
+
- [x] Create file type registry (.page, .post, etc.)
|
|
20
|
+
- [x] Build metadata extractor from file structure (path, name, dates)
|
|
21
|
+
- [x] Extract metadata from first heading as title (H1)
|
|
22
|
+
- [x] Implement content type handlers
|
|
23
|
+
- [x] Page handler
|
|
24
|
+
- [x] Post handler
|
|
25
|
+
- [x] Asset handler
|
|
26
|
+
|
|
27
|
+
## Phase 3: Template System
|
|
28
|
+
- [x] Design template engine integration (ERB)
|
|
29
|
+
- [x] Implement template loader from templates/ directory
|
|
30
|
+
- [x] Create template context builder
|
|
31
|
+
- [x] Add layout/partial support
|
|
32
|
+
- [x] Implement template caching for performance
|
|
33
|
+
|
|
34
|
+
## Phase 4: Content Processing
|
|
35
|
+
- [x] Implement markdown parser integration
|
|
36
|
+
- [x] Create content preprocessor pipeline
|
|
37
|
+
- [x] Extract metadata from filename (dates, slugs)
|
|
38
|
+
- [x] Extract title from first H1 in content
|
|
39
|
+
- [x] Derive metadata from folder structure and hierarchy
|
|
40
|
+
- [x] Implement content transformation pipeline
|
|
41
|
+
- [x] Add syntax highlighting support (Rouge)
|
|
42
|
+
|
|
43
|
+
## Phase 5: Build System
|
|
44
|
+
- [x] Design dependency graph for incremental builds
|
|
45
|
+
- [x] Implement file change detection
|
|
46
|
+
- [x] Create build cache system
|
|
47
|
+
- [x] Build output directory management (public/)
|
|
48
|
+
- [x] Implement parallel processing for speed
|
|
49
|
+
- [x] Add build statistics/reporting
|
|
50
|
+
- [x] Create clean build option
|
|
51
|
+
|
|
52
|
+
## Phase 6: CLI Commands - Build
|
|
53
|
+
- [x] Implement `jackdaw build` command
|
|
54
|
+
- [x] Full build option
|
|
55
|
+
- [x] Incremental build (default)
|
|
56
|
+
- [x] Watch mode option (deferred to serve command)
|
|
57
|
+
- [x] Verbose/debug output
|
|
58
|
+
- [x] Clean option
|
|
59
|
+
- [x] Add build configuration loading (skipped - convention over configuration)
|
|
60
|
+
- [x] Implement error handling and reporting
|
|
61
|
+
|
|
62
|
+
## Phase 7: CLI Commands - Serve
|
|
63
|
+
- [x] Implement `jackdaw serve` command
|
|
64
|
+
- [x] Choose and integrate web server (Puma with Rack)
|
|
65
|
+
- [x] Add live reload support (polling-based with JavaScript)
|
|
66
|
+
- [x] Implement file watching for auto-rebuild (Listen gem)
|
|
67
|
+
- [x] Configure port and host options
|
|
68
|
+
- [x] Add middleware for development (StaticFileServer + LiveReloadMiddleware)
|
|
69
|
+
- [x] Beautiful CLI output with rebuild stats
|
|
70
|
+
- [x] Graceful shutdown handling (Ctrl+C)
|
|
71
|
+
|
|
72
|
+
## Phase 8: CLI Commands - Create
|
|
73
|
+
- [x] Implement `jackdaw create <template> <name>` command
|
|
74
|
+
- [x] Create template scaffolding system
|
|
75
|
+
- [x] Add default built-in templates:
|
|
76
|
+
- [x] `page` template (basic page.html.erb)
|
|
77
|
+
- [x] `blog` template (blog post with date handling)
|
|
78
|
+
- [x] Support arbitrary user-defined templates in templates/ directory
|
|
79
|
+
- [x] Discover available templates from templates/ folder
|
|
80
|
+
- [x] Add date/timestamp handling for posts (auto-prefix for blog/post/article/news)
|
|
81
|
+
- [x] Allow users to add custom templates by creating template files
|
|
82
|
+
- [x] Implement `jackdaw template list` command
|
|
83
|
+
- [x] Add --dated and --no-date flags for override control
|
|
84
|
+
- [x] Support nested paths (e.g., `jackdaw create page company/about`)
|
|
85
|
+
- [x] Validate template existence before creating files
|
|
86
|
+
- [x] Add Builder warnings for missing templates
|
|
87
|
+
|
|
88
|
+
## Phase 9: Configuration System
|
|
89
|
+
- [x] Skipped - Maintaining config-less, convention-over-configuration approach
|
|
90
|
+
|
|
91
|
+
## Phase 10: Asset Handling
|
|
92
|
+
- [x] Implement asset copying
|
|
93
|
+
- [x] Keeping minimal - assets copied as-is for speed and simplicity
|
|
94
|
+
|
|
95
|
+
## Phase 11: Performance Optimization
|
|
96
|
+
- [x] Profile build performance
|
|
97
|
+
- [x] Implement multi-threading for file processing (Parallel gem)
|
|
98
|
+
- [x] Optimize file I/O operations (incremental builds with mtime checks)
|
|
99
|
+
- [ ] Add memory usage optimization
|
|
100
|
+
- [x] Benchmark against common static site generators
|
|
101
|
+
- Small (30 files): 164 files/sec clean, 5821 files/sec incremental
|
|
102
|
+
- Medium (150 files): 428 files/sec clean, 12343 files/sec incremental
|
|
103
|
+
- Large (600 files): 693 files/sec clean, 16280 files/sec incremental
|
|
104
|
+
- Incremental builds: 6-18x faster than clean builds
|
|
105
|
+
- [x] Cache template compilation (ERB caching in Renderer)
|
|
106
|
+
- [x] Optimize dependency graph traversal (mtime-based change detection)
|
|
107
|
+
|
|
108
|
+
## Phase 12: Documentation
|
|
109
|
+
- [x] Write comprehensive README with quickstart guide
|
|
110
|
+
- [x] Document CLI commands and options (in README)
|
|
111
|
+
- [x] Create project structure guide (in README)
|
|
112
|
+
- [x] Write template documentation (in README)
|
|
113
|
+
- [x] Add configuration reference (N/A - convention-based, no config needed)
|
|
114
|
+
- [x] Add troubleshooting guide (in README)
|
|
115
|
+
- [x] Consolidate all require statements to jackdaw.rb
|
|
116
|
+
- [x] Add comprehensive code comments to jackdaw.rb and project.rb
|
|
117
|
+
- [ ] Add code comments to remaining core files
|
|
118
|
+
- [ ] Create example site (basic example already created by `jackdaw new`)
|
|
119
|
+
|
|
120
|
+
## Phase 13: Testing
|
|
121
|
+
- [x] Unit tests for core components (Project, FileTypes, Scanner)
|
|
122
|
+
- [x] Unit tests for Renderer (21 tests)
|
|
123
|
+
- [x] Integration tests for Builder (18 tests)
|
|
124
|
+
- [x] End-to-end build tests (10 comprehensive workflow tests)
|
|
125
|
+
- [x] Test helper infrastructure with fixtures
|
|
126
|
+
- [x] Performance benchmarks (completed in Phase 11)
|
|
127
|
+
- **Total: 99 passing tests, 0 failures**
|
|
128
|
+
|
|
129
|
+
## Phase 14: Polish & Release
|
|
130
|
+
- [x] Code cleanup and refactoring (reduced complexity in Build command)
|
|
131
|
+
- [x] RuboCop compliance (all files passing, 0 offenses)
|
|
132
|
+
- [x] Error message improvements (beautiful CLI output with colors)
|
|
133
|
+
- [x] Add progress indicators (build stats, rebuild notifications)
|
|
134
|
+
- [x] Version 1.0.0 release preparation
|
|
135
|
+
- [x] Updated version to 1.0.0
|
|
136
|
+
- [x] Created comprehensive CHANGELOG.md
|
|
137
|
+
- [x] Updated gemspec with proper metadata
|
|
138
|
+
- [x] All 99 tests passing
|
|
139
|
+
- [ ] Publish to RubyGems (deferred)
|
|
140
|
+
|
|
141
|
+
## Phase 15: Essential Web Features
|
|
142
|
+
- [ ] RSS/Atom feed generation (for blog posts)
|
|
143
|
+
- [ ] Sitemap.xml generation (for SEO)
|
|
144
|
+
- [ ] SEO meta tag helpers (Open Graph, Twitter Cards)
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Nice-to-Have Features (Future)
|
|
149
|
+
- [ ] Search index generation (JSON for client-side search)
|
|
150
|
+
- [ ] Git integration (simple deploy command wrapper)
|
|
151
|
+
|
|
152
|
+
## Won't Implement (Against Project Philosophy)
|
|
153
|
+
- ~~Multiple template engine support~~ - ERB is sufficient, adds complexity
|
|
154
|
+
- ~~Image lazy loading helpers~~ - Can be done in templates/CSS
|
|
155
|
+
- ~~i18n/l10n support~~ - Too complex, users can handle with folder structure
|
|
156
|
+
- ~~CloudFlare/Netlify deployment helpers~~ - Too specific, manual deploy is simple enough
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Technical Decisions to Make
|
|
161
|
+
- [x] Template engine: ERB (built-in, fast, zero dependencies)
|
|
162
|
+
- [x] Markdown parser: Kramdown (pure Ruby, no compilation issues)
|
|
163
|
+
- [x] Server: Puma (fast, industry standard)
|
|
164
|
+
- [x] File watching: Listen gem (reliable, cross-platform)
|
|
165
|
+
- [ ] Config format: YAML vs TOML vs Ruby DSL? (may skip - convention over configuration)
|
|
166
|
+
- [x] Incremental build strategy: Timestamp-based (mtime checks)
|
|
167
|
+
- [x] Metadata approach: Convention-based (NO frontmatter, derive from structure/content)
|
data/benchmark.rb
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
require 'benchmark'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
require_relative 'lib/jackdaw'
|
|
8
|
+
|
|
9
|
+
# Benchmark Jackdaw performance
|
|
10
|
+
class JackdawBenchmark
|
|
11
|
+
BENCHMARK_DIR = 'benchmark-site.site'
|
|
12
|
+
SIZES = {
|
|
13
|
+
small: { pages: 10, blog_posts: 20 },
|
|
14
|
+
medium: { pages: 50, blog_posts: 100 },
|
|
15
|
+
large: { pages: 100, blog_posts: 500 }
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def initialize(size: :medium)
|
|
19
|
+
@size = size
|
|
20
|
+
@config = SIZES[size]
|
|
21
|
+
@project = Jackdaw::Project.new(BENCHMARK_DIR)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run
|
|
25
|
+
puts "\n#{colorize('🚀 Jackdaw Performance Benchmark', :bold, :magenta)}"
|
|
26
|
+
puts colorize("Site size: #{@size} (#{@config[:pages]} pages, #{@config[:blog_posts]} blog posts)", :cyan)
|
|
27
|
+
puts colorize('=' * 70, :cyan)
|
|
28
|
+
|
|
29
|
+
cleanup
|
|
30
|
+
create_test_site
|
|
31
|
+
run_benchmarks
|
|
32
|
+
cleanup
|
|
33
|
+
|
|
34
|
+
puts "\n#{colorize('✓ Benchmark complete!', :green)}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def create_test_site
|
|
40
|
+
print colorize('Creating test site...', :yellow)
|
|
41
|
+
@project.create!
|
|
42
|
+
create_templates
|
|
43
|
+
create_content
|
|
44
|
+
puts colorize(' Done!', :green)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def create_templates
|
|
48
|
+
# Layout
|
|
49
|
+
File.write(File.join(@project.templates_dir, 'layout.html.erb'), <<~ERB)
|
|
50
|
+
<!DOCTYPE html>
|
|
51
|
+
<html lang="en">
|
|
52
|
+
<head>
|
|
53
|
+
<meta charset="UTF-8">
|
|
54
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
55
|
+
<title><%= title %> - Benchmark Site</title>
|
|
56
|
+
<style>
|
|
57
|
+
body { max-width: 800px; margin: 0 auto; padding: 2rem; font-family: system-ui; line-height: 1.6; }
|
|
58
|
+
nav { margin-bottom: 2rem; }
|
|
59
|
+
</style>
|
|
60
|
+
</head>
|
|
61
|
+
<body>
|
|
62
|
+
<%= content %>
|
|
63
|
+
</body>
|
|
64
|
+
</html>
|
|
65
|
+
ERB
|
|
66
|
+
|
|
67
|
+
# Page template
|
|
68
|
+
File.write(File.join(@project.templates_dir, 'page.html.erb'), <<~ERB)
|
|
69
|
+
<main>
|
|
70
|
+
<%= content %>
|
|
71
|
+
</main>
|
|
72
|
+
ERB
|
|
73
|
+
|
|
74
|
+
# Blog template
|
|
75
|
+
File.write(File.join(@project.templates_dir, 'blog.html.erb'), <<~ERB)
|
|
76
|
+
<article>
|
|
77
|
+
<header>
|
|
78
|
+
<h1><%= title %></h1>
|
|
79
|
+
<time datetime="<%= date %>"><%= date.strftime('%B %d, %Y') %></time>
|
|
80
|
+
</header>
|
|
81
|
+
<%= content %>
|
|
82
|
+
</article>
|
|
83
|
+
ERB
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def create_content
|
|
87
|
+
# Create pages
|
|
88
|
+
@config[:pages].times do |i|
|
|
89
|
+
File.write(File.join(@project.src_dir, "page-#{i}.page.md"), generate_page_content(i))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Create blog posts
|
|
93
|
+
blog_dir = File.join(@project.src_dir, 'blog')
|
|
94
|
+
FileUtils.mkdir_p(blog_dir)
|
|
95
|
+
@config[:blog_posts].times do |i|
|
|
96
|
+
date = Date.today - i
|
|
97
|
+
File.write(File.join(blog_dir, "#{date}-post-#{i}.blog.md"), generate_blog_content(i))
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def generate_page_content(index)
|
|
102
|
+
<<~MD
|
|
103
|
+
# Page #{index}
|
|
104
|
+
|
|
105
|
+
This is test page number #{index} for benchmarking Jackdaw performance.
|
|
106
|
+
|
|
107
|
+
## Section 1
|
|
108
|
+
|
|
109
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
|
110
|
+
|
|
111
|
+
## Section 2
|
|
112
|
+
|
|
113
|
+
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
|
114
|
+
|
|
115
|
+
### Subsection
|
|
116
|
+
|
|
117
|
+
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
|
|
118
|
+
|
|
119
|
+
## Code Example
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
def hello_world
|
|
123
|
+
puts "Hello from page #{index}!"
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Bold text** and *italic text* and `code snippets`.
|
|
128
|
+
MD
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def generate_blog_content(index)
|
|
132
|
+
<<~MD
|
|
133
|
+
# Blog Post #{index}
|
|
134
|
+
|
|
135
|
+
This is test blog post number #{index} for benchmarking.
|
|
136
|
+
|
|
137
|
+
## Introduction
|
|
138
|
+
|
|
139
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec odio. Praesent libero.
|
|
140
|
+
|
|
141
|
+
## Main Content
|
|
142
|
+
|
|
143
|
+
Sed cursus ante dapibus diam. Sed nisi. Nulla quis sem at nibh elementum imperdiet.
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
function testFunction() {
|
|
147
|
+
console.log('Test from post #{index}');
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Conclusion
|
|
153
|
+
|
|
154
|
+
Duis sagittis ipsum. Praesent mauris. Fusce nec tellus sed augue semper porta.
|
|
155
|
+
|
|
156
|
+
- List item 1
|
|
157
|
+
- List item 2
|
|
158
|
+
- List item 3
|
|
159
|
+
MD
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def run_benchmarks
|
|
163
|
+
puts "\n#{colorize('Running benchmarks...', :bold)}\n"
|
|
164
|
+
|
|
165
|
+
# Clean build
|
|
166
|
+
clean_build_time = benchmark_build(clean: true)
|
|
167
|
+
total_files = @config[:pages] + @config[:blog_posts]
|
|
168
|
+
|
|
169
|
+
# Incremental build (no changes)
|
|
170
|
+
incremental_build_time = benchmark_build(clean: false)
|
|
171
|
+
|
|
172
|
+
# Incremental build (with change)
|
|
173
|
+
modify_single_file
|
|
174
|
+
incremental_change_time = benchmark_build(clean: false)
|
|
175
|
+
|
|
176
|
+
# Results
|
|
177
|
+
puts "\n#{colorize('Results:', :bold, :magenta)}"
|
|
178
|
+
puts colorize('-' * 70, :cyan)
|
|
179
|
+
|
|
180
|
+
puts "Total files: #{colorize(total_files.to_s, :cyan)}"
|
|
181
|
+
puts "Clean build time: #{colorize(format('%.3f seconds', clean_build_time), :green)}"
|
|
182
|
+
puts " Files/second: #{colorize(format('%.1f', total_files / clean_build_time), :cyan)}"
|
|
183
|
+
puts "Incremental (no change): #{colorize(format('%.3f seconds', incremental_build_time), :green)}"
|
|
184
|
+
puts " Files/second: #{colorize(format('%.1f', total_files / incremental_build_time), :cyan)}"
|
|
185
|
+
puts "Incremental (1 change): #{colorize(format('%.3f seconds', incremental_change_time), :green)}"
|
|
186
|
+
puts " Speedup vs clean: #{colorize(format('%.1fx', clean_build_time / incremental_change_time), :yellow)}"
|
|
187
|
+
|
|
188
|
+
puts colorize('-' * 70, :cyan)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def benchmark_build(clean: false)
|
|
192
|
+
builder = Jackdaw::Builder.new(@project, { clean: clean })
|
|
193
|
+
|
|
194
|
+
time = Benchmark.realtime do
|
|
195
|
+
builder.build
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
label = clean ? 'Clean build' : 'Incremental build'
|
|
199
|
+
puts " #{colorize(label, :yellow)}: #{colorize(format('%.3fs', time), :green)}"
|
|
200
|
+
|
|
201
|
+
time
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def modify_single_file
|
|
205
|
+
# Touch one file to trigger incremental rebuild
|
|
206
|
+
first_page = File.join(@project.src_dir, 'page-0.page.md')
|
|
207
|
+
content = File.read(first_page)
|
|
208
|
+
File.write(first_page, content + "\n\nUpdated content.")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def cleanup
|
|
212
|
+
FileUtils.rm_rf(BENCHMARK_DIR) if Dir.exist?(BENCHMARK_DIR)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def colorize(text, *colors)
|
|
216
|
+
codes = {
|
|
217
|
+
reset: "\e[0m",
|
|
218
|
+
bold: "\e[1m",
|
|
219
|
+
green: "\e[32m",
|
|
220
|
+
cyan: "\e[36m",
|
|
221
|
+
yellow: "\e[33m",
|
|
222
|
+
magenta: "\e[35m"
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
prefix = colors.map { |c| codes[c] }.join
|
|
226
|
+
"#{prefix}#{text}#{codes[:reset]}"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Run benchmark
|
|
231
|
+
if __FILE__ == $PROGRAM_NAME
|
|
232
|
+
size = ARGV[0]&.to_sym || :medium
|
|
233
|
+
benchmark = JackdawBenchmark.new(size: size)
|
|
234
|
+
benchmark.run
|
|
235
|
+
end
|
data/exe/jackdaw
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jackdaw
|
|
4
|
+
# Orchestrates the build process with incremental builds and parallel processing
|
|
5
|
+
class Builder
|
|
6
|
+
attr_reader :project, :scanner, :renderer, :stats
|
|
7
|
+
|
|
8
|
+
def initialize(project, options = {})
|
|
9
|
+
@project = project
|
|
10
|
+
@scanner = Scanner.new(project)
|
|
11
|
+
@renderer = Renderer.new(project)
|
|
12
|
+
@options = options
|
|
13
|
+
@stats = BuildStats.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Run full build
|
|
17
|
+
def build
|
|
18
|
+
start_time = Time.now
|
|
19
|
+
|
|
20
|
+
# Clean if requested
|
|
21
|
+
clean_output if @options[:clean]
|
|
22
|
+
|
|
23
|
+
# Ensure output directory exists
|
|
24
|
+
FileUtils.mkdir_p(project.output_dir)
|
|
25
|
+
|
|
26
|
+
# Build content files in parallel
|
|
27
|
+
build_content_files
|
|
28
|
+
|
|
29
|
+
# Copy assets in parallel
|
|
30
|
+
copy_assets
|
|
31
|
+
|
|
32
|
+
# Generate feeds and sitemap
|
|
33
|
+
generate_feeds_and_sitemap
|
|
34
|
+
|
|
35
|
+
@stats.total_time = Time.now - start_time
|
|
36
|
+
@stats
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Clean the output directory
|
|
40
|
+
def clean_output
|
|
41
|
+
return unless Dir.exist?(project.output_dir)
|
|
42
|
+
|
|
43
|
+
FileUtils.rm_rf(Dir.glob(File.join(project.output_dir, '*')))
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def build_content_files
|
|
49
|
+
content_files = scanner.content_files
|
|
50
|
+
|
|
51
|
+
# Process files in parallel
|
|
52
|
+
results = Parallel.map(content_files, in_threads: Parallel.processor_count) do |content_file|
|
|
53
|
+
process_content_file(content_file)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Aggregate results
|
|
57
|
+
results.each do |result|
|
|
58
|
+
case result[:status]
|
|
59
|
+
when :built
|
|
60
|
+
@stats.files_built += 1
|
|
61
|
+
when :skipped
|
|
62
|
+
@stats.files_skipped += 1
|
|
63
|
+
when :error
|
|
64
|
+
@stats.errors << result[:error]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def process_content_file(content_file)
|
|
70
|
+
# Check if template exists
|
|
71
|
+
template_file = File.join(project.templates_dir, "#{content_file.type}.html.erb")
|
|
72
|
+
unless File.exist?(template_file)
|
|
73
|
+
warning = "⚠️ Missing template '#{content_file.type}.html.erb' for #{content_file.path}"
|
|
74
|
+
puts "\e[33m#{warning}\e[0m" if @options[:verbose]
|
|
75
|
+
return { status: :error, file: content_file.path,
|
|
76
|
+
error: StandardError.new("Missing template: #{content_file.type}.html.erb") }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Check if we need to rebuild
|
|
80
|
+
return { status: :skipped, file: content_file.path } unless needs_rebuild?(content_file)
|
|
81
|
+
|
|
82
|
+
# Render and write
|
|
83
|
+
html = renderer.render_content(content_file)
|
|
84
|
+
output_file = content_file.output_file
|
|
85
|
+
|
|
86
|
+
FileUtils.mkdir_p(File.dirname(output_file))
|
|
87
|
+
File.write(output_file, html)
|
|
88
|
+
|
|
89
|
+
{ status: :built, file: content_file.path }
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
{ status: :error, file: content_file.path, error: e }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def copy_assets
|
|
95
|
+
asset_files = scanner.asset_files
|
|
96
|
+
|
|
97
|
+
results = Parallel.map(asset_files, in_threads: Parallel.processor_count) do |asset_file|
|
|
98
|
+
next { status: :skipped } unless needs_asset_copy?(asset_file)
|
|
99
|
+
|
|
100
|
+
asset_file.copy!
|
|
101
|
+
{ status: :copied }
|
|
102
|
+
rescue StandardError => e
|
|
103
|
+
{ status: :error, error: e }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
results.each do |result|
|
|
107
|
+
case result[:status]
|
|
108
|
+
when :copied
|
|
109
|
+
@stats.assets_copied += 1
|
|
110
|
+
when :skipped
|
|
111
|
+
@stats.assets_skipped += 1
|
|
112
|
+
when :error
|
|
113
|
+
@stats.errors << result[:error]
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def needs_rebuild?(content_file)
|
|
119
|
+
# Always rebuild if clean build
|
|
120
|
+
return true if @options[:clean]
|
|
121
|
+
|
|
122
|
+
output_file = content_file.output_file
|
|
123
|
+
|
|
124
|
+
# Rebuild if output doesn't exist
|
|
125
|
+
return true unless File.exist?(output_file)
|
|
126
|
+
|
|
127
|
+
output_mtime = File.mtime(output_file)
|
|
128
|
+
|
|
129
|
+
# Check if content file is newer
|
|
130
|
+
return true if content_file.mtime > output_mtime
|
|
131
|
+
|
|
132
|
+
# Check if template is newer
|
|
133
|
+
template_file = find_template_file(content_file.type)
|
|
134
|
+
return true if template_file && File.mtime(template_file) > output_mtime
|
|
135
|
+
|
|
136
|
+
# Check if layout is newer
|
|
137
|
+
layout_file = File.join(project.templates_dir, 'layout.html.erb')
|
|
138
|
+
return true if File.exist?(layout_file) && File.mtime(layout_file) > output_mtime
|
|
139
|
+
|
|
140
|
+
false
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def needs_asset_copy?(asset_file)
|
|
144
|
+
# Always copy if clean build
|
|
145
|
+
return true if @options[:clean]
|
|
146
|
+
|
|
147
|
+
output_file = asset_file.output_file
|
|
148
|
+
|
|
149
|
+
# Copy if output doesn't exist
|
|
150
|
+
return true unless File.exist?(output_file)
|
|
151
|
+
|
|
152
|
+
# Copy if source is newer
|
|
153
|
+
asset_file.mtime > File.mtime(output_file)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def find_template_file(type)
|
|
157
|
+
template_path = File.join(project.templates_dir, "#{type}.html.erb")
|
|
158
|
+
File.exist?(template_path) ? template_path : nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def generate_feeds_and_sitemap
|
|
162
|
+
# Generate RSS/Atom feeds only if there are blog posts
|
|
163
|
+
if has_blog_posts?
|
|
164
|
+
feed_generator = FeedGenerator.new(project)
|
|
165
|
+
feed_generator.generate_rss
|
|
166
|
+
feed_generator.generate_atom
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Always generate sitemap
|
|
170
|
+
sitemap_generator = SitemapGenerator.new(project)
|
|
171
|
+
sitemap_generator.generate
|
|
172
|
+
rescue StandardError => e
|
|
173
|
+
puts "⚠️ Warning: Failed to generate feeds/sitemap: #{e.message}" if @options[:verbose]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def has_blog_posts?
|
|
177
|
+
scanner.content_files.any? { |f| %w[blog post article news].include?(f.type) }
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Tracks build statistics
|
|
182
|
+
class BuildStats
|
|
183
|
+
attr_accessor :files_built, :files_skipped, :assets_copied, :assets_skipped, :errors, :total_time
|
|
184
|
+
|
|
185
|
+
def initialize
|
|
186
|
+
@files_built = 0
|
|
187
|
+
@files_skipped = 0
|
|
188
|
+
@assets_copied = 0
|
|
189
|
+
@assets_skipped = 0
|
|
190
|
+
@errors = []
|
|
191
|
+
@total_time = 0
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def total_files
|
|
195
|
+
files_built + files_skipped
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def total_assets
|
|
199
|
+
assets_copied + assets_skipped
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def success?
|
|
203
|
+
errors.empty?
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
data/lib/jackdaw/cli.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jackdaw
|
|
4
|
+
# Main CLI entry point
|
|
5
|
+
class CLI < Thor
|
|
6
|
+
include CLIHelpers
|
|
7
|
+
|
|
8
|
+
def self.exit_on_failure?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
desc 'build', 'Build the static site'
|
|
13
|
+
method_option :clean, type: :boolean, aliases: '-c', desc: 'Clean output directory before building'
|
|
14
|
+
method_option :watch, type: :boolean, aliases: '-w', desc: 'Watch for changes and rebuild'
|
|
15
|
+
method_option :verbose, type: :boolean, aliases: '-v', desc: 'Verbose output'
|
|
16
|
+
def build
|
|
17
|
+
Commands::Build.new(Project.new, options).execute
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc 'serve', 'Start development server'
|
|
21
|
+
method_option :port, type: :numeric, aliases: '-p', default: 4000, desc: 'Port to run server on'
|
|
22
|
+
method_option :host, type: :string, aliases: '-h', default: 'localhost', desc: 'Host to bind to'
|
|
23
|
+
method_option :livereload, type: :boolean, aliases: '-l', default: true, desc: 'Enable live reload'
|
|
24
|
+
def serve
|
|
25
|
+
Commands::Serve.new(Project.new, options).execute
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc 'create TEMPLATE NAME', 'Create a new content file from template'
|
|
29
|
+
method_option :dated, type: :boolean, desc: 'Add date prefix to filename'
|
|
30
|
+
method_option :no_date, type: :boolean, desc: 'Skip date prefix (overrides default behavior)'
|
|
31
|
+
def create(template, name)
|
|
32
|
+
Commands::Create.new(Project.new, template, name, options).execute
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc 'new NAME', 'Create a new site project'
|
|
36
|
+
def new(name)
|
|
37
|
+
Commands::New.new(name).execute
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
desc 'template SUBCOMMAND', 'Manage templates'
|
|
41
|
+
subcommand 'template', Commands::Template
|
|
42
|
+
|
|
43
|
+
desc 'version', 'Show version'
|
|
44
|
+
def version
|
|
45
|
+
puts "\n#{colorize('Jackdaw', :bold)} #{colorize("v#{Jackdaw::VERSION}", :cyan)}"
|
|
46
|
+
puts colorize('Lightning-fast static site generator', :magenta)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Jackdaw
|
|
4
|
+
# Shared helper methods for CLI output formatting
|
|
5
|
+
module CLIHelpers
|
|
6
|
+
# ANSI color codes for beautiful output
|
|
7
|
+
COLORS = {
|
|
8
|
+
reset: "\e[0m",
|
|
9
|
+
bold: "\e[1m",
|
|
10
|
+
green: "\e[32m",
|
|
11
|
+
cyan: "\e[36m",
|
|
12
|
+
yellow: "\e[33m",
|
|
13
|
+
magenta: "\e[35m",
|
|
14
|
+
blue: "\e[34m"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def colorize(text, color)
|
|
18
|
+
"#{COLORS[color]}#{text}#{COLORS[:reset]}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def success(text)
|
|
22
|
+
puts "#{colorize('✓', :green)} #{text}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def info(text)
|
|
26
|
+
puts "#{colorize('→', :cyan)} #{text}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def header(text)
|
|
30
|
+
puts "\n#{COLORS[:bold]}#{COLORS[:magenta]}#{text}#{COLORS[:reset]}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|