docyard 0.2.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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -1
  3. data/LICENSE.vscode-icons +42 -0
  4. data/README.md +86 -23
  5. data/lib/docyard/asset_handler.rb +33 -0
  6. data/lib/docyard/build/asset_bundler.rb +139 -0
  7. data/lib/docyard/build/file_copier.rb +105 -0
  8. data/lib/docyard/build/sitemap_generator.rb +57 -0
  9. data/lib/docyard/build/static_generator.rb +141 -0
  10. data/lib/docyard/builder.rb +104 -0
  11. data/lib/docyard/cli.rb +19 -0
  12. data/lib/docyard/components/base_processor.rb +24 -0
  13. data/lib/docyard/components/callout_processor.rb +121 -0
  14. data/lib/docyard/components/code_block_processor.rb +55 -0
  15. data/lib/docyard/components/code_detector.rb +59 -0
  16. data/lib/docyard/components/icon_detector.rb +57 -0
  17. data/lib/docyard/components/icon_processor.rb +51 -0
  18. data/lib/docyard/components/registry.rb +34 -0
  19. data/lib/docyard/components/table_wrapper_processor.rb +18 -0
  20. data/lib/docyard/components/tabs_parser.rb +60 -0
  21. data/lib/docyard/components/tabs_processor.rb +44 -0
  22. data/lib/docyard/config/validator.rb +171 -0
  23. data/lib/docyard/config.rb +135 -0
  24. data/lib/docyard/constants.rb +5 -0
  25. data/lib/docyard/icons/LICENSE.phosphor +21 -0
  26. data/lib/docyard/icons/file_types.rb +92 -0
  27. data/lib/docyard/icons/phosphor.rb +64 -0
  28. data/lib/docyard/icons.rb +40 -0
  29. data/lib/docyard/initializer.rb +93 -9
  30. data/lib/docyard/language_mapping.rb +52 -0
  31. data/lib/docyard/markdown.rb +27 -3
  32. data/lib/docyard/preview_server.rb +72 -0
  33. data/lib/docyard/rack_application.rb +77 -8
  34. data/lib/docyard/renderer.rb +56 -9
  35. data/lib/docyard/server.rb +5 -2
  36. data/lib/docyard/sidebar/config_parser.rb +180 -0
  37. data/lib/docyard/sidebar/item.rb +58 -0
  38. data/lib/docyard/sidebar/renderer.rb +33 -6
  39. data/lib/docyard/sidebar_builder.rb +54 -2
  40. data/lib/docyard/templates/assets/css/code.css +150 -2
  41. data/lib/docyard/templates/assets/css/components/callout.css +169 -0
  42. data/lib/docyard/templates/assets/css/components/code-block.css +196 -0
  43. data/lib/docyard/templates/assets/css/components/icon.css +16 -0
  44. data/lib/docyard/templates/assets/css/components/logo.css +44 -0
  45. data/lib/docyard/templates/assets/css/{components.css → components/navigation.css} +111 -53
  46. data/lib/docyard/templates/assets/css/components/tabs.css +299 -0
  47. data/lib/docyard/templates/assets/css/components/theme-toggle.css +69 -0
  48. data/lib/docyard/templates/assets/css/layout.css +14 -4
  49. data/lib/docyard/templates/assets/css/markdown.css +27 -17
  50. data/lib/docyard/templates/assets/css/reset.css +4 -0
  51. data/lib/docyard/templates/assets/css/variables.css +94 -3
  52. data/lib/docyard/templates/assets/favicon.svg +16 -0
  53. data/lib/docyard/templates/assets/js/components/code-block.js +162 -0
  54. data/lib/docyard/templates/assets/js/components/navigation.js +221 -0
  55. data/lib/docyard/templates/assets/js/components/tabs.js +338 -0
  56. data/lib/docyard/templates/assets/js/theme.js +12 -179
  57. data/lib/docyard/templates/assets/logo-dark.svg +4 -0
  58. data/lib/docyard/templates/assets/logo.svg +12 -0
  59. data/lib/docyard/templates/config/docyard.yml.erb +42 -0
  60. data/lib/docyard/templates/layouts/default.html.erb +32 -4
  61. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +46 -12
  62. data/lib/docyard/templates/markdown/guides/configuration.md.erb +202 -0
  63. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +247 -0
  64. data/lib/docyard/templates/markdown/index.md.erb +55 -59
  65. data/lib/docyard/templates/partials/_callout.html.erb +11 -0
  66. data/lib/docyard/templates/partials/_code_block.html.erb +6 -0
  67. data/lib/docyard/templates/partials/_icon.html.erb +1 -0
  68. data/lib/docyard/templates/partials/_icon_file_extension.html.erb +1 -0
  69. data/lib/docyard/templates/partials/_nav_group.html.erb +10 -4
  70. data/lib/docyard/templates/partials/_nav_leaf.html.erb +9 -1
  71. data/lib/docyard/templates/partials/_tabs.html.erb +40 -0
  72. data/lib/docyard/templates/partials/_theme_toggle.html.erb +13 -0
  73. data/lib/docyard/version.rb +1 -1
  74. data/lib/docyard.rb +8 -0
  75. metadata +91 -7
  76. data/lib/docyard/templates/markdown/core-concepts/file-structure.md.erb +0 -61
  77. data/lib/docyard/templates/markdown/core-concepts/markdown.md.erb +0 -90
  78. data/lib/docyard/templates/markdown/getting-started/introduction.md.erb +0 -30
  79. data/lib/docyard/templates/markdown/getting-started/quick-start.md.erb +0 -56
  80. 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: 0614d285d89d4f529607c5e4bdb50c823cce769a5628b90100234c9513f98690
4
- data.tar.gz: 2f7c134bca20f17766e54fb4bc3b02b602e735cf4d8c7e11f83f9158773bec22
3
+ metadata.gz: db5ddabfe4a3b7bf25c24876b915188bd4768ca543772ad0a8cfc971ee791fee
4
+ data.tar.gz: d906b1c2ab33102c9ade6e2cb23f6d067fdbf240d6a4a8e2aa27a52a9ba62b42
5
5
  SHA512:
6
- metadata.gz: 1259470e5245ff3ffc8796144a1e48fae46d851b44ca2c6cbeda0e8f83c782ebb815cfdfe0bdb3fa36fc25744a050859df44e5451376c8dadb4601ad9c3e2e98
7
- data.tar.gz: 90281b4af9dd20425d26d6ea14214de5a79237ddeec9038546b9142819e363051850c64942a1b56459e5e8e1c509d08a36b323c2e01e3bacd4353fc39755915a
6
+ metadata.gz: 2abc1d991cade7059cfaa0b54c2b6f88b33625937e8073cee9d8f61948149567a209a378efd225e9fe85b45b7579ef4e1d81599be5a8cbee970e9cb6e5218a3c
7
+ data.tar.gz: dcd2baa65fcdd4c00825c6f895c035ffd53603ec22503233b600925084b6512b651c9a009687682f1f5cab3a0c8d8e3d0a5c7586634d6da174e4e4c5a95f9a53
data/CHANGELOG.md CHANGED
@@ -7,6 +7,45 @@ 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
+
32
+ ## [0.3.0] - 2025-01-09
33
+
34
+ ### Added
35
+ - Configuration system with optional `docyard.yml` file (#20)
36
+ - Logo and favicon support with light/dark mode switching (#21)
37
+ - Dark mode with theme toggle and system preference detection (#14)
38
+ - Icon system with 24 Phosphor icons and `:icon:` syntax (#15)
39
+ - Callouts/Admonitions with 5 types (note, tip, important, warning, danger) (#16)
40
+ - Tabs component with keyboard navigation and icon auto-detection (#17, #18)
41
+ - Copy button for code blocks with visual feedback (#19)
42
+ - Component-based architecture with processors for extensibility
43
+ - Asset handler with dynamic concatenation of component files
44
+
45
+ ### Changed
46
+ - CSS architecture now uses CSS variables for comprehensive theming
47
+ - Markdown processing enhanced with preprocessor/postprocessor pattern
48
+
10
49
  ## [0.2.0] - 2025-11-08
11
50
 
12
51
  ### Added
@@ -46,7 +85,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
46
85
  - Initial gem structure
47
86
  - Project scaffolding
48
87
 
49
- [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.2.0...HEAD
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
90
+ [0.3.0]: https://github.com/sanifhimani/docyard/compare/v0.2.0...v0.3.0
50
91
  [0.2.0]: https://github.com/sanifhimani/docyard/compare/v0.1.0...v0.2.0
51
92
  [0.1.0]: https://github.com/sanifhimani/docyard/compare/v0.0.1...v0.1.0
52
93
  [0.0.1]: https://github.com/sanifhimani/docyard/releases/tag/v0.0.1
@@ -0,0 +1,42 @@
1
+ File Type Icons from VSCode Icons
2
+ =====================================
3
+
4
+ The file type icons embedded in lib/docyard/icons/file_types.rb are from the VSCode Icons project:
5
+ https://github.com/vscode-icons/vscode-icons
6
+
7
+ License: Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
8
+ https://creativecommons.org/licenses/by-sa/4.0/
9
+
10
+ Copyright (c) 2016 Roberto Huertas
11
+
12
+ Icons included:
13
+ - JavaScript (.js)
14
+ - TypeScript (.ts)
15
+ - JSX (.jsx)
16
+ - TSX (.tsx)
17
+ - Python (.py)
18
+ - Ruby (.rb)
19
+ - HTML (.html)
20
+ - CSS (.css)
21
+ - JSON (.json)
22
+ - YAML (.yaml)
23
+ - TOML (.toml)
24
+ - Go (.go)
25
+ - Rust (.rs)
26
+ - PHP (.php)
27
+ - SQL (.sql)
28
+ - MySQL (.mysql)
29
+ - PostgreSQL (.pgsql)
30
+ - GraphQL (.graphql)
31
+ - Vue (.vue)
32
+ - Svelte (.svelte)
33
+ - Protobuf (.proto)
34
+
35
+ These icons are used under the terms of the CC BY-SA 4.0 license.
36
+ You are free to:
37
+ - Share — copy and redistribute the material in any medium or format
38
+ - Adapt — remix, transform, and build upon the material for any purpose, even commercially
39
+
40
+ Under the following terms:
41
+ - Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made
42
+ - ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license
data/README.md CHANGED
@@ -5,33 +5,57 @@
5
5
 
6
6
  > Documentation generator for Ruby
7
7
 
8
- **Early development** - Core features 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
- - **Sidebar navigation** - Automatic sidebar with nested folders and collapsible sections
12
+ ### Core
13
+ - **Static site generation** - Build static sites with `docyard build`
13
14
  - **Hot reload** - Changes appear instantly while you write
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
21
+ - **Sidebar navigation** - Automatic sidebar with nested folders and collapsible sections
22
+ - **Sidebar customization** - Custom ordering, icons, and external links via config
23
+ - **Active page highlighting** - Always know where you are
24
+
25
+ ### Markdown
14
26
  - **GitHub Flavored Markdown** - Tables, task lists, strikethrough
15
- - **Syntax highlighting** - 100+ languages via Rouge
27
+ - **Syntax highlighting** - 100+ languages via Rouge with copy button
28
+ - **Markdown components**:
29
+ - **Callouts** - 5 types (note, tip, important, warning, danger) with GitHub alerts syntax
30
+ - **Tabs** - Code blocks, package managers, and custom tabs with keyboard navigation
31
+ - **Icons** - 24 Phosphor icons with `:icon:` syntax
16
32
  - **YAML frontmatter** - Add metadata to your pages
17
- - **Customizable error pages** - Make 404/500 pages your own
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
18
39
 
19
40
  ## Quick Start
20
41
 
21
42
  ```bash
22
- # Install the gem
43
+ # Install
23
44
  gem install docyard
24
45
 
25
- # Create a new docs project
26
- mkdir my-docs && cd my-docs
46
+ # Initialize
27
47
  docyard init
28
48
 
29
- # Start the dev server
49
+ # Start dev server
30
50
  docyard serve
51
+ # → http://localhost:4200
31
52
 
32
- # Visit http://localhost:4200
53
+ # Build for production
54
+ docyard build
33
55
  ```
34
56
 
57
+ Your site is ready to deploy! Upload the `dist/` folder to any static host.
58
+
35
59
  ## Installation
36
60
 
37
61
  Add to your Gemfile:
@@ -59,21 +83,27 @@ This creates:
59
83
  docs/
60
84
  index.md # Home page
61
85
  getting-started/
62
- introduction.md # Getting started guide
63
- installation.md # Installation instructions
64
- quick-start.md # Quick start guide
65
- core-concepts/
66
- file-structure.md # File structure guide
67
- 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
68
91
  ```
69
92
 
70
- ### Start Development Server
93
+ ### Commands
71
94
 
72
95
  ```bash
96
+ # Development server with hot reload
73
97
  docyard serve
74
-
75
- # Custom port and host
76
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
77
107
  ```
78
108
 
79
109
  ### Writing Docs
@@ -119,6 +149,33 @@ Write links with `.md` extension, they'll be automatically cleaned:
119
149
  [Guide](./guide/index.md) → /guide
120
150
  ```
121
151
 
152
+ ### Using Icons
153
+
154
+ Docyard includes 24 essential Phosphor icons that work out of the box. Just type `:icon-name:` in your markdown:
155
+
156
+ ```markdown
157
+ :check: Zero configuration
158
+ :lightning: Hot reload
159
+ :rocket-launch: Fast and lightweight
160
+
161
+ Use different weights:
162
+ :heart: → regular weight (default)
163
+ :heart:bold: → bold weight
164
+ :heart:fill: → filled version
165
+ ```
166
+
167
+ Available icons: `heart`, `check`, `x`, `warning`, `info`, `question`, `arrow-right`, `arrow-left`, `arrow-up`, `arrow-down`, `code`, `terminal`, `package`, `rocket-launch`, `star`, `lightning`, `moon-stars`, `sun`, `link-external`, `copy`, `github`, `file`, `terminal-window`, `warning-circle`.
168
+
169
+ Weights: `regular` (default), `bold`, `fill`, `light`, `thin`, `duotone`
170
+
171
+ Icons automatically match your text size and color.
172
+
173
+ **Adding new icons:**
174
+
175
+ 1. Get the SVG path from [phosphoricons.com](https://phosphoricons.com)
176
+ 2. Add to `lib/docyard/icons/phosphor.rb` under the appropriate weight
177
+ 3. Format: `"icon-name" => '<path d="..."/>',`
178
+
122
179
  ### Directory Structure
123
180
 
124
181
  ```
@@ -170,12 +227,18 @@ bundle exec rubocop
170
227
 
171
228
  ## Roadmap
172
229
 
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
+
173
238
  **Next up:**
174
- - Dark mode with theme toggle
175
- - Markdown components (callouts, code groups, collapsible sections)
176
- - Icon system
177
- - Client-side search
178
- - Static site generation (`docyard build`)
239
+ - Search functionality
240
+ - Table of contents
241
+ - More markdown components
179
242
 
180
243
  ## Contributing
181
244
 
@@ -3,6 +3,7 @@
3
3
  module Docyard
4
4
  class AssetHandler
5
5
  ASSETS_PATH = File.join(__dir__, "templates", "assets")
6
+ USER_ASSETS_PATH = "docs/assets"
6
7
 
7
8
  CONTENT_TYPES = {
8
9
  ".css" => "text/css; charset=utf-8",
@@ -21,6 +22,9 @@ module Docyard
21
22
 
22
23
  return forbidden_response if directory_traversal?(asset_path)
23
24
 
25
+ return serve_components_css if asset_path == "css/components.css"
26
+ return serve_components_js if asset_path == "js/components.js"
27
+
24
28
  file_path = build_file_path(asset_path)
25
29
  return not_found_response unless File.file?(file_path)
26
30
 
@@ -38,6 +42,9 @@ module Docyard
38
42
  end
39
43
 
40
44
  def build_file_path(asset_path)
45
+ user_path = File.join(USER_ASSETS_PATH, asset_path)
46
+ return user_path if File.file?(user_path)
47
+
41
48
  File.join(ASSETS_PATH, asset_path)
42
49
  end
43
50
 
@@ -48,6 +55,32 @@ module Docyard
48
55
  [200, { "Content-Type" => content_type }, [content]]
49
56
  end
50
57
 
58
+ def serve_components_css
59
+ content = concatenate_component_css
60
+ [200, { "Content-Type" => "text/css; charset=utf-8" }, [content]]
61
+ end
62
+
63
+ def concatenate_component_css
64
+ components_dir = File.join(ASSETS_PATH, "css", "components")
65
+ return "" unless Dir.exist?(components_dir)
66
+
67
+ css_files = Dir.glob(File.join(components_dir, "*.css"))
68
+ css_files.map { |file| File.read(file) }.join("\n\n")
69
+ end
70
+
71
+ def serve_components_js
72
+ content = concatenate_component_js
73
+ [200, { "Content-Type" => "application/javascript; charset=utf-8" }, [content]]
74
+ end
75
+
76
+ def concatenate_component_js
77
+ components_dir = File.join(ASSETS_PATH, "js", "components")
78
+ return "" unless Dir.exist?(components_dir)
79
+
80
+ js_files = Dir.glob(File.join(components_dir, "*.js"))
81
+ js_files.map { |file| File.read(file) }.join("\n\n")
82
+ end
83
+
51
84
  def detect_content_type(file_path)
52
85
  extension = File.extname(file_path)
53
86
  CONTENT_TYPES.fetch(extension, "application/octet-stream")
@@ -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