docyard 0.6.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 +34 -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 +82 -50
- data/lib/docyard/builder.rb +20 -10
- data/lib/docyard/cli.rb +6 -3
- data/lib/docyard/components/aliases.rb +29 -0
- data/lib/docyard/components/processors/callout_processor.rb +124 -0
- data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +106 -0
- data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +79 -0
- data/lib/docyard/components/processors/code_block_options_preprocessor.rb +78 -0
- data/lib/docyard/components/processors/code_block_processor.rb +175 -0
- data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +127 -0
- data/lib/docyard/components/processors/heading_anchor_processor.rb +39 -0
- data/lib/docyard/components/processors/icon_processor.rb +53 -0
- data/lib/docyard/components/processors/table_of_contents_processor.rb +68 -0
- data/lib/docyard/components/processors/table_wrapper_processor.rb +22 -0
- data/lib/docyard/components/processors/tabs_processor.rb +48 -0
- data/lib/docyard/components/support/code_block/feature_extractor.rb +117 -0
- data/lib/docyard/components/support/code_block/icon_detector.rb +44 -0
- data/lib/docyard/components/support/code_block/line_parser.rb +84 -0
- data/lib/docyard/components/support/code_block/line_wrapper.rb +50 -0
- data/lib/docyard/components/support/code_block/patterns.rb +55 -0
- data/lib/docyard/components/support/code_detector.rb +61 -0
- data/lib/docyard/components/support/tabs/icon_detector.rb +62 -0
- data/lib/docyard/components/support/tabs/parser.rb +195 -0
- data/lib/docyard/components/support/tabs/range_finder.rb +46 -0
- data/lib/docyard/config/branding_resolver.rb +183 -0
- data/lib/docyard/{constants.rb → config/constants.rb} +7 -4
- data/lib/docyard/config/validator.rb +122 -99
- data/lib/docyard/config.rb +38 -36
- data/lib/docyard/initializer.rb +15 -76
- data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
- data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +6 -3
- data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
- data/lib/docyard/navigation/sidebar/config_parser.rb +208 -0
- data/lib/docyard/navigation/sidebar/file_resolver.rb +78 -0
- data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +2 -1
- data/lib/docyard/navigation/sidebar/item.rb +96 -0
- 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 +144 -0
- data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
- data/lib/docyard/navigation/sidebar/tree_builder.rb +139 -0
- data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
- data/lib/docyard/navigation/sidebar_builder.rb +159 -0
- data/lib/docyard/rendering/icon_helpers.rb +13 -0
- data/lib/docyard/{icons → rendering/icons}/phosphor.rb +26 -1
- data/lib/docyard/{markdown.rb → rendering/markdown.rb} +19 -13
- data/lib/docyard/rendering/renderer.rb +163 -0
- 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 +74 -0
- data/lib/docyard/search/dev_indexer.rb +155 -0
- data/lib/docyard/search/pagefind_support.rb +33 -0
- data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +24 -19
- data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
- data/lib/docyard/server/pagefind_handler.rb +63 -0
- data/lib/docyard/{preview_server.rb → server/preview_server.rb} +2 -2
- data/lib/docyard/server/rack_application.rb +192 -0
- data/lib/docyard/server/resolution_result.rb +29 -0
- data/lib/docyard/{router.rb → server/router.rb} +4 -4
- 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 +561 -0
- 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 +503 -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 +610 -0
- 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 +19 -56
- 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 +6 -4
- 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 +9 -3
- data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
- data/lib/docyard/templates/partials/_search_modal.html.erb +41 -0
- data/lib/docyard/templates/partials/_search_trigger.html.erb +18 -0
- 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/utils/html_helpers.rb +14 -0
- data/lib/docyard/utils/path_resolver.rb +2 -1
- data/lib/docyard/utils/url_helpers.rb +20 -0
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +22 -15
- metadata +89 -50
- data/lib/docyard/components/callout_processor.rb +0 -121
- data/lib/docyard/components/code_block_diff_preprocessor.rb +0 -104
- data/lib/docyard/components/code_block_feature_extractor.rb +0 -113
- data/lib/docyard/components/code_block_focus_preprocessor.rb +0 -77
- data/lib/docyard/components/code_block_icon_detector.rb +0 -40
- data/lib/docyard/components/code_block_line_wrapper.rb +0 -46
- data/lib/docyard/components/code_block_options_preprocessor.rb +0 -76
- data/lib/docyard/components/code_block_patterns.rb +0 -51
- data/lib/docyard/components/code_block_processor.rb +0 -176
- data/lib/docyard/components/code_detector.rb +0 -59
- data/lib/docyard/components/code_line_parser.rb +0 -80
- data/lib/docyard/components/code_snippet_import_preprocessor.rb +0 -125
- data/lib/docyard/components/heading_anchor_processor.rb +0 -34
- data/lib/docyard/components/icon_detector.rb +0 -57
- data/lib/docyard/components/icon_processor.rb +0 -51
- data/lib/docyard/components/table_of_contents_processor.rb +0 -64
- data/lib/docyard/components/table_wrapper_processor.rb +0 -18
- data/lib/docyard/components/tabs_parser.rb +0 -191
- data/lib/docyard/components/tabs_processor.rb +0 -44
- data/lib/docyard/components/tabs_range_finder.rb +0 -42
- data/lib/docyard/rack_application.rb +0 -172
- data/lib/docyard/renderer.rb +0 -120
- data/lib/docyard/routing/resolution_result.rb +0 -31
- data/lib/docyard/sidebar/config_parser.rb +0 -180
- data/lib/docyard/sidebar/item.rb +0 -58
- data/lib/docyard/sidebar/renderer.rb +0 -137
- data/lib/docyard/sidebar/tree_builder.rb +0 -59
- data/lib/docyard/sidebar_builder.rb +0 -102
- 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
- /data/lib/docyard/{sidebar → navigation/sidebar}/title_extractor.rb +0 -0
- /data/lib/docyard/{icons → rendering/icons}/LICENSE.phosphor +0 -0
- /data/lib/docyard/{icons → rendering/icons}/file_types.rb +0 -0
- /data/lib/docyard/{icons.rb → rendering/icons.rb} +0 -0
- /data/lib/docyard/{language_mapping.rb → rendering/language_mapping.rb} +0 -0
- /data/lib/docyard/{file_watcher.rb → server/file_watcher.rb} +0 -0
- /data/lib/docyard/{errors.rb → utils/errors.rb} +0 -0
- /data/lib/docyard/{logging.rb → utils/logging.rb} +0 -0
|
@@ -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"))
|
|
@@ -4,30 +4,30 @@ require "webrick"
|
|
|
4
4
|
require "stringio"
|
|
5
5
|
require_relative "file_watcher"
|
|
6
6
|
require_relative "rack_application"
|
|
7
|
-
require_relative "config"
|
|
7
|
+
require_relative "../config"
|
|
8
8
|
|
|
9
9
|
module Docyard
|
|
10
10
|
class Server
|
|
11
11
|
DEFAULT_PORT = 4200
|
|
12
12
|
DEFAULT_HOST = "localhost"
|
|
13
13
|
|
|
14
|
-
attr_reader :port, :host, :docs_path, :config
|
|
14
|
+
attr_reader :port, :host, :docs_path, :config, :search_enabled
|
|
15
15
|
|
|
16
|
-
def initialize(port: DEFAULT_PORT, host: DEFAULT_HOST, docs_path: "docs")
|
|
16
|
+
def initialize(port: DEFAULT_PORT, host: DEFAULT_HOST, docs_path: "docs", search: false)
|
|
17
17
|
@port = port
|
|
18
18
|
@host = host
|
|
19
19
|
@docs_path = docs_path
|
|
20
|
+
@search_enabled = search
|
|
20
21
|
@config = Config.load
|
|
21
22
|
@file_watcher = FileWatcher.new(File.expand_path(docs_path))
|
|
22
|
-
@
|
|
23
|
-
|
|
24
|
-
file_watcher: @file_watcher,
|
|
25
|
-
config: @config
|
|
26
|
-
)
|
|
23
|
+
@search_indexer = nil
|
|
24
|
+
@app = nil
|
|
27
25
|
end
|
|
28
26
|
|
|
29
27
|
def start
|
|
30
28
|
validate_docs_directory!
|
|
29
|
+
generate_search_index if @search_enabled
|
|
30
|
+
initialize_app
|
|
31
31
|
print_server_info
|
|
32
32
|
@file_watcher.start
|
|
33
33
|
|
|
@@ -35,11 +35,33 @@ module Docyard
|
|
|
35
35
|
trap("INT") { shutdown_server }
|
|
36
36
|
|
|
37
37
|
http_server.start
|
|
38
|
-
|
|
38
|
+
cleanup
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
|
+
def generate_search_index
|
|
44
|
+
@search_indexer = Search::DevIndexer.new(
|
|
45
|
+
docs_path: File.expand_path(docs_path),
|
|
46
|
+
config: @config
|
|
47
|
+
)
|
|
48
|
+
@search_indexer.generate
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def initialize_app
|
|
52
|
+
@app = RackApplication.new(
|
|
53
|
+
docs_path: File.expand_path(docs_path),
|
|
54
|
+
file_watcher: @file_watcher,
|
|
55
|
+
config: @config,
|
|
56
|
+
pagefind_path: @search_indexer&.pagefind_path
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def cleanup
|
|
61
|
+
@file_watcher.stop
|
|
62
|
+
@search_indexer&.cleanup
|
|
63
|
+
end
|
|
64
|
+
|
|
43
65
|
def validate_docs_directory!
|
|
44
66
|
return if File.directory?(docs_path)
|
|
45
67
|
|
|
@@ -51,6 +73,7 @@ module Docyard
|
|
|
51
73
|
puts "Starting Docyard server..."
|
|
52
74
|
puts "=> Serving docs from: #{docs_path}/"
|
|
53
75
|
puts "=> Running at: http://#{host}:#{port}"
|
|
76
|
+
puts "=> Search: #{@search_enabled ? 'enabled' : 'disabled (use --search to enable)'}"
|
|
54
77
|
puts "=> Press Ctrl+C to stop\n"
|
|
55
78
|
end
|
|
56
79
|
|
|
@@ -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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "webrick"
|
|
4
|
-
require_relative "config"
|
|
4
|
+
require_relative "../config"
|
|
5
5
|
|
|
6
6
|
module Docyard
|
|
7
7
|
class PreviewServer
|
|
@@ -12,7 +12,7 @@ module Docyard
|
|
|
12
12
|
def initialize(port: DEFAULT_PORT)
|
|
13
13
|
@port = port
|
|
14
14
|
@config = Config.load
|
|
15
|
-
@output_dir = File.expand_path(@config.build.
|
|
15
|
+
@output_dir = File.expand_path(@config.build.output)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def start
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "rack"
|
|
5
|
+
require_relative "../navigation/sidebar_builder"
|
|
6
|
+
require_relative "../navigation/prev_next_builder"
|
|
7
|
+
require_relative "../navigation/breadcrumb_builder"
|
|
8
|
+
require_relative "../config/branding_resolver"
|
|
9
|
+
require_relative "../config/constants"
|
|
10
|
+
require_relative "../rendering/template_resolver"
|
|
11
|
+
require_relative "../routing/fallback_resolver"
|
|
12
|
+
require_relative "pagefind_handler"
|
|
13
|
+
|
|
14
|
+
module Docyard
|
|
15
|
+
class RackApplication
|
|
16
|
+
def initialize(docs_path:, file_watcher:, config: nil, pagefind_path: nil)
|
|
17
|
+
@docs_path = docs_path
|
|
18
|
+
@file_watcher = file_watcher
|
|
19
|
+
@config = config
|
|
20
|
+
@router = Router.new(docs_path: docs_path)
|
|
21
|
+
@renderer = Renderer.new(base_url: config&.build&.base || "/", config: config)
|
|
22
|
+
@asset_handler = AssetHandler.new
|
|
23
|
+
@pagefind_handler = PagefindHandler.new(pagefind_path: pagefind_path, config: config)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call(env)
|
|
27
|
+
handle_request(env)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
attr_reader :docs_path, :file_watcher, :config, :router, :renderer, :asset_handler, :pagefind_handler
|
|
33
|
+
|
|
34
|
+
def handle_request(env)
|
|
35
|
+
path = env["PATH_INFO"]
|
|
36
|
+
|
|
37
|
+
return handle_reload_check(env) if path == Constants::RELOAD_ENDPOINT
|
|
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
|
|
43
|
+
|
|
44
|
+
handle_documentation_request(path)
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
handle_error(e)
|
|
47
|
+
end
|
|
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
|
+
|
|
55
|
+
result = router.resolve(path)
|
|
56
|
+
|
|
57
|
+
if result.found?
|
|
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)
|
|
86
|
+
else
|
|
87
|
+
render_not_found_page
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def redirect_to(path)
|
|
92
|
+
[Constants::STATUS_REDIRECT, { "Location" => path }, []]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def render_documentation_page(file_path, current_path)
|
|
96
|
+
markdown = Markdown.new(File.read(file_path))
|
|
97
|
+
template_resolver = TemplateResolver.new(markdown.frontmatter, @config&.data)
|
|
98
|
+
branding = branding_options
|
|
99
|
+
|
|
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)
|
|
104
|
+
|
|
105
|
+
[Constants::STATUS_OK, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
|
|
106
|
+
end
|
|
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
|
+
|
|
119
|
+
def render_not_found_page
|
|
120
|
+
html = renderer.render_not_found
|
|
121
|
+
[Constants::STATUS_NOT_FOUND, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def build_sidebar_instance(current_path, header_ctas = [])
|
|
125
|
+
SidebarBuilder.new(
|
|
126
|
+
docs_path: docs_path,
|
|
127
|
+
current_path: current_path,
|
|
128
|
+
config: config,
|
|
129
|
+
header_ctas: header_ctas
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def build_prev_next(sidebar_builder, current_path, markdown)
|
|
134
|
+
PrevNextBuilder.new(
|
|
135
|
+
sidebar_tree: sidebar_builder.tree,
|
|
136
|
+
current_path: current_path,
|
|
137
|
+
frontmatter: markdown.frontmatter,
|
|
138
|
+
config: navigation_config
|
|
139
|
+
).to_html
|
|
140
|
+
end
|
|
141
|
+
|
|
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
|
|
147
|
+
|
|
148
|
+
def breadcrumbs_enabled?
|
|
149
|
+
config&.navigation&.breadcrumbs != false
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def navigation_config
|
|
153
|
+
{}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def branding_options
|
|
157
|
+
BrandingResolver.new(config).resolve
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def handle_reload_check(env)
|
|
161
|
+
since = parse_since_timestamp(env)
|
|
162
|
+
reload_needed = file_watcher.changed_since?(since)
|
|
163
|
+
|
|
164
|
+
build_reload_response(reload_needed)
|
|
165
|
+
rescue StandardError => e
|
|
166
|
+
log_reload_error(e)
|
|
167
|
+
build_reload_response(false)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def parse_since_timestamp(env)
|
|
171
|
+
query = Rack::Utils.parse_query(env["QUERY_STRING"])
|
|
172
|
+
query["since"] ? Time.at(query["since"].to_f) : Time.now
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def log_reload_error(error)
|
|
176
|
+
Docyard.logger.error "Reload check error: #{error.message}"
|
|
177
|
+
Docyard.logger.debug error.backtrace.join("\n")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def build_reload_response(reload_needed)
|
|
181
|
+
response_body = { reload: reload_needed, timestamp: Time.now.to_f }.to_json
|
|
182
|
+
[Constants::STATUS_OK, { "Content-Type" => Constants::CONTENT_TYPE_JSON }, [response_body]]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def handle_error(error)
|
|
186
|
+
Docyard.logger.error "Request error: #{error.message}"
|
|
187
|
+
Docyard.logger.debug error.backtrace.join("\n")
|
|
188
|
+
[Constants::STATUS_INTERNAL_ERROR, { "Content-Type" => Constants::CONTENT_TYPE_HTML },
|
|
189
|
+
[renderer.render_server_error(error)]]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
class ResolutionResult
|
|
5
|
+
attr_reader :file_path, :status
|
|
6
|
+
|
|
7
|
+
def self.found(file_path)
|
|
8
|
+
new(file_path: file_path, status: :found)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.not_found
|
|
12
|
+
new(file_path: nil, status: :not_found)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(file_path:, status:)
|
|
16
|
+
@file_path = file_path
|
|
17
|
+
@status = status
|
|
18
|
+
freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def found?
|
|
22
|
+
status == :found
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def not_found?
|
|
26
|
+
status == :not_found
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -12,18 +12,18 @@ module Docyard
|
|
|
12
12
|
clean_path = sanitize_path(request_path)
|
|
13
13
|
|
|
14
14
|
file_path = File.join(docs_path, "#{clean_path}#{Constants::MARKDOWN_EXTENSION}")
|
|
15
|
-
return
|
|
15
|
+
return ResolutionResult.found(file_path) if File.file?(file_path)
|
|
16
16
|
|
|
17
17
|
index_path = File.join(docs_path, clean_path, "#{Constants::INDEX_FILE}#{Constants::MARKDOWN_EXTENSION}")
|
|
18
|
-
return
|
|
18
|
+
return ResolutionResult.found(index_path) if File.file?(index_path)
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
ResolutionResult.not_found
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
25
|
def sanitize_path(request_path)
|
|
26
|
-
clean = request_path.delete_prefix("/")
|
|
26
|
+
clean = request_path.delete_prefix("/").delete_suffix("/")
|
|
27
27
|
clean = Constants::INDEX_FILE if clean.empty?
|
|
28
28
|
clean.delete_suffix(Constants::MARKDOWN_EXTENSION)
|
|
29
29
|
end
|