docyard 1.0.2 → 1.1.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -1
  3. data/lib/docyard/build/asset_bundler.rb +7 -33
  4. data/lib/docyard/build/file_copier.rb +7 -15
  5. data/lib/docyard/build/llms_txt_generator.rb +0 -2
  6. data/lib/docyard/build/sitemap_generator.rb +1 -1
  7. data/lib/docyard/build/static_generator.rb +30 -32
  8. data/lib/docyard/build/step_runner.rb +88 -0
  9. data/lib/docyard/build/validator.rb +98 -0
  10. data/lib/docyard/builder.rb +82 -55
  11. data/lib/docyard/cli.rb +36 -4
  12. data/lib/docyard/components/aliases.rb +0 -4
  13. data/lib/docyard/components/processors/callout_processor.rb +1 -1
  14. data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +1 -1
  15. data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +1 -1
  16. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +2 -2
  17. data/lib/docyard/components/processors/code_group_processor.rb +1 -1
  18. data/lib/docyard/components/processors/icon_processor.rb +2 -2
  19. data/lib/docyard/components/processors/tabs_processor.rb +1 -1
  20. data/lib/docyard/config/schema/definition.rb +29 -0
  21. data/lib/docyard/config/schema/sections.rb +63 -0
  22. data/lib/docyard/config/schema/simple_sections.rb +78 -0
  23. data/lib/docyard/config/schema.rb +28 -31
  24. data/lib/docyard/config/type_validators.rb +121 -0
  25. data/lib/docyard/config/validator.rb +136 -61
  26. data/lib/docyard/config.rb +1 -13
  27. data/lib/docyard/diagnostic.rb +89 -0
  28. data/lib/docyard/diagnostic_context.rb +48 -0
  29. data/lib/docyard/doctor/code_block_checker.rb +136 -0
  30. data/lib/docyard/doctor/component_checker.rb +49 -0
  31. data/lib/docyard/doctor/component_checkers/abbreviation_checker.rb +74 -0
  32. data/lib/docyard/doctor/component_checkers/badge_checker.rb +71 -0
  33. data/lib/docyard/doctor/component_checkers/base.rb +111 -0
  34. data/lib/docyard/doctor/component_checkers/callout_checker.rb +34 -0
  35. data/lib/docyard/doctor/component_checkers/cards_checker.rb +57 -0
  36. data/lib/docyard/doctor/component_checkers/code_group_checker.rb +47 -0
  37. data/lib/docyard/doctor/component_checkers/details_checker.rb +51 -0
  38. data/lib/docyard/doctor/component_checkers/icon_checker.rb +36 -0
  39. data/lib/docyard/doctor/component_checkers/image_attrs_checker.rb +46 -0
  40. data/lib/docyard/doctor/component_checkers/space_after_colons_checker.rb +45 -0
  41. data/lib/docyard/doctor/component_checkers/steps_checker.rb +35 -0
  42. data/lib/docyard/doctor/component_checkers/tabs_checker.rb +35 -0
  43. data/lib/docyard/doctor/component_checkers/tooltip_checker.rb +67 -0
  44. data/lib/docyard/doctor/component_checkers/unknown_type_checker.rb +34 -0
  45. data/lib/docyard/doctor/config_checker.rb +19 -0
  46. data/lib/docyard/doctor/config_fixer.rb +87 -0
  47. data/lib/docyard/doctor/content_checker.rb +164 -0
  48. data/lib/docyard/doctor/file_scanner.rb +113 -0
  49. data/lib/docyard/doctor/image_checker.rb +103 -0
  50. data/lib/docyard/doctor/link_checker.rb +91 -0
  51. data/lib/docyard/doctor/markdown_fixer.rb +62 -0
  52. data/lib/docyard/doctor/orphan_checker.rb +82 -0
  53. data/lib/docyard/doctor/reporter.rb +152 -0
  54. data/lib/docyard/doctor/sidebar_checker.rb +127 -0
  55. data/lib/docyard/doctor/sidebar_fixer.rb +47 -0
  56. data/lib/docyard/doctor.rb +178 -0
  57. data/lib/docyard/editor_launcher.rb +119 -0
  58. data/lib/docyard/errors.rb +0 -49
  59. data/lib/docyard/initializer.rb +32 -39
  60. data/lib/docyard/navigation/sidebar/local_config_loader.rb +44 -21
  61. data/lib/docyard/rendering/icon_helpers.rb +1 -3
  62. data/lib/docyard/search/build_indexer.rb +39 -24
  63. data/lib/docyard/search/dev_indexer.rb +9 -23
  64. data/lib/docyard/server/dev_server.rb +55 -13
  65. data/lib/docyard/server/error_overlay.rb +73 -0
  66. data/lib/docyard/server/file_watcher.rb +0 -1
  67. data/lib/docyard/server/page_diagnostics.rb +27 -0
  68. data/lib/docyard/server/preview_server.rb +17 -13
  69. data/lib/docyard/server/rack_application.rb +64 -3
  70. data/lib/docyard/server/resolution_result.rb +0 -4
  71. data/lib/docyard/templates/assets/css/error-overlay.css +669 -0
  72. data/lib/docyard/templates/assets/css/variables.css +1 -1
  73. data/lib/docyard/templates/assets/fonts/Inter-Variable.woff2 +0 -0
  74. data/lib/docyard/templates/assets/js/components/relative-time.js +42 -0
  75. data/lib/docyard/templates/assets/js/error-overlay.js +547 -0
  76. data/lib/docyard/templates/assets/js/hot-reload.js +35 -7
  77. data/lib/docyard/templates/errors/404.html.erb +1 -1
  78. data/lib/docyard/templates/errors/500.html.erb +1 -1
  79. data/lib/docyard/templates/partials/_head.html.erb +1 -1
  80. data/lib/docyard/templates/partials/_page_actions.html.erb +1 -1
  81. data/lib/docyard/ui.rb +80 -0
  82. data/lib/docyard/utils/logging.rb +5 -1
  83. data/lib/docyard/utils/text_formatter.rb +0 -6
  84. data/lib/docyard/version.rb +1 -1
  85. data/lib/docyard.rb +4 -0
  86. metadata +47 -25
  87. data/lib/docyard/config/key_validator.rb +0 -30
  88. data/lib/docyard/config/validation_helpers.rb +0 -83
  89. data/lib/docyard/config/validators/navigation.rb +0 -43
  90. data/lib/docyard/config/validators/section.rb +0 -114
  91. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
@@ -4,7 +4,6 @@ require "fileutils"
4
4
  require "tmpdir"
5
5
  require "open3"
6
6
  require "parallel"
7
- require "tty-progressbar"
8
7
  require_relative "../utils/path_utils"
9
8
 
10
9
  module Docyard
@@ -14,13 +13,15 @@ module Docyard
14
13
 
15
14
  PARALLEL_THRESHOLD = 10
16
15
 
17
- attr_reader :docs_path, :config, :temp_dir, :pagefind_path
16
+ attr_reader :docs_path, :config, :temp_dir, :pagefind_path, :page_count, :total_pages
18
17
 
19
18
  def initialize(docs_path:, config:)
20
19
  @docs_path = docs_path
21
20
  @config = config
22
21
  @temp_dir = nil
23
22
  @pagefind_path = nil
23
+ @page_count = 0
24
+ @total_pages = 0
24
25
  end
25
26
 
26
27
  def generate
@@ -29,10 +30,9 @@ module Docyard
29
30
 
30
31
  @temp_dir = Dir.mktmpdir("docyard-search-")
31
32
  generate_html_files
32
- page_count = run_pagefind
33
+ @page_count = run_pagefind
33
34
  @pagefind_path = File.join(temp_dir, "_docyard", "pagefind")
34
35
 
35
- log_success(page_count)
36
36
  pagefind_path
37
37
  rescue StandardError => e
38
38
  Docyard.logger.warn("Search index generation failed: #{e.message}")
@@ -58,36 +58,27 @@ module Docyard
58
58
  markdown_files = Dir.glob(File.join(docs_path, "**", "*.md"))
59
59
  markdown_files = filter_excluded_files(markdown_files)
60
60
  markdown_files = filter_non_indexable_files(markdown_files)
61
-
62
- progress = TTY::ProgressBar.new(
63
- "Indexing search [:bar] :current/:total (:percent)",
64
- total: markdown_files.size,
65
- width: 50
66
- )
67
- mutex = Mutex.new
61
+ @total_pages = markdown_files.size
68
62
 
69
63
  Logging.start_buffering
70
64
  if markdown_files.size >= PARALLEL_THRESHOLD
71
- generate_files_in_parallel(markdown_files, progress, mutex)
65
+ generate_files_in_parallel(markdown_files)
72
66
  else
73
- generate_files_sequentially(markdown_files, progress)
67
+ generate_files_sequentially(markdown_files)
74
68
  end
75
- Logging.flush_warnings
76
69
  end
77
70
 
78
- def generate_files_in_parallel(markdown_files, progress, mutex)
71
+ def generate_files_in_parallel(markdown_files)
79
72
  Parallel.each(markdown_files, in_threads: Parallel.processor_count) do |file_path|
80
73
  renderer = thread_local_renderer
81
74
  generate_html_file(file_path, renderer)
82
- mutex.synchronize { progress.advance }
83
75
  end
84
76
  end
85
77
 
86
- def generate_files_sequentially(markdown_files, progress)
78
+ def generate_files_sequentially(markdown_files)
87
79
  renderer = Renderer.new(base_url: "/", config: config)
88
80
  markdown_files.each do |file_path|
89
81
  generate_html_file(file_path, renderer)
90
- progress.advance
91
82
  end
92
83
  end
93
84
 
@@ -153,11 +144,6 @@ module Docyard
153
144
  match = output.match(/Indexed (\d+) page/i)
154
145
  match ? match[1].to_i : 0
155
146
  end
156
-
157
- def log_success(page_count)
158
- Docyard.logger.info("* Search index generated (#{page_count} pages indexed)")
159
- Docyard.logger.debug("* Temp directory: #{temp_dir}")
160
- end
161
147
  end
162
148
  end
163
149
  end
@@ -9,13 +9,15 @@ require_relative "sse_server"
9
9
  require_relative "file_watcher"
10
10
  require_relative "../config"
11
11
  require_relative "../navigation/sidebar/cache"
12
+ require_relative "../doctor/config_checker"
13
+ require_relative "../doctor/sidebar_checker"
12
14
 
13
15
  module Docyard
14
16
  class DevServer
15
17
  DEFAULT_PORT = 4200
16
18
  DEFAULT_HOST = "localhost"
17
19
 
18
- attr_reader :port, :host, :docs_path, :config, :search_enabled
20
+ attr_reader :port, :host, :docs_path, :config, :search_enabled, :global_diagnostics
19
21
 
20
22
  def initialize(port: DEFAULT_PORT, host: DEFAULT_HOST, docs_path: "docs", search: false)
21
23
  @port = port
@@ -28,12 +30,13 @@ module Docyard
28
30
  @file_watcher = nil
29
31
  @launcher = nil
30
32
  @sidebar_cache = nil
33
+ @global_diagnostics = []
31
34
  end
32
35
 
33
36
  def start
34
37
  validate_docs_directory!
35
38
  build_sidebar_cache
36
- generate_search_index if @search_enabled
39
+ collect_global_diagnostics
37
40
  setup_hot_reload
38
41
  print_server_info
39
42
  run_server
@@ -51,6 +54,14 @@ module Docyard
51
54
  @sidebar_cache.build
52
55
  end
53
56
 
57
+ def collect_global_diagnostics
58
+ @global_diagnostics.clear
59
+ @global_diagnostics.concat(Doctor::ConfigChecker.new(@config).check)
60
+ @global_diagnostics.concat(Doctor::SidebarChecker.new(File.expand_path(docs_path)).check)
61
+ rescue StandardError => e
62
+ Docyard.logger.warn("Diagnostic collection failed: #{e.message}")
63
+ end
64
+
54
65
  def generate_search_index
55
66
  @search_indexer = Search::DevIndexer.new(
56
67
  docs_path: File.expand_path(docs_path),
@@ -75,11 +86,22 @@ module Docyard
75
86
  end
76
87
 
77
88
  def handle_file_change(change_type)
78
- invalidate_sidebar_cache if change_type == :full
89
+ if change_type == :full
90
+ reload_config
91
+ invalidate_sidebar_cache
92
+ collect_global_diagnostics
93
+ @sse_server.broadcast("diagnostics", { global: @global_diagnostics.map(&:to_h) })
94
+ end
79
95
  log_file_change(change_type)
80
96
  @sse_server.broadcast("reload", { type: change_type.to_s })
81
97
  end
82
98
 
99
+ def reload_config
100
+ @config = Config.load
101
+ rescue ConfigError => e
102
+ Docyard.logger.warn("Config reload failed: #{e.message}")
103
+ end
104
+
83
105
  def invalidate_sidebar_cache
84
106
  @sidebar_cache&.invalidate
85
107
  @sidebar_cache&.build
@@ -88,7 +110,7 @@ module Docyard
88
110
  def log_file_change(change_type)
89
111
  message = case change_type
90
112
  when :content then "Content changed, reloading..."
91
- when :config then "Config changed, full reload..."
113
+ when :full then "Config changed, full reload..."
92
114
  when :asset then "Asset changed, reloading..."
93
115
  else "File changed, reloading..."
94
116
  end
@@ -104,17 +126,36 @@ module Docyard
104
126
  def validate_docs_directory!
105
127
  return if File.directory?(docs_path)
106
128
 
107
- abort "Error: #{docs_path}/ directory not found.\n" \
129
+ abort "#{UI.error('Error:')} #{docs_path}/ directory not found.\n" \
108
130
  "Run `docyard init` first to create the docs structure."
109
131
  end
110
132
 
111
- def print_server_info
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")
133
+ def print_server_info # rubocop:disable Metrics/AbcSize
134
+ search_status = @search_enabled ? UI.green("enabled") : UI.dim("disabled")
135
+
136
+ puts
137
+ puts " #{UI.bold('Docyard')} v#{Docyard::VERSION}"
138
+ puts
139
+ puts " Serving #{docs_path}/"
140
+ puts " #{UI.cyan("http://#{host}:#{port}")}"
141
+ puts
142
+ puts " Hot reload: #{UI.cyan("ws://#{host}:#{sse_port}")}"
143
+ puts " Search: #{search_status}"
144
+ print_indexing_status if @search_enabled
145
+ puts
146
+ puts " #{UI.dim('Press Ctrl+C to stop')}"
147
+ puts
148
+ end
149
+
150
+ def print_indexing_status
151
+ print " Indexing: #{UI.dim('in progress')}"
152
+ $stdout.flush
153
+ generate_search_index
154
+ count = @search_indexer&.page_count || 0
155
+ total = @search_indexer&.total_pages || 0
156
+ print "\r Indexing: #{UI.green("done (#{count}/#{total} pages)")}\n"
157
+ $stdout.flush
158
+ Logging.flush_warnings
118
159
  end
119
160
 
120
161
  def run_server
@@ -132,7 +173,8 @@ module Docyard
132
173
  config: @config,
133
174
  pagefind_path: @search_indexer&.pagefind_path,
134
175
  sse_port: sse_port,
135
- sidebar_cache: @sidebar_cache
176
+ sidebar_cache: @sidebar_cache,
177
+ global_diagnostics: @global_diagnostics
136
178
  )
137
179
  end
138
180
 
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../editor_launcher"
5
+
6
+ module Docyard
7
+ class ErrorOverlay
8
+ CATEGORY_LABELS = {
9
+ CONFIG: "Configuration",
10
+ SIDEBAR: "Sidebar",
11
+ CONTENT: "Content",
12
+ COMPONENT: "Component",
13
+ LINK: "Links",
14
+ IMAGE: "Images",
15
+ SYNTAX: "Syntax",
16
+ ORPHAN: "Orphan Pages"
17
+ }.freeze
18
+
19
+ GLOBAL_CATEGORIES = %i[CONFIG SIDEBAR ORPHAN].freeze
20
+
21
+ class << self
22
+ def render(diagnostics:, current_file:, sse_port:)
23
+ return "" if diagnostics.empty?
24
+
25
+ attrs = build_data_attributes(diagnostics, current_file, sse_port)
26
+ render_overlay_html(attrs)
27
+ end
28
+
29
+ private
30
+
31
+ def build_data_attributes(diagnostics, current_file, sse_port)
32
+ global_count = diagnostics.count { |d| GLOBAL_CATEGORIES.include?(d.category) }
33
+ page_count = diagnostics.length - global_count
34
+
35
+ {
36
+ diagnostics: escape_json(diagnostics),
37
+ current_file: escape_html(current_file),
38
+ error_count: diagnostics.count(&:error?),
39
+ warning_count: diagnostics.count(&:warning?),
40
+ global_count: global_count,
41
+ page_count: page_count,
42
+ sse_port: sse_port,
43
+ editor_available: EditorLauncher.available?
44
+ }
45
+ end
46
+
47
+ def render_overlay_html(attrs)
48
+ <<~HTML
49
+ <div id="docyard-error-overlay" class="docyard-error-overlay"
50
+ data-diagnostics='#{attrs[:diagnostics]}'
51
+ data-current-file="#{attrs[:current_file]}"
52
+ data-error-count="#{attrs[:error_count]}"
53
+ data-warning-count="#{attrs[:warning_count]}"
54
+ data-global-count="#{attrs[:global_count]}"
55
+ data-page-count="#{attrs[:page_count]}"
56
+ data-sse-port="#{attrs[:sse_port]}"
57
+ data-editor-available="#{attrs[:editor_available]}">
58
+ </div>
59
+ <link rel="stylesheet" href="/_docyard/error-overlay.css">
60
+ <script src="/_docyard/error-overlay.js"></script>
61
+ HTML
62
+ end
63
+
64
+ def escape_json(diagnostics)
65
+ JSON.generate(diagnostics.map(&:to_h)).gsub("'", "&#39;")
66
+ end
67
+
68
+ def escape_html(str)
69
+ str.to_s.gsub("&", "&amp;").gsub('"', "&quot;").gsub("<", "&lt;").gsub(">", "&gt;")
70
+ end
71
+ end
72
+ end
73
+ end
@@ -5,7 +5,6 @@ require "listen"
5
5
  module Docyard
6
6
  class FileWatcher
7
7
  DEBOUNCE_DELAY = 0.1
8
- ROOT_CONFIG_FILE = "docyard.yml"
9
8
  CONFIG_FILES = %w[docyard.yml _sidebar.yml].freeze
10
9
  CONTENT_EXTENSIONS = %w[.md .markdown].freeze
11
10
  ASSET_EXTENSIONS = %w[.css .js .html .erb].freeze
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../doctor/content_checker"
4
+ require_relative "../doctor/component_checker"
5
+ require_relative "../doctor/link_checker"
6
+ require_relative "../doctor/image_checker"
7
+
8
+ module Docyard
9
+ class PageDiagnostics
10
+ def initialize(docs_path)
11
+ @docs_path = docs_path
12
+ @content_checker = Doctor::ContentChecker.new(docs_path)
13
+ @component_checker = Doctor::ComponentChecker.new(docs_path)
14
+ @link_checker = Doctor::LinkChecker.new(docs_path)
15
+ @image_checker = Doctor::ImageChecker.new(docs_path)
16
+ end
17
+
18
+ def check(content, file_path)
19
+ [
20
+ @content_checker.check_file(content, file_path),
21
+ @component_checker.check_file(content, file_path),
22
+ @link_checker.check_file(content, file_path),
23
+ @image_checker.check_file(content, file_path)
24
+ ].flatten
25
+ end
26
+ end
27
+ end
@@ -9,6 +9,8 @@ require_relative "static_file_app"
9
9
 
10
10
  module Docyard
11
11
  class PreviewServer
12
+ include Utils::UrlHelpers
13
+
12
14
  DEFAULT_PORT = 4000
13
15
 
14
16
  attr_reader :port, :output_dir, :base_url
@@ -30,17 +32,26 @@ module Docyard
30
32
  private
31
33
 
32
34
  def validate_output_directory!
33
- return if File.directory?(output_dir)
35
+ unless File.directory?(output_dir)
36
+ abort "#{UI.error('Error:')} #{output_dir}/ directory not found.\n" \
37
+ "Run `docyard build` first to build the site."
38
+ end
39
+
40
+ return if Dir.glob(File.join(output_dir, "**", "*")).any? { |f| File.file?(f) }
34
41
 
35
- abort "Error: #{output_dir}/ directory not found.\n" \
42
+ abort "#{UI.error('Error:')} #{output_dir}/ is empty.\n" \
36
43
  "Run `docyard build` first to build the site."
37
44
  end
38
45
 
39
46
  def print_server_info
40
- Docyard.logger.info("Starting preview server...")
41
- Docyard.logger.info("* Version: #{Docyard::VERSION}")
42
- Docyard.logger.info("* Running at: http://localhost:#{port}#{base_url}")
43
- Docyard.logger.info("Use Ctrl+C to stop\n")
47
+ puts
48
+ puts " #{UI.bold('Docyard')} v#{Docyard::VERSION}"
49
+ puts
50
+ puts " Previewing #{output_dir}/"
51
+ puts " #{UI.cyan("http://localhost:#{port}#{base_url}")}"
52
+ puts
53
+ puts " #{UI.dim('Press Ctrl+C to stop')}"
54
+ puts
44
55
  end
45
56
 
46
57
  def run_server
@@ -65,12 +76,5 @@ module Docyard
65
76
  config.quiet
66
77
  end
67
78
  end
68
-
69
- def normalize_base_url(url)
70
- return "/" if url.nil? || url.empty?
71
-
72
- url = "/#{url}" unless url.start_with?("/")
73
- url.end_with?("/") ? url : "#{url}/"
74
- end
75
79
  end
76
80
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
3
  require "rack"
5
4
  require_relative "../navigation/page_navigation_builder"
6
5
  require_relative "../navigation/sidebar_builder"
@@ -9,20 +8,29 @@ require_relative "../constants"
9
8
  require_relative "../rendering/template_resolver"
10
9
  require_relative "../routing/fallback_resolver"
11
10
  require_relative "pagefind_handler"
11
+ require_relative "page_diagnostics"
12
+ require_relative "error_overlay"
13
+ require_relative "../editor_launcher"
12
14
 
13
15
  module Docyard
14
16
  class RackApplication
15
- def initialize(docs_path:, config: nil, pagefind_path: nil, sse_port: nil, sidebar_cache: nil)
17
+ OVERLAY_RESET_SCRIPT = "<script>try{sessionStorage.setItem('docyard-error-overlay'," \
18
+ "'{\"dismissed\":false,\"lastTotalCount\":0}')}catch(e){}</script>"
19
+
20
+ def initialize(docs_path:, config: nil, pagefind_path: nil, sse_port: nil, sidebar_cache: nil,
21
+ global_diagnostics: [])
16
22
  @docs_path = docs_path
17
23
  @config = config
18
24
  @sse_port = sse_port
19
25
  @dev_mode = !sse_port.nil?
20
26
  @sidebar_cache = sidebar_cache
27
+ @global_diagnostics = global_diagnostics
21
28
  @router = Router.new(docs_path: docs_path)
22
29
  @renderer = Renderer.new(base_url: "/", config: config, dev_mode: @dev_mode,
23
30
  sse_port: sse_port)
24
31
  @asset_handler = AssetHandler.new(public_dir: config&.public_dir || "docs/public")
25
32
  @pagefind_handler = PagefindHandler.new(pagefind_path: pagefind_path, config: config)
33
+ @page_diagnostics = PageDiagnostics.new(docs_path) if @dev_mode
26
34
  end
27
35
 
28
36
  def call(env)
@@ -36,6 +44,8 @@ module Docyard
36
44
  def handle_request(env)
37
45
  path = env["PATH_INFO"]
38
46
 
47
+ return handle_open_in_editor(env) if path == "/__docyard/open-in-editor"
48
+ return serve_overlay_asset(path) if path.start_with?("/_docyard/error-overlay")
39
49
  return pagefind_handler.serve(path) if path.start_with?(Constants::PAGEFIND_PREFIX)
40
50
  return asset_handler.serve_docyard_assets(path) if path.start_with?(Constants::DOCYARD_ASSETS_PREFIX)
41
51
 
@@ -94,7 +104,8 @@ module Docyard
94
104
  end
95
105
 
96
106
  def render_documentation_page(file_path, current_path)
97
- markdown = Markdown.new(File.read(file_path))
107
+ content = File.read(file_path)
108
+ markdown = Markdown.new(content)
98
109
  template_resolver = TemplateResolver.new(markdown.frontmatter, @config&.data)
99
110
  branding = branding_options
100
111
 
@@ -103,6 +114,8 @@ module Docyard
103
114
  template_options: template_resolver.to_options,
104
115
  current_path: current_path)
105
116
 
117
+ html = inject_error_overlay(html, content, file_path) if dev_mode
118
+
106
119
  [Constants::STATUS_OK, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
107
120
  end
108
121
 
@@ -156,5 +169,53 @@ module Docyard
156
169
  user_agent = env["HTTP_USER_AGENT"]&.slice(0, 50)
157
170
  user_agent ? "#{method} #{path} - #{user_agent}" : "#{method} #{path}"
158
171
  end
172
+
173
+ def inject_error_overlay(html, content, file_path)
174
+ page_diags = @page_diagnostics.check(content, file_path)
175
+ all_diagnostics = @global_diagnostics + page_diags
176
+ return html.sub("</body>", "#{OVERLAY_RESET_SCRIPT}</body>") if all_diagnostics.empty?
177
+
178
+ current_file = file_path.delete_prefix("#{@docs_path}/")
179
+ overlay = ErrorOverlay.render(
180
+ diagnostics: all_diagnostics,
181
+ current_file: current_file,
182
+ sse_port: @sse_port
183
+ )
184
+
185
+ html.sub("</body>", "#{overlay}</body>")
186
+ end
187
+
188
+ def handle_open_in_editor(env)
189
+ params = Rack::Utils.parse_query(env["QUERY_STRING"])
190
+ file = params["file"]
191
+ line = params["line"]&.to_i || 1
192
+
193
+ return [400, {}, ["Missing file parameter"]] unless file
194
+ return [404, {}, ["No editor detected"]] unless EditorLauncher.available?
195
+
196
+ full_path = File.join(@docs_path, file)
197
+ EditorLauncher.open(full_path, line)
198
+ [200, {}, ["OK"]]
199
+ end
200
+
201
+ def serve_overlay_asset(path)
202
+ asset_path = resolve_overlay_asset_path(path)
203
+ return [404, {}, ["Not found"]] unless asset_path && File.exist?(asset_path)
204
+
205
+ content_type = path.end_with?(".css") ? "text/css" : "application/javascript"
206
+ [200, { "Content-Type" => content_type }, [File.read(asset_path)]]
207
+ end
208
+
209
+ def resolve_overlay_asset_path(path)
210
+ asset_name = path.delete_prefix("/_docyard/")
211
+ css_path = File.join(templates_path, "assets", asset_name.sub("error-overlay", "css/error-overlay"))
212
+ return css_path if File.exist?(css_path)
213
+
214
+ File.join(templates_path, "assets", asset_name.sub("error-overlay", "js/error-overlay"))
215
+ end
216
+
217
+ def templates_path
218
+ File.expand_path("../templates", __dir__)
219
+ end
159
220
  end
160
221
  end
@@ -21,9 +21,5 @@ module Docyard
21
21
  def found?
22
22
  status == :found
23
23
  end
24
-
25
- def not_found?
26
- status == :not_found
27
- end
28
24
  end
29
25
  end