docyard 0.8.0 → 1.0.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 (189) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -1
  3. data/README.md +8 -253
  4. data/exe/docyard +6 -0
  5. data/lib/docyard/build/asset_bundler.rb +2 -2
  6. data/lib/docyard/build/file_copier.rb +12 -5
  7. data/lib/docyard/build/llms_txt_generator.rb +103 -0
  8. data/lib/docyard/build/sitemap_generator.rb +1 -1
  9. data/lib/docyard/build/static_generator.rb +115 -79
  10. data/lib/docyard/builder.rb +6 -2
  11. data/lib/docyard/cli.rb +14 -4
  12. data/lib/docyard/components/aliases.rb +12 -0
  13. data/lib/docyard/components/processors/abbreviation_processor.rb +72 -0
  14. data/lib/docyard/components/processors/accordion_processor.rb +81 -0
  15. data/lib/docyard/components/processors/badge_processor.rb +72 -0
  16. data/lib/docyard/components/processors/callout_processor.rb +9 -3
  17. data/lib/docyard/components/processors/cards_processor.rb +100 -0
  18. data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
  19. data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
  20. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +34 -3
  21. data/lib/docyard/components/processors/code_block_processor.rb +11 -24
  22. data/lib/docyard/components/processors/code_group_processor.rb +182 -0
  23. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +7 -1
  24. data/lib/docyard/components/processors/custom_anchor_processor.rb +42 -0
  25. data/lib/docyard/components/processors/file_tree_processor.rb +150 -0
  26. data/lib/docyard/components/processors/icon_processor.rb +8 -2
  27. data/lib/docyard/components/processors/image_caption_processor.rb +96 -0
  28. data/lib/docyard/components/processors/include_processor.rb +86 -0
  29. data/lib/docyard/components/processors/steps_processor.rb +89 -0
  30. data/lib/docyard/components/processors/tabs_processor.rb +9 -1
  31. data/lib/docyard/components/processors/tooltip_processor.rb +57 -0
  32. data/lib/docyard/components/processors/video_embed_processor.rb +207 -0
  33. data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
  34. data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
  35. data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
  36. data/lib/docyard/components/support/code_detector.rb +2 -12
  37. data/lib/docyard/components/support/code_group/html_builder.rb +118 -0
  38. data/lib/docyard/components/support/markdown_code_block_helper.rb +56 -0
  39. data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
  40. data/lib/docyard/components/support/tabs/parser.rb +6 -23
  41. data/lib/docyard/config/analytics_resolver.rb +24 -0
  42. data/lib/docyard/config/branding_resolver.rb +84 -58
  43. data/lib/docyard/config/key_validator.rb +30 -0
  44. data/lib/docyard/config/logo_detector.rb +39 -0
  45. data/lib/docyard/config/schema.rb +39 -0
  46. data/lib/docyard/config/section.rb +21 -0
  47. data/lib/docyard/config/validation_helpers.rb +83 -0
  48. data/lib/docyard/config/validator.rb +45 -144
  49. data/lib/docyard/config/validators/navigation.rb +43 -0
  50. data/lib/docyard/config/validators/section.rb +114 -0
  51. data/lib/docyard/config.rb +45 -96
  52. data/lib/docyard/constants.rb +59 -0
  53. data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
  54. data/lib/docyard/initializer.rb +100 -49
  55. data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
  56. data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
  57. data/lib/docyard/navigation/sidebar/cache.rb +96 -0
  58. data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
  59. data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
  60. data/lib/docyard/navigation/sidebar/item.rb +6 -1
  61. data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
  62. data/lib/docyard/navigation/sidebar/renderer.rb +18 -3
  63. data/lib/docyard/navigation/sidebar_builder.rb +43 -81
  64. data/lib/docyard/rendering/branding_variables.rb +65 -0
  65. data/lib/docyard/rendering/icon_helpers.rb +14 -1
  66. data/lib/docyard/rendering/icons/devicons.rb +63 -0
  67. data/lib/docyard/rendering/icons.rb +26 -27
  68. data/lib/docyard/rendering/markdown.rb +20 -15
  69. data/lib/docyard/rendering/og_helpers.rb +36 -0
  70. data/lib/docyard/rendering/renderer.rb +87 -58
  71. data/lib/docyard/rendering/template_resolver.rb +14 -0
  72. data/lib/docyard/routing/fallback_resolver.rb +3 -3
  73. data/lib/docyard/search/build_indexer.rb +2 -2
  74. data/lib/docyard/search/dev_indexer.rb +36 -28
  75. data/lib/docyard/search/pagefind_support.rb +1 -1
  76. data/lib/docyard/server/asset_handler.rb +40 -15
  77. data/lib/docyard/server/dev_server.rb +90 -55
  78. data/lib/docyard/server/file_watcher.rb +68 -18
  79. data/lib/docyard/server/pagefind_handler.rb +1 -1
  80. data/lib/docyard/server/preview_server.rb +29 -33
  81. data/lib/docyard/server/rack_application.rb +38 -70
  82. data/lib/docyard/server/router.rb +11 -7
  83. data/lib/docyard/server/sse_server.rb +157 -0
  84. data/lib/docyard/server/static_file_app.rb +42 -0
  85. data/lib/docyard/templates/assets/css/components/abbreviation.css +86 -0
  86. data/lib/docyard/templates/assets/css/components/accordion.css +138 -0
  87. data/lib/docyard/templates/assets/css/components/badges.css +47 -0
  88. data/lib/docyard/templates/assets/css/components/banner.css +233 -0
  89. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
  90. data/lib/docyard/templates/assets/css/components/callout.css +26 -6
  91. data/lib/docyard/templates/assets/css/components/cards.css +100 -0
  92. data/lib/docyard/templates/assets/css/components/code-block.css +14 -2
  93. data/lib/docyard/templates/assets/css/components/code-group.css +294 -0
  94. data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
  95. data/lib/docyard/templates/assets/css/components/figure.css +22 -0
  96. data/lib/docyard/templates/assets/css/components/file-tree.css +125 -0
  97. data/lib/docyard/templates/assets/css/components/heading-anchor.css +21 -13
  98. data/lib/docyard/templates/assets/css/components/icon.css +5 -0
  99. data/lib/docyard/templates/assets/css/components/lightbox.css +65 -0
  100. data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
  101. data/lib/docyard/templates/assets/css/components/navigation.css +32 -3
  102. data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
  103. data/lib/docyard/templates/assets/css/components/prev-next.css +20 -22
  104. data/lib/docyard/templates/assets/css/components/search.css +6 -10
  105. data/lib/docyard/templates/assets/css/components/steps.css +122 -0
  106. data/lib/docyard/templates/assets/css/components/tab-bar.css +7 -4
  107. data/lib/docyard/templates/assets/css/components/table-of-contents.css +57 -11
  108. data/lib/docyard/templates/assets/css/components/tabs.css +13 -5
  109. data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
  110. data/lib/docyard/templates/assets/css/components/tooltip.css +113 -0
  111. data/lib/docyard/templates/assets/css/components/video.css +41 -0
  112. data/lib/docyard/templates/assets/css/landing.css +82 -13
  113. data/lib/docyard/templates/assets/css/layout.css +17 -0
  114. data/lib/docyard/templates/assets/css/markdown.css +25 -3
  115. data/lib/docyard/templates/assets/css/variables.css +13 -1
  116. data/lib/docyard/templates/assets/js/components/abbreviation.js +85 -0
  117. data/lib/docyard/templates/assets/js/components/banner.js +81 -0
  118. data/lib/docyard/templates/assets/js/components/code-group.js +286 -0
  119. data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
  120. data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
  121. data/lib/docyard/templates/assets/js/components/file-tree.js +39 -0
  122. data/lib/docyard/templates/assets/js/components/lightbox.js +72 -0
  123. data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
  124. data/lib/docyard/templates/assets/js/components/search.js +3 -3
  125. data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
  126. data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
  127. data/lib/docyard/templates/assets/js/components/tooltip.js +118 -0
  128. data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
  129. data/lib/docyard/templates/errors/404.html.erb +114 -5
  130. data/lib/docyard/templates/errors/500.html.erb +173 -10
  131. data/lib/docyard/templates/init/_sidebar.yml +36 -0
  132. data/lib/docyard/templates/init/docyard.yml +36 -0
  133. data/lib/docyard/templates/init/pages/components.md +146 -0
  134. data/lib/docyard/templates/init/pages/getting-started.md +94 -0
  135. data/lib/docyard/templates/init/pages/index.md +22 -0
  136. data/lib/docyard/templates/layouts/default.html.erb +11 -0
  137. data/lib/docyard/templates/layouts/splash.html.erb +15 -1
  138. data/lib/docyard/templates/partials/_accordion.html.erb +9 -0
  139. data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
  140. data/lib/docyard/templates/partials/_banner.html.erb +27 -0
  141. data/lib/docyard/templates/partials/_card.html.erb +23 -0
  142. data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
  143. data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
  144. data/lib/docyard/templates/partials/_footer.html.erb +1 -1
  145. data/lib/docyard/templates/partials/_head.html.erb +79 -4
  146. data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
  147. data/lib/docyard/templates/partials/_nav_group.html.erb +6 -0
  148. data/lib/docyard/templates/partials/_nav_leaf.html.erb +3 -0
  149. data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
  150. data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
  151. data/lib/docyard/templates/partials/_step.html.erb +14 -0
  152. data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
  153. data/lib/docyard/utils/git_info.rb +157 -0
  154. data/lib/docyard/utils/hash_utils.rb +31 -0
  155. data/lib/docyard/utils/html_helpers.rb +8 -0
  156. data/lib/docyard/utils/logging.rb +44 -3
  157. data/lib/docyard/utils/path_resolver.rb +0 -10
  158. data/lib/docyard/utils/path_utils.rb +73 -0
  159. data/lib/docyard/version.rb +1 -1
  160. data/lib/docyard.rb +2 -2
  161. metadata +114 -47
  162. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  163. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  164. data/.github/pull_request_template.md +0 -14
  165. data/.github/workflows/ci.yml +0 -49
  166. data/.rubocop.yml +0 -42
  167. data/CODE_OF_CONDUCT.md +0 -132
  168. data/CONTRIBUTING.md +0 -55
  169. data/LICENSE.vscode-icons +0 -42
  170. data/Rakefile +0 -8
  171. data/lib/docyard/config/constants.rb +0 -31
  172. data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
  173. data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
  174. data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -78
  175. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
  176. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -69
  177. data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -47
  178. data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
  179. data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
  180. data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
  181. data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -139
  182. data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
  183. data/lib/docyard/rendering/icons/file_types.rb +0 -79
  184. data/lib/docyard/rendering/icons/phosphor.rb +0 -90
  185. data/lib/docyard/rendering/language_mapping.rb +0 -52
  186. data/lib/docyard/templates/assets/js/reload.js +0 -98
  187. data/lib/docyard/templates/partials/_icon.html.erb +0 -1
  188. data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
  189. data/sig/docyard.rbs +0 -4
@@ -1,13 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "webrick"
4
- require "stringio"
5
- require_relative "file_watcher"
3
+ require "puma"
4
+ require "puma/configuration"
5
+ require "puma/launcher"
6
+ require "puma/log_writer"
6
7
  require_relative "rack_application"
8
+ require_relative "sse_server"
9
+ require_relative "file_watcher"
7
10
  require_relative "../config"
11
+ require_relative "../navigation/sidebar/cache"
8
12
 
9
13
  module Docyard
10
- class Server
14
+ class DevServer
11
15
  DEFAULT_PORT = 4200
12
16
  DEFAULT_HOST = "localhost"
13
17
 
@@ -19,27 +23,34 @@ module Docyard
19
23
  @docs_path = docs_path
20
24
  @search_enabled = search
21
25
  @config = Config.load
22
- @file_watcher = FileWatcher.new(File.expand_path(docs_path))
23
26
  @search_indexer = nil
24
- @app = nil
27
+ @sse_server = nil
28
+ @file_watcher = nil
29
+ @launcher = nil
30
+ @sidebar_cache = nil
25
31
  end
26
32
 
27
33
  def start
28
34
  validate_docs_directory!
35
+ build_sidebar_cache
29
36
  generate_search_index if @search_enabled
30
- initialize_app
37
+ setup_hot_reload
31
38
  print_server_info
32
- @file_watcher.start
33
-
34
- http_server.mount_proc("/") { |req, res| handle_request(req, res) }
35
- trap("INT") { shutdown_server }
36
-
37
- http_server.start
39
+ run_server
40
+ ensure
38
41
  cleanup
39
42
  end
40
43
 
41
44
  private
42
45
 
46
+ def build_sidebar_cache
47
+ @sidebar_cache = Sidebar::Cache.new(
48
+ docs_path: File.expand_path(docs_path),
49
+ config: @config
50
+ )
51
+ @sidebar_cache.build
52
+ end
53
+
43
54
  def generate_search_index
44
55
  @search_indexer = Search::DevIndexer.new(
45
56
  docs_path: File.expand_path(docs_path),
@@ -48,17 +59,45 @@ module Docyard
48
59
  @search_indexer.generate
49
60
  end
50
61
 
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
62
+ def setup_hot_reload
63
+ @sse_server = SSEServer.new(port: sse_port)
64
+ @sse_server.start
65
+
66
+ @file_watcher = FileWatcher.new(
67
+ docs_path: docs_path,
68
+ on_change: ->(change_type) { handle_file_change(change_type) }
57
69
  )
70
+ @file_watcher.start
71
+ end
72
+
73
+ def sse_port
74
+ port + 1
75
+ end
76
+
77
+ def handle_file_change(change_type)
78
+ invalidate_sidebar_cache if change_type == :full
79
+ log_file_change(change_type)
80
+ @sse_server.broadcast("reload", { type: change_type.to_s })
81
+ end
82
+
83
+ def invalidate_sidebar_cache
84
+ @sidebar_cache&.invalidate
85
+ @sidebar_cache&.build
86
+ end
87
+
88
+ def log_file_change(change_type)
89
+ message = case change_type
90
+ when :content then "Content changed, reloading..."
91
+ when :config then "Config changed, full reload..."
92
+ when :asset then "Asset changed, reloading..."
93
+ else "File changed, reloading..."
94
+ end
95
+ Docyard.logger.info("* #{message}")
58
96
  end
59
97
 
60
98
  def cleanup
61
- @file_watcher.stop
99
+ @file_watcher&.stop
100
+ @sse_server&.stop
62
101
  @search_indexer&.cleanup
63
102
  end
64
103
 
@@ -70,48 +109,44 @@ module Docyard
70
109
  end
71
110
 
72
111
  def print_server_info
73
- puts "Starting Docyard server..."
74
- puts "=> Serving docs from: #{docs_path}/"
75
- puts "=> Running at: http://#{host}:#{port}"
76
- puts "=> Search: #{@search_enabled ? 'enabled' : 'disabled (use --search to enable)'}"
77
- puts "=> Press Ctrl+C to stop\n"
112
+ Docyard.logger.info("Starting Docyard server...")
113
+ Docyard.logger.info("* Version: #{Docyard::VERSION}")
114
+ Docyard.logger.info("* Running at: http://#{host}:#{port}")
115
+ Docyard.logger.info("* Hot reload: ws://127.0.0.1:#{sse_port}")
116
+ Docyard.logger.info("* Search: #{@search_enabled ? 'enabled' : 'disabled (use --search to enable)'}")
117
+ Docyard.logger.info("Use Ctrl+C to stop\n")
78
118
  end
79
119
 
80
- def shutdown_server
81
- puts "\nShutting down server..."
82
- http_server.shutdown
83
- end
120
+ def run_server
121
+ app = build_rack_app
122
+ puma_config = build_puma_config(app)
123
+ log_writer = Puma::LogWriter.strings
84
124
 
85
- def http_server
86
- @http_server ||= WEBrick::HTTPServer.new(
87
- Port: port,
88
- BindAddress: host,
89
- AccessLog: [],
90
- Logger: WEBrick::Log.new(File::NULL)
91
- )
125
+ @launcher = Puma::Launcher.new(puma_config, log_writer: log_writer)
126
+ @launcher.run
92
127
  end
93
128
 
94
- def handle_request(req, res)
95
- env = build_rack_env(req)
96
- status, headers, body = @app.call(env)
97
-
98
- res.status = status
99
- headers.each { |key, value| res[key] = value }
100
- body.each { |chunk| res.body << chunk }
129
+ def build_rack_app
130
+ RackApplication.new(
131
+ docs_path: File.expand_path(docs_path),
132
+ config: @config,
133
+ pagefind_path: @search_indexer&.pagefind_path,
134
+ sse_port: sse_port,
135
+ sidebar_cache: @sidebar_cache
136
+ )
101
137
  end
102
138
 
103
- def build_rack_env(req)
104
- {
105
- "REQUEST_METHOD" => req.request_method,
106
- "PATH_INFO" => req.path,
107
- "QUERY_STRING" => req.query_string || "",
108
- "SERVER_NAME" => req.host,
109
- "SERVER_PORT" => req.port.to_s,
110
- "rack.version" => Rack::VERSION,
111
- "rack.url_scheme" => "http",
112
- "rack.input" => StringIO.new,
113
- "rack.errors" => $stderr
114
- }
139
+ def build_puma_config(app)
140
+ server_host = host
141
+ server_port = port
142
+
143
+ Puma::Configuration.new do |config|
144
+ config.bind "tcp://#{server_host}:#{server_port}"
145
+ config.app app
146
+ config.workers 0
147
+ config.threads 4, 8
148
+ config.quiet
149
+ end
115
150
  end
116
151
  end
117
152
  end
@@ -4,39 +4,89 @@ require "listen"
4
4
 
5
5
  module Docyard
6
6
  class FileWatcher
7
- attr_reader :last_modified_time
7
+ DEBOUNCE_DELAY = 0.1
8
+ ROOT_CONFIG_FILE = "docyard.yml"
9
+ CONFIG_FILES = %w[docyard.yml _sidebar.yml].freeze
10
+ CONTENT_EXTENSIONS = %w[.md .markdown].freeze
11
+ ASSET_EXTENSIONS = %w[.css .js .html .erb].freeze
8
12
 
9
- def initialize(docs_path)
10
- @docs_path = docs_path
11
- @last_modified_time = Time.now
12
- @listener = nil
13
+ def initialize(docs_path:, on_change:)
14
+ @docs_path = File.expand_path(docs_path)
15
+ @project_root = File.dirname(@docs_path)
16
+ @on_change = on_change
17
+ @docs_listener = nil
18
+ @config_listener = nil
19
+ @pending_changes = { content: false, config: false, asset: false }
20
+ @debounce_timer = nil
21
+ @mutex = Mutex.new
13
22
  end
14
23
 
15
24
  def start
16
- @listener = Listen.to(@docs_path, only: /\.md$/) do |modified, added, removed|
17
- handle_changes(modified, added, removed)
25
+ @docs_listener = Listen.to(@docs_path, latency: DEBOUNCE_DELAY) do |modified, added, removed|
26
+ handle_changes(modified + added + removed)
18
27
  end
28
+ @docs_listener.start
19
29
 
20
- @listener.start
30
+ @config_listener = Listen.to(@project_root, only: /\Adocyard\.yml\z/) do |modified, added, removed|
31
+ handle_changes(modified + added + removed)
32
+ end
33
+ @config_listener.start
21
34
  end
22
35
 
23
36
  def stop
24
- @listener&.stop
25
- rescue StandardError => e
26
- Docyard.logger.error "Error stopping file watcher: #{e.class} - #{e.message}"
37
+ @docs_listener&.stop
38
+ @config_listener&.stop
39
+ @debounce_timer&.kill
27
40
  end
28
41
 
29
- def changed_since?(timestamp)
30
- @last_modified_time > timestamp
42
+ private
43
+
44
+ def handle_changes(paths)
45
+ return if paths.empty?
46
+
47
+ @mutex.synchronize do
48
+ paths.each { |path| categorize_change(path) }
49
+ schedule_notification
50
+ end
31
51
  end
32
52
 
33
- private
53
+ def categorize_change(path)
54
+ filename = File.basename(path)
55
+
56
+ if CONFIG_FILES.include?(filename)
57
+ @pending_changes[:config] = true
58
+ elsif CONTENT_EXTENSIONS.include?(File.extname(path))
59
+ @pending_changes[:content] = true
60
+ elsif ASSET_EXTENSIONS.include?(File.extname(path))
61
+ @pending_changes[:asset] = true
62
+ end
63
+ end
64
+
65
+ def schedule_notification
66
+ @debounce_timer&.kill
67
+ @debounce_timer = Thread.new do
68
+ sleep DEBOUNCE_DELAY
69
+ send_notification
70
+ end
71
+ end
72
+
73
+ def send_notification
74
+ changes = nil
75
+ @mutex.synchronize do
76
+ changes = @pending_changes.dup
77
+ @pending_changes = { content: false, config: false, asset: false }
78
+ end
79
+
80
+ change_type = determine_change_type(changes)
81
+ @on_change.call(change_type) if change_type
82
+ end
34
83
 
35
- def handle_changes(modified, added, removed)
36
- return if modified.empty? && added.empty? && removed.empty?
84
+ def determine_change_type(changes)
85
+ return :full if changes[:config]
86
+ return :content if changes[:content]
87
+ return :asset if changes[:asset]
37
88
 
38
- @last_modified_time = Time.now
39
- Docyard.logger.info "Files changed, triggering reload..."
89
+ nil
40
90
  end
41
91
  end
42
92
  end
@@ -31,7 +31,7 @@ module Docyard
31
31
  return File.join(pagefind_path, relative_path) if pagefind_path && Dir.exist?(pagefind_path)
32
32
 
33
33
  output_dir = config&.build&.output_dir || "dist"
34
- File.join(output_dir, "pagefind", relative_path)
34
+ File.join(output_dir, "_docyard", "pagefind", relative_path)
35
35
  end
36
36
 
37
37
  def serve_file(file_path)
@@ -1,7 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "webrick"
3
+ require "puma"
4
+ require "puma/configuration"
5
+ require "puma/launcher"
6
+ require "puma/log_writer"
4
7
  require_relative "../config"
8
+ require_relative "static_file_app"
5
9
 
6
10
  module Docyard
7
11
  class PreviewServer
@@ -13,16 +17,13 @@ module Docyard
13
17
  @port = port
14
18
  @config = Config.load
15
19
  @output_dir = File.expand_path(@config.build.output)
20
+ @launcher = nil
16
21
  end
17
22
 
18
23
  def start
19
24
  validate_output_directory!
20
25
  print_server_info
21
-
22
- server = create_server
23
- trap("INT") { shutdown_server(server) }
24
-
25
- server.start
26
+ run_server
26
27
  end
27
28
 
28
29
  private
@@ -35,38 +36,33 @@ module Docyard
35
36
  end
36
37
 
37
38
  def print_server_info
38
- puts "Preview server starting..."
39
- puts "=> Serving from: #{output_dir}/"
40
- puts "=> Running at: http://localhost:#{port}"
41
- puts "=> Press Ctrl+C to stop\n"
39
+ Docyard.logger.info("Starting preview server...")
40
+ Docyard.logger.info("* Version: #{Docyard::VERSION}")
41
+ Docyard.logger.info("* Running at: http://localhost:#{port}")
42
+ Docyard.logger.info("Use Ctrl+C to stop\n")
42
43
  end
43
44
 
44
- def create_server
45
- WEBrick::HTTPServer.new(
46
- Port: port,
47
- DocumentRoot: output_dir,
48
- AccessLog: [],
49
- Logger: WEBrick::Log.new(File::NULL),
50
- MimeTypes: mime_types
51
- )
52
- end
45
+ def run_server
46
+ app = StaticFileApp.new(output_dir)
47
+ puma_config = build_puma_config(app)
48
+ log_writer = Puma::LogWriter.strings
53
49
 
54
- def mime_types
55
- WEBrick::HTTPUtils::DefaultMimeTypes.merge(
56
- {
57
- "css" => "text/css",
58
- "js" => "application/javascript",
59
- "json" => "application/json",
60
- "svg" => "image/svg+xml",
61
- "woff" => "font/woff",
62
- "woff2" => "font/woff2"
63
- }
64
- )
50
+ @launcher = Puma::Launcher.new(puma_config, log_writer: log_writer)
51
+ @launcher.run
52
+ rescue Interrupt
53
+ Docyard.logger.info("\nShutting down preview server...")
65
54
  end
66
55
 
67
- def shutdown_server(server)
68
- puts "\nShutting down preview server..."
69
- server.shutdown
56
+ def build_puma_config(app)
57
+ server_port = port
58
+
59
+ Puma::Configuration.new do |config|
60
+ config.bind "tcp://localhost:#{server_port}"
61
+ config.app app
62
+ config.workers 0
63
+ config.threads 1, 4
64
+ config.quiet
65
+ end
70
66
  end
71
67
  end
72
68
  end
@@ -2,24 +2,26 @@
2
2
 
3
3
  require "json"
4
4
  require "rack"
5
+ require_relative "../navigation/page_navigation_builder"
5
6
  require_relative "../navigation/sidebar_builder"
6
- require_relative "../navigation/prev_next_builder"
7
- require_relative "../navigation/breadcrumb_builder"
8
7
  require_relative "../config/branding_resolver"
9
- require_relative "../config/constants"
8
+ require_relative "../constants"
10
9
  require_relative "../rendering/template_resolver"
11
10
  require_relative "../routing/fallback_resolver"
12
11
  require_relative "pagefind_handler"
13
12
 
14
13
  module Docyard
15
14
  class RackApplication
16
- def initialize(docs_path:, file_watcher:, config: nil, pagefind_path: nil)
15
+ def initialize(docs_path:, config: nil, pagefind_path: nil, sse_port: nil, sidebar_cache: nil)
17
16
  @docs_path = docs_path
18
- @file_watcher = file_watcher
19
17
  @config = config
18
+ @sse_port = sse_port
19
+ @dev_mode = !sse_port.nil?
20
+ @sidebar_cache = sidebar_cache
20
21
  @router = Router.new(docs_path: docs_path)
21
- @renderer = Renderer.new(base_url: config&.build&.base || "/", config: config)
22
- @asset_handler = AssetHandler.new
22
+ @renderer = Renderer.new(base_url: config&.build&.base || "/", config: config, dev_mode: @dev_mode,
23
+ sse_port: sse_port)
24
+ @asset_handler = AssetHandler.new(public_dir: config&.public_dir || "docs/public")
23
25
  @pagefind_handler = PagefindHandler.new(pagefind_path: pagefind_path, config: config)
24
26
  end
25
27
 
@@ -29,21 +31,20 @@ module Docyard
29
31
 
30
32
  private
31
33
 
32
- attr_reader :docs_path, :file_watcher, :config, :router, :renderer, :asset_handler, :pagefind_handler
34
+ attr_reader :docs_path, :config, :router, :renderer, :asset_handler, :pagefind_handler, :dev_mode
33
35
 
34
36
  def handle_request(env)
35
37
  path = env["PATH_INFO"]
36
38
 
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
39
  return pagefind_handler.serve(path) if path.start_with?(Constants::PAGEFIND_PREFIX)
40
+ return asset_handler.serve_docyard_assets(path) if path.start_with?(Constants::DOCYARD_ASSETS_PREFIX)
40
41
 
41
42
  public_response = asset_handler.serve_public_file(path)
42
43
  return public_response if public_response
43
44
 
44
45
  handle_documentation_request(path)
45
46
  rescue StandardError => e
46
- handle_error(e)
47
+ handle_error(e, env)
47
48
  end
48
49
 
49
50
  def handle_documentation_request(path)
@@ -106,14 +107,20 @@ module Docyard
106
107
  end
107
108
 
108
109
  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
+ navigation_builder.build(
111
+ current_path: current_path,
112
+ markdown: markdown,
113
+ header_ctas: header_ctas,
114
+ show_sidebar: template_resolver.show_sidebar?
115
+ )
116
+ end
110
117
 
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
- }
118
+ def navigation_builder
119
+ @navigation_builder ||= Navigation::PageNavigationBuilder.new(
120
+ docs_path: docs_path,
121
+ config: config,
122
+ sidebar_cache: @sidebar_cache
123
+ )
117
124
  end
118
125
 
119
126
  def render_not_found_page
@@ -126,67 +133,28 @@ module Docyard
126
133
  docs_path: docs_path,
127
134
  current_path: current_path,
128
135
  config: config,
129
- header_ctas: header_ctas
136
+ header_ctas: header_ctas,
137
+ sidebar_cache: @sidebar_cache
130
138
  )
131
139
  end
132
140
 
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
141
  def branding_options
157
142
  BrandingResolver.new(config).resolve
158
143
  end
159
144
 
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")
145
+ def handle_error(error, env)
146
+ request_context = build_request_context(env)
147
+ Docyard.logger.error("Request error: #{error.message} [#{request_context}]")
148
+ Docyard.logger.debug(error.backtrace.join("\n"))
188
149
  [Constants::STATUS_INTERNAL_ERROR, { "Content-Type" => Constants::CONTENT_TYPE_HTML },
189
150
  [renderer.render_server_error(error)]]
190
151
  end
152
+
153
+ def build_request_context(env)
154
+ method = env["REQUEST_METHOD"]
155
+ path = env["PATH_INFO"]
156
+ user_agent = env["HTTP_USER_AGENT"]&.slice(0, 50)
157
+ user_agent ? "#{method} #{path} - #{user_agent}" : "#{method} #{path}"
158
+ end
191
159
  end
192
160
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../utils/path_utils"
4
+
3
5
  module Docyard
4
6
  class Router
5
7
  attr_reader :docs_path
@@ -11,11 +13,11 @@ module Docyard
11
13
  def resolve(request_path)
12
14
  clean_path = sanitize_path(request_path)
13
15
 
14
- file_path = File.join(docs_path, "#{clean_path}#{Constants::MARKDOWN_EXTENSION}")
15
- return ResolutionResult.found(file_path) if File.file?(file_path)
16
+ file_path = safe_file_path("#{clean_path}#{Constants::MARKDOWN_EXTENSION}")
17
+ return ResolutionResult.found(file_path) if file_path && File.file?(file_path)
16
18
 
17
- index_path = File.join(docs_path, clean_path, "#{Constants::INDEX_FILE}#{Constants::MARKDOWN_EXTENSION}")
18
- return ResolutionResult.found(index_path) if File.file?(index_path)
19
+ index_path = safe_file_path(File.join(clean_path, "#{Constants::INDEX_FILE}#{Constants::MARKDOWN_EXTENSION}"))
20
+ return ResolutionResult.found(index_path) if index_path && File.file?(index_path)
19
21
 
20
22
  ResolutionResult.not_found
21
23
  end
@@ -23,9 +25,11 @@ module Docyard
23
25
  private
24
26
 
25
27
  def sanitize_path(request_path)
26
- clean = request_path.delete_prefix("/").delete_suffix("/")
27
- clean = Constants::INDEX_FILE if clean.empty?
28
- clean.delete_suffix(Constants::MARKDOWN_EXTENSION)
28
+ Utils::PathUtils.sanitize_url_path(request_path)
29
+ end
30
+
31
+ def safe_file_path(relative_path)
32
+ Utils::PathUtils.resolve_safe_path(relative_path, docs_path)
29
33
  end
30
34
  end
31
35
  end