docyard 0.9.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 (159) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  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/processors/callout_processor.rb +1 -1
  13. data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
  14. data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
  15. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +11 -1
  16. data/lib/docyard/components/processors/code_block_processor.rb +5 -24
  17. data/lib/docyard/components/processors/code_group_processor.rb +6 -22
  18. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +1 -0
  19. data/lib/docyard/components/processors/file_tree_processor.rb +1 -2
  20. data/lib/docyard/components/processors/icon_processor.rb +8 -2
  21. data/lib/docyard/components/processors/include_processor.rb +10 -10
  22. data/lib/docyard/components/processors/video_embed_processor.rb +14 -3
  23. data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
  24. data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
  25. data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
  26. data/lib/docyard/components/support/code_detector.rb +2 -12
  27. data/lib/docyard/components/support/code_group/html_builder.rb +2 -6
  28. data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
  29. data/lib/docyard/components/support/tabs/parser.rb +6 -23
  30. data/lib/docyard/config/analytics_resolver.rb +24 -0
  31. data/lib/docyard/config/branding_resolver.rb +58 -27
  32. data/lib/docyard/config/key_validator.rb +30 -0
  33. data/lib/docyard/config/logo_detector.rb +8 -8
  34. data/lib/docyard/config/schema.rb +39 -0
  35. data/lib/docyard/config/section.rb +21 -0
  36. data/lib/docyard/config/validation_helpers.rb +83 -0
  37. data/lib/docyard/config/validator.rb +45 -144
  38. data/lib/docyard/config/validators/navigation.rb +43 -0
  39. data/lib/docyard/config/validators/section.rb +114 -0
  40. data/lib/docyard/config.rb +46 -102
  41. data/lib/docyard/constants.rb +59 -0
  42. data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
  43. data/lib/docyard/initializer.rb +100 -49
  44. data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
  45. data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
  46. data/lib/docyard/navigation/sidebar/cache.rb +96 -0
  47. data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
  48. data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
  49. data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
  50. data/lib/docyard/navigation/sidebar/renderer.rb +12 -1
  51. data/lib/docyard/navigation/sidebar_builder.rb +43 -81
  52. data/lib/docyard/rendering/branding_variables.rb +65 -0
  53. data/lib/docyard/rendering/icon_helpers.rb +14 -1
  54. data/lib/docyard/rendering/icons/devicons.rb +63 -0
  55. data/lib/docyard/rendering/icons.rb +26 -27
  56. data/lib/docyard/rendering/markdown.rb +5 -23
  57. data/lib/docyard/rendering/og_helpers.rb +36 -0
  58. data/lib/docyard/rendering/renderer.rb +87 -59
  59. data/lib/docyard/rendering/template_resolver.rb +14 -0
  60. data/lib/docyard/routing/fallback_resolver.rb +3 -3
  61. data/lib/docyard/search/build_indexer.rb +2 -2
  62. data/lib/docyard/search/dev_indexer.rb +36 -28
  63. data/lib/docyard/search/pagefind_support.rb +1 -1
  64. data/lib/docyard/server/asset_handler.rb +39 -15
  65. data/lib/docyard/server/dev_server.rb +90 -55
  66. data/lib/docyard/server/file_watcher.rb +68 -18
  67. data/lib/docyard/server/pagefind_handler.rb +1 -1
  68. data/lib/docyard/server/preview_server.rb +29 -33
  69. data/lib/docyard/server/rack_application.rb +38 -70
  70. data/lib/docyard/server/router.rb +11 -7
  71. data/lib/docyard/server/sse_server.rb +157 -0
  72. data/lib/docyard/server/static_file_app.rb +42 -0
  73. data/lib/docyard/templates/assets/css/components/banner.css +31 -0
  74. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
  75. data/lib/docyard/templates/assets/css/components/callout.css +26 -6
  76. data/lib/docyard/templates/assets/css/components/code-block.css +4 -2
  77. data/lib/docyard/templates/assets/css/components/code-group.css +20 -7
  78. data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
  79. data/lib/docyard/templates/assets/css/components/file-tree.css +5 -4
  80. data/lib/docyard/templates/assets/css/components/icon.css +5 -0
  81. data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
  82. data/lib/docyard/templates/assets/css/components/navigation.css +25 -3
  83. data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
  84. data/lib/docyard/templates/assets/css/components/prev-next.css +14 -7
  85. data/lib/docyard/templates/assets/css/components/search.css +6 -10
  86. data/lib/docyard/templates/assets/css/components/tab-bar.css +7 -4
  87. data/lib/docyard/templates/assets/css/components/table-of-contents.css +57 -11
  88. data/lib/docyard/templates/assets/css/components/tabs.css +12 -4
  89. data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
  90. data/lib/docyard/templates/assets/css/landing.css +82 -13
  91. data/lib/docyard/templates/assets/css/layout.css +17 -0
  92. data/lib/docyard/templates/assets/css/markdown.css +22 -2
  93. data/lib/docyard/templates/assets/css/variables.css +13 -1
  94. data/lib/docyard/templates/assets/js/components/code-group.js +4 -1
  95. data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
  96. data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
  97. data/lib/docyard/templates/assets/js/components/file-tree.js +5 -5
  98. data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
  99. data/lib/docyard/templates/assets/js/components/search.js +3 -3
  100. data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
  101. data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
  102. data/lib/docyard/templates/assets/js/components/tooltip.js +4 -4
  103. data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
  104. data/lib/docyard/templates/errors/404.html.erb +114 -5
  105. data/lib/docyard/templates/errors/500.html.erb +173 -10
  106. data/lib/docyard/templates/init/_sidebar.yml +36 -0
  107. data/lib/docyard/templates/init/docyard.yml +36 -0
  108. data/lib/docyard/templates/init/pages/components.md +146 -0
  109. data/lib/docyard/templates/init/pages/getting-started.md +94 -0
  110. data/lib/docyard/templates/init/pages/index.md +22 -0
  111. data/lib/docyard/templates/layouts/default.html.erb +10 -0
  112. data/lib/docyard/templates/layouts/splash.html.erb +14 -1
  113. data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
  114. data/lib/docyard/templates/partials/_banner.html.erb +1 -1
  115. data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
  116. data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
  117. data/lib/docyard/templates/partials/_footer.html.erb +1 -1
  118. data/lib/docyard/templates/partials/_head.html.erb +79 -4
  119. data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
  120. data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
  121. data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
  122. data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
  123. data/lib/docyard/utils/git_info.rb +157 -0
  124. data/lib/docyard/utils/hash_utils.rb +31 -0
  125. data/lib/docyard/utils/html_helpers.rb +8 -0
  126. data/lib/docyard/utils/logging.rb +44 -3
  127. data/lib/docyard/utils/path_resolver.rb +0 -10
  128. data/lib/docyard/utils/path_utils.rb +73 -0
  129. data/lib/docyard/version.rb +1 -1
  130. data/lib/docyard.rb +2 -2
  131. metadata +77 -47
  132. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  133. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  134. data/.github/pull_request_template.md +0 -14
  135. data/.github/workflows/ci.yml +0 -49
  136. data/.rubocop.yml +0 -42
  137. data/CODE_OF_CONDUCT.md +0 -132
  138. data/CONTRIBUTING.md +0 -55
  139. data/LICENSE.vscode-icons +0 -42
  140. data/Rakefile +0 -8
  141. data/lib/docyard/config/constants.rb +0 -31
  142. data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
  143. data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
  144. data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -90
  145. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
  146. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -71
  147. data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -51
  148. data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
  149. data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
  150. data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
  151. data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -140
  152. data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
  153. data/lib/docyard/rendering/icons/file_types.rb +0 -79
  154. data/lib/docyard/rendering/icons/phosphor.rb +0 -93
  155. data/lib/docyard/rendering/language_mapping.rb +0 -52
  156. data/lib/docyard/templates/assets/js/reload.js +0 -98
  157. data/lib/docyard/templates/partials/_icon.html.erb +0 -1
  158. data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
  159. data/sig/docyard.rbs +0 -4
@@ -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
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "json"
5
+
6
+ module Docyard
7
+ class SSEServer
8
+ HEARTBEAT_INTERVAL = 15
9
+ DEFAULT_PORT = 4201
10
+
11
+ def initialize(port: DEFAULT_PORT)
12
+ @port = port
13
+ @connections = []
14
+ @mutex = Mutex.new
15
+ @running = false
16
+ @server = nil
17
+ @accept_thread = nil
18
+ @heartbeat_thread = nil
19
+ end
20
+
21
+ attr_reader :port
22
+
23
+ def start
24
+ @running = true
25
+ @server = TCPServer.new("127.0.0.1", @port)
26
+ @server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
27
+
28
+ start_accept_thread
29
+ start_heartbeat_thread
30
+ end
31
+
32
+ def stop
33
+ @running = false
34
+ close_server
35
+ @accept_thread&.kill
36
+ @heartbeat_thread&.kill
37
+ close_all_connections
38
+ end
39
+
40
+ def broadcast(event_type, data = {})
41
+ message = format_sse_message(event_type, data)
42
+ dead_connections = []
43
+
44
+ @mutex.synchronize do
45
+ @connections.each do |conn|
46
+ write_to_connection(conn, message) or dead_connections << conn
47
+ end
48
+
49
+ dead_connections.each { |conn| remove_connection_unsafe(conn) }
50
+ end
51
+ end
52
+
53
+ def connection_count
54
+ @mutex.synchronize { @connections.size }
55
+ end
56
+
57
+ private
58
+
59
+ def close_server
60
+ @server&.close
61
+ rescue StandardError
62
+ nil
63
+ end
64
+
65
+ def start_accept_thread
66
+ @accept_thread = Thread.new do
67
+ while @running
68
+ begin
69
+ client = @server.accept
70
+ Thread.new { handle_new_connection(client) }
71
+ rescue IOError, Errno::EBADF
72
+ break unless @running
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def handle_new_connection(client)
79
+ request = read_http_request(client)
80
+ return close_client(client) unless valid_sse_request?(request)
81
+
82
+ send_sse_headers(client)
83
+ @mutex.synchronize { @connections << client }
84
+ rescue StandardError
85
+ close_client(client)
86
+ end
87
+
88
+ def close_client(client)
89
+ client.close
90
+ rescue StandardError
91
+ nil
92
+ end
93
+
94
+ def read_http_request(client)
95
+ lines = []
96
+ while (line = client.gets)
97
+ break if line.strip.empty?
98
+
99
+ lines << line
100
+ end
101
+ lines.join
102
+ end
103
+
104
+ def valid_sse_request?(request)
105
+ request.include?("GET /_docyard/events") || request.include?("GET / ")
106
+ end
107
+
108
+ def send_sse_headers(client)
109
+ headers = [
110
+ "HTTP/1.1 200 OK",
111
+ "Content-Type: text/event-stream",
112
+ "Cache-Control: no-cache",
113
+ "Connection: keep-alive",
114
+ "Access-Control-Allow-Origin: *",
115
+ "",
116
+ ""
117
+ ].join("\r\n")
118
+
119
+ client.write(headers)
120
+ client.write("retry: 1000\n\n")
121
+ client.flush
122
+ end
123
+
124
+ def start_heartbeat_thread
125
+ @heartbeat_thread = Thread.new do
126
+ while @running
127
+ sleep HEARTBEAT_INTERVAL
128
+ broadcast("heartbeat", { time: Time.now.to_i }) if @running
129
+ end
130
+ end
131
+ end
132
+
133
+ def write_to_connection(conn, message)
134
+ conn.write_nonblock(message)
135
+ true
136
+ rescue IO::WaitWritable, IOError, Errno::EPIPE, Errno::ECONNRESET, Errno::ETIMEDOUT
137
+ false
138
+ end
139
+
140
+ def format_sse_message(event_type, data)
141
+ json_data = data.to_json
142
+ "event: #{event_type}\ndata: #{json_data}\n\n"
143
+ end
144
+
145
+ def remove_connection_unsafe(conn)
146
+ @connections.delete(conn)
147
+ close_client(conn)
148
+ end
149
+
150
+ def close_all_connections
151
+ @mutex.synchronize do
152
+ @connections.each { |conn| close_client(conn) }
153
+ @connections.clear
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/mime"
4
+
5
+ module Docyard
6
+ class StaticFileApp
7
+ def initialize(root)
8
+ @root = root
9
+ end
10
+
11
+ def call(env)
12
+ path = env["PATH_INFO"]
13
+ file_path = File.join(@root, path)
14
+
15
+ if path.end_with?("/") || File.directory?(file_path)
16
+ index_path = File.join(file_path, "index.html")
17
+ return serve_file(index_path) if File.file?(index_path)
18
+ elsif File.file?(file_path)
19
+ return serve_file(file_path)
20
+ end
21
+
22
+ serve_not_found
23
+ end
24
+
25
+ private
26
+
27
+ def serve_file(path)
28
+ content = File.read(path)
29
+ content_type = Rack::Mime.mime_type(File.extname(path), "application/octet-stream")
30
+ [200, { "content-type" => content_type }, [content]]
31
+ end
32
+
33
+ def serve_not_found
34
+ error_page = File.join(@root, "404.html")
35
+ if File.file?(error_page)
36
+ [404, { "content-type" => "text/html; charset=utf-8" }, [File.read(error_page)]]
37
+ else
38
+ [404, { "content-type" => "text/plain" }, ["Not Found"]]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -53,6 +53,10 @@ body.has-announcement .secondary-header {
53
53
  top: calc(var(--header-height) + var(--tab-bar-height) + var(--announcement-height));
54
54
  }
55
55
 
56
+ body.has-announcement:not(.has-tabs) .secondary-header {
57
+ top: calc(var(--header-height) + var(--announcement-height));
58
+ }
59
+
56
60
  body.has-announcement .layout {
57
61
  padding-top: calc(var(--header-height) + var(--tab-bar-height) + var(--announcement-height));
58
62
  }
@@ -70,6 +74,33 @@ body.has-announcement:not(.has-tabs) .layout {
70
74
  padding-top: calc(var(--header-height) + var(--announcement-height));
71
75
  }
72
76
 
77
+ @media (max-width: 1280px) and (min-width: 1025px) {
78
+ body.has-announcement .layout {
79
+ padding-top: calc(var(--header-height) + var(--tab-bar-height) + var(--announcement-height) + 3rem);
80
+ }
81
+
82
+ body.has-announcement:not(.has-tabs) .layout {
83
+ padding-top: calc(var(--header-height) + var(--announcement-height) + 3rem);
84
+ }
85
+ }
86
+
87
+ /* Mobile: tab bar is hidden, so don't include tab-bar-height */
88
+ @media (max-width: 1024px) {
89
+ body.has-announcement .secondary-header {
90
+ top: calc(var(--header-height) + var(--announcement-height));
91
+ }
92
+
93
+ body.has-announcement .layout {
94
+ padding-top: calc(var(--header-height) + var(--announcement-height) + 3rem);
95
+ }
96
+
97
+ body.has-announcement .sidebar {
98
+ top: 0;
99
+ height: 100vh;
100
+ height: 100dvh;
101
+ }
102
+ }
103
+
73
104
  .docyard-announcement__content {
74
105
  display: flex;
75
106
  align-items: center;
@@ -40,7 +40,8 @@
40
40
  box-shadow: 0 0 0 var(--ring-width) oklch(from var(--ring) l c h / 50%);
41
41
  }
42
42
 
43
- .breadcrumb-toggle .docyard-icon {
43
+ .breadcrumb-toggle .docyard-icon,
44
+ .breadcrumb-toggle i[class*="ph-"] {
44
45
  width: 18px;
45
46
  height: 18px;
46
47
  }
@@ -15,16 +15,17 @@
15
15
 
16
16
  .docyard-callout__icon {
17
17
  flex-shrink: 0;
18
- width: 1.25rem;
19
- height: 1.25rem;
18
+ width: 1.5rem;
19
+ height: 1.5rem;
20
20
  display: flex;
21
21
  align-items: center;
22
22
  justify-content: center;
23
23
  }
24
24
 
25
- .docyard-callout__icon .docyard-icon {
26
- width: 100%;
27
- height: 100%;
25
+ .docyard-callout__icon .docyard-icon,
26
+ .docyard-callout__icon i[class*="ph-"] {
27
+ font-size: 1.25rem;
28
+ display: inline-block;
28
29
  }
29
30
 
30
31
  .docyard-callout__icon svg {
@@ -98,11 +99,30 @@
98
99
  }
99
100
 
100
101
  .docyard-callout__body :not(pre)>code {
101
- background-color: oklch(from var(--input) l c h / 30%);
102
102
  padding: 0.125rem 0.375rem;
103
103
  border-radius: var(--radius-sm);
104
104
  }
105
105
 
106
+ .docyard-callout--note .docyard-callout__body :not(pre)>code {
107
+ background-color: oklch(from var(--callout-note-border) l c h / 15%);
108
+ }
109
+
110
+ .docyard-callout--tip .docyard-callout__body :not(pre)>code {
111
+ background-color: oklch(from var(--callout-tip-border) l c h / 15%);
112
+ }
113
+
114
+ .docyard-callout--important .docyard-callout__body :not(pre)>code {
115
+ background-color: oklch(from var(--callout-important-border) l c h / 15%);
116
+ }
117
+
118
+ .docyard-callout--warning .docyard-callout__body :not(pre)>code {
119
+ background-color: oklch(from var(--callout-warning-border) l c h / 15%);
120
+ }
121
+
122
+ .docyard-callout--danger .docyard-callout__body :not(pre)>code {
123
+ background-color: oklch(from var(--callout-danger-border) l c h / 15%);
124
+ }
125
+
106
126
  .docyard-callout--note {
107
127
  border-color: var(--callout-note-border);
108
128
  background-color: var(--callout-note-background);
@@ -57,7 +57,8 @@
57
57
  flex-shrink: 0;
58
58
  }
59
59
 
60
- .docyard-code-block__icon .docyard-icon {
60
+ .docyard-code-block__icon .docyard-icon,
61
+ .docyard-code-block__icon i[class*="ph-"] {
61
62
  width: 1rem;
62
63
  height: 1rem;
63
64
  display: inline-flex;
@@ -468,7 +469,8 @@
468
469
  gap: var(--spacing-2);
469
470
  }
470
471
 
471
- .docyard-code-block__icon .docyard-icon {
472
+ .docyard-code-block__icon .docyard-icon,
473
+ .docyard-code-block__icon i[class*="ph-"] {
472
474
  width: 0.875rem;
473
475
  height: 0.875rem;
474
476
  }
@@ -44,7 +44,7 @@
44
44
  .docyard-code-group__tab {
45
45
  display: inline-flex;
46
46
  align-items: center;
47
- justify-content: center;
47
+ justify-content: flex-start;
48
48
  gap: var(--spacing-1-5);
49
49
  padding: var(--spacing-2) var(--spacing-2-5);
50
50
  background: transparent;
@@ -60,9 +60,7 @@
60
60
  flex-shrink: 0;
61
61
  line-height: 1.5;
62
62
  outline: none;
63
- max-width: 12rem;
64
- overflow: hidden;
65
- text-overflow: ellipsis;
63
+ max-width: 14rem;
66
64
  }
67
65
 
68
66
  .docyard-code-group__tab:hover {
@@ -79,7 +77,8 @@
79
77
  border-radius: var(--radius-sm);
80
78
  }
81
79
 
82
- .docyard-code-group__tab .docyard-icon {
80
+ .docyard-code-group__tab .docyard-icon,
81
+ .docyard-code-group__tab i[class*="ph-"] {
83
82
  width: 1rem;
84
83
  height: 1rem;
85
84
  display: inline-flex;
@@ -91,6 +90,13 @@
91
90
  height: 100%;
92
91
  }
93
92
 
93
+ .docyard-code-group__tab-label {
94
+ overflow: hidden;
95
+ text-overflow: ellipsis;
96
+ direction: rtl;
97
+ text-align: left;
98
+ }
99
+
94
100
  .docyard-code-group__indicator {
95
101
  position: absolute;
96
102
  bottom: 0;
@@ -100,12 +106,18 @@
100
106
  border-radius: 1px;
101
107
  pointer-events: none;
102
108
  z-index: 1;
109
+ opacity: 0;
103
110
  transition:
104
111
  transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
105
- width 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
112
+ width 0.3s cubic-bezier(0.34, 1.56, 0.64, 1),
113
+ opacity 0.15s ease;
106
114
  will-change: transform, width;
107
115
  }
108
116
 
117
+ .docyard-code-group__indicator.is-ready {
118
+ opacity: 1;
119
+ }
120
+
109
121
  .docyard-code-group__tabs-scroll-container.can-scroll-left .docyard-code-group__tabs {
110
122
  mask-image: linear-gradient(to right, transparent, black 2rem);
111
123
  -webkit-mask-image: linear-gradient(to right, transparent, black 2rem);
@@ -252,7 +264,8 @@
252
264
  gap: var(--spacing-1);
253
265
  }
254
266
 
255
- .docyard-code-group__tab .docyard-icon {
267
+ .docyard-code-group__tab .docyard-icon,
268
+ .docyard-code-group__tab i[class*="ph-"] {
256
269
  width: 0.875rem;
257
270
  height: 0.875rem;
258
271
  }