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
@@ -1,84 +1,133 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "fileutils"
4
- require "tty-progressbar"
4
+ require_relative "build/step_runner"
5
5
 
6
6
  module Docyard
7
7
  class Builder
8
- attr_reader :config, :clean, :verbose, :start_time
8
+ attr_reader :config, :clean, :verbose, :strict, :start_time
9
9
 
10
- def initialize(clean: true, verbose: false)
10
+ def initialize(clean: true, verbose: false, strict: false)
11
11
  @config = Config.new
12
12
  @clean = clean
13
13
  @verbose = verbose
14
+ @strict = strict || config.build.strict
14
15
  @start_time = Time.now
16
+ @step_runner = Build::StepRunner.new(verbose: verbose)
15
17
  end
16
18
 
17
19
  def build
18
- prepare_output_directory
19
- log "Building static site..."
20
-
21
- pages_built = generate_static_pages
22
- bundles_created = bundle_assets
23
- assets_copied = copy_static_files
24
- generate_seo_files
25
- pages_indexed = generate_search_index
20
+ return false unless passes_validation?
26
21
 
27
- display_summary(pages_built, bundles_created, assets_copied, pages_indexed)
22
+ print_header
23
+ prepare_output_directory
24
+ execute_build_steps
25
+ print_summary
28
26
  true
29
27
  rescue StandardError => e
30
- error "Build failed: #{e.message}"
31
- error e.backtrace.first if verbose
28
+ print_error(e)
32
29
  false
33
30
  end
34
31
 
35
32
  private
36
33
 
37
- def prepare_output_directory
38
- output_dir = config.build.output
34
+ def print_header
35
+ puts
36
+ puts " #{UI.bold('Docyard')} v#{VERSION}"
37
+ puts
38
+ puts " Building to #{UI.dim("#{config.build.output}/")}..."
39
+ puts
40
+ end
41
+
42
+ def passes_validation?
43
+ require_relative "build/validator"
44
+ validator = Build::Validator.new(config, strict: strict)
45
+ return true if validator.valid?
46
+
47
+ validator.print_errors
48
+ false
49
+ end
39
50
 
40
- if clean && Dir.exist?(output_dir)
41
- log "[✓] Cleaning #{output_dir}/ directory"
42
- FileUtils.rm_rf(output_dir)
43
- end
51
+ def execute_build_steps
52
+ @step_runner.run("Generating pages") { generate_static_pages }
53
+ @step_runner.run("Bundling assets") { bundle_assets }
54
+ @step_runner.run("Copying files") { copy_static_files }
55
+ @step_runner.run("Generating SEO") { generate_seo_files }
56
+ @step_runner.run("Indexing search") { generate_search_index }
57
+ end
44
58
 
59
+ def print_error(error)
60
+ puts UI.error("failed")
61
+ puts
62
+ puts " #{UI.error('Error:')} #{error.message}"
63
+ puts " #{error.backtrace.first}" if verbose
64
+ puts
65
+ end
66
+
67
+ def print_summary
68
+ puts
69
+ puts " #{UI.success('Build complete')} in #{format('%.2fs', build_duration)}"
70
+ puts " #{UI.dim(output_summary)}"
71
+ puts
72
+ @step_runner.print_timing_breakdown if verbose
73
+ end
74
+
75
+ def build_duration
76
+ Time.now - start_time
77
+ end
78
+
79
+ def output_summary
80
+ "Output: #{config.build.output}/ (#{format_size(calculate_output_size)})"
81
+ end
82
+
83
+ def format_size(bytes)
84
+ kb = bytes / 1024.0
85
+ kb >= 1000 ? format("%.1f MB", kb / 1024.0) : format("%.1f KB", kb)
86
+ end
87
+
88
+ def calculate_output_size
89
+ Dir.glob(File.join(config.build.output, "**", "*"))
90
+ .select { |f| File.file?(f) }
91
+ .sum { |f| File.size(f) }
92
+ end
93
+
94
+ def prepare_output_directory
95
+ output_dir = config.build.output
96
+ FileUtils.rm_rf(output_dir) if clean && Dir.exist?(output_dir)
45
97
  FileUtils.mkdir_p(output_dir)
46
98
  end
47
99
 
48
100
  def generate_static_pages
49
101
  require_relative "build/static_generator"
50
- generator = Build::StaticGenerator.new(config, verbose: verbose)
51
- generator.generate
102
+ Build::StaticGenerator.new(config, verbose: verbose).generate
52
103
  end
53
104
 
54
105
  def bundle_assets
55
106
  require_relative "build/asset_bundler"
56
- bundler = Build::AssetBundler.new(config, verbose: verbose)
57
- bundler.bundle
107
+ [Build::AssetBundler.new(config, verbose: verbose).bundle, nil]
58
108
  end
59
109
 
60
110
  def copy_static_files
61
111
  require_relative "build/file_copier"
62
- copier = Build::FileCopier.new(config, verbose: verbose)
63
- copier.copy
112
+ Build::FileCopier.new(config, verbose: verbose).copy
64
113
  end
65
114
 
66
115
  def generate_seo_files
67
116
  require_relative "build/sitemap_generator"
68
- sitemap_gen = Build::SitemapGenerator.new(config)
69
- sitemap_gen.generate
117
+ sitemap_result = Build::SitemapGenerator.new(config).generate
70
118
 
71
119
  require_relative "build/llms_txt_generator"
72
- llms_gen = Build::LlmsTxtGenerator.new(config)
73
- llms_gen.generate
120
+ Build::LlmsTxtGenerator.new(config).generate
74
121
 
75
122
  File.write(File.join(config.build.output, "robots.txt"), robots_txt_content)
76
- log "[+] Generated robots.txt"
123
+
124
+ result = ["sitemap.xml", "robots.txt", "llms.txt"]
125
+ details = ["sitemap.xml (#{sitemap_result} URLs)", "robots.txt", "llms.txt", "llms-full.txt"]
126
+ [result, details]
77
127
  end
78
128
 
79
129
  def generate_search_index
80
- indexer = Search::BuildIndexer.new(config, verbose: verbose)
81
- indexer.index
130
+ Search::BuildIndexer.new(config, verbose: verbose).index
82
131
  end
83
132
 
84
133
  def robots_txt_content
@@ -92,27 +141,5 @@ module Docyard
92
141
  Sitemap: #{base}sitemap.xml
93
142
  ROBOTS
94
143
  end
95
-
96
- def display_summary(pages, bundles, assets, indexed = 0)
97
- elapsed = Time.now - start_time
98
-
99
- puts "\n#{'=' * 50}"
100
- puts "Build complete in #{format('%.2f', elapsed)}s"
101
- puts "Output: #{config.build.output}/"
102
-
103
- summary = "#{pages} pages, #{bundles} bundles, #{assets} static files"
104
- summary += ", #{indexed} pages indexed" if indexed.positive?
105
- puts summary
106
-
107
- puts "=" * 50
108
- end
109
-
110
- def log(message)
111
- Docyard.logger.info(message)
112
- end
113
-
114
- def error(message)
115
- Docyard.logger.error(message)
116
- end
117
144
  end
118
145
  end
data/lib/docyard/cli.rb CHANGED
@@ -8,6 +8,8 @@ module Docyard
8
8
  true
9
9
  end
10
10
 
11
+ class_option :no_color, type: :boolean, default: false, desc: "Disable colored output"
12
+
11
13
  desc "version", "Show docyard version"
12
14
  def version
13
15
  puts "docyard #{Docyard::VERSION}"
@@ -17,6 +19,7 @@ module Docyard
17
19
  method_option :force, type: :boolean, default: false, aliases: "-f",
18
20
  desc: "Overwrite existing files"
19
21
  def init(project_name = nil)
22
+ apply_global_options
20
23
  initializer = Docyard::Initializer.new(project_name, force: options[:force])
21
24
  exit(1) unless initializer.run
22
25
  end
@@ -24,23 +27,28 @@ module Docyard
24
27
  desc "build", "Build static site for production"
25
28
  method_option :clean, type: :boolean, default: true, desc: "Clean output directory before building"
26
29
  method_option :verbose, type: :boolean, default: false, aliases: "-v", desc: "Show verbose output"
30
+ method_option :strict, type: :boolean, default: false, desc: "Fail on any validation errors"
27
31
  def build
32
+ apply_global_options
28
33
  require_relative "builder"
29
34
  builder = Docyard::Builder.new(
30
35
  clean: options[:clean],
31
- verbose: options[:verbose]
36
+ verbose: options[:verbose],
37
+ strict: options[:strict]
32
38
  )
33
39
  exit(1) unless builder.build
34
40
  rescue ConfigError => e
35
- Docyard.logger.error(e.message)
36
- exit(1)
41
+ print_config_error(e)
37
42
  end
38
43
 
39
44
  desc "preview", "Preview the built site locally"
40
45
  method_option :port, type: :numeric, default: 4000, aliases: "-p", desc: "Port to run preview server on"
41
46
  def preview
47
+ apply_global_options
42
48
  require_relative "server/preview_server"
43
49
  Docyard::PreviewServer.new(port: options[:port]).start
50
+ rescue ConfigError => e
51
+ print_config_error(e)
44
52
  end
45
53
 
46
54
  desc "serve", "Start the development server"
@@ -49,6 +57,7 @@ module Docyard
49
57
  method_option :search, type: :boolean, default: false, aliases: "-s",
50
58
  desc: "Enable search indexing (slower startup)"
51
59
  def serve
60
+ apply_global_options
52
61
  require_relative "server/dev_server"
53
62
  config = Docyard::Config.load
54
63
  server = Docyard::DevServer.new(
@@ -59,7 +68,30 @@ module Docyard
59
68
  )
60
69
  server.start
61
70
  rescue ConfigError => e
62
- Docyard.logger.error(e.message)
71
+ print_config_error(e)
72
+ end
73
+
74
+ desc "doctor", "Check documentation for issues"
75
+ method_option :fix, type: :boolean, default: false, desc: "Auto-fix fixable issues"
76
+ def doctor
77
+ apply_global_options
78
+ require_relative "doctor"
79
+ doctor = Docyard::Doctor.new(fix: options[:fix])
80
+ exit(doctor.run)
81
+ end
82
+
83
+ private
84
+
85
+ def apply_global_options
86
+ UI.enabled = false if options[:no_color]
87
+ end
88
+
89
+ def print_config_error(error)
90
+ puts
91
+ puts " #{UI.bold('Docyard')} v#{VERSION}"
92
+ puts
93
+ puts " #{UI.red(error.message)}"
94
+ puts
63
95
  exit(1)
64
96
  end
65
97
  end
@@ -26,11 +26,7 @@ module Docyard
26
26
  TabsProcessor = Processors::TabsProcessor
27
27
  TooltipProcessor = Processors::TooltipProcessor
28
28
 
29
- CodeDetector = Support::CodeDetector
30
- IconDetector = Support::Tabs::IconDetector
31
-
32
29
  CodeBlockFeatureExtractor = Support::CodeBlock::FeatureExtractor
33
- CodeBlockIconDetector = Support::CodeBlock::IconDetector
34
30
  CodeBlockLineWrapper = Support::CodeBlock::LineWrapper
35
31
  CodeBlockPatterns = Support::CodeBlock::Patterns
36
32
  CodeLineParser = Support::CodeBlock::LineParser
@@ -43,7 +43,7 @@ module Docyard
43
43
  private
44
44
 
45
45
  def process_container_syntax(markdown)
46
- markdown.gsub(/^:::[ \t]*(\w+)(?:[ \t]+([^\n]+?))?[ \t]*\n(.*?)^:::[ \t]*$/m) do
46
+ markdown.gsub(/^:::(\w+)(?:[ \t]+([^\n]+?))?[ \t]*\n(.*?)^:::[ \t]*$/m) do
47
47
  match = Regexp.last_match
48
48
  next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
49
49
 
@@ -12,7 +12,7 @@ module Docyard
12
12
  self.priority = 6
13
13
 
14
14
  CODE_BLOCK_REGEX = /^```(\w*).*?\n(.*?)^```/m
15
- TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
15
+ TABS_BLOCK_REGEX = /^:::tabs[ \t]*\n.*?^:::[ \t]*$/m
16
16
 
17
17
  def preprocess(content)
18
18
  context[:code_block_diff_lines] ||= []
@@ -20,7 +20,7 @@ module Docyard
20
20
  }x
21
21
 
22
22
  CODE_BLOCK_REGEX = /^```(\w*).*?\n(.*?)^```/m
23
- TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
23
+ TABS_BLOCK_REGEX = /^:::tabs[ \t]*\n.*?^:::[ \t]*$/m
24
24
 
25
25
  def preprocess(content)
26
26
  context[:code_block_focus_lines] ||= []
@@ -10,8 +10,8 @@ module Docyard
10
10
  self.priority = 5
11
11
 
12
12
  CODE_FENCE_REGEX = /^```(\w+)(?:\s*\[([^\]]+)\])?(:\S+)?(?:\s*\{([^}\n]+)\})?/
13
- TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
14
- CODE_GROUP_BLOCK_REGEX = /^:::[ \t]*code-group[ \t]*\n.*?^:::[ \t]*$/m
13
+ TABS_BLOCK_REGEX = /^:::tabs[ \t]*\n.*?^:::[ \t]*$/m
14
+ CODE_GROUP_BLOCK_REGEX = /^:::code-group[ \t]*\n.*?^:::[ \t]*$/m
15
15
  EXCLUDED_LANGUAGES = %w[filetree].freeze
16
16
 
17
17
  def preprocess(content)
@@ -22,7 +22,7 @@ module Docyard
22
22
 
23
23
  self.priority = 12
24
24
 
25
- CODE_GROUP_PATTERN = /^:::[ \t]*code-group[ \t]*\n(.*?)^:::[ \t]*$/m
25
+ CODE_GROUP_PATTERN = /^:::code-group[ \t]*\n(.*?)^:::[ \t]*$/m
26
26
  CODE_BLOCK_PATTERN = /```(\w*)\s*\[([^\]]+)\]([^\n]*)\n(.*?)```/m
27
27
 
28
28
  CodeBlockFeatureExtractor = Support::CodeBlock::FeatureExtractor
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../base_processor"
4
+ require_relative "../../rendering/icons"
4
5
 
5
6
  module Docyard
6
7
  module Components
@@ -9,7 +10,6 @@ module Docyard
9
10
  self.priority = 20
10
11
 
11
12
  ICON_PATTERN = /:([a-z][a-z0-9-]*):(?:([a-z]+):)?/i
12
- VALID_WEIGHTS = %w[regular bold fill light thin duotone].freeze
13
13
 
14
14
  def postprocess(html)
15
15
  segments = split_preserving_code_blocks(html)
@@ -49,7 +49,7 @@ module Docyard
49
49
  end
50
50
 
51
51
  def render_icon(name, weight)
52
- weight = "regular" unless VALID_WEIGHTS.include?(weight)
52
+ weight = "regular" unless Icons::VALID_WEIGHTS.include?(weight)
53
53
  weight_class = weight == "regular" ? "ph" : "ph-#{weight}"
54
54
  %(<i class="#{weight_class} ph-#{name}" aria-hidden="true"></i>)
55
55
  end
@@ -21,7 +21,7 @@ module Docyard
21
21
 
22
22
  @code_block_ranges = find_code_block_ranges(content)
23
23
 
24
- content.gsub(/^:::[ \t]*tabs[ \t]*\n(.*?)^:::[ \t]*$/m) do
24
+ content.gsub(/^:::tabs[ \t]*\n(.*?)^:::[ \t]*$/m) do
25
25
  match = Regexp.last_match
26
26
  next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
27
27
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sections"
4
+
5
+ module Docyard
6
+ class Config
7
+ module Schema
8
+ DEFINITION = {
9
+ title: { type: :string },
10
+ description: { type: :string },
11
+ url: { type: :url },
12
+ og_image: { type: :string },
13
+ twitter: { type: :string },
14
+ source: { type: :string },
15
+ sidebar: { type: :enum, values: SIDEBAR_MODES },
16
+ branding: BRANDING_SCHEMA,
17
+ socials: SOCIALS_SCHEMA,
18
+ tabs: TABS_SCHEMA,
19
+ build: BUILD_SCHEMA,
20
+ search: SEARCH_SCHEMA,
21
+ navigation: NAVIGATION_SCHEMA,
22
+ announcement: ANNOUNCEMENT_SCHEMA,
23
+ repo: REPO_SCHEMA,
24
+ analytics: ANALYTICS_SCHEMA,
25
+ feedback: FEEDBACK_SCHEMA
26
+ }.freeze
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "simple_sections"
4
+
5
+ module Docyard
6
+ class Config
7
+ module Schema
8
+ SOCIALS_SCHEMA = {
9
+ type: :hash,
10
+ allow_extra_keys: true,
11
+ keys: {
12
+ github: { type: :url },
13
+ twitter: { type: :url },
14
+ discord: { type: :url },
15
+ slack: { type: :url },
16
+ linkedin: { type: :url },
17
+ youtube: { type: :url },
18
+ bluesky: { type: :url },
19
+ custom: {
20
+ type: :array,
21
+ items: {
22
+ type: :hash,
23
+ keys: { icon: { type: :string }, href: { type: :url } }
24
+ }
25
+ }
26
+ }
27
+ }.freeze
28
+
29
+ TABS_SCHEMA = {
30
+ type: :array,
31
+ items: {
32
+ type: :hash,
33
+ keys: {
34
+ text: { type: :string },
35
+ href: { type: :string },
36
+ icon: { type: :string },
37
+ external: { type: :boolean }
38
+ }
39
+ }
40
+ }.freeze
41
+
42
+ NAVIGATION_SCHEMA = {
43
+ type: :hash,
44
+ keys: {
45
+ breadcrumbs: { type: :boolean },
46
+ cta: {
47
+ type: :array,
48
+ max_items: 2,
49
+ items: {
50
+ type: :hash,
51
+ keys: {
52
+ text: { type: :string },
53
+ href: { type: :string },
54
+ variant: { type: :enum, values: CTA_VARIANTS },
55
+ external: { type: :boolean }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }.freeze
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class Config
5
+ module Schema
6
+ BRANDING_SCHEMA = {
7
+ type: :hash,
8
+ keys: {
9
+ logo: { type: :file_or_url },
10
+ favicon: { type: :file_or_url },
11
+ credits: { type: :boolean },
12
+ copyright: { type: :string },
13
+ color: { type: :color }
14
+ }
15
+ }.freeze
16
+
17
+ BUILD_SCHEMA = {
18
+ type: :hash,
19
+ keys: {
20
+ output: { type: :string, format: :no_slashes },
21
+ base: { type: :string, format: :starts_with_slash },
22
+ strict: { type: :boolean }
23
+ }
24
+ }.freeze
25
+
26
+ SEARCH_SCHEMA = {
27
+ type: :hash,
28
+ keys: {
29
+ enabled: { type: :boolean },
30
+ placeholder: { type: :string },
31
+ exclude: { type: :array, items: { type: :string } }
32
+ }
33
+ }.freeze
34
+
35
+ ANNOUNCEMENT_SCHEMA = {
36
+ type: :hash,
37
+ keys: {
38
+ text: { type: :string },
39
+ link: { type: :string },
40
+ dismissible: { type: :boolean },
41
+ button: {
42
+ type: :hash,
43
+ keys: { text: { type: :string }, link: { type: :string } }
44
+ }
45
+ }
46
+ }.freeze
47
+
48
+ REPO_SCHEMA = {
49
+ type: :hash,
50
+ keys: {
51
+ url: { type: :url },
52
+ branch: { type: :string },
53
+ edit_path: { type: :string },
54
+ edit_link: { type: :boolean },
55
+ last_updated: { type: :boolean }
56
+ }
57
+ }.freeze
58
+
59
+ ANALYTICS_SCHEMA = {
60
+ type: :hash,
61
+ keys: {
62
+ google: { type: :string },
63
+ plausible: { type: :string },
64
+ fathom: { type: :string },
65
+ script: { type: :string }
66
+ }
67
+ }.freeze
68
+
69
+ FEEDBACK_SCHEMA = {
70
+ type: :hash,
71
+ keys: {
72
+ enabled: { type: :boolean },
73
+ question: { type: :string }
74
+ }
75
+ }.freeze
76
+ end
77
+ end
78
+ end
@@ -3,37 +3,34 @@
3
3
  module Docyard
4
4
  class Config
5
5
  module Schema
6
- TOP_LEVEL = %w[
7
- title description url og_image twitter source
8
- branding socials tabs sidebar
9
- build search navigation announcement
10
- repo analytics feedback
11
- ].freeze
12
-
13
- SECTIONS = {
14
- "branding" => %w[logo favicon credits copyright color],
15
- "build" => %w[output base],
16
- "search" => %w[enabled placeholder exclude],
17
- "navigation" => %w[cta breadcrumbs],
18
- "repo" => %w[url branch edit_path edit_link last_updated],
19
- "analytics" => %w[google plausible fathom script],
20
- "announcement" => %w[text link button dismissible],
21
- "feedback" => %w[enabled question]
22
- }.freeze
23
-
24
- TAB = %w[text href icon external].freeze
25
-
26
- CTA = %w[text href variant external].freeze
27
-
28
- ANNOUNCEMENT_BUTTON = %w[text link].freeze
29
-
30
- SIDEBAR_ITEM = %w[text icon badge badge_type items collapsed index group collapsible].freeze
31
-
32
- SIDEBAR_EXTERNAL_LINK = %w[link text icon target].freeze
33
-
34
- SOCIALS_BUILTIN = %w[github twitter discord slack linkedin youtube bluesky custom].freeze
35
-
36
- CUSTOM_SOCIAL = %w[icon href].freeze
6
+ SIDEBAR_MODES = %w[config auto distributed].freeze
7
+ CTA_VARIANTS = %w[primary secondary].freeze
8
+ SIDEBAR_ITEM_KEYS = %w[text icon badge badge_type items collapsed index group collapsible].freeze
9
+ SIDEBAR_EXTERNAL_LINK_KEYS = %w[link text icon target].freeze
10
+
11
+ class << self
12
+ def validate_keys(hash, valid_keys, context:)
13
+ return [] unless hash.is_a?(Hash)
14
+
15
+ unknown = hash.keys.map(&:to_s) - valid_keys
16
+ unknown.map { |key| build_key_error(key, valid_keys, context) }
17
+ end
18
+
19
+ def build_key_error(key, valid_keys, context)
20
+ suggestion = find_key_suggestion(key, valid_keys)
21
+ msg = "unknown key '#{key}'"
22
+ msg += ". Did you mean '#{suggestion}'?" if suggestion
23
+ fix = suggestion ? { type: :rename, from: key, to: suggestion } : nil
24
+ { context: context, message: msg, key: key, fix: fix }
25
+ end
26
+
27
+ def find_key_suggestion(key, valid_keys)
28
+ checker = DidYouMean::SpellChecker.new(dictionary: valid_keys)
29
+ checker.correct(key).first
30
+ end
31
+ end
37
32
  end
38
33
  end
39
34
  end
35
+
36
+ require_relative "schema/definition"