docyard 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -2
  3. data/README.md +80 -33
  4. data/lib/docyard/build/asset_bundler.rb +139 -0
  5. data/lib/docyard/build/file_copier.rb +105 -0
  6. data/lib/docyard/build/sitemap_generator.rb +57 -0
  7. data/lib/docyard/build/static_generator.rb +141 -0
  8. data/lib/docyard/builder.rb +104 -0
  9. data/lib/docyard/cli.rb +19 -0
  10. data/lib/docyard/components/heading_anchor_processor.rb +34 -0
  11. data/lib/docyard/components/table_of_contents_processor.rb +64 -0
  12. data/lib/docyard/components/table_wrapper_processor.rb +18 -0
  13. data/lib/docyard/config.rb +15 -2
  14. data/lib/docyard/icons/phosphor.rb +3 -1
  15. data/lib/docyard/initializer.rb +80 -14
  16. data/lib/docyard/markdown.rb +19 -0
  17. data/lib/docyard/prev_next_builder.rb +159 -0
  18. data/lib/docyard/preview_server.rb +72 -0
  19. data/lib/docyard/rack_application.rb +25 -3
  20. data/lib/docyard/renderer.rb +33 -8
  21. data/lib/docyard/sidebar/config_parser.rb +180 -0
  22. data/lib/docyard/sidebar/item.rb +58 -0
  23. data/lib/docyard/sidebar/renderer.rb +33 -6
  24. data/lib/docyard/sidebar_builder.rb +45 -1
  25. data/lib/docyard/templates/assets/css/components/callout.css +1 -1
  26. data/lib/docyard/templates/assets/css/components/code-block.css +2 -2
  27. data/lib/docyard/templates/assets/css/components/heading-anchor.css +77 -0
  28. data/lib/docyard/templates/assets/css/components/navigation.css +65 -7
  29. data/lib/docyard/templates/assets/css/components/prev-next.css +114 -0
  30. data/lib/docyard/templates/assets/css/components/table-of-contents.css +269 -0
  31. data/lib/docyard/templates/assets/css/components/tabs.css +3 -2
  32. data/lib/docyard/templates/assets/css/components/theme-toggle.css +8 -0
  33. data/lib/docyard/templates/assets/css/layout.css +58 -1
  34. data/lib/docyard/templates/assets/css/markdown.css +20 -11
  35. data/lib/docyard/templates/assets/css/variables.css +1 -0
  36. data/lib/docyard/templates/assets/js/components/heading-anchor.js +90 -0
  37. data/lib/docyard/templates/assets/js/components/navigation.js +225 -0
  38. data/lib/docyard/templates/assets/js/components/table-of-contents.js +301 -0
  39. data/lib/docyard/templates/assets/js/theme.js +2 -185
  40. data/lib/docyard/templates/config/docyard.yml.erb +32 -10
  41. data/lib/docyard/templates/layouts/default.html.erb +10 -2
  42. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +46 -12
  43. data/lib/docyard/templates/markdown/guides/configuration.md.erb +202 -0
  44. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +247 -0
  45. data/lib/docyard/templates/markdown/index.md.erb +55 -59
  46. data/lib/docyard/templates/partials/_heading_anchor.html.erb +1 -0
  47. data/lib/docyard/templates/partials/_nav_group.html.erb +10 -4
  48. data/lib/docyard/templates/partials/_nav_leaf.html.erb +9 -1
  49. data/lib/docyard/templates/partials/_prev_next.html.erb +23 -0
  50. data/lib/docyard/templates/partials/_table_of_contents.html.erb +45 -0
  51. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +8 -0
  52. data/lib/docyard/version.rb +1 -1
  53. data/lib/docyard.rb +8 -0
  54. metadata +67 -10
  55. data/lib/docyard/templates/markdown/components/callouts.md.erb +0 -204
  56. data/lib/docyard/templates/markdown/components/icons.md.erb +0 -125
  57. data/lib/docyard/templates/markdown/components/tabs.md.erb +0 -686
  58. data/lib/docyard/templates/markdown/configuration.md.erb +0 -202
  59. data/lib/docyard/templates/markdown/core-concepts/file-structure.md.erb +0 -61
  60. data/lib/docyard/templates/markdown/core-concepts/markdown.md.erb +0 -90
  61. data/lib/docyard/templates/markdown/getting-started/introduction.md.erb +0 -30
  62. data/lib/docyard/templates/markdown/getting-started/quick-start.md.erb +0 -56
  63. 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: eab96fe7bde5b0471a5a60996f818f5909d159e34a96034506e5095730e64988
4
- data.tar.gz: 319e4251a18b40c28960c2854079420feac035e472bf9f5f03845cccfc310879
3
+ metadata.gz: 52a27eaf396879abae3d0b091e5a488629022bb777838fafc17c0bb07e15d65b
4
+ data.tar.gz: ec6a567e2e411800f67f96351a80f7e60823ac399df8cac7e82b7c3a9a1370b5
5
5
  SHA512:
6
- metadata.gz: 25c8f7d91d19d3905141894e7cf8f2968b7d7d20343f9fda48e5f105f952ba3f627227063a888dd70b7c17187950ac83e649196afbbde215e643e5f723157c1f
7
- data.tar.gz: 338baa09ab053ffba20f745263f6bed124276ce707d8b2adc4d96ebf0a18b0b195f7097edfba8ffcde9265abe7cda3eaefd6710f01e3bf71b3836b87c21eb429
6
+ metadata.gz: 6e0d9995254e35e291250db40c9572d3bde1a76d86433d0fbcfb6e2a8602d6e68ad6be910d7a03036814dcdc0fba64289d68c760d9d6f1336376e2f709ec35b8
7
+ data.tar.gz: 127131f44ea7140f227a8e95468b96e09256b5fced9ef557e37e5a1caefc888adb902a8b2d164474cb1f31ae0ddd4b76b1c4684b39e5125ddce17c84bbd754e7
data/CHANGELOG.md CHANGED
@@ -7,7 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.3.0] - 2025-01-09
10
+ ## [0.5.0] - 2025-11-18
11
+
12
+ ### Added
13
+ - **Table of Contents** - Auto-generated TOC from h2-h4 headings with clickable anchor links and smooth scrolling (#30)
14
+ - **Previous/Next Navigation** - Auto-detection from sidebar order with frontmatter override support and configurable labels (#31)
15
+
16
+ ## [0.4.0] - 2025-11-16
17
+
18
+ ### Added
19
+ - **Static site generation** - Build system with `docyard build` command (#27)
20
+ - **Preview server** - Test builds locally with `docyard preview` command (#27)
21
+ - **Asset bundling** - CSS/JS minification with content hashing for cache busting (#27)
22
+ - **SEO files** - Automatic generation of sitemap.xml and robots.txt (#27)
23
+ - **Base URL support** - Deploy to subdirectories with configurable base_url (#27)
24
+ - **Sidebar customization** - Config-driven navigation with custom ordering, icons, and external links (#26)
25
+ - **Improved init templates** - Practical, helpful templates showcasing all features (#28)
26
+ - **Clean init output** - Minimal, helpful success message with clear next steps (#28)
27
+
28
+ ### Changed
29
+ - Init command now creates focused, practical templates (4 files vs 9 previously) (#28)
30
+ - Templates now only include implemented features (no images/HTML/escaping) (#28)
31
+ - Config file (docyard.yml) is cleaner with better comments and examples (#28)
32
+
33
+ ### Fixed
34
+ - Code block CSS transition performance with GPU acceleration (#25)
35
+ - Component CSS accessibility and performance improvements (#24)
36
+ - Table responsive styling with proper wrapper element (#23)
37
+
38
+ ## [0.3.0] - 2025-11-09
11
39
 
12
40
  ### Added
13
41
  - Configuration system with optional `docyard.yml` file (#20)
@@ -63,7 +91,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
63
91
  - Initial gem structure
64
92
  - Project scaffolding
65
93
 
66
- [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.3.0...HEAD
94
+ [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.5.0...HEAD
95
+ [0.5.0]: https://github.com/sanifhimani/docyard/compare/v0.4.0...v0.5.0
96
+ [0.4.0]: https://github.com/sanifhimani/docyard/compare/v0.3.0...v0.4.0
67
97
  [0.3.0]: https://github.com/sanifhimani/docyard/compare/v0.2.0...v0.3.0
68
98
  [0.2.0]: https://github.com/sanifhimani/docyard/compare/v0.1.0...v0.2.0
69
99
  [0.1.0]: https://github.com/sanifhimani/docyard/compare/v0.0.1...v0.1.0
data/README.md CHANGED
@@ -5,41 +5,59 @@
5
5
 
6
6
  > Documentation generator for Ruby
7
7
 
8
- **Early development** - Core features and components work, but missing search and build command. See [roadmap](#roadmap).
8
+ Build beautiful documentation sites with hot reload, dark mode, and powerful markdown components.
9
9
 
10
10
  ## Features
11
11
 
12
- - **Configuration system** - Optional `docyard.yml` for site metadata, branding, and build settings
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
- - **Hot reload** - Changes appear instantly while you write
22
+ - **Sidebar customization** - Custom ordering, icons, and external links via config
23
+ - **Table of Contents** - Auto-generated TOC with heading anchors and smooth scrolling
24
+ - **Previous/Next navigation** - Auto-detection from sidebar with frontmatter override support
25
+ - **Active page highlighting** - Always know where you are
26
+
27
+ ### Markdown
16
28
  - **GitHub Flavored Markdown** - Tables, task lists, strikethrough
17
- - **Syntax highlighting** - 100+ languages via Rouge
29
+ - **Syntax highlighting** - 100+ languages via Rouge with copy button
18
30
  - **Markdown components**:
19
31
  - **Callouts** - 5 types (note, tip, important, warning, danger) with GitHub alerts syntax
20
32
  - **Tabs** - Code blocks, package managers, and custom tabs with keyboard navigation
21
33
  - **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
34
  - **YAML frontmatter** - Add metadata to your pages
25
- - **Customizable error pages** - Make 404/500 pages your own
35
+
36
+ ### Production
37
+ - **Asset bundling** - Minified CSS/JS with content hashing for cache busting
38
+ - **SEO** - Automatic sitemap.xml and robots.txt generation
39
+ - **Preview server** - Test production builds locally before deploying
40
+ - **Mobile responsive** - Looks great on all devices
26
41
 
27
42
  ## Quick Start
28
43
 
29
44
  ```bash
30
- # Install the gem
45
+ # Install
31
46
  gem install docyard
32
47
 
33
- # Create a new docs project
34
- mkdir my-docs && cd my-docs
48
+ # Initialize
35
49
  docyard init
36
50
 
37
- # Start the dev server
51
+ # Start dev server
38
52
  docyard serve
53
+ # → http://localhost:4200
39
54
 
40
- # Visit http://localhost:4200
55
+ # Build for production
56
+ docyard build
41
57
  ```
42
58
 
59
+ Your site is ready to deploy! Upload the `dist/` folder to any static host.
60
+
43
61
  ## Installation
44
62
 
45
63
  Add to your Gemfile:
@@ -67,21 +85,27 @@ This creates:
67
85
  docs/
68
86
  index.md # Home page
69
87
  getting-started/
70
- introduction.md # Getting started guide
71
- installation.md # Installation instructions
72
- quick-start.md # Quick start guide
73
- core-concepts/
74
- file-structure.md # File structure guide
75
- markdown.md # Markdown guide
88
+ installation.md # Installation guide
89
+ guides/
90
+ markdown-features.md # Markdown features showcase
91
+ configuration.md # Configuration guide
92
+ docyard.yml # Optional configuration
76
93
  ```
77
94
 
78
- ### Start Development Server
95
+ ### Commands
79
96
 
80
97
  ```bash
98
+ # Development server with hot reload
81
99
  docyard serve
82
-
83
- # Custom port and host
84
100
  docyard serve --port 3000 --host 0.0.0.0
101
+
102
+ # Build for production
103
+ docyard build
104
+ docyard build --no-clean # Don't clean output directory
105
+
106
+ # Preview production build
107
+ docyard preview
108
+ docyard preview --port 4001
85
109
  ```
86
110
 
87
111
  ### Writing Docs
@@ -117,6 +141,32 @@ description: Page description
117
141
 
118
142
  Currently supported:
119
143
  - `title` - Page title (shown in `<title>` tag)
144
+ - `prev` - Customize or disable previous link
145
+ - `next` - Customize or disable next link
146
+
147
+ ### Customizing Navigation
148
+
149
+ Control previous/next links per page via frontmatter:
150
+
151
+ ```yaml
152
+ ---
153
+ title: My Page
154
+ prev: false # Disable previous link
155
+ next:
156
+ text: Custom Next Page
157
+ link: /custom-path
158
+ ---
159
+ ```
160
+
161
+ Configure labels globally in `docyard.yml`:
162
+
163
+ ```yaml
164
+ navigation:
165
+ footer:
166
+ enabled: true
167
+ prev_text: "← Back"
168
+ next_text: "Forward →"
169
+ ```
120
170
 
121
171
  ### Linking Between Pages
122
172
 
@@ -205,18 +255,15 @@ bundle exec rubocop
205
255
 
206
256
  ## Roadmap
207
257
 
208
- **v0.3.0 - Recently shipped:**
209
- - Configuration system (docyard.yml)
210
- - Logo and favicon support
211
- - Dark mode with theme toggle
212
- - Icon system (24 Phosphor icons)
213
- - Callouts/Admonitions
214
- - Tabs component
215
- - Copy button for code blocks
216
-
217
- **Next up (v0.4.0):**
218
- - Sidebar customization
219
- - Static site generation (`docyard build`)
258
+ **v0.5.0 - Just shipped:**
259
+ - Table of Contents with heading anchors
260
+ - Previous/Next page navigation with auto-detection
261
+
262
+ **Next up (v0.6.0+):**
263
+ - Code block enhancements (line numbers, highlighting, diffs)
264
+ - Search functionality (client-side with Cmd/K)
265
+ - Details/collapsible blocks
266
+ - More markdown extensions
220
267
 
221
268
  ## Contributing
222
269
 
@@ -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