docyard 0.7.0 → 0.8.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/.rubocop.yml +5 -1
- data/CHANGELOG.md +20 -1
- data/lib/docyard/build/asset_bundler.rb +22 -7
- data/lib/docyard/build/file_copier.rb +49 -27
- data/lib/docyard/build/sitemap_generator.rb +6 -6
- data/lib/docyard/build/static_generator.rb +85 -12
- data/lib/docyard/builder.rb +6 -6
- data/lib/docyard/config/branding_resolver.rb +126 -17
- data/lib/docyard/config/constants.rb +6 -4
- data/lib/docyard/config/validator.rb +122 -99
- data/lib/docyard/config.rb +36 -43
- data/lib/docyard/initializer.rb +15 -76
- data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
- data/lib/docyard/navigation/prev_next_builder.rb +4 -1
- data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
- data/lib/docyard/navigation/sidebar/config_parser.rb +136 -108
- data/lib/docyard/navigation/sidebar/file_resolver.rb +78 -0
- data/lib/docyard/navigation/sidebar/file_system_scanner.rb +2 -1
- data/lib/docyard/navigation/sidebar/item.rb +45 -7
- data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
- data/lib/docyard/navigation/sidebar/metadata_extractor.rb +69 -0
- data/lib/docyard/navigation/sidebar/metadata_reader.rb +47 -0
- data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
- data/lib/docyard/navigation/sidebar/renderer.rb +55 -37
- data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
- data/lib/docyard/navigation/sidebar/tree_builder.rb +99 -26
- data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
- data/lib/docyard/navigation/sidebar_builder.rb +105 -36
- data/lib/docyard/rendering/icon_helpers.rb +13 -0
- data/lib/docyard/rendering/icons/phosphor.rb +23 -1
- data/lib/docyard/rendering/markdown.rb +5 -0
- data/lib/docyard/rendering/renderer.rb +74 -34
- data/lib/docyard/rendering/template_resolver.rb +172 -0
- data/lib/docyard/routing/fallback_resolver.rb +92 -0
- data/lib/docyard/search/build_indexer.rb +1 -1
- data/lib/docyard/search/dev_indexer.rb +51 -6
- data/lib/docyard/search/pagefind_support.rb +2 -0
- data/lib/docyard/server/asset_handler.rb +24 -19
- data/lib/docyard/server/pagefind_handler.rb +63 -0
- data/lib/docyard/server/preview_server.rb +1 -1
- data/lib/docyard/server/rack_application.rb +81 -64
- data/lib/docyard/templates/assets/css/code.css +18 -51
- data/lib/docyard/templates/assets/css/components/breadcrumbs.css +143 -0
- data/lib/docyard/templates/assets/css/components/callout.css +67 -67
- data/lib/docyard/templates/assets/css/components/code-block.css +180 -282
- data/lib/docyard/templates/assets/css/components/heading-anchor.css +28 -15
- data/lib/docyard/templates/assets/css/components/icon.css +0 -1
- data/lib/docyard/templates/assets/css/components/logo.css +0 -2
- data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
- data/lib/docyard/templates/assets/css/components/navigation.css +186 -167
- data/lib/docyard/templates/assets/css/components/prev-next.css +76 -47
- data/lib/docyard/templates/assets/css/components/search.css +186 -174
- data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
- data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
- data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
- data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
- data/lib/docyard/templates/assets/css/landing.css +815 -0
- data/lib/docyard/templates/assets/css/layout.css +489 -87
- data/lib/docyard/templates/assets/css/main.css +1 -3
- data/lib/docyard/templates/assets/css/markdown.css +111 -93
- data/lib/docyard/templates/assets/css/reset.css +0 -3
- data/lib/docyard/templates/assets/css/typography.css +43 -41
- data/lib/docyard/templates/assets/css/variables.css +268 -208
- data/lib/docyard/templates/assets/favicon.svg +7 -8
- data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
- data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
- data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
- data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
- data/lib/docyard/templates/assets/js/components/search.js +0 -75
- data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
- data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
- data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
- data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
- data/lib/docyard/templates/assets/js/theme.js +0 -3
- data/lib/docyard/templates/assets/logo-dark.svg +8 -2
- data/lib/docyard/templates/assets/logo.svg +7 -4
- data/lib/docyard/templates/config/docyard.yml.erb +37 -34
- data/lib/docyard/templates/errors/404.html.erb +1 -1
- data/lib/docyard/templates/errors/500.html.erb +1 -1
- data/lib/docyard/templates/layouts/default.html.erb +18 -67
- data/lib/docyard/templates/layouts/splash.html.erb +176 -0
- data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
- data/lib/docyard/templates/partials/_code_block.html.erb +5 -3
- data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
- data/lib/docyard/templates/partials/_features.html.erb +15 -0
- data/lib/docyard/templates/partials/_footer.html.erb +42 -0
- data/lib/docyard/templates/partials/_head.html.erb +22 -0
- data/lib/docyard/templates/partials/_header.html.erb +49 -0
- data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
- data/lib/docyard/templates/partials/_hero.html.erb +27 -0
- data/lib/docyard/templates/partials/_nav_group.html.erb +25 -11
- data/lib/docyard/templates/partials/_nav_leaf.html.erb +1 -1
- data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
- data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
- data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
- data/lib/docyard/templates/partials/_prev_next.html.erb +8 -2
- data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
- data/lib/docyard/templates/partials/_search_modal.html.erb +2 -6
- data/lib/docyard/templates/partials/_search_trigger.html.erb +2 -6
- data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
- data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
- data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
- data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
- data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
- data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
- data/lib/docyard/version.rb +1 -1
- metadata +33 -5
- data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
- data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
- data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
- data/lib/docyard/templates/markdown/index.md.erb +0 -82
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 77d317cd25d544eef47b5bdcecb0515b8937da93b347d39cac6fd1f87bb43b19
|
|
4
|
+
data.tar.gz: bf97d9449013ba8bf370682271990f64b2c2092bfdc9bf0deb99cf2b67056cc2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e30597716798800240f4436c423284d1972fe5fa0abf1de8007b3279dde0a87ef32210cb830c84769c15db0c82e6c372337ae06baaecdb71fa866342c9697a3
|
|
7
|
+
data.tar.gz: f0ae1edf5937153c9ed3ccb1cc7c599c7f650e16470ad976ef0918c09bc030502ecf89dad6a8a7c79ee8055530b61a65d556bbfd147e08a86349a2475777ee54
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.0] - 2026-01-13
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Landing Pages** - Hero sections, feature grids, and custom footer layouts for documentation homepages (#45)
|
|
14
|
+
- **Tab Navigation** - Top-level navigation tabs for organizing documentation into sections like Guide, API, Components (#52)
|
|
15
|
+
- **Header CTAs** - Configurable call-to-action buttons in the header with primary/secondary variants (#51)
|
|
16
|
+
- **Breadcrumbs** - Path navigation with auto-truncation for deep nesting and configurable via `navigation.breadcrumbs` (#54)
|
|
17
|
+
- **Doc Page Footer** - Social icons, "Built with Docyard" attribution, and copyright text in TOC column (#55)
|
|
18
|
+
- **Auto-detect Branding** - Automatic logo and favicon detection from `docs/public/` directory (#49)
|
|
19
|
+
- **Social Icon Mapping** - 16 social platform icons with automatic platform-to-icon mapping (#55)
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- **Sidebar Overhaul** - Per-section `_sidebar.yml` files, improved collapsible behavior, and better active state handling (#50, #53)
|
|
23
|
+
- **Config Schema** - Reorganized configuration with `branding`, `navigation`, and `socials` sections (#48)
|
|
24
|
+
- **Sidebar Convention** - Section-based sidebar configuration in `docs/<section>/_sidebar.yml` (#47)
|
|
25
|
+
- **UI Refresh** - Updated typography, spacing, and visual consistency across components (#44)
|
|
26
|
+
- **Logo Update** - New logo with cyan accent and dark mode support (#46)
|
|
27
|
+
|
|
10
28
|
## [0.7.0] - 2026-01-01
|
|
11
29
|
|
|
12
30
|
### Added
|
|
@@ -122,7 +140,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
122
140
|
- Initial gem structure
|
|
123
141
|
- Project scaffolding
|
|
124
142
|
|
|
125
|
-
[Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.
|
|
143
|
+
[Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.8.0...HEAD
|
|
144
|
+
[0.8.0]: https://github.com/sanifhimani/docyard/compare/v0.7.0...v0.8.0
|
|
126
145
|
[0.7.0]: https://github.com/sanifhimani/docyard/compare/v0.6.0...v0.7.0
|
|
127
146
|
[0.6.0]: https://github.com/sanifhimani/docyard/compare/v0.5.0...v0.6.0
|
|
128
147
|
[0.5.0]: https://github.com/sanifhimani/docyard/compare/v0.4.0...v0.5.0
|
|
@@ -35,6 +35,8 @@ module Docyard
|
|
|
35
35
|
main_css = File.read(File.join(ASSETS_PATH, "css", "main.css"))
|
|
36
36
|
css_content = resolve_css_imports(main_css)
|
|
37
37
|
minified = CSSminify.compress(css_content)
|
|
38
|
+
minified = fix_calc_whitespace(minified)
|
|
39
|
+
minified = fix_css_math_functions(minified)
|
|
38
40
|
hash = generate_hash(minified)
|
|
39
41
|
|
|
40
42
|
write_bundled_asset(minified, hash, "css")
|
|
@@ -43,6 +45,19 @@ module Docyard
|
|
|
43
45
|
hash
|
|
44
46
|
end
|
|
45
47
|
|
|
48
|
+
def fix_calc_whitespace(css)
|
|
49
|
+
css
|
|
50
|
+
.gsub(/\)\+(?!\s)/, ") + ")
|
|
51
|
+
.gsub(/\)-(?![-\s])/, ") - ")
|
|
52
|
+
.gsub(/(\d[a-z]*)\+(?=[\w(])/, '\1 + ')
|
|
53
|
+
.gsub(/([lch])\+(?=[\d.])/, '\1 + ')
|
|
54
|
+
.gsub(/([lch])-(?=[\d.])/, '\1 - ')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def fix_css_math_functions(css)
|
|
58
|
+
css.gsub(/\bmax\(0,/, "max(0px,").gsub(/\bmin\(0,/, "min(0px,)")
|
|
59
|
+
end
|
|
60
|
+
|
|
46
61
|
def resolve_css_imports(css_content)
|
|
47
62
|
css_content.gsub(/@import url\('([^']+)'\);/) do |match|
|
|
48
63
|
import_file = Regexp.last_match(1)
|
|
@@ -92,8 +107,8 @@ module Docyard
|
|
|
92
107
|
end
|
|
93
108
|
|
|
94
109
|
def update_html_references(css_hash, js_hash)
|
|
95
|
-
html_files = Dir.glob(File.join(config.build.
|
|
96
|
-
base_url = normalize_base_url(config.build.
|
|
110
|
+
html_files = Dir.glob(File.join(config.build.output, "**", "*.html"))
|
|
111
|
+
base_url = normalize_base_url(config.build.base)
|
|
97
112
|
|
|
98
113
|
html_files.each do |file|
|
|
99
114
|
content = replace_asset_references(File.read(file), css_hash, js_hash, base_url)
|
|
@@ -104,15 +119,15 @@ module Docyard
|
|
|
104
119
|
end
|
|
105
120
|
|
|
106
121
|
def replace_asset_references(content, css_hash, js_hash, base_url)
|
|
107
|
-
content.gsub(%r{/
|
|
108
|
-
.gsub(%r{/
|
|
109
|
-
.gsub(%r{/
|
|
110
|
-
.gsub(%r{<script src="/
|
|
122
|
+
content.gsub(%r{/_docyard/css/main\.css}, "#{base_url}_docyard/bundle.#{css_hash}.css")
|
|
123
|
+
.gsub(%r{/_docyard/js/theme\.js}, "#{base_url}_docyard/bundle.#{js_hash}.js")
|
|
124
|
+
.gsub(%r{/_docyard/js/components\.js}, "")
|
|
125
|
+
.gsub(%r{<script src="/_docyard/js/reload\.js"></script>}, "")
|
|
111
126
|
end
|
|
112
127
|
|
|
113
128
|
def write_bundled_asset(content, hash, extension)
|
|
114
129
|
filename = "bundle.#{hash}.#{extension}"
|
|
115
|
-
output_path = File.join(config.build.
|
|
130
|
+
output_path = File.join(config.build.output, "_docyard", filename)
|
|
116
131
|
FileUtils.mkdir_p(File.dirname(output_path))
|
|
117
132
|
File.write(output_path, content)
|
|
118
133
|
end
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
module Docyard
|
|
4
4
|
module Build
|
|
5
5
|
class FileCopier
|
|
6
|
+
DOCYARD_OUTPUT_DIR = "_docyard"
|
|
7
|
+
|
|
6
8
|
attr_reader :config, :verbose
|
|
7
9
|
|
|
8
10
|
def initialize(config, verbose: false)
|
|
@@ -14,7 +16,7 @@ module Docyard
|
|
|
14
16
|
puts "\nCopying static assets..."
|
|
15
17
|
|
|
16
18
|
count = 0
|
|
17
|
-
count +=
|
|
19
|
+
count += copy_public_files
|
|
18
20
|
count += copy_branding_assets
|
|
19
21
|
|
|
20
22
|
log "[✓] Copied #{count} static files"
|
|
@@ -23,25 +25,22 @@ module Docyard
|
|
|
23
25
|
|
|
24
26
|
private
|
|
25
27
|
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
return 0 unless Dir.exist?(
|
|
29
|
-
|
|
30
|
-
output_assets_dir = File.join(config.build.output_dir, "assets")
|
|
31
|
-
FileUtils.mkdir_p(output_assets_dir)
|
|
28
|
+
def copy_public_files
|
|
29
|
+
public_dir = Constants::PUBLIC_DIR
|
|
30
|
+
return 0 unless Dir.exist?(public_dir)
|
|
32
31
|
|
|
33
|
-
files =
|
|
34
|
-
files.each { |file|
|
|
32
|
+
files = find_files_in_dir(public_dir)
|
|
33
|
+
files.each { |file| copy_single_file(file, "#{public_dir}/", config.build.output) }
|
|
35
34
|
|
|
36
|
-
log "[✓] Copied #{files.size}
|
|
35
|
+
log "[✓] Copied #{files.size} public files from #{public_dir}/" if files.any?
|
|
37
36
|
files.size
|
|
38
37
|
end
|
|
39
38
|
|
|
40
|
-
def
|
|
41
|
-
Dir.glob(File.join(
|
|
39
|
+
def find_files_in_dir(dir)
|
|
40
|
+
Dir.glob(File.join(dir, "**", "*")).select { |f| File.file?(f) }
|
|
42
41
|
end
|
|
43
42
|
|
|
44
|
-
def
|
|
43
|
+
def copy_single_file(file, prefix, output_dir)
|
|
45
44
|
relative_path = file.delete_prefix(prefix)
|
|
46
45
|
dest_path = File.join(output_dir, relative_path)
|
|
47
46
|
|
|
@@ -60,26 +59,49 @@ module Docyard
|
|
|
60
59
|
end
|
|
61
60
|
|
|
62
61
|
def copy_default_branding_assets
|
|
63
|
-
templates_assets =
|
|
64
|
-
count =
|
|
62
|
+
templates_assets = templates_assets_path
|
|
63
|
+
count = copy_branding_files(templates_assets)
|
|
64
|
+
count + copy_fonts(templates_assets)
|
|
65
|
+
end
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
def templates_assets_path
|
|
68
|
+
File.join(__dir__, "..", "templates", "assets")
|
|
69
|
+
end
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
def copy_branding_files(templates_assets)
|
|
72
|
+
branding_files = %w[logo.svg logo-dark.svg favicon.svg]
|
|
73
|
+
branding_files.sum { |asset_file| copy_asset_to_docyard(templates_assets, asset_file, "default branding") }
|
|
74
|
+
end
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
def copy_asset_to_docyard(source_dir, filename, label)
|
|
77
|
+
source_path = File.join(source_dir, filename)
|
|
78
|
+
return 0 unless File.exist?(source_path)
|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
dest_path = File.join(config.build.output, DOCYARD_OUTPUT_DIR, filename)
|
|
81
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
82
|
+
FileUtils.cp(source_path, dest_path)
|
|
83
|
+
log " Copied #{label}: #{filename}" if verbose
|
|
84
|
+
1
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def copy_fonts(templates_assets)
|
|
88
|
+
fonts_dir = File.join(templates_assets, "fonts")
|
|
89
|
+
return 0 unless Dir.exist?(fonts_dir)
|
|
90
|
+
|
|
91
|
+
font_files = Dir.glob(File.join(fonts_dir, "*")).select { |f| File.file?(f) }
|
|
92
|
+
font_files.sum { |font_file| copy_single_font(font_file) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def copy_single_font(font_file)
|
|
96
|
+
dest_path = File.join(config.build.output, DOCYARD_OUTPUT_DIR, "fonts", File.basename(font_file))
|
|
97
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
98
|
+
FileUtils.cp(font_file, dest_path)
|
|
99
|
+
log " Copied font: #{File.basename(font_file)}" if verbose
|
|
100
|
+
1
|
|
79
101
|
end
|
|
80
102
|
|
|
81
103
|
def copy_user_branding_assets
|
|
82
|
-
%w[logo
|
|
104
|
+
%w[logo favicon].sum { |asset_key| copy_single_branding_asset(asset_key) }
|
|
83
105
|
end
|
|
84
106
|
|
|
85
107
|
def copy_single_branding_asset(asset_key)
|
|
@@ -89,7 +111,7 @@ module Docyard
|
|
|
89
111
|
full_path = File.join("docs", asset_path)
|
|
90
112
|
return 0 unless File.exist?(full_path)
|
|
91
113
|
|
|
92
|
-
dest_path = File.join(config.build.
|
|
114
|
+
dest_path = File.join(config.build.output, asset_path)
|
|
93
115
|
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
94
116
|
FileUtils.cp(full_path, dest_path)
|
|
95
117
|
|
|
@@ -15,7 +15,7 @@ module Docyard
|
|
|
15
15
|
urls = collect_urls
|
|
16
16
|
sitemap_content = build_sitemap(urls)
|
|
17
17
|
|
|
18
|
-
output_path = File.join(config.build.
|
|
18
|
+
output_path = File.join(config.build.output, "sitemap.xml")
|
|
19
19
|
File.write(output_path, sitemap_content)
|
|
20
20
|
|
|
21
21
|
puts "[✓] Generated sitemap.xml (#{urls.size} URLs)"
|
|
@@ -24,10 +24,10 @@ module Docyard
|
|
|
24
24
|
private
|
|
25
25
|
|
|
26
26
|
def collect_urls
|
|
27
|
-
html_files = Dir.glob(File.join(config.build.
|
|
27
|
+
html_files = Dir.glob(File.join(config.build.output, "**", "index.html"))
|
|
28
28
|
|
|
29
29
|
html_files.map do |file|
|
|
30
|
-
relative_path = file.delete_prefix(config.build.
|
|
30
|
+
relative_path = file.delete_prefix(config.build.output).delete_suffix("/index.html")
|
|
31
31
|
url_path = relative_path.empty? ? "/" : relative_path
|
|
32
32
|
lastmod = File.mtime(file).utc.iso8601
|
|
33
33
|
|
|
@@ -36,15 +36,15 @@ module Docyard
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def build_sitemap(urls)
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
base = config.build.base
|
|
40
|
+
base = base.chop if base.end_with?("/")
|
|
41
41
|
|
|
42
42
|
xml = ['<?xml version="1.0" encoding="UTF-8"?>']
|
|
43
43
|
xml << '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
|
|
44
44
|
|
|
45
45
|
urls.each do |url|
|
|
46
46
|
xml << " <url>"
|
|
47
|
-
xml << " <loc>#{
|
|
47
|
+
xml << " <loc>#{base}#{url[:loc]}</loc>"
|
|
48
48
|
xml << " <lastmod>#{url[:lastmod]}</lastmod>"
|
|
49
49
|
xml << " </url>"
|
|
50
50
|
end
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "tty-progressbar"
|
|
4
|
+
require_relative "../rendering/template_resolver"
|
|
5
|
+
require_relative "../navigation/prev_next_builder"
|
|
6
|
+
require_relative "../navigation/breadcrumb_builder"
|
|
4
7
|
|
|
5
8
|
module Docyard
|
|
6
9
|
module Build
|
|
@@ -10,10 +13,12 @@ module Docyard
|
|
|
10
13
|
def initialize(config, verbose: false)
|
|
11
14
|
@config = config
|
|
12
15
|
@verbose = verbose
|
|
13
|
-
@renderer = Renderer.new(base_url: config.build.
|
|
16
|
+
@renderer = Renderer.new(base_url: config.build.base, config: config)
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def generate
|
|
20
|
+
copy_custom_landing_page if custom_landing_page?
|
|
21
|
+
|
|
17
22
|
markdown_files = collect_markdown_files
|
|
18
23
|
puts "\n[✓] Found #{markdown_files.size} markdown files"
|
|
19
24
|
|
|
@@ -33,24 +38,72 @@ module Docyard
|
|
|
33
38
|
|
|
34
39
|
private
|
|
35
40
|
|
|
41
|
+
def custom_landing_page?
|
|
42
|
+
File.file?("docs/index.html")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def copy_custom_landing_page
|
|
46
|
+
output_path = File.join(config.build.output, "index.html")
|
|
47
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
48
|
+
FileUtils.cp("docs/index.html", output_path)
|
|
49
|
+
log "[✓] Copied custom landing page (index.html)"
|
|
50
|
+
end
|
|
51
|
+
|
|
36
52
|
def collect_markdown_files
|
|
37
|
-
Dir.glob(File.join("docs", "**", "*.md"))
|
|
53
|
+
files = Dir.glob(File.join("docs", "**", "*.md"))
|
|
54
|
+
files.reject! { |f| f == "docs/index.md" } if custom_landing_page?
|
|
55
|
+
files
|
|
38
56
|
end
|
|
39
57
|
|
|
40
58
|
def generate_page(markdown_file_path)
|
|
41
59
|
output_path = determine_output_path(markdown_file_path)
|
|
42
60
|
current_path = determine_current_path(markdown_file_path)
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
html_content =
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
html_content = render_markdown_file(markdown_file_path, current_path)
|
|
63
|
+
html_content = apply_search_exclusion(html_content, current_path)
|
|
64
|
+
write_output(output_path, html_content)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def apply_search_exclusion(html_content, current_path)
|
|
68
|
+
return html_content unless excluded_from_search?(current_path)
|
|
69
|
+
|
|
70
|
+
html_content.gsub("data-pagefind-body", "data-pagefind-ignore")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def excluded_from_search?(path)
|
|
74
|
+
exclude_patterns = config.search.exclude || []
|
|
75
|
+
exclude_patterns.any? do |pattern|
|
|
76
|
+
next false unless pattern.start_with?("/")
|
|
77
|
+
|
|
78
|
+
File.fnmatch(pattern, path, File::FNM_PATHNAME)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def render_markdown_file(markdown_file_path, current_path)
|
|
83
|
+
markdown = Markdown.new(File.read(markdown_file_path))
|
|
84
|
+
template_resolver = TemplateResolver.new(markdown.frontmatter, config.data)
|
|
85
|
+
branding = branding_options
|
|
50
86
|
|
|
87
|
+
navigation = build_navigation_html(template_resolver, current_path, markdown, branding[:header_ctas])
|
|
88
|
+
renderer.render_file(markdown_file_path, **navigation, branding: branding,
|
|
89
|
+
template_options: template_resolver.to_options,
|
|
90
|
+
current_path: current_path)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_navigation_html(template_resolver, current_path, markdown, header_ctas)
|
|
94
|
+
return { sidebar_html: "", prev_next_html: "", breadcrumbs: nil } unless template_resolver.show_sidebar?
|
|
95
|
+
|
|
96
|
+
sidebar_builder = build_sidebar_instance(current_path, header_ctas)
|
|
97
|
+
{
|
|
98
|
+
sidebar_html: sidebar_builder.to_html,
|
|
99
|
+
prev_next_html: build_prev_next(sidebar_builder, current_path, markdown),
|
|
100
|
+
breadcrumbs: build_breadcrumbs(sidebar_builder.tree, current_path)
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def write_output(output_path, html_content)
|
|
51
105
|
FileUtils.mkdir_p(File.dirname(output_path))
|
|
52
106
|
File.write(output_path, html_content)
|
|
53
|
-
|
|
54
107
|
log "Generated: #{output_path}" if verbose
|
|
55
108
|
end
|
|
56
109
|
|
|
@@ -59,7 +112,7 @@ module Docyard
|
|
|
59
112
|
base_name = File.basename(relative_path, ".md")
|
|
60
113
|
dir_name = File.dirname(relative_path)
|
|
61
114
|
|
|
62
|
-
output_dir = config.build.
|
|
115
|
+
output_dir = config.build.output
|
|
63
116
|
|
|
64
117
|
if base_name == "index"
|
|
65
118
|
File.join(output_dir, dir_name, "index.html")
|
|
@@ -80,14 +133,34 @@ module Docyard
|
|
|
80
133
|
end
|
|
81
134
|
end
|
|
82
135
|
|
|
83
|
-
def
|
|
136
|
+
def build_sidebar_instance(current_path, header_ctas = [])
|
|
84
137
|
SidebarBuilder.new(
|
|
85
138
|
docs_path: "docs",
|
|
86
139
|
current_path: current_path,
|
|
87
|
-
config: config
|
|
140
|
+
config: config,
|
|
141
|
+
header_ctas: header_ctas
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def build_prev_next(sidebar_builder, current_path, markdown)
|
|
146
|
+
PrevNextBuilder.new(
|
|
147
|
+
sidebar_tree: sidebar_builder.tree,
|
|
148
|
+
current_path: current_path,
|
|
149
|
+
frontmatter: markdown.frontmatter,
|
|
150
|
+
config: {}
|
|
88
151
|
).to_html
|
|
89
152
|
end
|
|
90
153
|
|
|
154
|
+
def build_breadcrumbs(sidebar_tree, current_path)
|
|
155
|
+
return nil unless breadcrumbs_enabled?
|
|
156
|
+
|
|
157
|
+
BreadcrumbBuilder.new(sidebar_tree: sidebar_tree, current_path: current_path)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def breadcrumbs_enabled?
|
|
161
|
+
config.navigation.breadcrumbs != false
|
|
162
|
+
end
|
|
163
|
+
|
|
91
164
|
def branding_options
|
|
92
165
|
BrandingResolver.new(config).resolve
|
|
93
166
|
end
|
data/lib/docyard/builder.rb
CHANGED
|
@@ -35,7 +35,7 @@ module Docyard
|
|
|
35
35
|
private
|
|
36
36
|
|
|
37
37
|
def prepare_output_directory
|
|
38
|
-
output_dir = config.build.
|
|
38
|
+
output_dir = config.build.output
|
|
39
39
|
|
|
40
40
|
if clean && Dir.exist?(output_dir)
|
|
41
41
|
log "[✓] Cleaning #{output_dir}/ directory"
|
|
@@ -68,7 +68,7 @@ module Docyard
|
|
|
68
68
|
sitemap_gen = Build::SitemapGenerator.new(config)
|
|
69
69
|
sitemap_gen.generate
|
|
70
70
|
|
|
71
|
-
File.write(File.join(config.build.
|
|
71
|
+
File.write(File.join(config.build.output, "robots.txt"), robots_txt_content)
|
|
72
72
|
log "[+] Generated robots.txt"
|
|
73
73
|
end
|
|
74
74
|
|
|
@@ -78,14 +78,14 @@ module Docyard
|
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def robots_txt_content
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
base = config.build.base
|
|
82
|
+
base = "#{base}/" unless base.end_with?("/")
|
|
83
83
|
|
|
84
84
|
<<~ROBOTS
|
|
85
85
|
User-agent: *
|
|
86
86
|
Allow: /
|
|
87
87
|
|
|
88
|
-
Sitemap: #{
|
|
88
|
+
Sitemap: #{base}sitemap.xml
|
|
89
89
|
ROBOTS
|
|
90
90
|
end
|
|
91
91
|
|
|
@@ -94,7 +94,7 @@ module Docyard
|
|
|
94
94
|
|
|
95
95
|
puts "\n#{'=' * 50}"
|
|
96
96
|
puts "Build complete in #{format('%.2f', elapsed)}s"
|
|
97
|
-
puts "Output: #{config.build.
|
|
97
|
+
puts "Output: #{config.build.output}/"
|
|
98
98
|
|
|
99
99
|
summary = "#{pages} pages, #{bundles} bundles, #{assets} static files"
|
|
100
100
|
summary += ", #{indexed} pages indexed" if indexed.positive?
|
|
@@ -6,6 +6,15 @@ module Docyard
|
|
|
6
6
|
@config = config
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
+
SOCIAL_ICON_MAP = {
|
|
10
|
+
"x" => "x-logo", "twitter" => "x-logo", "discord" => "discord-logo",
|
|
11
|
+
"linkedin" => "linkedin-logo", "youtube" => "youtube-logo", "instagram" => "instagram-logo",
|
|
12
|
+
"facebook" => "facebook-logo", "tiktok" => "tiktok-logo", "twitch" => "twitch-logo",
|
|
13
|
+
"reddit" => "reddit-logo", "mastodon" => "mastodon-logo", "threads" => "threads-logo",
|
|
14
|
+
"pinterest" => "pinterest-logo", "medium" => "medium-logo", "slack" => "slack-logo",
|
|
15
|
+
"gitlab" => "gitlab-logo"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
9
18
|
def resolve
|
|
10
19
|
return default_branding unless config
|
|
11
20
|
|
|
@@ -23,52 +32,152 @@ module Docyard
|
|
|
23
32
|
logo: Constants::DEFAULT_LOGO_PATH,
|
|
24
33
|
logo_dark: Constants::DEFAULT_LOGO_DARK_PATH,
|
|
25
34
|
favicon: nil,
|
|
26
|
-
|
|
27
|
-
|
|
35
|
+
credits: true,
|
|
36
|
+
social: []
|
|
28
37
|
}
|
|
29
38
|
end
|
|
30
39
|
|
|
31
40
|
def config_branding_options
|
|
32
|
-
site_options
|
|
41
|
+
site_options
|
|
42
|
+
.merge(logo_options)
|
|
43
|
+
.merge(search_options)
|
|
44
|
+
.merge(credits_options)
|
|
45
|
+
.merge(social_options)
|
|
46
|
+
.merge(navigation_options)
|
|
47
|
+
.merge(tabs_options)
|
|
33
48
|
end
|
|
34
49
|
|
|
35
50
|
def site_options
|
|
36
51
|
{
|
|
37
|
-
site_title: config.
|
|
38
|
-
site_description: config.
|
|
39
|
-
favicon: config.branding.favicon
|
|
52
|
+
site_title: config.title || Constants::DEFAULT_SITE_TITLE,
|
|
53
|
+
site_description: config.description || "",
|
|
54
|
+
favicon: config.branding.favicon || auto_detect_favicon
|
|
40
55
|
}
|
|
41
56
|
end
|
|
42
57
|
|
|
43
58
|
def logo_options
|
|
44
59
|
branding = config.branding
|
|
60
|
+
logo = branding.logo || auto_detect_logo
|
|
61
|
+
has_custom_logo = !logo.nil?
|
|
45
62
|
{
|
|
46
|
-
logo:
|
|
47
|
-
logo_dark:
|
|
63
|
+
logo: logo || Constants::DEFAULT_LOGO_PATH,
|
|
64
|
+
logo_dark: detect_dark_logo(logo) || Constants::DEFAULT_LOGO_DARK_PATH,
|
|
65
|
+
has_custom_logo: has_custom_logo
|
|
48
66
|
}
|
|
49
67
|
end
|
|
50
68
|
|
|
69
|
+
def auto_detect_logo
|
|
70
|
+
detect_public_file("logo", %w[svg png])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def auto_detect_favicon
|
|
74
|
+
detect_public_file("favicon", %w[ico svg png])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def detect_public_file(name, extensions)
|
|
78
|
+
extensions.each do |ext|
|
|
79
|
+
path = File.join(Constants::PUBLIC_DIR, "#{name}.#{ext}")
|
|
80
|
+
return "#{name}.#{ext}" if File.exist?(path)
|
|
81
|
+
end
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def detect_dark_logo(logo)
|
|
86
|
+
return nil unless logo
|
|
87
|
+
|
|
88
|
+
ext = File.extname(logo)
|
|
89
|
+
base = File.basename(logo, ext)
|
|
90
|
+
dark_filename = "#{base}-dark#{ext}"
|
|
91
|
+
|
|
92
|
+
if File.absolute_path?(logo)
|
|
93
|
+
dark_path = File.join(File.dirname(logo), dark_filename)
|
|
94
|
+
File.exist?(dark_path) ? dark_path : logo
|
|
95
|
+
else
|
|
96
|
+
dark_path = File.join("docs/public", dark_filename)
|
|
97
|
+
File.exist?(dark_path) ? dark_filename : logo
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
51
101
|
def search_options
|
|
52
102
|
{
|
|
53
103
|
search_enabled: config.search.enabled != false,
|
|
54
|
-
search_placeholder: config.search.placeholder || "Search
|
|
104
|
+
search_placeholder: config.search.placeholder || "Search..."
|
|
55
105
|
}
|
|
56
106
|
end
|
|
57
107
|
|
|
58
|
-
def
|
|
59
|
-
appearance = config.branding.appearance || {}
|
|
108
|
+
def credits_options
|
|
60
109
|
{
|
|
61
|
-
|
|
62
|
-
|
|
110
|
+
credits: config.branding.credits != false,
|
|
111
|
+
copyright: config.branding.copyright
|
|
63
112
|
}
|
|
64
113
|
end
|
|
65
114
|
|
|
66
|
-
def
|
|
67
|
-
|
|
115
|
+
def social_options
|
|
116
|
+
socials = config.socials || {}
|
|
117
|
+
{
|
|
118
|
+
social: normalize_social_links(socials)
|
|
119
|
+
}
|
|
68
120
|
end
|
|
69
121
|
|
|
70
|
-
def
|
|
71
|
-
|
|
122
|
+
def normalize_social_links(socials)
|
|
123
|
+
return [] unless socials.is_a?(Hash) && socials.any?
|
|
124
|
+
|
|
125
|
+
socials.filter_map { |platform, url| build_social_link(platform.to_s, url) }
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def build_social_link(platform, url)
|
|
129
|
+
return if platform == "custom" || !valid_url?(url)
|
|
130
|
+
|
|
131
|
+
{ platform: platform, url: url, icon: SOCIAL_ICON_MAP[platform] || platform }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def valid_url?(url)
|
|
135
|
+
url.is_a?(String) && !url.strip.empty?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def navigation_options
|
|
139
|
+
cta_items = config.navigation.cta || []
|
|
140
|
+
{
|
|
141
|
+
header_ctas: normalize_cta_items(cta_items)
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def normalize_cta_items(items)
|
|
146
|
+
return [] unless items.is_a?(Array)
|
|
147
|
+
|
|
148
|
+
items.first(2).filter_map do |item|
|
|
149
|
+
next unless item.is_a?(Hash) && item["text"] && item["href"]
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
text: item["text"],
|
|
153
|
+
href: item["href"],
|
|
154
|
+
variant: item["variant"] || "primary",
|
|
155
|
+
external: item["external"] == true
|
|
156
|
+
}
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def tabs_options
|
|
161
|
+
tab_items = config.tabs || []
|
|
162
|
+
{
|
|
163
|
+
tabs: normalize_tab_items(tab_items),
|
|
164
|
+
has_tabs: tab_items.any?
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def normalize_tab_items(items)
|
|
169
|
+
return [] unless items.is_a?(Array)
|
|
170
|
+
|
|
171
|
+
items.filter_map do |item|
|
|
172
|
+
next unless item.is_a?(Hash) && item["text"] && item["href"]
|
|
173
|
+
|
|
174
|
+
{
|
|
175
|
+
text: item["text"],
|
|
176
|
+
href: item["href"],
|
|
177
|
+
icon: item["icon"],
|
|
178
|
+
external: item["external"] == true
|
|
179
|
+
}
|
|
180
|
+
end
|
|
72
181
|
end
|
|
73
182
|
end
|
|
74
183
|
end
|