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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69c121af95a08c379f260d0d444aa4bc5272fdfe2d0699866091885c9f503601
4
- data.tar.gz: 4550cdcc1d6863b545c4900aac9adb10f14d96a7394ccd3fe4c8924969b278dc
3
+ metadata.gz: a6ab36d72a5ae740f0ba203c29ab88b2698f0010bbc980ac0ef3a8b7d5f9f579
4
+ data.tar.gz: f4df0ae89a078729eb319733ac7dab36943fa9e228daaba9fb75e6323a2f6fab
5
5
  SHA512:
6
- metadata.gz: 690e11d0792b71540f09d26fe45dc46d57558b3aad1b3cf80f3e08c592a4056408b6b8a57bfc47f2459feba94e481038f11ccef287534a9f82e557c255728b53
7
- data.tar.gz: 8baa1a707f2ff94152499b66c19596a57984c5ff763d2cf43390d986030e2e2eea994febac3089766a764ec1d6314a61479fe2caa54434ee5e64c86ade6431e1
6
+ metadata.gz: 4c4e05cb41cf1259a74a73c24c45fa265da9fbaf6725905e9893f442e9e1a2dbf1979ba03c5aac030bafd9e2f574fa0fec50f1f11ee5fd583353e3fd7b8329ea
7
+ data.tar.gz: f53424206fd8224b815232c4c2babca237852829f4f43f8fe537f63136c65e7b25ae75d6276a4b07416b4c24cc8274b822946c6f03a2e6aa2c6f394cc5334327
data/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.0] - 2026-01-30
11
+
12
+ ### Added
13
+ - **Doctor Command** - Health check CLI with `docyard doctor` to find broken links, missing images, orphan pages, and config issues (#135)
14
+ - **Auto-fix** - Automatically fix common issues with `docyard doctor --fix` including typos, string booleans, and missing slashes (#135)
15
+ - **Config Validation** - Validate `docyard.yml` and `_sidebar.yml` with helpful error messages and suggestions (#138)
16
+ - **Dev Server Error Overlay** - Bottom sheet overlay showing errors with syntax-highlighted code snippets, line numbers, and clickable file paths (#143)
17
+ - **Strict Mode** - Fail builds on validation errors with `--strict` flag or `build.strict` config (#143)
18
+ - **Colored CLI Output** - ANSI colors for better readability with `--no-color` flag and `NO_COLOR` env support (#142)
19
+ - **CLI Progress Spinners** - Animated spinners and step-by-step progress display for build and serve commands (#136)
20
+ - **Verbose Timing Breakdown** - Per-page timing in `docyard build --verbose` output (#141)
21
+
22
+ ### Fixed
23
+ - "Last updated" timestamps now calculate relative time client-side for accurate display (#134)
24
+
25
+ ### Changed
26
+ - Build output format updated with progress indicators (#136, #141)
27
+ - Config validation now runs on every build (#138)
28
+
29
+ ### Documentation
30
+ - Added `docyard doctor` command reference
31
+ - Added strict mode documentation to CLI and configuration references
32
+ - Added error overlay mention to dev server documentation
33
+ - Updated build output examples to match current format
34
+
10
35
  ## [1.0.2] - 2026-01-23
11
36
 
12
37
  ### Fixed
@@ -224,7 +249,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
224
249
  - Initial gem structure
225
250
  - Project scaffolding
226
251
 
227
- [Unreleased]: https://github.com/sanifhimani/docyard/compare/v1.0.2...HEAD
252
+ [Unreleased]: https://github.com/sanifhimani/docyard/compare/v1.1.0...HEAD
253
+ [1.1.0]: https://github.com/sanifhimani/docyard/compare/v1.0.2...v1.1.0
228
254
  [1.0.2]: https://github.com/sanifhimani/docyard/compare/v1.0.1...v1.0.2
229
255
  [1.0.1]: https://github.com/sanifhimani/docyard/compare/v1.0.0...v1.0.1
230
256
  [1.0.0]: https://github.com/sanifhimani/docyard/compare/v0.9.0...v1.0.0
@@ -7,6 +7,8 @@ require "digest"
7
7
  module Docyard
8
8
  module Build
9
9
  class AssetBundler
10
+ include Utils::UrlHelpers
11
+
10
12
  ASSETS_PATH = File.join(__dir__, "..", "templates", "assets")
11
13
 
12
14
  attr_reader :config, :verbose
@@ -17,21 +19,17 @@ module Docyard
17
19
  end
18
20
 
19
21
  def bundle
20
- Docyard.logger.info("\nBundling assets...")
21
-
22
- css_hash = bundle_css
23
- js_hash = bundle_js
22
+ css_hash, css_size = bundle_css
23
+ js_hash, js_size = bundle_js
24
24
 
25
25
  update_html_references(css_hash, js_hash)
26
26
 
27
- 2
27
+ [css_size, js_size]
28
28
  end
29
29
 
30
30
  private
31
31
 
32
32
  def bundle_css
33
- log " Bundling CSS..."
34
-
35
33
  main_css = File.read(File.join(ASSETS_PATH, "css", "main.css"))
36
34
  css_content = resolve_css_imports(main_css)
37
35
  minified = CSSminify.compress(css_content)
@@ -41,9 +39,8 @@ module Docyard
41
39
  hash = generate_hash(minified)
42
40
 
43
41
  write_bundled_asset(minified, hash, "css")
44
- log_compression_stats(css_content, minified, "CSS")
45
42
 
46
- hash
43
+ [hash, minified.bytesize]
47
44
  end
48
45
 
49
46
  def fix_calc_whitespace(css)
@@ -86,8 +83,6 @@ module Docyard
86
83
  end
87
84
 
88
85
  def bundle_js
89
- log " Bundling JS..."
90
-
91
86
  theme_js = File.read(File.join(ASSETS_PATH, "js", "theme.js"))
92
87
  components_js = concatenate_component_js
93
88
  js_content = [theme_js, components_js].join("\n")
@@ -96,9 +91,8 @@ module Docyard
96
91
  hash = generate_hash(minified)
97
92
 
98
93
  write_bundled_asset(minified, hash, "js")
99
- log_compression_stats(js_content, minified, "JS")
100
94
 
101
- hash
95
+ [hash, minified.bytesize]
102
96
  end
103
97
 
104
98
  def replace_js_asset_urls(js_content)
@@ -127,8 +121,6 @@ module Docyard
127
121
  content = replace_asset_references(File.read(file), css_hash, js_hash, base_url)
128
122
  File.write(file, content)
129
123
  end
130
-
131
- log " [✓] Updated asset references in #{html_files.size} HTML files"
132
124
  end
133
125
 
134
126
  def replace_asset_references(content, css_hash, js_hash, base_url)
@@ -154,24 +146,6 @@ module Docyard
154
146
  FileUtils.mkdir_p(File.dirname(output_path))
155
147
  File.write(output_path, content)
156
148
  end
157
-
158
- def log_compression_stats(original, minified, label)
159
- original_size = (original.bytesize / 1024.0).round(1)
160
- minified_size = (minified.bytesize / 1024.0).round(1)
161
- reduction = (((original_size - minified_size) / original_size) * 100).round(0)
162
- log " [✓] #{label}: #{original_size} KB -> #{minified_size} KB (-#{reduction}%)"
163
- end
164
-
165
- def normalize_base_url(url)
166
- return "/" if url.nil? || url.empty? || url == "/"
167
-
168
- url = "/#{url}" unless url.start_with?("/")
169
- url.end_with?("/") ? url : "#{url}/"
170
- end
171
-
172
- def log(message)
173
- Docyard.logger.info(message)
174
- end
175
149
  end
176
150
  end
177
151
  end
@@ -13,14 +13,12 @@ module Docyard
13
13
  end
14
14
 
15
15
  def copy
16
- Docyard.logger.info("\nCopying static assets...")
17
-
16
+ @copied_files = []
18
17
  count = 0
19
18
  count += copy_public_files
20
19
  count += copy_branding_assets
21
-
22
- log "[✓] Copied #{count} static files"
23
- count
20
+ details = verbose ? @copied_files : nil
21
+ [count, details]
24
22
  end
25
23
 
26
24
  private
@@ -32,7 +30,6 @@ module Docyard
32
30
  files = find_files_in_dir(public_dir)
33
31
  files.each { |file| copy_single_file(file, "#{public_dir}/", config.build.output) }
34
32
 
35
- log "[✓] Copied #{files.size} public files from #{public_dir}/" if files.any?
36
33
  files.size
37
34
  end
38
35
 
@@ -47,14 +44,13 @@ module Docyard
47
44
  FileUtils.mkdir_p(File.dirname(dest_path))
48
45
  FileUtils.cp(file, dest_path)
49
46
 
50
- log " Copied: #{relative_path}" if verbose
47
+ @copied_files << relative_path if verbose
51
48
  end
52
49
 
53
50
  def copy_branding_assets
54
51
  count = 0
55
52
  count += copy_default_branding_assets
56
53
  count += copy_user_branding_assets
57
- log "[✓] Copied #{count} branding assets" if count.positive?
58
54
  count
59
55
  end
60
56
 
@@ -80,7 +76,7 @@ module Docyard
80
76
  dest_path = File.join(config.build.output, DOCYARD_OUTPUT_DIR, filename)
81
77
  FileUtils.mkdir_p(File.dirname(dest_path))
82
78
  FileUtils.cp(source_path, dest_path)
83
- log " Copied #{label}: #{filename}" if verbose
79
+ @copied_files << "#{filename} (#{label})" if verbose
84
80
  1
85
81
  end
86
82
 
@@ -96,7 +92,7 @@ module Docyard
96
92
  dest_path = File.join(config.build.output, DOCYARD_OUTPUT_DIR, "fonts", File.basename(font_file))
97
93
  FileUtils.mkdir_p(File.dirname(dest_path))
98
94
  FileUtils.cp(font_file, dest_path)
99
- log " Copied font: #{File.basename(font_file)}" if verbose
95
+ @copied_files << "#{File.basename(font_file)} (font)" if verbose
100
96
  1
101
97
  end
102
98
 
@@ -122,13 +118,9 @@ module Docyard
122
118
  FileUtils.mkdir_p(File.dirname(dest_path))
123
119
  FileUtils.cp(full_path, dest_path)
124
120
 
125
- log " Copied user branding: #{asset_path}" if verbose
121
+ @copied_files << "#{asset_path} (user branding)" if verbose
126
122
  1
127
123
  end
128
-
129
- def log(message)
130
- Docyard.logger.info(message)
131
- end
132
124
  end
133
125
  end
134
126
  end
@@ -50,7 +50,6 @@ module Docyard
50
50
  def generate_llms_txt(pages)
51
51
  output_path = File.join(config.build.output, "llms.txt")
52
52
  File.write(output_path, build_llms_txt_content(pages))
53
- Docyard.logger.info("[✓] Generated llms.txt (#{pages.size} pages)")
54
53
  end
55
54
 
56
55
  def build_llms_txt_content(pages)
@@ -70,7 +69,6 @@ module Docyard
70
69
  def generate_llms_full_txt(pages)
71
70
  output_path = File.join(config.build.output, "llms-full.txt")
72
71
  File.write(output_path, build_llms_full_txt_content(pages))
73
- Docyard.logger.info("[✓] Generated llms-full.txt")
74
72
  end
75
73
 
76
74
  def build_llms_full_txt_content(pages)
@@ -18,7 +18,7 @@ module Docyard
18
18
  output_path = File.join(config.build.output, "sitemap.xml")
19
19
  File.write(output_path, sitemap_content)
20
20
 
21
- Docyard.logger.info("[✓] Generated sitemap.xml (#{urls.size} URLs)")
21
+ urls.size
22
22
  end
23
23
 
24
24
  private
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "parallel"
4
- require "tty-progressbar"
5
4
  require_relative "../rendering/template_resolver"
6
5
  require_relative "../navigation/page_navigation_builder"
7
6
  require_relative "../navigation/sidebar/cache"
@@ -32,13 +31,11 @@ module Docyard
32
31
  copy_custom_landing_page if custom_landing_page?
33
32
 
34
33
  markdown_files = collect_markdown_files
35
- Docyard.logger.info("\n[✓] Found #{markdown_files.size} markdown files")
36
-
37
- generate_all_pages(markdown_files)
34
+ generated_pages = generate_all_pages(markdown_files)
38
35
  generate_error_page
39
36
  generate_root_fallback_if_needed
40
37
 
41
- markdown_files.size
38
+ [markdown_files.size, build_verbose_details(generated_pages)]
42
39
  ensure
43
40
  Utils::GitInfo.clear_cache
44
41
  end
@@ -46,20 +43,14 @@ module Docyard
46
43
  private
47
44
 
48
45
  def generate_all_pages(markdown_files)
49
- progress = TTY::ProgressBar.new(
50
- "Generating pages [:bar] :current/:total (:percent)",
51
- total: markdown_files.size,
52
- width: 50
53
- )
54
- mutex = Mutex.new
55
-
56
46
  Logging.start_buffering
57
- if markdown_files.size >= PARALLEL_THRESHOLD
58
- generate_pages_in_parallel(markdown_files, progress, mutex)
59
- else
60
- generate_pages_sequentially(markdown_files, progress)
61
- end
47
+ pages = if markdown_files.size >= PARALLEL_THRESHOLD
48
+ generate_pages_in_parallel(markdown_files)
49
+ else
50
+ generate_pages_sequentially(markdown_files)
51
+ end
62
52
  Logging.flush_warnings
53
+ pages
63
54
  end
64
55
 
65
56
  def custom_landing_page?
@@ -72,7 +63,6 @@ module Docyard
72
63
  FileUtils.mkdir_p(File.dirname(output_path))
73
64
  FileUtils.cp(File.join(docs_path, "index.html"), output_path)
74
65
  end
75
- log "[✓] Copied custom landing page (index.html)"
76
66
  end
77
67
 
78
68
  def collect_markdown_files
@@ -81,23 +71,28 @@ module Docyard
81
71
  files
82
72
  end
83
73
 
84
- def generate_pages_in_parallel(markdown_files, progress, mutex)
85
- Parallel.each(markdown_files, in_threads: Parallel.processor_count) do |file_path|
86
- generate_page(file_path, thread_local_renderer)
87
- mutex.synchronize { progress.advance }
74
+ def generate_pages_in_parallel(markdown_files)
75
+ Parallel.map(markdown_files, in_threads: Parallel.processor_count) do |file_path|
76
+ generate_page_with_timing(file_path, thread_local_renderer)
88
77
  ensure
89
78
  Thread.current[:docyard_build_renderer] = nil
90
79
  end
91
80
  end
92
81
 
93
- def generate_pages_sequentially(markdown_files, progress)
82
+ def generate_pages_sequentially(markdown_files)
94
83
  renderer = build_renderer
95
- markdown_files.each do |file_path|
96
- generate_page(file_path, renderer)
97
- progress.advance
84
+ markdown_files.map do |file_path|
85
+ generate_page_with_timing(file_path, renderer)
98
86
  end
99
87
  end
100
88
 
89
+ def generate_page_with_timing(file_path, renderer)
90
+ page_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
91
+ output_path = generate_page(file_path, renderer)
92
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - page_start
93
+ [output_path, elapsed]
94
+ end
95
+
101
96
  def thread_local_renderer
102
97
  Thread.current[:docyard_build_renderer] ||= build_renderer
103
98
  end
@@ -114,6 +109,7 @@ module Docyard
114
109
  html_content = render_markdown_file(markdown_file_path, current_path, renderer)
115
110
  html_content = apply_search_exclusion(html_content, current_path)
116
111
  write_output(output_path, html_content)
112
+ output_path
117
113
  end
118
114
 
119
115
  def apply_search_exclusion(html_content, current_path)
@@ -165,7 +161,6 @@ module Docyard
165
161
  FileUtils.mkdir_p(File.dirname(output_path))
166
162
  File.write(output_path, html_content)
167
163
  end
168
- log "Generated: #{output_path}" if verbose
169
164
  end
170
165
 
171
166
  def build_sidebar_cache
@@ -184,8 +179,13 @@ module Docyard
184
179
  config.source
185
180
  end
186
181
 
187
- def log(message)
188
- Docyard.logger.info(message) if verbose
182
+ def build_verbose_details(generated_pages)
183
+ return nil unless verbose
184
+
185
+ generated_pages.map do |output_path, elapsed|
186
+ path = output_path.delete_prefix("#{config.build.output}/")
187
+ "#{path.ljust(30)} #{format('%<t>.2fs', t: elapsed)}"
188
+ end
189
189
  end
190
190
 
191
191
  def generate_error_page
@@ -194,7 +194,6 @@ module Docyard
194
194
  docs_path: docs_path,
195
195
  renderer: build_renderer
196
196
  ).generate
197
- log "[✓] Generated 404.html"
198
197
  end
199
198
 
200
199
  def generate_root_fallback_if_needed
@@ -204,8 +203,7 @@ module Docyard
204
203
  sidebar_cache: sidebar_cache,
205
204
  renderer: build_renderer
206
205
  )
207
- target = generator.generate_if_needed
208
- log "[✓] Generated root redirect to #{target}" if target
206
+ generator.generate_if_needed
209
207
  end
210
208
  end
211
209
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Build
5
+ class StepRunner
6
+ STEP_SHORT_LABELS = {
7
+ "Generating pages" => "Pages",
8
+ "Bundling assets" => "Assets",
9
+ "Copying files" => "Files",
10
+ "Generating SEO" => "SEO",
11
+ "Indexing search" => "Search"
12
+ }.freeze
13
+
14
+ attr_reader :verbose, :step_timings
15
+
16
+ def initialize(verbose: false)
17
+ @verbose = verbose
18
+ @step_timings = []
19
+ end
20
+
21
+ def run(label, &)
22
+ print " #{label.ljust(20)}#{UI.dim('in progress')}"
23
+ $stdout.flush
24
+ result, details, elapsed = execute(label, &)
25
+ print_result(label, result, elapsed)
26
+ print_details(details) if verbose && details&.any?
27
+ result
28
+ end
29
+
30
+ def print_timing_breakdown
31
+ total = step_timings.sum { |t| t[:elapsed] }
32
+ sorted = step_timings.sort_by { |t| -t[:elapsed] }
33
+
34
+ puts " #{UI.bold('Timing:')}"
35
+ sorted.each { |timing| puts " #{UI.dim(format_timing_line(timing, total))}" }
36
+ puts
37
+ end
38
+
39
+ private
40
+
41
+ def execute(label)
42
+ step_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
43
+ result, details = yield
44
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - step_start
45
+ @step_timings << { label: STEP_SHORT_LABELS.fetch(label, label), elapsed: elapsed }
46
+ [result, details, elapsed]
47
+ end
48
+
49
+ def print_result(label, result, elapsed)
50
+ timing_suffix = verbose ? UI.dim(" in #{format('%<t>.2fs', t: elapsed)}") : ""
51
+ print "\r #{label.ljust(20)}#{UI.green(format_result(label, result))}#{timing_suffix}\n"
52
+ $stdout.flush
53
+ end
54
+
55
+ def print_details(details)
56
+ details.each { |detail| puts " #{UI.dim(detail)}" }
57
+ end
58
+
59
+ def format_result(label, result)
60
+ case label
61
+ when "Generating pages" then "done (#{result} pages)"
62
+ when "Bundling assets" then format_assets_result(result)
63
+ when "Copying files" then "done (#{result} files)"
64
+ when "Generating SEO" then "done (#{result.join(', ')})"
65
+ when "Indexing search" then "done (#{result} pages indexed)"
66
+ else "done"
67
+ end
68
+ end
69
+
70
+ def format_assets_result(result)
71
+ css, js = result
72
+ "done (#{format_size(css)} CSS, #{format_size(js)} JS)"
73
+ end
74
+
75
+ def format_size(bytes)
76
+ kb = bytes / 1024.0
77
+ kb >= 1000 ? format("%.1f MB", kb / 1024.0) : format("%.1f KB", kb)
78
+ end
79
+
80
+ def format_timing_line(timing, total)
81
+ label = timing[:label].ljust(12)
82
+ secs = format("%<t>5.2fs", t: timing[:elapsed])
83
+ pct = total.positive? ? (timing[:elapsed] / total * 100).round : 0
84
+ "#{label} #{secs} (#{format('%<p>2d', p: pct)}%)"
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Build
5
+ class Validator
6
+ attr_reader :config, :strict, :diagnostics
7
+
8
+ def initialize(config, strict: false)
9
+ @config = config
10
+ @strict = strict
11
+ @diagnostics = []
12
+ end
13
+
14
+ def valid?
15
+ @diagnostics = collect_diagnostics
16
+ failing_diagnostics.empty?
17
+ end
18
+
19
+ def errors
20
+ diagnostics.select(&:error?)
21
+ end
22
+
23
+ def failing_diagnostics
24
+ strict ? diagnostics : errors
25
+ end
26
+
27
+ def warnings
28
+ diagnostics.select(&:warning?)
29
+ end
30
+
31
+ def print_errors(context: "Build")
32
+ return if failing_diagnostics.empty?
33
+
34
+ print_header
35
+ puts " #{UI.red("#{context} failed due to validation errors:")}"
36
+ puts
37
+ failing_diagnostics.each { |d| puts format_diagnostic_line(d) }
38
+ puts
39
+ end
40
+
41
+ def print_warnings
42
+ return if warnings.empty?
43
+
44
+ puts
45
+ puts " #{UI.yellow("#{warnings.size} warning(s) found:")}"
46
+ warnings.each { |d| puts " #{UI.yellow('warn ')} #{d.location.ljust(26)} #{d.message}" }
47
+ puts
48
+ end
49
+
50
+ private
51
+
52
+ def print_header
53
+ puts
54
+ puts " #{UI.bold('Docyard')} v#{VERSION}"
55
+ puts
56
+ end
57
+
58
+ def format_diagnostic_line(diagnostic)
59
+ if diagnostic.error?
60
+ " #{UI.red('error')} #{diagnostic.location.ljust(26)} #{diagnostic.message}"
61
+ else
62
+ " #{UI.yellow('warn ')} #{diagnostic.location.ljust(26)} #{diagnostic.message}"
63
+ end
64
+ end
65
+
66
+ def collect_diagnostics
67
+ strict ? collect_strict_diagnostics : collect_essential_diagnostics
68
+ end
69
+
70
+ def collect_essential_diagnostics
71
+ require_relative "../doctor/config_checker"
72
+ require_relative "../doctor/sidebar_checker"
73
+
74
+ [
75
+ Doctor::ConfigChecker.new(config).check,
76
+ Doctor::SidebarChecker.new(docs_path).check
77
+ ].flatten
78
+ end
79
+
80
+ def collect_strict_diagnostics
81
+ require_relative "../doctor"
82
+
83
+ file_scanner = Doctor::FileScanner.new(docs_path)
84
+ scanner_diagnostics = file_scanner.scan
85
+
86
+ [
87
+ collect_essential_diagnostics,
88
+ scanner_diagnostics,
89
+ Doctor::OrphanChecker.new(docs_path, config).check
90
+ ].flatten
91
+ end
92
+
93
+ def docs_path
94
+ config.source
95
+ end
96
+ end
97
+ end
98
+ end