docyard 0.3.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +24 -1
- data/README.md +55 -33
- data/lib/docyard/build/asset_bundler.rb +139 -0
- data/lib/docyard/build/file_copier.rb +105 -0
- data/lib/docyard/build/sitemap_generator.rb +57 -0
- data/lib/docyard/build/static_generator.rb +141 -0
- data/lib/docyard/builder.rb +104 -0
- data/lib/docyard/cli.rb +19 -0
- data/lib/docyard/components/table_wrapper_processor.rb +18 -0
- data/lib/docyard/config.rb +4 -2
- data/lib/docyard/icons/phosphor.rb +1 -0
- data/lib/docyard/initializer.rb +80 -14
- data/lib/docyard/markdown.rb +13 -0
- data/lib/docyard/preview_server.rb +72 -0
- data/lib/docyard/rack_application.rb +1 -1
- data/lib/docyard/renderer.rb +17 -3
- data/lib/docyard/sidebar/config_parser.rb +180 -0
- data/lib/docyard/sidebar/item.rb +58 -0
- data/lib/docyard/sidebar/renderer.rb +33 -6
- data/lib/docyard/sidebar_builder.rb +45 -1
- data/lib/docyard/templates/assets/css/components/callout.css +1 -1
- data/lib/docyard/templates/assets/css/components/code-block.css +2 -2
- data/lib/docyard/templates/assets/css/components/navigation.css +65 -7
- data/lib/docyard/templates/assets/css/components/tabs.css +3 -2
- data/lib/docyard/templates/assets/css/components/theme-toggle.css +8 -0
- data/lib/docyard/templates/assets/css/markdown.css +20 -11
- data/lib/docyard/templates/assets/js/components/navigation.js +221 -0
- data/lib/docyard/templates/assets/js/theme.js +2 -185
- data/lib/docyard/templates/config/docyard.yml.erb +32 -10
- data/lib/docyard/templates/layouts/default.html.erb +1 -1
- data/lib/docyard/templates/markdown/getting-started/installation.md.erb +46 -12
- data/lib/docyard/templates/markdown/guides/configuration.md.erb +202 -0
- data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +247 -0
- data/lib/docyard/templates/markdown/index.md.erb +55 -59
- data/lib/docyard/templates/partials/_nav_group.html.erb +10 -4
- data/lib/docyard/templates/partials/_nav_leaf.html.erb +9 -1
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +8 -0
- metadata +55 -10
- data/lib/docyard/templates/markdown/components/callouts.md.erb +0 -204
- data/lib/docyard/templates/markdown/components/icons.md.erb +0 -125
- data/lib/docyard/templates/markdown/components/tabs.md.erb +0 -686
- data/lib/docyard/templates/markdown/configuration.md.erb +0 -202
- data/lib/docyard/templates/markdown/core-concepts/file-structure.md.erb +0 -61
- data/lib/docyard/templates/markdown/core-concepts/markdown.md.erb +0 -90
- data/lib/docyard/templates/markdown/getting-started/introduction.md.erb +0 -30
- data/lib/docyard/templates/markdown/getting-started/quick-start.md.erb +0 -56
- data/lib/docyard/templates/partials/_icons.html.erb +0 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db5ddabfe4a3b7bf25c24876b915188bd4768ca543772ad0a8cfc971ee791fee
|
|
4
|
+
data.tar.gz: d906b1c2ab33102c9ade6e2cb23f6d067fdbf240d6a4a8e2aa27a52a9ba62b42
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2abc1d991cade7059cfaa0b54c2b6f88b33625937e8073cee9d8f61948149567a209a378efd225e9fe85b45b7579ef4e1d81599be5a8cbee970e9cb6e5218a3c
|
|
7
|
+
data.tar.gz: dcd2baa65fcdd4c00825c6f895c035ffd53603ec22503233b600925084b6512b651c9a009687682f1f5cab3a0c8d8e3d0a5c7586634d6da174e4e4c5a95f9a53
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2025-01-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Static site generation** - Build system with `docyard build` command (#27)
|
|
14
|
+
- **Preview server** - Test builds locally with `docyard preview` command (#27)
|
|
15
|
+
- **Asset bundling** - CSS/JS minification with content hashing for cache busting (#27)
|
|
16
|
+
- **SEO files** - Automatic generation of sitemap.xml and robots.txt (#27)
|
|
17
|
+
- **Base URL support** - Deploy to subdirectories with configurable base_url (#27)
|
|
18
|
+
- **Sidebar customization** - Config-driven navigation with custom ordering, icons, and external links (#26)
|
|
19
|
+
- **Improved init templates** - Practical, helpful templates showcasing all features (#28)
|
|
20
|
+
- **Clean init output** - Minimal, helpful success message with clear next steps (#28)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Init command now creates focused, practical templates (4 files vs 9 previously) (#28)
|
|
24
|
+
- Templates now only include implemented features (no images/HTML/escaping) (#28)
|
|
25
|
+
- Config file (docyard.yml) is cleaner with better comments and examples (#28)
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
- Code block CSS transition performance with GPU acceleration (#25)
|
|
29
|
+
- Component CSS accessibility and performance improvements (#24)
|
|
30
|
+
- Table responsive styling with proper wrapper element (#23)
|
|
31
|
+
|
|
10
32
|
## [0.3.0] - 2025-01-09
|
|
11
33
|
|
|
12
34
|
### Added
|
|
@@ -63,7 +85,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
63
85
|
- Initial gem structure
|
|
64
86
|
- Project scaffolding
|
|
65
87
|
|
|
66
|
-
[Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.
|
|
88
|
+
[Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.4.0...HEAD
|
|
89
|
+
[0.4.0]: https://github.com/sanifhimani/docyard/compare/v0.3.0...v0.4.0
|
|
67
90
|
[0.3.0]: https://github.com/sanifhimani/docyard/compare/v0.2.0...v0.3.0
|
|
68
91
|
[0.2.0]: https://github.com/sanifhimani/docyard/compare/v0.1.0...v0.2.0
|
|
69
92
|
[0.1.0]: https://github.com/sanifhimani/docyard/compare/v0.0.1...v0.1.0
|
data/README.md
CHANGED
|
@@ -5,41 +5,57 @@
|
|
|
5
5
|
|
|
6
6
|
> Documentation generator for Ruby
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Build beautiful documentation sites with hot reload, dark mode, and powerful markdown components.
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
### Core
|
|
13
|
+
- **Static site generation** - Build static sites with `docyard build`
|
|
14
|
+
- **Hot reload** - Changes appear instantly while you write
|
|
13
15
|
- **Dark mode** - Beautiful light/dark theme with system preference detection
|
|
16
|
+
- **Configuration system** - Optional `docyard.yml` for site metadata, branding, and build settings
|
|
17
|
+
- **Custom branding** - Logo and favicon with light/dark mode support
|
|
18
|
+
- **Base URL support** - Deploy to subdirectories or custom paths
|
|
19
|
+
|
|
20
|
+
### Navigation
|
|
14
21
|
- **Sidebar navigation** - Automatic sidebar with nested folders and collapsible sections
|
|
15
|
-
- **
|
|
22
|
+
- **Sidebar customization** - Custom ordering, icons, and external links via config
|
|
23
|
+
- **Active page highlighting** - Always know where you are
|
|
24
|
+
|
|
25
|
+
### Markdown
|
|
16
26
|
- **GitHub Flavored Markdown** - Tables, task lists, strikethrough
|
|
17
|
-
- **Syntax highlighting** - 100+ languages via Rouge
|
|
27
|
+
- **Syntax highlighting** - 100+ languages via Rouge with copy button
|
|
18
28
|
- **Markdown components**:
|
|
19
29
|
- **Callouts** - 5 types (note, tip, important, warning, danger) with GitHub alerts syntax
|
|
20
30
|
- **Tabs** - Code blocks, package managers, and custom tabs with keyboard navigation
|
|
21
31
|
- **Icons** - 24 Phosphor icons with `:icon:` syntax
|
|
22
|
-
- **Code block enhancements** - Copy button with visual feedback
|
|
23
|
-
- **Custom branding** - Logo and favicon with light/dark mode support
|
|
24
32
|
- **YAML frontmatter** - Add metadata to your pages
|
|
25
|
-
|
|
33
|
+
|
|
34
|
+
### Production
|
|
35
|
+
- **Asset bundling** - Minified CSS/JS with content hashing for cache busting
|
|
36
|
+
- **SEO** - Automatic sitemap.xml and robots.txt generation
|
|
37
|
+
- **Preview server** - Test production builds locally before deploying
|
|
38
|
+
- **Mobile responsive** - Looks great on all devices
|
|
26
39
|
|
|
27
40
|
## Quick Start
|
|
28
41
|
|
|
29
42
|
```bash
|
|
30
|
-
# Install
|
|
43
|
+
# Install
|
|
31
44
|
gem install docyard
|
|
32
45
|
|
|
33
|
-
#
|
|
34
|
-
mkdir my-docs && cd my-docs
|
|
46
|
+
# Initialize
|
|
35
47
|
docyard init
|
|
36
48
|
|
|
37
|
-
# Start
|
|
49
|
+
# Start dev server
|
|
38
50
|
docyard serve
|
|
51
|
+
# → http://localhost:4200
|
|
39
52
|
|
|
40
|
-
#
|
|
53
|
+
# Build for production
|
|
54
|
+
docyard build
|
|
41
55
|
```
|
|
42
56
|
|
|
57
|
+
Your site is ready to deploy! Upload the `dist/` folder to any static host.
|
|
58
|
+
|
|
43
59
|
## Installation
|
|
44
60
|
|
|
45
61
|
Add to your Gemfile:
|
|
@@ -67,21 +83,27 @@ This creates:
|
|
|
67
83
|
docs/
|
|
68
84
|
index.md # Home page
|
|
69
85
|
getting-started/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
markdown.md # Markdown guide
|
|
86
|
+
installation.md # Installation guide
|
|
87
|
+
guides/
|
|
88
|
+
markdown-features.md # Markdown features showcase
|
|
89
|
+
configuration.md # Configuration guide
|
|
90
|
+
docyard.yml # Optional configuration
|
|
76
91
|
```
|
|
77
92
|
|
|
78
|
-
###
|
|
93
|
+
### Commands
|
|
79
94
|
|
|
80
95
|
```bash
|
|
96
|
+
# Development server with hot reload
|
|
81
97
|
docyard serve
|
|
82
|
-
|
|
83
|
-
# Custom port and host
|
|
84
98
|
docyard serve --port 3000 --host 0.0.0.0
|
|
99
|
+
|
|
100
|
+
# Build for production
|
|
101
|
+
docyard build
|
|
102
|
+
docyard build --no-clean # Don't clean output directory
|
|
103
|
+
|
|
104
|
+
# Preview production build
|
|
105
|
+
docyard preview
|
|
106
|
+
docyard preview --port 4001
|
|
85
107
|
```
|
|
86
108
|
|
|
87
109
|
### Writing Docs
|
|
@@ -205,18 +227,18 @@ bundle exec rubocop
|
|
|
205
227
|
|
|
206
228
|
## Roadmap
|
|
207
229
|
|
|
208
|
-
**v0.
|
|
209
|
-
-
|
|
210
|
-
-
|
|
211
|
-
-
|
|
212
|
-
-
|
|
213
|
-
-
|
|
214
|
-
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
-
|
|
219
|
-
-
|
|
230
|
+
**v0.4.0 - Just shipped:**
|
|
231
|
+
- Static site generation with `docyard build`
|
|
232
|
+
- Asset bundling with minification and cache busting
|
|
233
|
+
- SEO files (sitemap.xml, robots.txt)
|
|
234
|
+
- Preview server for testing builds
|
|
235
|
+
- Sidebar customization via config
|
|
236
|
+
- Improved init templates
|
|
237
|
+
|
|
238
|
+
**Next up:**
|
|
239
|
+
- Search functionality
|
|
240
|
+
- Table of contents
|
|
241
|
+
- More markdown components
|
|
220
242
|
|
|
221
243
|
## Contributing
|
|
222
244
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cssminify"
|
|
4
|
+
require "terser"
|
|
5
|
+
require "digest"
|
|
6
|
+
|
|
7
|
+
module Docyard
|
|
8
|
+
module Build
|
|
9
|
+
class AssetBundler
|
|
10
|
+
ASSETS_PATH = File.join(__dir__, "..", "templates", "assets")
|
|
11
|
+
|
|
12
|
+
attr_reader :config, :verbose
|
|
13
|
+
|
|
14
|
+
def initialize(config, verbose: false)
|
|
15
|
+
@config = config
|
|
16
|
+
@verbose = verbose
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def bundle
|
|
20
|
+
puts "\nBundling assets..."
|
|
21
|
+
|
|
22
|
+
css_hash = bundle_css
|
|
23
|
+
js_hash = bundle_js
|
|
24
|
+
|
|
25
|
+
update_html_references(css_hash, js_hash)
|
|
26
|
+
|
|
27
|
+
2
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def bundle_css
|
|
33
|
+
log " Bundling CSS..."
|
|
34
|
+
|
|
35
|
+
main_css = File.read(File.join(ASSETS_PATH, "css", "main.css"))
|
|
36
|
+
css_content = resolve_css_imports(main_css)
|
|
37
|
+
minified = CSSminify.compress(css_content)
|
|
38
|
+
hash = generate_hash(minified)
|
|
39
|
+
|
|
40
|
+
write_bundled_asset(minified, hash, "css")
|
|
41
|
+
log_compression_stats(css_content, minified, "CSS")
|
|
42
|
+
|
|
43
|
+
hash
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def resolve_css_imports(css_content)
|
|
47
|
+
css_content.gsub(/@import url\('([^']+)'\);/) do |match|
|
|
48
|
+
import_file = Regexp.last_match(1)
|
|
49
|
+
|
|
50
|
+
if import_file == "components.css"
|
|
51
|
+
concatenate_component_css
|
|
52
|
+
else
|
|
53
|
+
file_path = File.join(ASSETS_PATH, "css", import_file)
|
|
54
|
+
File.exist?(file_path) ? File.read(file_path) : match
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def concatenate_component_css
|
|
60
|
+
components_dir = File.join(ASSETS_PATH, "css", "components")
|
|
61
|
+
return "" unless Dir.exist?(components_dir)
|
|
62
|
+
|
|
63
|
+
css_files = Dir.glob(File.join(components_dir, "*.css"))
|
|
64
|
+
css_files.map { |file| File.read(file) }.join("\n\n")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def bundle_js
|
|
68
|
+
log " Bundling JS..."
|
|
69
|
+
|
|
70
|
+
theme_js = File.read(File.join(ASSETS_PATH, "js", "theme.js"))
|
|
71
|
+
components_js = concatenate_component_js
|
|
72
|
+
js_content = [theme_js, components_js].join("\n")
|
|
73
|
+
minified = Terser.compile(js_content)
|
|
74
|
+
hash = generate_hash(minified)
|
|
75
|
+
|
|
76
|
+
write_bundled_asset(minified, hash, "js")
|
|
77
|
+
log_compression_stats(js_content, minified, "JS")
|
|
78
|
+
|
|
79
|
+
hash
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def concatenate_component_js
|
|
83
|
+
components_dir = File.join(ASSETS_PATH, "js", "components")
|
|
84
|
+
return "" unless Dir.exist?(components_dir)
|
|
85
|
+
|
|
86
|
+
js_files = Dir.glob(File.join(components_dir, "*.js"))
|
|
87
|
+
js_files.map { |file| File.read(file) }.join("\n\n")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def generate_hash(content)
|
|
91
|
+
Digest::MD5.hexdigest(content)[0..7]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def update_html_references(css_hash, js_hash)
|
|
95
|
+
html_files = Dir.glob(File.join(config.build.output_dir, "**", "*.html"))
|
|
96
|
+
base_url = normalize_base_url(config.build.base_url)
|
|
97
|
+
|
|
98
|
+
html_files.each do |file|
|
|
99
|
+
content = replace_asset_references(File.read(file), css_hash, js_hash, base_url)
|
|
100
|
+
File.write(file, content)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
log " [✓] Updated asset references in #{html_files.size} HTML files"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def replace_asset_references(content, css_hash, js_hash, base_url)
|
|
107
|
+
content.gsub(%r{/assets/css/main\.css}, "#{base_url}assets/bundle.#{css_hash}.css")
|
|
108
|
+
.gsub(%r{/assets/js/theme\.js}, "#{base_url}assets/bundle.#{js_hash}.js")
|
|
109
|
+
.gsub(%r{/assets/js/components\.js}, "")
|
|
110
|
+
.gsub(%r{<script src="/assets/js/reload\.js"></script>}, "")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def write_bundled_asset(content, hash, extension)
|
|
114
|
+
filename = "bundle.#{hash}.#{extension}"
|
|
115
|
+
output_path = File.join(config.build.output_dir, "assets", filename)
|
|
116
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
117
|
+
File.write(output_path, content)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def log_compression_stats(original, minified, label)
|
|
121
|
+
original_size = (original.bytesize / 1024.0).round(1)
|
|
122
|
+
minified_size = (minified.bytesize / 1024.0).round(1)
|
|
123
|
+
reduction = (((original_size - minified_size) / original_size) * 100).round(0)
|
|
124
|
+
log " [✓] #{label}: #{original_size} KB -> #{minified_size} KB (-#{reduction}%)"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def normalize_base_url(url)
|
|
128
|
+
return "/" if url.nil? || url.empty? || url == "/"
|
|
129
|
+
|
|
130
|
+
url = "/#{url}" unless url.start_with?("/")
|
|
131
|
+
url.end_with?("/") ? url : "#{url}/"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def log(message)
|
|
135
|
+
puts message
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Build
|
|
5
|
+
class FileCopier
|
|
6
|
+
attr_reader :config, :verbose
|
|
7
|
+
|
|
8
|
+
def initialize(config, verbose: false)
|
|
9
|
+
@config = config
|
|
10
|
+
@verbose = verbose
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def copy
|
|
14
|
+
puts "\nCopying static assets..."
|
|
15
|
+
|
|
16
|
+
count = 0
|
|
17
|
+
count += copy_user_assets
|
|
18
|
+
count += copy_branding_assets
|
|
19
|
+
|
|
20
|
+
log "[✓] Copied #{count} static files"
|
|
21
|
+
count
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def copy_user_assets
|
|
27
|
+
user_assets_dir = "docs/assets"
|
|
28
|
+
return 0 unless Dir.exist?(user_assets_dir)
|
|
29
|
+
|
|
30
|
+
output_assets_dir = File.join(config.build.output_dir, "assets")
|
|
31
|
+
FileUtils.mkdir_p(output_assets_dir)
|
|
32
|
+
|
|
33
|
+
files = find_user_asset_files(user_assets_dir)
|
|
34
|
+
files.each { |file| copy_single_asset(file, "docs/assets/", output_assets_dir) }
|
|
35
|
+
|
|
36
|
+
log "[✓] Copied #{files.size} user assets from docs/assets/" if files.any?
|
|
37
|
+
files.size
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def find_user_asset_files(assets_dir)
|
|
41
|
+
Dir.glob(File.join(assets_dir, "**", "*")).select { |f| File.file?(f) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def copy_single_asset(file, prefix, output_dir)
|
|
45
|
+
relative_path = file.delete_prefix(prefix)
|
|
46
|
+
dest_path = File.join(output_dir, relative_path)
|
|
47
|
+
|
|
48
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
49
|
+
FileUtils.cp(file, dest_path)
|
|
50
|
+
|
|
51
|
+
log " Copied: #{relative_path}" if verbose
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def copy_branding_assets
|
|
55
|
+
count = 0
|
|
56
|
+
count += copy_default_branding_assets
|
|
57
|
+
count += copy_user_branding_assets
|
|
58
|
+
log "[✓] Copied #{count} branding assets" if count.positive?
|
|
59
|
+
count
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def copy_default_branding_assets
|
|
63
|
+
templates_assets = File.join(__dir__, "..", "templates", "assets")
|
|
64
|
+
count = 0
|
|
65
|
+
|
|
66
|
+
["logo.svg", "logo-dark.svg", "favicon.svg"].each do |asset_file|
|
|
67
|
+
source_path = File.join(templates_assets, asset_file)
|
|
68
|
+
next unless File.exist?(source_path)
|
|
69
|
+
|
|
70
|
+
dest_path = File.join(config.build.output_dir, "assets", asset_file)
|
|
71
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
72
|
+
FileUtils.cp(source_path, dest_path)
|
|
73
|
+
|
|
74
|
+
log " Copied default branding: #{asset_file}" if verbose
|
|
75
|
+
count += 1
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
count
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def copy_user_branding_assets
|
|
82
|
+
%w[logo logo_dark favicon].sum { |asset_key| copy_single_branding_asset(asset_key) }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def copy_single_branding_asset(asset_key)
|
|
86
|
+
asset_path = config.branding.send(asset_key)
|
|
87
|
+
return 0 if asset_path.nil? || asset_path.start_with?("http://", "https://")
|
|
88
|
+
|
|
89
|
+
full_path = File.join("docs", asset_path)
|
|
90
|
+
return 0 unless File.exist?(full_path)
|
|
91
|
+
|
|
92
|
+
dest_path = File.join(config.build.output_dir, "assets", asset_path)
|
|
93
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
94
|
+
FileUtils.cp(full_path, dest_path)
|
|
95
|
+
|
|
96
|
+
log " Copied user branding: #{asset_path}" if verbose
|
|
97
|
+
1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def log(message)
|
|
101
|
+
puts message
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Build
|
|
7
|
+
class SitemapGenerator
|
|
8
|
+
attr_reader :config
|
|
9
|
+
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate
|
|
15
|
+
urls = collect_urls
|
|
16
|
+
sitemap_content = build_sitemap(urls)
|
|
17
|
+
|
|
18
|
+
output_path = File.join(config.build.output_dir, "sitemap.xml")
|
|
19
|
+
File.write(output_path, sitemap_content)
|
|
20
|
+
|
|
21
|
+
puts "[✓] Generated sitemap.xml (#{urls.size} URLs)"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def collect_urls
|
|
27
|
+
html_files = Dir.glob(File.join(config.build.output_dir, "**", "index.html"))
|
|
28
|
+
|
|
29
|
+
html_files.map do |file|
|
|
30
|
+
relative_path = file.delete_prefix(config.build.output_dir).delete_suffix("/index.html")
|
|
31
|
+
url_path = relative_path.empty? ? "/" : relative_path
|
|
32
|
+
lastmod = File.mtime(file).utc.iso8601
|
|
33
|
+
|
|
34
|
+
{ loc: url_path, lastmod: lastmod }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build_sitemap(urls)
|
|
39
|
+
base_url = config.build.base_url
|
|
40
|
+
base_url = base_url.chop if base_url.end_with?("/")
|
|
41
|
+
|
|
42
|
+
xml = ['<?xml version="1.0" encoding="UTF-8"?>']
|
|
43
|
+
xml << '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
|
|
44
|
+
|
|
45
|
+
urls.each do |url|
|
|
46
|
+
xml << " <url>"
|
|
47
|
+
xml << " <loc>#{base_url}#{url[:loc]}</loc>"
|
|
48
|
+
xml << " <lastmod>#{url[:lastmod]}</lastmod>"
|
|
49
|
+
xml << " </url>"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
xml << "</urlset>"
|
|
53
|
+
xml.join("\n")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-progressbar"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Build
|
|
7
|
+
class StaticGenerator
|
|
8
|
+
attr_reader :config, :verbose, :renderer
|
|
9
|
+
|
|
10
|
+
def initialize(config, verbose: false)
|
|
11
|
+
@config = config
|
|
12
|
+
@verbose = verbose
|
|
13
|
+
@renderer = Renderer.new(base_url: config.build.base_url)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def generate
|
|
17
|
+
markdown_files = collect_markdown_files
|
|
18
|
+
puts "\n[✓] Found #{markdown_files.size} markdown files"
|
|
19
|
+
|
|
20
|
+
progress = TTY::ProgressBar.new(
|
|
21
|
+
"Generating pages [:bar] :current/:total (:percent%)",
|
|
22
|
+
total: markdown_files.size,
|
|
23
|
+
width: 50
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
markdown_files.each do |file_path|
|
|
27
|
+
generate_page(file_path)
|
|
28
|
+
progress.advance
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
markdown_files.size
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def collect_markdown_files
|
|
37
|
+
Dir.glob(File.join("docs", "**", "*.md"))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def generate_page(markdown_file_path)
|
|
41
|
+
output_path = determine_output_path(markdown_file_path)
|
|
42
|
+
current_path = determine_current_path(markdown_file_path)
|
|
43
|
+
|
|
44
|
+
sidebar_html = build_sidebar(current_path)
|
|
45
|
+
html_content = renderer.render_file(
|
|
46
|
+
markdown_file_path,
|
|
47
|
+
sidebar_html: sidebar_html,
|
|
48
|
+
branding: branding_options
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
52
|
+
File.write(output_path, html_content)
|
|
53
|
+
|
|
54
|
+
log "Generated: #{output_path}" if verbose
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def determine_output_path(markdown_file_path)
|
|
58
|
+
relative_path = markdown_file_path.delete_prefix("docs/")
|
|
59
|
+
base_name = File.basename(relative_path, ".md")
|
|
60
|
+
dir_name = File.dirname(relative_path)
|
|
61
|
+
|
|
62
|
+
output_dir = config.build.output_dir
|
|
63
|
+
|
|
64
|
+
if base_name == "index"
|
|
65
|
+
File.join(output_dir, dir_name, "index.html")
|
|
66
|
+
else
|
|
67
|
+
File.join(output_dir, dir_name, base_name, "index.html")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def determine_current_path(markdown_file_path)
|
|
72
|
+
relative_path = markdown_file_path.delete_prefix("docs/")
|
|
73
|
+
base_name = File.basename(relative_path, ".md")
|
|
74
|
+
dir_name = File.dirname(relative_path)
|
|
75
|
+
|
|
76
|
+
if base_name == "index"
|
|
77
|
+
dir_name == "." ? "/" : "/#{dir_name}"
|
|
78
|
+
else
|
|
79
|
+
dir_name == "." ? "/#{base_name}" : "/#{dir_name}/#{base_name}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_sidebar(current_path)
|
|
84
|
+
SidebarBuilder.new(
|
|
85
|
+
docs_path: "docs",
|
|
86
|
+
current_path: current_path,
|
|
87
|
+
config: config
|
|
88
|
+
).to_html
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def branding_options
|
|
92
|
+
default_branding.merge(config_branding_options)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def default_branding
|
|
96
|
+
{
|
|
97
|
+
site_title: Constants::DEFAULT_SITE_TITLE,
|
|
98
|
+
site_description: "",
|
|
99
|
+
logo: Constants::DEFAULT_LOGO_PATH,
|
|
100
|
+
logo_dark: Constants::DEFAULT_LOGO_DARK_PATH,
|
|
101
|
+
favicon: nil,
|
|
102
|
+
display_logo: true,
|
|
103
|
+
display_title: true
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def config_branding_options
|
|
108
|
+
site = config.site
|
|
109
|
+
branding = config.branding
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
site_title: site.title || Constants::DEFAULT_SITE_TITLE,
|
|
113
|
+
site_description: site.description || "",
|
|
114
|
+
logo: resolve_logo(branding.logo, branding.logo_dark),
|
|
115
|
+
logo_dark: resolve_logo_dark(branding.logo, branding.logo_dark),
|
|
116
|
+
favicon: branding.favicon
|
|
117
|
+
}.merge(appearance_options(branding.appearance))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def appearance_options(appearance)
|
|
121
|
+
appearance ||= {}
|
|
122
|
+
{
|
|
123
|
+
display_logo: appearance["logo"] != false,
|
|
124
|
+
display_title: appearance["title"] != false
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def resolve_logo(logo, logo_dark)
|
|
129
|
+
logo || logo_dark || Constants::DEFAULT_LOGO_PATH
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def resolve_logo_dark(logo, logo_dark)
|
|
133
|
+
logo_dark || logo || Constants::DEFAULT_LOGO_DARK_PATH
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def log(message)
|
|
137
|
+
puts message if verbose
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|