docyard 1.0.2 → 1.2.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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -1
  3. data/README.md +56 -11
  4. data/lib/docyard/build/asset_bundler.rb +19 -29
  5. data/lib/docyard/build/file_copier.rb +7 -15
  6. data/lib/docyard/build/llms_txt_generator.rb +0 -2
  7. data/lib/docyard/build/sitemap_generator.rb +1 -1
  8. data/lib/docyard/build/social_cards/card_renderer.rb +132 -0
  9. data/lib/docyard/build/social_cards/doc_card.rb +97 -0
  10. data/lib/docyard/build/social_cards/homepage_card.rb +98 -0
  11. data/lib/docyard/build/social_cards_generator.rb +188 -0
  12. data/lib/docyard/build/static_generator.rb +30 -32
  13. data/lib/docyard/build/step_runner.rb +90 -0
  14. data/lib/docyard/build/validator.rb +98 -0
  15. data/lib/docyard/builder.rb +92 -55
  16. data/lib/docyard/cli.rb +63 -4
  17. data/lib/docyard/components/aliases.rb +0 -4
  18. data/lib/docyard/components/processors/callout_processor.rb +1 -1
  19. data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +1 -1
  20. data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +1 -1
  21. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +2 -2
  22. data/lib/docyard/components/processors/code_group_processor.rb +1 -1
  23. data/lib/docyard/components/processors/icon_processor.rb +2 -2
  24. data/lib/docyard/components/processors/tabs_processor.rb +1 -1
  25. data/lib/docyard/config/branding_resolver.rb +1 -1
  26. data/lib/docyard/config/schema/definition.rb +30 -0
  27. data/lib/docyard/config/schema/sections.rb +62 -0
  28. data/lib/docyard/config/schema/simple_sections.rb +85 -0
  29. data/lib/docyard/config/schema.rb +28 -31
  30. data/lib/docyard/config/type_validators.rb +121 -0
  31. data/lib/docyard/config/validator.rb +136 -61
  32. data/lib/docyard/config.rb +4 -14
  33. data/lib/docyard/customizer.rb +196 -0
  34. data/lib/docyard/diagnostic.rb +89 -0
  35. data/lib/docyard/diagnostic_context.rb +48 -0
  36. data/lib/docyard/doctor/code_block_checker.rb +136 -0
  37. data/lib/docyard/doctor/component_checker.rb +49 -0
  38. data/lib/docyard/doctor/component_checkers/abbreviation_checker.rb +74 -0
  39. data/lib/docyard/doctor/component_checkers/badge_checker.rb +71 -0
  40. data/lib/docyard/doctor/component_checkers/base.rb +111 -0
  41. data/lib/docyard/doctor/component_checkers/callout_checker.rb +34 -0
  42. data/lib/docyard/doctor/component_checkers/cards_checker.rb +57 -0
  43. data/lib/docyard/doctor/component_checkers/code_group_checker.rb +47 -0
  44. data/lib/docyard/doctor/component_checkers/details_checker.rb +51 -0
  45. data/lib/docyard/doctor/component_checkers/icon_checker.rb +36 -0
  46. data/lib/docyard/doctor/component_checkers/image_attrs_checker.rb +46 -0
  47. data/lib/docyard/doctor/component_checkers/space_after_colons_checker.rb +45 -0
  48. data/lib/docyard/doctor/component_checkers/steps_checker.rb +35 -0
  49. data/lib/docyard/doctor/component_checkers/tabs_checker.rb +35 -0
  50. data/lib/docyard/doctor/component_checkers/tooltip_checker.rb +67 -0
  51. data/lib/docyard/doctor/component_checkers/unknown_type_checker.rb +34 -0
  52. data/lib/docyard/doctor/config_checker.rb +19 -0
  53. data/lib/docyard/doctor/config_fixer.rb +87 -0
  54. data/lib/docyard/doctor/content_checker.rb +164 -0
  55. data/lib/docyard/doctor/file_scanner.rb +113 -0
  56. data/lib/docyard/doctor/image_checker.rb +103 -0
  57. data/lib/docyard/doctor/link_checker.rb +91 -0
  58. data/lib/docyard/doctor/markdown_fixer.rb +62 -0
  59. data/lib/docyard/doctor/orphan_checker.rb +82 -0
  60. data/lib/docyard/doctor/reporter.rb +152 -0
  61. data/lib/docyard/doctor/sidebar_checker.rb +127 -0
  62. data/lib/docyard/doctor/sidebar_fixer.rb +47 -0
  63. data/lib/docyard/doctor.rb +178 -0
  64. data/lib/docyard/editor_launcher.rb +119 -0
  65. data/lib/docyard/errors.rb +0 -49
  66. data/lib/docyard/initializer.rb +32 -39
  67. data/lib/docyard/navigation/sidebar/local_config_loader.rb +44 -21
  68. data/lib/docyard/rendering/icon_helpers.rb +1 -3
  69. data/lib/docyard/rendering/markdown.rb +4 -0
  70. data/lib/docyard/rendering/og_helpers.rb +19 -1
  71. data/lib/docyard/rendering/renderer.rb +10 -1
  72. data/lib/docyard/search/build_indexer.rb +45 -25
  73. data/lib/docyard/search/dev_indexer.rb +12 -25
  74. data/lib/docyard/search/pagefind_binary.rb +185 -0
  75. data/lib/docyard/search/pagefind_support.rb +9 -7
  76. data/lib/docyard/server/asset_handler.rb +28 -2
  77. data/lib/docyard/server/dev_server.rb +56 -13
  78. data/lib/docyard/server/error_overlay.rb +73 -0
  79. data/lib/docyard/server/file_watcher.rb +10 -6
  80. data/lib/docyard/server/page_diagnostics.rb +27 -0
  81. data/lib/docyard/server/preview_server.rb +17 -13
  82. data/lib/docyard/server/rack_application.rb +65 -4
  83. data/lib/docyard/server/resolution_result.rb +0 -4
  84. data/lib/docyard/templates/assets/css/error-overlay.css +669 -0
  85. data/lib/docyard/templates/assets/css/variables.css +1 -7
  86. data/lib/docyard/templates/assets/fonts/Inter-Variable.woff2 +0 -0
  87. data/lib/docyard/templates/assets/js/components/abbreviation.js +20 -11
  88. data/lib/docyard/templates/assets/js/components/code-block.js +8 -3
  89. data/lib/docyard/templates/assets/js/components/code-group.js +20 -3
  90. data/lib/docyard/templates/assets/js/components/file-tree.js +9 -3
  91. data/lib/docyard/templates/assets/js/components/heading-anchor.js +71 -72
  92. data/lib/docyard/templates/assets/js/components/lightbox.js +10 -3
  93. data/lib/docyard/templates/assets/js/components/relative-time.js +42 -0
  94. data/lib/docyard/templates/assets/js/components/tabs.js +8 -3
  95. data/lib/docyard/templates/assets/js/components/tooltip.js +32 -23
  96. data/lib/docyard/templates/assets/js/error-overlay.js +547 -0
  97. data/lib/docyard/templates/assets/js/hot-reload.js +77 -9
  98. data/lib/docyard/templates/errors/404.html.erb +1 -1
  99. data/lib/docyard/templates/errors/500.html.erb +1 -1
  100. data/lib/docyard/templates/partials/_head.html.erb +5 -1
  101. data/lib/docyard/templates/partials/_page_actions.html.erb +1 -1
  102. data/lib/docyard/templates/partials/_scripts.html.erb +3 -0
  103. data/lib/docyard/ui.rb +80 -0
  104. data/lib/docyard/utils/logging.rb +5 -1
  105. data/lib/docyard/utils/text_formatter.rb +0 -6
  106. data/lib/docyard/version.rb +1 -1
  107. data/lib/docyard.rb +6 -0
  108. metadata +53 -25
  109. data/lib/docyard/config/key_validator.rb +0 -30
  110. data/lib/docyard/config/validation_helpers.rb +0 -83
  111. data/lib/docyard/config/validators/navigation.rb +0 -43
  112. data/lib/docyard/config/validators/section.rb +0 -114
  113. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
@@ -1,84 +1,143 @@
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("Social cards") { generate_social_cards } if social_cards_enabled?
56
+ @step_runner.run("Generating SEO") { generate_seo_files }
57
+ @step_runner.run("Indexing search") { generate_search_index }
58
+ end
59
+
60
+ def print_error(error)
61
+ puts UI.error("failed")
62
+ puts
63
+ puts " #{UI.error('Error:')} #{error.message}"
64
+ puts " #{error.backtrace.first}" if verbose
65
+ puts
66
+ end
67
+
68
+ def print_summary
69
+ puts
70
+ puts " #{UI.success('Build complete')} in #{format('%.2fs', build_duration)}"
71
+ puts " #{UI.dim(output_summary)}"
72
+ puts
73
+ @step_runner.print_timing_breakdown if verbose
74
+ end
44
75
 
76
+ def build_duration
77
+ Time.now - start_time
78
+ end
79
+
80
+ def output_summary
81
+ "Output: #{config.build.output}/ (#{format_size(calculate_output_size)})"
82
+ end
83
+
84
+ def format_size(bytes)
85
+ kb = bytes / 1024.0
86
+ kb >= 1000 ? format("%.1f MB", kb / 1024.0) : format("%.1f KB", kb)
87
+ end
88
+
89
+ def calculate_output_size
90
+ Dir.glob(File.join(config.build.output, "**", "*"))
91
+ .select { |f| File.file?(f) }
92
+ .sum { |f| File.size(f) }
93
+ end
94
+
95
+ def prepare_output_directory
96
+ output_dir = config.build.output
97
+ FileUtils.rm_rf(output_dir) if clean && Dir.exist?(output_dir)
45
98
  FileUtils.mkdir_p(output_dir)
46
99
  end
47
100
 
48
101
  def generate_static_pages
49
102
  require_relative "build/static_generator"
50
- generator = Build::StaticGenerator.new(config, verbose: verbose)
51
- generator.generate
103
+ Build::StaticGenerator.new(config, verbose: verbose).generate
52
104
  end
53
105
 
54
106
  def bundle_assets
55
107
  require_relative "build/asset_bundler"
56
- bundler = Build::AssetBundler.new(config, verbose: verbose)
57
- bundler.bundle
108
+ [Build::AssetBundler.new(config, verbose: verbose).bundle, nil]
58
109
  end
59
110
 
60
111
  def copy_static_files
61
112
  require_relative "build/file_copier"
62
- copier = Build::FileCopier.new(config, verbose: verbose)
63
- copier.copy
113
+ Build::FileCopier.new(config, verbose: verbose).copy
64
114
  end
65
115
 
66
116
  def generate_seo_files
67
117
  require_relative "build/sitemap_generator"
68
- sitemap_gen = Build::SitemapGenerator.new(config)
69
- sitemap_gen.generate
118
+ sitemap_result = Build::SitemapGenerator.new(config).generate
70
119
 
71
120
  require_relative "build/llms_txt_generator"
72
- llms_gen = Build::LlmsTxtGenerator.new(config)
73
- llms_gen.generate
121
+ Build::LlmsTxtGenerator.new(config).generate
74
122
 
75
123
  File.write(File.join(config.build.output, "robots.txt"), robots_txt_content)
76
- log "[+] Generated robots.txt"
124
+
125
+ result = ["sitemap.xml", "robots.txt", "llms.txt"]
126
+ details = ["sitemap.xml (#{sitemap_result} URLs)", "robots.txt", "llms.txt", "llms-full.txt"]
127
+ [result, details]
77
128
  end
78
129
 
79
130
  def generate_search_index
80
- indexer = Search::BuildIndexer.new(config, verbose: verbose)
81
- indexer.index
131
+ Search::BuildIndexer.new(config, verbose: verbose).index
132
+ end
133
+
134
+ def generate_social_cards
135
+ require_relative "build/social_cards_generator"
136
+ Build::SocialCardsGenerator.new(config, verbose: verbose).generate
137
+ end
138
+
139
+ def social_cards_enabled?
140
+ config.social_cards&.enabled == true
82
141
  end
83
142
 
84
143
  def robots_txt_content
@@ -92,27 +151,5 @@ module Docyard
92
151
  Sitemap: #{base}sitemap.xml
93
152
  ROBOTS
94
153
  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
154
  end
118
155
  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,57 @@ 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
+ desc "customize", "Generate theme customization files"
84
+ method_option :minimal, type: :boolean, default: false, aliases: "-m",
85
+ desc: "Generate minimal files without comments"
86
+ def customize
87
+ apply_global_options
88
+ require_relative "customizer"
89
+ Docyard::Customizer.new(minimal: options[:minimal]).generate
90
+ rescue ConfigError => e
91
+ print_config_error(e)
92
+ rescue Errno::EACCES => e
93
+ print_file_error("Permission denied", e.message)
94
+ rescue Errno::ENOSPC
95
+ print_file_error("Disk full", "No space left on device")
96
+ rescue SystemCallError => e
97
+ print_file_error("File operation failed", e.message)
98
+ end
99
+
100
+ private
101
+
102
+ def apply_global_options
103
+ UI.enabled = false if options[:no_color]
104
+ end
105
+
106
+ def print_config_error(error)
107
+ puts
108
+ puts " #{UI.bold('Docyard')} v#{VERSION}"
109
+ puts
110
+ puts " #{UI.red(error.message)}"
111
+ puts
112
+ exit(1)
113
+ end
114
+
115
+ def print_file_error(title, message)
116
+ puts
117
+ puts " #{UI.bold('Docyard')} v#{VERSION}"
118
+ puts
119
+ puts " #{UI.red("#{title}:")}"
120
+ puts " #{message}"
121
+ puts
63
122
  exit(1)
64
123
  end
65
124
  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
 
@@ -38,7 +38,7 @@ module Docyard
38
38
  site_options, logo_options, search_options, credits_options, social_options,
39
39
  navigation_options, tabs_options, announcement_options, repo_options, analytics_options,
40
40
  color_options
41
- ].reduce({}, :merge)
41
+ ].reduce({}, :merge).merge(social_cards_enabled: config.social_cards&.enabled == true)
42
42
  end
43
43
 
44
44
  def site_options
@@ -0,0 +1,30 @@
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
+ social_cards: SOCIAL_CARDS_SCHEMA
27
+ }.freeze
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,62 @@
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
+ custom: {
19
+ type: :array,
20
+ items: {
21
+ type: :hash,
22
+ keys: { icon: { type: :string }, href: { type: :url } }
23
+ }
24
+ }
25
+ }
26
+ }.freeze
27
+
28
+ TABS_SCHEMA = {
29
+ type: :array,
30
+ items: {
31
+ type: :hash,
32
+ keys: {
33
+ text: { type: :string },
34
+ href: { type: :string },
35
+ icon: { type: :string },
36
+ external: { type: :boolean }
37
+ }
38
+ }
39
+ }.freeze
40
+
41
+ NAVIGATION_SCHEMA = {
42
+ type: :hash,
43
+ keys: {
44
+ breadcrumbs: { type: :boolean },
45
+ cta: {
46
+ type: :array,
47
+ max_items: 2,
48
+ items: {
49
+ type: :hash,
50
+ keys: {
51
+ text: { type: :string },
52
+ href: { type: :string },
53
+ variant: { type: :enum, values: CTA_VARIANTS },
54
+ external: { type: :boolean }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }.freeze
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,85 @@
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
+
77
+ SOCIAL_CARDS_SCHEMA = {
78
+ type: :hash,
79
+ keys: {
80
+ enabled: { type: :boolean }
81
+ }
82
+ }.freeze
83
+ end
84
+ end
85
+ 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"