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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Routing
|
|
5
|
+
class FallbackResolver
|
|
6
|
+
attr_reader :docs_path, :sidebar_builder
|
|
7
|
+
|
|
8
|
+
def initialize(docs_path:, sidebar_builder:)
|
|
9
|
+
@docs_path = docs_path
|
|
10
|
+
@sidebar_builder = sidebar_builder
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def resolve_fallback(request_path)
|
|
14
|
+
return nil if file_exists?(request_path)
|
|
15
|
+
|
|
16
|
+
find_first_item_in_section(request_path)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def file_exists?(request_path)
|
|
22
|
+
clean_path = sanitize_path(request_path)
|
|
23
|
+
|
|
24
|
+
file_path = File.join(docs_path, "#{clean_path}.md")
|
|
25
|
+
return true if File.file?(file_path)
|
|
26
|
+
|
|
27
|
+
index_path = File.join(docs_path, clean_path, "index.md")
|
|
28
|
+
File.file?(index_path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def sanitize_path(request_path)
|
|
32
|
+
clean = request_path.to_s.delete_prefix("/").delete_suffix("/")
|
|
33
|
+
clean = "index" if clean.empty?
|
|
34
|
+
clean.delete_suffix(".md")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def find_first_item_in_section(request_path)
|
|
38
|
+
tree = sidebar_builder.tree
|
|
39
|
+
|
|
40
|
+
if root_path?(request_path)
|
|
41
|
+
find_first_navigable_item(tree)
|
|
42
|
+
else
|
|
43
|
+
section = find_section_in_tree(tree, request_path)
|
|
44
|
+
section ? find_first_navigable_item(section[:children] || []) : nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def root_path?(request_path)
|
|
49
|
+
request_path.nil? || request_path == "/" || request_path.empty?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def find_section_in_tree(tree, path)
|
|
53
|
+
normalized_path = normalize_path(path)
|
|
54
|
+
|
|
55
|
+
tree.each do |item|
|
|
56
|
+
return item if path_matches_section?(item, normalized_path)
|
|
57
|
+
|
|
58
|
+
if item[:children]&.any?
|
|
59
|
+
found = find_section_in_tree(item[:children], path)
|
|
60
|
+
return found if found
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def normalize_path(path)
|
|
68
|
+
path.to_s.delete_prefix("/").delete_suffix("/").downcase
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def path_matches_section?(item, normalized_path)
|
|
72
|
+
return false unless item[:type] == :directory
|
|
73
|
+
|
|
74
|
+
item_path = item[:title].to_s.downcase.gsub(/\s+/, "-")
|
|
75
|
+
item_path == normalized_path
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def find_first_navigable_item(items)
|
|
79
|
+
items.each do |item|
|
|
80
|
+
return item[:path] if item[:path] && item[:type] == :file
|
|
81
|
+
|
|
82
|
+
if item[:children]&.any?
|
|
83
|
+
found = find_first_navigable_item(item[:children])
|
|
84
|
+
return found if found
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
nil
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -25,10 +25,10 @@ module Docyard
|
|
|
25
25
|
|
|
26
26
|
@temp_dir = Dir.mktmpdir("docyard-search-")
|
|
27
27
|
generate_html_files
|
|
28
|
-
run_pagefind
|
|
28
|
+
page_count = run_pagefind
|
|
29
29
|
@pagefind_path = File.join(temp_dir, "pagefind")
|
|
30
30
|
|
|
31
|
-
log_success
|
|
31
|
+
log_success(page_count)
|
|
32
32
|
pagefind_path
|
|
33
33
|
rescue StandardError => e
|
|
34
34
|
warn "[!] Search index generation failed: #{e.message}"
|
|
@@ -52,6 +52,8 @@ module Docyard
|
|
|
52
52
|
|
|
53
53
|
def generate_html_files
|
|
54
54
|
markdown_files = Dir.glob(File.join(docs_path, "**", "*.md"))
|
|
55
|
+
markdown_files = filter_excluded_files(markdown_files)
|
|
56
|
+
markdown_files = filter_non_indexable_files(markdown_files)
|
|
55
57
|
renderer = Renderer.new(base_url: "/", config: config)
|
|
56
58
|
|
|
57
59
|
progress = TTY::ProgressBar.new(
|
|
@@ -66,6 +68,45 @@ module Docyard
|
|
|
66
68
|
end
|
|
67
69
|
end
|
|
68
70
|
|
|
71
|
+
def filter_excluded_files(files)
|
|
72
|
+
exclude_patterns = config.search.exclude || []
|
|
73
|
+
return files if exclude_patterns.empty?
|
|
74
|
+
|
|
75
|
+
files.reject do |file_path|
|
|
76
|
+
url_path = file_to_url_path(file_path)
|
|
77
|
+
exclude_patterns.any? { |pattern| File.fnmatch(pattern, url_path, File::FNM_PATHNAME) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def filter_non_indexable_files(files)
|
|
82
|
+
files.reject do |file_path|
|
|
83
|
+
content = File.read(file_path)
|
|
84
|
+
markdown = Markdown.new(content)
|
|
85
|
+
frontmatter = markdown.frontmatter
|
|
86
|
+
|
|
87
|
+
uses_splash_template?(frontmatter)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def uses_splash_template?(frontmatter)
|
|
92
|
+
return true if frontmatter["template"] == "splash"
|
|
93
|
+
return true if frontmatter.key?("landing")
|
|
94
|
+
|
|
95
|
+
frontmatter.key?("hero") || frontmatter.key?("features")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def file_to_url_path(file_path)
|
|
99
|
+
relative_path = file_path.delete_prefix("#{docs_path}/")
|
|
100
|
+
base_name = File.basename(relative_path, ".md")
|
|
101
|
+
dir_name = File.dirname(relative_path)
|
|
102
|
+
|
|
103
|
+
if base_name == "index"
|
|
104
|
+
dir_name == "." ? "/" : "/#{dir_name}"
|
|
105
|
+
else
|
|
106
|
+
dir_name == "." ? "/#{base_name}" : "/#{dir_name}/#{base_name}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
69
110
|
def generate_html_file(markdown_file, renderer)
|
|
70
111
|
relative_path = markdown_file.delete_prefix("#{docs_path}/")
|
|
71
112
|
output_path = determine_output_path(relative_path)
|
|
@@ -97,12 +138,16 @@ module Docyard
|
|
|
97
138
|
|
|
98
139
|
raise "Pagefind failed: #{stderr}" unless status.success?
|
|
99
140
|
|
|
100
|
-
stdout
|
|
141
|
+
extract_page_count(stdout)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def extract_page_count(output)
|
|
145
|
+
match = output.match(/Indexed (\d+) page/i)
|
|
146
|
+
match ? match[1].to_i : 0
|
|
101
147
|
end
|
|
102
148
|
|
|
103
|
-
def log_success
|
|
104
|
-
|
|
105
|
-
puts "=> Search index generated (#{page_count} pages)"
|
|
149
|
+
def log_success(page_count)
|
|
150
|
+
puts "=> Search index generated (#{page_count} pages indexed)"
|
|
106
151
|
puts "=> Temp directory: #{temp_dir}" if ENV["DOCYARD_DEBUG"]
|
|
107
152
|
end
|
|
108
153
|
end
|
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Docyard
|
|
4
4
|
class AssetHandler
|
|
5
|
-
|
|
6
|
-
USER_ASSETS_PATH = "docs/assets"
|
|
5
|
+
TEMPLATES_ASSETS_PATH = File.join(__dir__, "../templates", "assets")
|
|
7
6
|
|
|
8
7
|
CONTENT_TYPES = {
|
|
9
8
|
".css" => "text/css; charset=utf-8",
|
|
@@ -11,41 +10,47 @@ module Docyard
|
|
|
11
10
|
".png" => "image/png",
|
|
12
11
|
".jpg" => "image/jpeg",
|
|
13
12
|
".jpeg" => "image/jpeg",
|
|
13
|
+
".gif" => "image/gif",
|
|
14
|
+
".webp" => "image/webp",
|
|
14
15
|
".svg" => "image/svg+xml",
|
|
15
|
-
".woff" => "font/
|
|
16
|
+
".woff" => "font/woff",
|
|
16
17
|
".woff2" => "font/woff2",
|
|
17
|
-
".
|
|
18
|
+
".ttf" => "font/ttf",
|
|
19
|
+
".ico" => "image/x-icon",
|
|
20
|
+
".pdf" => "application/pdf",
|
|
21
|
+
".mp4" => "video/mp4",
|
|
22
|
+
".webm" => "video/webm"
|
|
18
23
|
}.freeze
|
|
19
24
|
|
|
20
|
-
def
|
|
21
|
-
asset_path =
|
|
25
|
+
def serve_docyard_assets(request_path)
|
|
26
|
+
asset_path = request_path.delete_prefix("/_docyard/")
|
|
22
27
|
|
|
23
28
|
return forbidden_response if directory_traversal?(asset_path)
|
|
24
29
|
|
|
25
30
|
return serve_components_css if asset_path == "css/components.css"
|
|
26
31
|
return serve_components_js if asset_path == "js/components.js"
|
|
27
32
|
|
|
28
|
-
file_path =
|
|
33
|
+
file_path = File.join(TEMPLATES_ASSETS_PATH, asset_path)
|
|
29
34
|
return not_found_response unless File.file?(file_path)
|
|
30
35
|
|
|
31
36
|
serve_file(file_path)
|
|
32
37
|
end
|
|
33
38
|
|
|
34
|
-
|
|
39
|
+
def serve_public_file(request_path)
|
|
40
|
+
asset_path = request_path.delete_prefix("/")
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
request_path.delete_prefix("/assets/")
|
|
38
|
-
end
|
|
42
|
+
return nil if directory_traversal?(asset_path)
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
file_path = File.join(Constants::PUBLIC_DIR, asset_path)
|
|
45
|
+
return nil unless File.file?(file_path)
|
|
46
|
+
|
|
47
|
+
serve_file(file_path)
|
|
42
48
|
end
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
user_path = File.join(USER_ASSETS_PATH, asset_path)
|
|
46
|
-
return user_path if File.file?(user_path)
|
|
50
|
+
private
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
def directory_traversal?(path)
|
|
53
|
+
path.include?("..")
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
def serve_file(file_path)
|
|
@@ -61,7 +66,7 @@ module Docyard
|
|
|
61
66
|
end
|
|
62
67
|
|
|
63
68
|
def concatenate_component_css
|
|
64
|
-
components_dir = File.join(
|
|
69
|
+
components_dir = File.join(TEMPLATES_ASSETS_PATH, "css", "components")
|
|
65
70
|
return "" unless Dir.exist?(components_dir)
|
|
66
71
|
|
|
67
72
|
css_files = Dir.glob(File.join(components_dir, "*.css"))
|
|
@@ -74,7 +79,7 @@ module Docyard
|
|
|
74
79
|
end
|
|
75
80
|
|
|
76
81
|
def concatenate_component_js
|
|
77
|
-
components_dir = File.join(
|
|
82
|
+
components_dir = File.join(TEMPLATES_ASSETS_PATH, "js", "components")
|
|
78
83
|
return "" unless Dir.exist?(components_dir)
|
|
79
84
|
|
|
80
85
|
js_files = Dir.glob(File.join(components_dir, "*.js"))
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
class PagefindHandler
|
|
5
|
+
CONTENT_TYPES = {
|
|
6
|
+
".js" => "application/javascript; charset=utf-8",
|
|
7
|
+
".css" => "text/css; charset=utf-8",
|
|
8
|
+
".json" => "application/json; charset=utf-8"
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
def initialize(pagefind_path:, config:)
|
|
12
|
+
@pagefind_path = pagefind_path
|
|
13
|
+
@config = config
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def serve(path)
|
|
17
|
+
relative_path = path.delete_prefix(Constants::PAGEFIND_PREFIX)
|
|
18
|
+
return not_found if relative_path.include?("..")
|
|
19
|
+
|
|
20
|
+
file_path = resolve_file(relative_path)
|
|
21
|
+
return not_found unless file_path && File.file?(file_path)
|
|
22
|
+
|
|
23
|
+
serve_file(file_path)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :pagefind_path, :config
|
|
29
|
+
|
|
30
|
+
def resolve_file(relative_path)
|
|
31
|
+
return File.join(pagefind_path, relative_path) if pagefind_path && Dir.exist?(pagefind_path)
|
|
32
|
+
|
|
33
|
+
output_dir = config&.build&.output_dir || "dist"
|
|
34
|
+
File.join(output_dir, "pagefind", relative_path)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def serve_file(file_path)
|
|
38
|
+
content = File.binread(file_path)
|
|
39
|
+
content_type = content_type_for(file_path)
|
|
40
|
+
|
|
41
|
+
[Constants::STATUS_OK, build_headers(content_type), [content]]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_headers(content_type)
|
|
45
|
+
{
|
|
46
|
+
"Content-Type" => content_type,
|
|
47
|
+
"Cache-Control" => "no-cache, no-store, must-revalidate",
|
|
48
|
+
"Pragma" => "no-cache",
|
|
49
|
+
"Expires" => "0"
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def content_type_for(file_path)
|
|
54
|
+
extension = File.extname(file_path)
|
|
55
|
+
CONTENT_TYPES.fetch(extension, "application/octet-stream")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def not_found
|
|
59
|
+
message = "Pagefind not found. Run 'docyard build' first."
|
|
60
|
+
[Constants::STATUS_NOT_FOUND, { "Content-Type" => "text/plain" }, [message]]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -4,25 +4,23 @@ require "json"
|
|
|
4
4
|
require "rack"
|
|
5
5
|
require_relative "../navigation/sidebar_builder"
|
|
6
6
|
require_relative "../navigation/prev_next_builder"
|
|
7
|
+
require_relative "../navigation/breadcrumb_builder"
|
|
7
8
|
require_relative "../config/branding_resolver"
|
|
8
9
|
require_relative "../config/constants"
|
|
10
|
+
require_relative "../rendering/template_resolver"
|
|
11
|
+
require_relative "../routing/fallback_resolver"
|
|
12
|
+
require_relative "pagefind_handler"
|
|
9
13
|
|
|
10
14
|
module Docyard
|
|
11
15
|
class RackApplication
|
|
12
|
-
PAGEFIND_CONTENT_TYPES = {
|
|
13
|
-
".js" => "application/javascript; charset=utf-8",
|
|
14
|
-
".css" => "text/css; charset=utf-8",
|
|
15
|
-
".json" => "application/json; charset=utf-8"
|
|
16
|
-
}.freeze
|
|
17
|
-
|
|
18
16
|
def initialize(docs_path:, file_watcher:, config: nil, pagefind_path: nil)
|
|
19
17
|
@docs_path = docs_path
|
|
20
18
|
@file_watcher = file_watcher
|
|
21
19
|
@config = config
|
|
22
|
-
@pagefind_path = pagefind_path
|
|
23
20
|
@router = Router.new(docs_path: docs_path)
|
|
24
|
-
@renderer = Renderer.new(base_url: config&.build&.
|
|
21
|
+
@renderer = Renderer.new(base_url: config&.build&.base || "/", config: config)
|
|
25
22
|
@asset_handler = AssetHandler.new
|
|
23
|
+
@pagefind_handler = PagefindHandler.new(pagefind_path: pagefind_path, config: config)
|
|
26
24
|
end
|
|
27
25
|
|
|
28
26
|
def call(env)
|
|
@@ -31,14 +29,17 @@ module Docyard
|
|
|
31
29
|
|
|
32
30
|
private
|
|
33
31
|
|
|
34
|
-
attr_reader :docs_path, :file_watcher, :config, :
|
|
32
|
+
attr_reader :docs_path, :file_watcher, :config, :router, :renderer, :asset_handler, :pagefind_handler
|
|
35
33
|
|
|
36
34
|
def handle_request(env)
|
|
37
35
|
path = env["PATH_INFO"]
|
|
38
36
|
|
|
39
37
|
return handle_reload_check(env) if path == Constants::RELOAD_ENDPOINT
|
|
40
|
-
return asset_handler.
|
|
41
|
-
return
|
|
38
|
+
return asset_handler.serve_docyard_assets(path) if path.start_with?(Constants::DOCYARD_ASSETS_PREFIX)
|
|
39
|
+
return pagefind_handler.serve(path) if path.start_with?(Constants::PAGEFIND_PREFIX)
|
|
40
|
+
|
|
41
|
+
public_response = asset_handler.serve_public_file(path)
|
|
42
|
+
return public_response if public_response
|
|
42
43
|
|
|
43
44
|
handle_documentation_request(path)
|
|
44
45
|
rescue StandardError => e
|
|
@@ -46,45 +47,90 @@ module Docyard
|
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def handle_documentation_request(path)
|
|
50
|
+
if root_path?(path)
|
|
51
|
+
html_response = serve_custom_landing_page
|
|
52
|
+
return html_response if html_response
|
|
53
|
+
end
|
|
54
|
+
|
|
49
55
|
result = router.resolve(path)
|
|
50
56
|
|
|
51
57
|
if result.found?
|
|
52
58
|
render_documentation_page(result.file_path, path)
|
|
59
|
+
else
|
|
60
|
+
try_fallback_redirect(path)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def root_path?(path)
|
|
65
|
+
path == "/" || path.empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def serve_custom_landing_page
|
|
69
|
+
html_path = File.join(docs_path, "index.html")
|
|
70
|
+
return nil unless File.file?(html_path)
|
|
71
|
+
|
|
72
|
+
html = File.read(html_path)
|
|
73
|
+
[Constants::STATUS_OK, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def try_fallback_redirect(path)
|
|
77
|
+
sidebar_builder = build_sidebar_instance(path)
|
|
78
|
+
fallback_resolver = Routing::FallbackResolver.new(
|
|
79
|
+
docs_path: docs_path,
|
|
80
|
+
sidebar_builder: sidebar_builder
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
fallback_path = fallback_resolver.resolve_fallback(path)
|
|
84
|
+
if fallback_path
|
|
85
|
+
redirect_to(fallback_path)
|
|
53
86
|
else
|
|
54
87
|
render_not_found_page
|
|
55
88
|
end
|
|
56
89
|
end
|
|
57
90
|
|
|
91
|
+
def redirect_to(path)
|
|
92
|
+
[Constants::STATUS_REDIRECT, { "Location" => path }, []]
|
|
93
|
+
end
|
|
94
|
+
|
|
58
95
|
def render_documentation_page(file_path, current_path)
|
|
59
|
-
|
|
96
|
+
markdown = Markdown.new(File.read(file_path))
|
|
97
|
+
template_resolver = TemplateResolver.new(markdown.frontmatter, @config&.data)
|
|
98
|
+
branding = branding_options
|
|
60
99
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
branding: branding_options
|
|
66
|
-
)
|
|
100
|
+
navigation = build_navigation_html(template_resolver, current_path, markdown, branding[:header_ctas])
|
|
101
|
+
html = renderer.render_file(file_path, **navigation, branding: branding,
|
|
102
|
+
template_options: template_resolver.to_options,
|
|
103
|
+
current_path: current_path)
|
|
67
104
|
|
|
68
105
|
[Constants::STATUS_OK, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
|
|
69
106
|
end
|
|
70
107
|
|
|
108
|
+
def build_navigation_html(template_resolver, current_path, markdown, header_ctas)
|
|
109
|
+
return { sidebar_html: "", prev_next_html: "", breadcrumbs: nil } unless template_resolver.show_sidebar?
|
|
110
|
+
|
|
111
|
+
sidebar_builder = build_sidebar_instance(current_path, header_ctas)
|
|
112
|
+
{
|
|
113
|
+
sidebar_html: sidebar_builder.to_html,
|
|
114
|
+
prev_next_html: build_prev_next(sidebar_builder, current_path, markdown),
|
|
115
|
+
breadcrumbs: build_breadcrumbs(sidebar_builder.tree, current_path)
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
71
119
|
def render_not_found_page
|
|
72
120
|
html = renderer.render_not_found
|
|
73
121
|
[Constants::STATUS_NOT_FOUND, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
|
|
74
122
|
end
|
|
75
123
|
|
|
76
|
-
def build_sidebar_instance(current_path)
|
|
124
|
+
def build_sidebar_instance(current_path, header_ctas = [])
|
|
77
125
|
SidebarBuilder.new(
|
|
78
126
|
docs_path: docs_path,
|
|
79
127
|
current_path: current_path,
|
|
80
|
-
config: config
|
|
128
|
+
config: config,
|
|
129
|
+
header_ctas: header_ctas
|
|
81
130
|
)
|
|
82
131
|
end
|
|
83
132
|
|
|
84
|
-
def build_prev_next(sidebar_builder, current_path,
|
|
85
|
-
markdown_content = File.read(file_path)
|
|
86
|
-
markdown = Markdown.new(markdown_content)
|
|
87
|
-
|
|
133
|
+
def build_prev_next(sidebar_builder, current_path, markdown)
|
|
88
134
|
PrevNextBuilder.new(
|
|
89
135
|
sidebar_tree: sidebar_builder.tree,
|
|
90
136
|
current_path: current_path,
|
|
@@ -93,10 +139,18 @@ module Docyard
|
|
|
93
139
|
).to_html
|
|
94
140
|
end
|
|
95
141
|
|
|
96
|
-
def
|
|
97
|
-
return
|
|
142
|
+
def build_breadcrumbs(sidebar_tree, current_path)
|
|
143
|
+
return nil unless breadcrumbs_enabled?
|
|
144
|
+
|
|
145
|
+
BreadcrumbBuilder.new(sidebar_tree: sidebar_tree, current_path: current_path)
|
|
146
|
+
end
|
|
98
147
|
|
|
99
|
-
|
|
148
|
+
def breadcrumbs_enabled?
|
|
149
|
+
config&.navigation&.breadcrumbs != false
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def navigation_config
|
|
153
|
+
{}
|
|
100
154
|
end
|
|
101
155
|
|
|
102
156
|
def branding_options
|
|
@@ -134,42 +188,5 @@ module Docyard
|
|
|
134
188
|
[Constants::STATUS_INTERNAL_ERROR, { "Content-Type" => Constants::CONTENT_TYPE_HTML },
|
|
135
189
|
[renderer.render_server_error(error)]]
|
|
136
190
|
end
|
|
137
|
-
|
|
138
|
-
def serve_pagefind(path)
|
|
139
|
-
relative_path = path.delete_prefix(Constants::PAGEFIND_PREFIX)
|
|
140
|
-
return pagefind_not_found if relative_path.include?("..")
|
|
141
|
-
|
|
142
|
-
file_path = resolve_pagefind_file(relative_path)
|
|
143
|
-
return pagefind_not_found unless file_path && File.file?(file_path)
|
|
144
|
-
|
|
145
|
-
content = File.binread(file_path)
|
|
146
|
-
content_type = pagefind_content_type(file_path)
|
|
147
|
-
|
|
148
|
-
headers = {
|
|
149
|
-
"Content-Type" => content_type,
|
|
150
|
-
"Cache-Control" => "no-cache, no-store, must-revalidate",
|
|
151
|
-
"Pragma" => "no-cache",
|
|
152
|
-
"Expires" => "0"
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
[Constants::STATUS_OK, headers, [content]]
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def resolve_pagefind_file(relative_path)
|
|
159
|
-
return File.join(pagefind_path, relative_path) if pagefind_path && Dir.exist?(pagefind_path)
|
|
160
|
-
|
|
161
|
-
output_dir = config&.build&.output_dir || "dist"
|
|
162
|
-
File.join(output_dir, "pagefind", relative_path)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def pagefind_content_type(file_path)
|
|
166
|
-
extension = File.extname(file_path)
|
|
167
|
-
PAGEFIND_CONTENT_TYPES.fetch(extension, "application/octet-stream")
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def pagefind_not_found
|
|
171
|
-
message = "Pagefind not found. Run 'docyard build' first."
|
|
172
|
-
[Constants::STATUS_NOT_FOUND, { "Content-Type" => "text/plain" }, [message]]
|
|
173
|
-
end
|
|
174
191
|
end
|
|
175
192
|
end
|