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.
Files changed (177) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/CHANGELOG.md +34 -1
  4. data/lib/docyard/build/asset_bundler.rb +22 -7
  5. data/lib/docyard/build/file_copier.rb +49 -27
  6. data/lib/docyard/build/sitemap_generator.rb +6 -6
  7. data/lib/docyard/build/static_generator.rb +82 -50
  8. data/lib/docyard/builder.rb +20 -10
  9. data/lib/docyard/cli.rb +6 -3
  10. data/lib/docyard/components/aliases.rb +29 -0
  11. data/lib/docyard/components/processors/callout_processor.rb +124 -0
  12. data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +106 -0
  13. data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +79 -0
  14. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +78 -0
  15. data/lib/docyard/components/processors/code_block_processor.rb +175 -0
  16. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +127 -0
  17. data/lib/docyard/components/processors/heading_anchor_processor.rb +39 -0
  18. data/lib/docyard/components/processors/icon_processor.rb +53 -0
  19. data/lib/docyard/components/processors/table_of_contents_processor.rb +68 -0
  20. data/lib/docyard/components/processors/table_wrapper_processor.rb +22 -0
  21. data/lib/docyard/components/processors/tabs_processor.rb +48 -0
  22. data/lib/docyard/components/support/code_block/feature_extractor.rb +117 -0
  23. data/lib/docyard/components/support/code_block/icon_detector.rb +44 -0
  24. data/lib/docyard/components/support/code_block/line_parser.rb +84 -0
  25. data/lib/docyard/components/support/code_block/line_wrapper.rb +50 -0
  26. data/lib/docyard/components/support/code_block/patterns.rb +55 -0
  27. data/lib/docyard/components/support/code_detector.rb +61 -0
  28. data/lib/docyard/components/support/tabs/icon_detector.rb +62 -0
  29. data/lib/docyard/components/support/tabs/parser.rb +195 -0
  30. data/lib/docyard/components/support/tabs/range_finder.rb +46 -0
  31. data/lib/docyard/config/branding_resolver.rb +183 -0
  32. data/lib/docyard/{constants.rb → config/constants.rb} +7 -4
  33. data/lib/docyard/config/validator.rb +122 -99
  34. data/lib/docyard/config.rb +38 -36
  35. data/lib/docyard/initializer.rb +15 -76
  36. data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
  37. data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +6 -3
  38. data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
  39. data/lib/docyard/navigation/sidebar/config_parser.rb +208 -0
  40. data/lib/docyard/navigation/sidebar/file_resolver.rb +78 -0
  41. data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +2 -1
  42. data/lib/docyard/navigation/sidebar/item.rb +96 -0
  43. data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
  44. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +69 -0
  45. data/lib/docyard/navigation/sidebar/metadata_reader.rb +47 -0
  46. data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
  47. data/lib/docyard/navigation/sidebar/renderer.rb +144 -0
  48. data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
  49. data/lib/docyard/navigation/sidebar/tree_builder.rb +139 -0
  50. data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
  51. data/lib/docyard/navigation/sidebar_builder.rb +159 -0
  52. data/lib/docyard/rendering/icon_helpers.rb +13 -0
  53. data/lib/docyard/{icons → rendering/icons}/phosphor.rb +26 -1
  54. data/lib/docyard/{markdown.rb → rendering/markdown.rb} +19 -13
  55. data/lib/docyard/rendering/renderer.rb +163 -0
  56. data/lib/docyard/rendering/template_resolver.rb +172 -0
  57. data/lib/docyard/routing/fallback_resolver.rb +92 -0
  58. data/lib/docyard/search/build_indexer.rb +74 -0
  59. data/lib/docyard/search/dev_indexer.rb +155 -0
  60. data/lib/docyard/search/pagefind_support.rb +33 -0
  61. data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +24 -19
  62. data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
  63. data/lib/docyard/server/pagefind_handler.rb +63 -0
  64. data/lib/docyard/{preview_server.rb → server/preview_server.rb} +2 -2
  65. data/lib/docyard/server/rack_application.rb +192 -0
  66. data/lib/docyard/server/resolution_result.rb +29 -0
  67. data/lib/docyard/{router.rb → server/router.rb} +4 -4
  68. data/lib/docyard/templates/assets/css/code.css +18 -51
  69. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +143 -0
  70. data/lib/docyard/templates/assets/css/components/callout.css +67 -67
  71. data/lib/docyard/templates/assets/css/components/code-block.css +180 -282
  72. data/lib/docyard/templates/assets/css/components/heading-anchor.css +28 -15
  73. data/lib/docyard/templates/assets/css/components/icon.css +0 -1
  74. data/lib/docyard/templates/assets/css/components/logo.css +0 -2
  75. data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
  76. data/lib/docyard/templates/assets/css/components/navigation.css +186 -167
  77. data/lib/docyard/templates/assets/css/components/prev-next.css +76 -47
  78. data/lib/docyard/templates/assets/css/components/search.css +561 -0
  79. data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
  80. data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
  81. data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
  82. data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
  83. data/lib/docyard/templates/assets/css/landing.css +815 -0
  84. data/lib/docyard/templates/assets/css/layout.css +503 -87
  85. data/lib/docyard/templates/assets/css/main.css +1 -3
  86. data/lib/docyard/templates/assets/css/markdown.css +111 -93
  87. data/lib/docyard/templates/assets/css/reset.css +0 -3
  88. data/lib/docyard/templates/assets/css/typography.css +43 -41
  89. data/lib/docyard/templates/assets/css/variables.css +268 -208
  90. data/lib/docyard/templates/assets/favicon.svg +7 -8
  91. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
  92. data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
  93. data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
  94. data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
  95. data/lib/docyard/templates/assets/js/components/search.js +610 -0
  96. data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
  97. data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
  98. data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
  99. data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
  100. data/lib/docyard/templates/assets/js/theme.js +0 -3
  101. data/lib/docyard/templates/assets/logo-dark.svg +8 -2
  102. data/lib/docyard/templates/assets/logo.svg +7 -4
  103. data/lib/docyard/templates/config/docyard.yml.erb +37 -34
  104. data/lib/docyard/templates/errors/404.html.erb +1 -1
  105. data/lib/docyard/templates/errors/500.html.erb +1 -1
  106. data/lib/docyard/templates/layouts/default.html.erb +19 -56
  107. data/lib/docyard/templates/layouts/splash.html.erb +176 -0
  108. data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
  109. data/lib/docyard/templates/partials/_code_block.html.erb +6 -4
  110. data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
  111. data/lib/docyard/templates/partials/_features.html.erb +15 -0
  112. data/lib/docyard/templates/partials/_footer.html.erb +42 -0
  113. data/lib/docyard/templates/partials/_head.html.erb +22 -0
  114. data/lib/docyard/templates/partials/_header.html.erb +49 -0
  115. data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
  116. data/lib/docyard/templates/partials/_hero.html.erb +27 -0
  117. data/lib/docyard/templates/partials/_nav_group.html.erb +25 -11
  118. data/lib/docyard/templates/partials/_nav_leaf.html.erb +1 -1
  119. data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
  120. data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
  121. data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
  122. data/lib/docyard/templates/partials/_prev_next.html.erb +9 -3
  123. data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
  124. data/lib/docyard/templates/partials/_search_modal.html.erb +41 -0
  125. data/lib/docyard/templates/partials/_search_trigger.html.erb +18 -0
  126. data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
  127. data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
  128. data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
  129. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
  130. data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
  131. data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
  132. data/lib/docyard/utils/html_helpers.rb +14 -0
  133. data/lib/docyard/utils/path_resolver.rb +2 -1
  134. data/lib/docyard/utils/url_helpers.rb +20 -0
  135. data/lib/docyard/version.rb +1 -1
  136. data/lib/docyard.rb +22 -15
  137. metadata +89 -50
  138. data/lib/docyard/components/callout_processor.rb +0 -121
  139. data/lib/docyard/components/code_block_diff_preprocessor.rb +0 -104
  140. data/lib/docyard/components/code_block_feature_extractor.rb +0 -113
  141. data/lib/docyard/components/code_block_focus_preprocessor.rb +0 -77
  142. data/lib/docyard/components/code_block_icon_detector.rb +0 -40
  143. data/lib/docyard/components/code_block_line_wrapper.rb +0 -46
  144. data/lib/docyard/components/code_block_options_preprocessor.rb +0 -76
  145. data/lib/docyard/components/code_block_patterns.rb +0 -51
  146. data/lib/docyard/components/code_block_processor.rb +0 -176
  147. data/lib/docyard/components/code_detector.rb +0 -59
  148. data/lib/docyard/components/code_line_parser.rb +0 -80
  149. data/lib/docyard/components/code_snippet_import_preprocessor.rb +0 -125
  150. data/lib/docyard/components/heading_anchor_processor.rb +0 -34
  151. data/lib/docyard/components/icon_detector.rb +0 -57
  152. data/lib/docyard/components/icon_processor.rb +0 -51
  153. data/lib/docyard/components/table_of_contents_processor.rb +0 -64
  154. data/lib/docyard/components/table_wrapper_processor.rb +0 -18
  155. data/lib/docyard/components/tabs_parser.rb +0 -191
  156. data/lib/docyard/components/tabs_processor.rb +0 -44
  157. data/lib/docyard/components/tabs_range_finder.rb +0 -42
  158. data/lib/docyard/rack_application.rb +0 -172
  159. data/lib/docyard/renderer.rb +0 -120
  160. data/lib/docyard/routing/resolution_result.rb +0 -31
  161. data/lib/docyard/sidebar/config_parser.rb +0 -180
  162. data/lib/docyard/sidebar/item.rb +0 -58
  163. data/lib/docyard/sidebar/renderer.rb +0 -137
  164. data/lib/docyard/sidebar/tree_builder.rb +0 -59
  165. data/lib/docyard/sidebar_builder.rb +0 -102
  166. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
  167. data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
  168. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
  169. data/lib/docyard/templates/markdown/index.md.erb +0 -82
  170. /data/lib/docyard/{sidebar → navigation/sidebar}/title_extractor.rb +0 -0
  171. /data/lib/docyard/{icons → rendering/icons}/LICENSE.phosphor +0 -0
  172. /data/lib/docyard/{icons → rendering/icons}/file_types.rb +0 -0
  173. /data/lib/docyard/{icons.rb → rendering/icons.rb} +0 -0
  174. /data/lib/docyard/{language_mapping.rb → rendering/language_mapping.rb} +0 -0
  175. /data/lib/docyard/{file_watcher.rb → server/file_watcher.rb} +0 -0
  176. /data/lib/docyard/{errors.rb → utils/errors.rb} +0 -0
  177. /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
- ASSETS_PATH = File.join(__dir__, "templates", "assets")
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/woff2",
16
+ ".woff" => "font/woff",
16
17
  ".woff2" => "font/woff2",
17
- ".ico" => "image/x-icon"
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 serve(request_path)
21
- asset_path = extract_asset_path(request_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 = build_file_path(asset_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
- private
39
+ def serve_public_file(request_path)
40
+ asset_path = request_path.delete_prefix("/")
35
41
 
36
- def extract_asset_path(request_path)
37
- request_path.delete_prefix("/assets/")
38
- end
42
+ return nil if directory_traversal?(asset_path)
39
43
 
40
- def directory_traversal?(path)
41
- path.include?("..")
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
- 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)
50
+ private
47
51
 
48
- File.join(ASSETS_PATH, asset_path)
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(ASSETS_PATH, "css", "components")
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(ASSETS_PATH, "js", "components")
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
- @app = RackApplication.new(
23
- docs_path: File.expand_path(docs_path),
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
- @file_watcher.stop
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.output_dir)
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 Routing::ResolutionResult.found(file_path) if File.file?(file_path)
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 Routing::ResolutionResult.found(index_path) if File.file?(index_path)
18
+ return ResolutionResult.found(index_path) if File.file?(index_path)
19
19
 
20
- Routing::ResolutionResult.not_found
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