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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -1
- data/LICENSE.vscode-icons +42 -0
- data/README.md +86 -23
- data/lib/docyard/asset_handler.rb +33 -0
- 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/base_processor.rb +24 -0
- data/lib/docyard/components/callout_processor.rb +121 -0
- data/lib/docyard/components/code_block_processor.rb +55 -0
- data/lib/docyard/components/code_detector.rb +59 -0
- data/lib/docyard/components/icon_detector.rb +57 -0
- data/lib/docyard/components/icon_processor.rb +51 -0
- data/lib/docyard/components/registry.rb +34 -0
- data/lib/docyard/components/table_wrapper_processor.rb +18 -0
- data/lib/docyard/components/tabs_parser.rb +60 -0
- data/lib/docyard/components/tabs_processor.rb +44 -0
- data/lib/docyard/config/validator.rb +171 -0
- data/lib/docyard/config.rb +135 -0
- data/lib/docyard/constants.rb +5 -0
- data/lib/docyard/icons/LICENSE.phosphor +21 -0
- data/lib/docyard/icons/file_types.rb +92 -0
- data/lib/docyard/icons/phosphor.rb +64 -0
- data/lib/docyard/icons.rb +40 -0
- data/lib/docyard/initializer.rb +93 -9
- data/lib/docyard/language_mapping.rb +52 -0
- data/lib/docyard/markdown.rb +27 -3
- data/lib/docyard/preview_server.rb +72 -0
- data/lib/docyard/rack_application.rb +77 -8
- data/lib/docyard/renderer.rb +56 -9
- data/lib/docyard/server.rb +5 -2
- 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 +54 -2
- data/lib/docyard/templates/assets/css/code.css +150 -2
- data/lib/docyard/templates/assets/css/components/callout.css +169 -0
- data/lib/docyard/templates/assets/css/components/code-block.css +196 -0
- data/lib/docyard/templates/assets/css/components/icon.css +16 -0
- data/lib/docyard/templates/assets/css/components/logo.css +44 -0
- data/lib/docyard/templates/assets/css/{components.css → components/navigation.css} +111 -53
- data/lib/docyard/templates/assets/css/components/tabs.css +299 -0
- data/lib/docyard/templates/assets/css/components/theme-toggle.css +69 -0
- data/lib/docyard/templates/assets/css/layout.css +14 -4
- data/lib/docyard/templates/assets/css/markdown.css +27 -17
- data/lib/docyard/templates/assets/css/reset.css +4 -0
- data/lib/docyard/templates/assets/css/variables.css +94 -3
- data/lib/docyard/templates/assets/favicon.svg +16 -0
- data/lib/docyard/templates/assets/js/components/code-block.js +162 -0
- data/lib/docyard/templates/assets/js/components/navigation.js +221 -0
- data/lib/docyard/templates/assets/js/components/tabs.js +338 -0
- data/lib/docyard/templates/assets/js/theme.js +12 -179
- data/lib/docyard/templates/assets/logo-dark.svg +4 -0
- data/lib/docyard/templates/assets/logo.svg +12 -0
- data/lib/docyard/templates/config/docyard.yml.erb +42 -0
- data/lib/docyard/templates/layouts/default.html.erb +32 -4
- 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/_callout.html.erb +11 -0
- data/lib/docyard/templates/partials/_code_block.html.erb +6 -0
- data/lib/docyard/templates/partials/_icon.html.erb +1 -0
- data/lib/docyard/templates/partials/_icon_file_extension.html.erb +1 -0
- 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/templates/partials/_tabs.html.erb +40 -0
- data/lib/docyard/templates/partials/_theme_toggle.html.erb +13 -0
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +8 -0
- metadata +91 -7
- 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,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.
|
|
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
|
-
|
|
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`
|
|
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
|
-
|
|
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
|
|
43
|
+
# Install
|
|
23
44
|
gem install docyard
|
|
24
45
|
|
|
25
|
-
#
|
|
26
|
-
mkdir my-docs && cd my-docs
|
|
46
|
+
# Initialize
|
|
27
47
|
docyard init
|
|
28
48
|
|
|
29
|
-
# Start
|
|
49
|
+
# Start dev server
|
|
30
50
|
docyard serve
|
|
51
|
+
# → http://localhost:4200
|
|
31
52
|
|
|
32
|
-
#
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
###
|
|
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
|
-
-
|
|
175
|
-
-
|
|
176
|
-
-
|
|
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
|