docyard 0.9.0 → 1.0.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +57 -1
- data/README.md +8 -253
- data/exe/docyard +6 -0
- data/lib/docyard/build/asset_bundler.rb +24 -2
- data/lib/docyard/build/error_page_generator.rb +33 -0
- data/lib/docyard/build/file_copier.rb +12 -5
- data/lib/docyard/build/file_writer.rb +19 -0
- data/lib/docyard/build/llms_txt_generator.rb +103 -0
- data/lib/docyard/build/root_fallback_generator.rb +66 -0
- data/lib/docyard/build/sitemap_generator.rb +1 -1
- data/lib/docyard/build/static_generator.rb +119 -81
- data/lib/docyard/builder.rb +6 -2
- data/lib/docyard/cli.rb +14 -4
- data/lib/docyard/components/processors/callout_processor.rb +1 -1
- data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
- data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
- data/lib/docyard/components/processors/code_block_options_preprocessor.rb +11 -1
- data/lib/docyard/components/processors/code_block_processor.rb +5 -24
- data/lib/docyard/components/processors/code_group_processor.rb +6 -22
- data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +1 -0
- data/lib/docyard/components/processors/file_tree_processor.rb +1 -2
- data/lib/docyard/components/processors/icon_processor.rb +8 -2
- data/lib/docyard/components/processors/include_processor.rb +10 -10
- data/lib/docyard/components/processors/video_embed_processor.rb +14 -3
- data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
- data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
- data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
- data/lib/docyard/components/support/code_detector.rb +2 -12
- data/lib/docyard/components/support/code_group/html_builder.rb +2 -6
- data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
- data/lib/docyard/components/support/tabs/parser.rb +6 -23
- data/lib/docyard/config/analytics_resolver.rb +24 -0
- data/lib/docyard/config/branding_resolver.rb +58 -27
- data/lib/docyard/config/key_validator.rb +30 -0
- data/lib/docyard/config/logo_detector.rb +8 -8
- data/lib/docyard/config/schema.rb +39 -0
- data/lib/docyard/config/section.rb +21 -0
- data/lib/docyard/config/validation_helpers.rb +83 -0
- data/lib/docyard/config/validator.rb +45 -144
- data/lib/docyard/config/validators/navigation.rb +43 -0
- data/lib/docyard/config/validators/section.rb +114 -0
- data/lib/docyard/config.rb +46 -102
- data/lib/docyard/constants.rb +59 -0
- data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
- data/lib/docyard/initializer.rb +100 -49
- data/lib/docyard/navigation/breadcrumb_builder.rb +45 -6
- data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
- data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
- data/lib/docyard/navigation/sidebar/cache.rb +96 -0
- data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
- data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
- data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
- data/lib/docyard/navigation/sidebar/renderer.rb +12 -1
- data/lib/docyard/navigation/sidebar_builder.rb +43 -81
- data/lib/docyard/rendering/branding_variables.rb +65 -0
- data/lib/docyard/rendering/icon_helpers.rb +14 -1
- data/lib/docyard/rendering/icons/devicons.rb +63 -0
- data/lib/docyard/rendering/icons.rb +26 -27
- data/lib/docyard/rendering/markdown.rb +5 -23
- data/lib/docyard/rendering/og_helpers.rb +36 -0
- data/lib/docyard/rendering/renderer.rb +96 -61
- data/lib/docyard/rendering/template_resolver.rb +14 -0
- data/lib/docyard/routing/fallback_resolver.rb +3 -3
- data/lib/docyard/search/build_indexer.rb +2 -2
- data/lib/docyard/search/dev_indexer.rb +36 -28
- data/lib/docyard/search/pagefind_support.rb +1 -1
- data/lib/docyard/server/asset_handler.rb +39 -15
- data/lib/docyard/server/dev_server.rb +90 -55
- data/lib/docyard/server/file_watcher.rb +68 -18
- data/lib/docyard/server/pagefind_handler.rb +1 -1
- data/lib/docyard/server/preview_server.rb +29 -33
- data/lib/docyard/server/rack_application.rb +39 -71
- data/lib/docyard/server/router.rb +11 -7
- data/lib/docyard/server/sse_server.rb +157 -0
- data/lib/docyard/server/static_file_app.rb +42 -0
- data/lib/docyard/templates/assets/css/components/banner.css +31 -0
- data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
- data/lib/docyard/templates/assets/css/components/callout.css +26 -6
- data/lib/docyard/templates/assets/css/components/code-block.css +4 -2
- data/lib/docyard/templates/assets/css/components/code-group.css +20 -7
- data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
- data/lib/docyard/templates/assets/css/components/file-tree.css +5 -4
- data/lib/docyard/templates/assets/css/components/heading-anchor.css +2 -2
- data/lib/docyard/templates/assets/css/components/icon.css +5 -0
- data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
- data/lib/docyard/templates/assets/css/components/navigation.css +25 -3
- data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
- data/lib/docyard/templates/assets/css/components/prev-next.css +14 -7
- data/lib/docyard/templates/assets/css/components/search.css +6 -10
- data/lib/docyard/templates/assets/css/components/tab-bar.css +9 -6
- data/lib/docyard/templates/assets/css/components/table-of-contents.css +63 -17
- data/lib/docyard/templates/assets/css/components/tabs.css +12 -4
- data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
- data/lib/docyard/templates/assets/css/landing.css +82 -13
- data/lib/docyard/templates/assets/css/layout.css +32 -16
- data/lib/docyard/templates/assets/css/markdown.css +22 -2
- data/lib/docyard/templates/assets/css/variables.css +14 -1
- data/lib/docyard/templates/assets/js/components/code-group.js +4 -1
- data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
- data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
- data/lib/docyard/templates/assets/js/components/file-tree.js +5 -5
- data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
- data/lib/docyard/templates/assets/js/components/search.js +3 -3
- data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
- data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
- data/lib/docyard/templates/assets/js/components/tooltip.js +4 -4
- data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
- data/lib/docyard/templates/errors/404.html.erb +125 -5
- data/lib/docyard/templates/errors/500.html.erb +184 -10
- data/lib/docyard/templates/errors/redirect.html.erb +12 -0
- data/lib/docyard/templates/init/_sidebar.yml +36 -0
- data/lib/docyard/templates/init/docyard.yml +36 -0
- data/lib/docyard/templates/init/pages/components.md +146 -0
- data/lib/docyard/templates/init/pages/getting-started.md +94 -0
- data/lib/docyard/templates/init/pages/index.md +22 -0
- data/lib/docyard/templates/layouts/default.html.erb +10 -0
- data/lib/docyard/templates/layouts/splash.html.erb +14 -1
- data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
- data/lib/docyard/templates/partials/_banner.html.erb +1 -1
- data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
- data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
- data/lib/docyard/templates/partials/_footer.html.erb +1 -1
- data/lib/docyard/templates/partials/_head.html.erb +80 -5
- data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
- data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
- data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
- data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
- data/lib/docyard/utils/git_info.rb +157 -0
- data/lib/docyard/utils/hash_utils.rb +31 -0
- data/lib/docyard/utils/html_helpers.rb +8 -0
- data/lib/docyard/utils/logging.rb +44 -3
- data/lib/docyard/utils/path_resolver.rb +0 -10
- data/lib/docyard/utils/path_utils.rb +73 -0
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +2 -2
- metadata +81 -47
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- data/.github/pull_request_template.md +0 -14
- data/.github/workflows/ci.yml +0 -49
- data/.rubocop.yml +0 -42
- data/CODE_OF_CONDUCT.md +0 -132
- data/CONTRIBUTING.md +0 -55
- data/LICENSE.vscode-icons +0 -42
- data/Rakefile +0 -8
- data/lib/docyard/config/constants.rb +0 -31
- data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
- data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
- data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -90
- data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
- data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -71
- data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -51
- data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
- data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
- data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
- data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -140
- data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
- data/lib/docyard/rendering/icons/file_types.rb +0 -79
- data/lib/docyard/rendering/icons/phosphor.rb +0 -93
- data/lib/docyard/rendering/language_mapping.rb +0 -52
- data/lib/docyard/templates/assets/js/reload.js +0 -98
- data/lib/docyard/templates/partials/_icon.html.erb +0 -1
- data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
- data/sig/docyard.rbs +0 -4
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
class Config
|
|
5
|
+
class Section
|
|
6
|
+
def initialize(data)
|
|
7
|
+
@data = data || {}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def method_missing(method, *args)
|
|
11
|
+
return super unless args.empty?
|
|
12
|
+
|
|
13
|
+
@data[method.to_s]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def respond_to_missing?(method, include_private = false)
|
|
17
|
+
@data.key?(method.to_s) || super
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
class Config
|
|
5
|
+
module ValidationHelpers
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def validate_string(value, field_name)
|
|
9
|
+
return if value.nil? || value.is_a?(String)
|
|
10
|
+
|
|
11
|
+
add_error(field: field_name, error: "must be a string", got: value.class.name, fix: "Change to a string value")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def validate_boolean(value, field_name)
|
|
15
|
+
return if [true, false].include?(value)
|
|
16
|
+
|
|
17
|
+
add_error(field: field_name, error: "must be true or false", got: value.inspect, fix: "Change to true or false")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def validate_url(value, field_name)
|
|
21
|
+
return if value.nil? || value.is_a?(String)
|
|
22
|
+
|
|
23
|
+
add_error(field: field_name, error: "must be a URL string", got: value.class.name,
|
|
24
|
+
fix: "Change to a URL string")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate_array(value, field_name)
|
|
28
|
+
return if value.nil? || value.is_a?(Array)
|
|
29
|
+
|
|
30
|
+
add_array_error(field_name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate_file_path_or_url(value, field_name)
|
|
34
|
+
return if value.nil?
|
|
35
|
+
return add_type_error(field_name, "file path or URL (string)", value.class.name) unless value.is_a?(String)
|
|
36
|
+
return if url?(value)
|
|
37
|
+
|
|
38
|
+
public_dir = File.join(@config["source"] || "docs", "public")
|
|
39
|
+
file_path = File.absolute_path?(value) ? value : File.join(public_dir, value)
|
|
40
|
+
return if File.exist?(file_path)
|
|
41
|
+
|
|
42
|
+
add_error(field: field_name, error: "file not found", got: value,
|
|
43
|
+
fix: "Place the file in #{public_dir}/ directory (e.g., 'logo.svg' for #{public_dir}/logo.svg)")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_no_slashes(value, field_name)
|
|
47
|
+
return if value.nil? || !value.is_a?(String)
|
|
48
|
+
return unless value.include?("/") || value.include?("\\")
|
|
49
|
+
|
|
50
|
+
add_error(field: field_name, error: "cannot contain slashes", got: value,
|
|
51
|
+
fix: "Use a simple directory name like 'dist' or '_site'")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def validate_starts_with_slash(value, field_name)
|
|
55
|
+
return if value.nil? || value.start_with?("/")
|
|
56
|
+
|
|
57
|
+
add_error(field: field_name, error: "must start with /", got: value, fix: "Change to '/#{value}'")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def url?(value)
|
|
61
|
+
value.match?(%r{\Ahttps?://})
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def add_error(error_data)
|
|
65
|
+
@errors << error_data
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def add_type_error(field, expected, got)
|
|
69
|
+
add_error(field: field, error: "must be a #{expected}", got: got, fix: "Change to a #{expected}")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def add_hash_error(field)
|
|
73
|
+
add_error(field: field, error: "must be a hash", got: @config[field].class.name,
|
|
74
|
+
fix: "Change to a hash with platform names as keys and URLs as values")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def add_array_error(field)
|
|
78
|
+
value = field.split(".").reduce(@config) { |h, k| h&.[](k) }
|
|
79
|
+
add_error(field: field, error: "must be an array", got: value.class.name, fix: "Change to an array")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -1,194 +1,95 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "validation_helpers"
|
|
4
|
+
require_relative "schema"
|
|
5
|
+
require_relative "key_validator"
|
|
6
|
+
require_relative "validators/section"
|
|
7
|
+
require_relative "validators/navigation"
|
|
8
|
+
|
|
3
9
|
module Docyard
|
|
4
10
|
class Config
|
|
5
11
|
class Validator
|
|
12
|
+
include ValidationHelpers
|
|
13
|
+
include Validators::Section
|
|
14
|
+
include Validators::Navigation
|
|
15
|
+
|
|
6
16
|
def initialize(config_data)
|
|
7
17
|
@config = config_data
|
|
8
18
|
@errors = []
|
|
19
|
+
@key_errors = []
|
|
9
20
|
end
|
|
10
21
|
|
|
11
22
|
def validate!
|
|
23
|
+
validate_unknown_keys
|
|
12
24
|
validate_top_level
|
|
13
25
|
validate_branding_section
|
|
14
26
|
validate_socials_section
|
|
15
27
|
validate_tabs_section
|
|
28
|
+
validate_sidebar_setting
|
|
16
29
|
validate_build_section
|
|
17
30
|
validate_search_section
|
|
18
31
|
validate_navigation_section
|
|
32
|
+
validate_announcement_section
|
|
33
|
+
validate_feedback_section
|
|
19
34
|
|
|
35
|
+
raise_key_errors if @key_errors.any?
|
|
20
36
|
raise ConfigError, format_errors if @errors.any?
|
|
21
37
|
end
|
|
22
38
|
|
|
23
39
|
private
|
|
24
40
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
41
|
+
def validate_unknown_keys
|
|
42
|
+
validate_top_level_keys
|
|
43
|
+
validate_section_keys
|
|
44
|
+
validate_array_item_keys
|
|
28
45
|
end
|
|
29
46
|
|
|
30
|
-
def
|
|
31
|
-
|
|
32
|
-
return unless branding
|
|
33
|
-
|
|
34
|
-
validate_file_path_or_url(branding["logo"], "branding.logo")
|
|
35
|
-
validate_file_path_or_url(branding["favicon"], "branding.favicon")
|
|
36
|
-
validate_boolean(branding["credits"], "branding.credits") if branding.key?("credits")
|
|
47
|
+
def validate_top_level_keys
|
|
48
|
+
@key_errors.concat(KeyValidator.validate(@config, Schema::TOP_LEVEL, context: "docyard.yml"))
|
|
37
49
|
end
|
|
38
50
|
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return add_hash_error("socials") unless socials.is_a?(Hash)
|
|
51
|
+
def validate_section_keys
|
|
52
|
+
Schema::SECTIONS.each do |section, valid_keys|
|
|
53
|
+
next unless @config[section].is_a?(Hash)
|
|
43
54
|
|
|
44
|
-
|
|
45
|
-
|
|
55
|
+
@key_errors.concat(KeyValidator.validate(@config[section], valid_keys, context: section))
|
|
56
|
+
end
|
|
46
57
|
end
|
|
47
58
|
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
custom.each_with_index do |item, index|
|
|
53
|
-
validate_string(item["icon"], "socials.custom[#{index}].icon")
|
|
54
|
-
validate_url(item["href"], "socials.custom[#{index}].href")
|
|
55
|
-
end
|
|
59
|
+
def validate_array_item_keys
|
|
60
|
+
validate_tabs_keys
|
|
61
|
+
validate_cta_keys
|
|
62
|
+
validate_announcement_button_keys
|
|
56
63
|
end
|
|
57
64
|
|
|
58
|
-
def
|
|
65
|
+
def validate_tabs_keys
|
|
59
66
|
tabs = @config["tabs"]
|
|
60
|
-
return unless tabs
|
|
61
|
-
return add_array_error("tabs") unless tabs.is_a?(Array)
|
|
67
|
+
return unless tabs.is_a?(Array)
|
|
62
68
|
|
|
63
|
-
tabs.each_with_index do |tab,
|
|
64
|
-
|
|
65
|
-
validate_string(tab["href"], "tabs[#{index}].href")
|
|
66
|
-
validate_boolean(tab["external"], "tabs[#{index}].external") if tab.key?("external")
|
|
69
|
+
tabs.each_with_index do |tab, idx|
|
|
70
|
+
@key_errors.concat(KeyValidator.validate(tab, Schema::TAB, context: "tabs[#{idx}]"))
|
|
67
71
|
end
|
|
68
72
|
end
|
|
69
73
|
|
|
70
|
-
def
|
|
71
|
-
build = @config["build"]
|
|
72
|
-
return unless build
|
|
73
|
-
|
|
74
|
-
validate_string(build["output"], "build.output")
|
|
75
|
-
validate_no_slashes(build["output"], "build.output")
|
|
76
|
-
validate_string(build["base"], "build.base")
|
|
77
|
-
validate_starts_with_slash(build["base"], "build.base")
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def validate_search_section
|
|
81
|
-
search = @config["search"]
|
|
82
|
-
return unless search
|
|
83
|
-
|
|
84
|
-
validate_boolean(search["enabled"], "search.enabled") if search.key?("enabled")
|
|
85
|
-
validate_string(search["placeholder"], "search.placeholder") if search.key?("placeholder")
|
|
86
|
-
validate_array(search["exclude"], "search.exclude") if search.key?("exclude")
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def validate_navigation_section
|
|
74
|
+
def validate_cta_keys
|
|
90
75
|
cta = @config.dig("navigation", "cta")
|
|
91
|
-
return
|
|
92
|
-
return add_array_error("navigation.cta") unless cta.is_a?(Array)
|
|
76
|
+
return unless cta.is_a?(Array)
|
|
93
77
|
|
|
94
|
-
validate_cta_max_count(cta)
|
|
95
|
-
validate_cta_items(cta)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def validate_cta_items(cta)
|
|
99
78
|
cta.each_with_index do |item, idx|
|
|
100
|
-
|
|
101
|
-
validate_string(item["href"], "navigation.cta[#{idx}].href")
|
|
102
|
-
validate_cta_variant(item["variant"], idx) if item.key?("variant")
|
|
103
|
-
validate_boolean(item["external"], "navigation.cta[#{idx}].external") if item.key?("external")
|
|
79
|
+
@key_errors.concat(KeyValidator.validate(item, Schema::CTA, context: "navigation.cta[#{idx}]"))
|
|
104
80
|
end
|
|
105
81
|
end
|
|
106
82
|
|
|
107
|
-
def
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
add_error(field: "navigation.cta", error: "maximum 2 CTAs allowed",
|
|
111
|
-
got: "#{cta.length} items", fix: "Remove extra CTA items to have at most 2")
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def validate_cta_variant(variant, idx)
|
|
115
|
-
return if variant.nil? || %w[primary secondary].include?(variant)
|
|
116
|
-
|
|
117
|
-
add_error(field: "navigation.cta[#{idx}].variant", error: "must be 'primary' or 'secondary'",
|
|
118
|
-
got: variant, fix: "Change to 'primary' or 'secondary'")
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def validate_string(value, field_name)
|
|
122
|
-
return if value.nil? || value.is_a?(String)
|
|
123
|
-
|
|
124
|
-
add_error(field: field_name, error: "must be a string", got: value.class.name, fix: "Change to a string value")
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def validate_boolean(value, field_name)
|
|
128
|
-
return if [true, false].include?(value)
|
|
129
|
-
|
|
130
|
-
add_error(field: field_name, error: "must be true or false", got: value.inspect, fix: "Change to true or false")
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def validate_url(value, field_name)
|
|
134
|
-
return if value.nil? || value.is_a?(String)
|
|
135
|
-
|
|
136
|
-
add_error(field: field_name, error: "must be a URL string",
|
|
137
|
-
got: value.class.name, fix: "Change to a URL string")
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def validate_array(value, field_name)
|
|
141
|
-
return if value.nil? || value.is_a?(Array)
|
|
142
|
-
|
|
143
|
-
add_array_error(field_name)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def validate_file_path_or_url(value, field_name)
|
|
147
|
-
return if value.nil?
|
|
148
|
-
return add_type_error(field_name, "file path or URL (string)", value.class.name) unless value.is_a?(String)
|
|
149
|
-
return if url?(value)
|
|
150
|
-
|
|
151
|
-
file_path = File.absolute_path?(value) ? value : File.join("docs/public", value)
|
|
152
|
-
return if File.exist?(file_path)
|
|
153
|
-
|
|
154
|
-
add_error(field: field_name, error: "file not found", got: value,
|
|
155
|
-
fix: "Place the file in docs/public/ directory (e.g., 'logo.svg' for docs/public/logo.svg)")
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def validate_no_slashes(value, field_name)
|
|
159
|
-
return if value.nil? || !value.is_a?(String)
|
|
160
|
-
return unless value.include?("/") || value.include?("\\")
|
|
161
|
-
|
|
162
|
-
add_error(field: field_name, error: "cannot contain slashes", got: value,
|
|
163
|
-
fix: "Use a simple directory name like 'dist' or '_site'")
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def validate_starts_with_slash(value, field_name)
|
|
167
|
-
return if value.nil? || value.start_with?("/")
|
|
168
|
-
|
|
169
|
-
add_error(field: field_name, error: "must start with /", got: value, fix: "Change to '/#{value}'")
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def url?(value)
|
|
173
|
-
value.match?(%r{\Ahttps?://})
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def add_error(error_data)
|
|
177
|
-
@errors << error_data
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
def add_type_error(field, expected, got)
|
|
181
|
-
add_error(field: field, error: "must be a #{expected}", got: got, fix: "Change to a #{expected}")
|
|
182
|
-
end
|
|
83
|
+
def validate_announcement_button_keys
|
|
84
|
+
button = @config.dig("announcement", "button")
|
|
85
|
+
return unless button.is_a?(Hash)
|
|
183
86
|
|
|
184
|
-
|
|
185
|
-
add_error(field: field, error: "must be a hash", got: @config[field].class.name,
|
|
186
|
-
fix: "Change to a hash with platform names as keys and URLs as values")
|
|
87
|
+
@key_errors.concat(KeyValidator.validate(button, Schema::ANNOUNCEMENT_BUTTON, context: "announcement.button"))
|
|
187
88
|
end
|
|
188
89
|
|
|
189
|
-
def
|
|
190
|
-
|
|
191
|
-
|
|
90
|
+
def raise_key_errors
|
|
91
|
+
messages = @key_errors.map { |e| "#{e[:context]}: #{e[:message]}" }
|
|
92
|
+
raise ConfigError, "Error in docyard.yml:\n#{messages.join("\n")}"
|
|
192
93
|
end
|
|
193
94
|
|
|
194
95
|
def format_errors
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
class Config
|
|
5
|
+
module Validators
|
|
6
|
+
module Navigation
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def validate_navigation_section
|
|
10
|
+
cta = @config.dig("navigation", "cta")
|
|
11
|
+
return if cta.nil?
|
|
12
|
+
return add_array_error("navigation.cta") unless cta.is_a?(Array)
|
|
13
|
+
|
|
14
|
+
validate_cta_max_count(cta)
|
|
15
|
+
validate_cta_items(cta)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def validate_cta_items(cta)
|
|
19
|
+
cta.each_with_index do |item, idx|
|
|
20
|
+
validate_string(item["text"], "navigation.cta[#{idx}].text")
|
|
21
|
+
validate_string(item["href"], "navigation.cta[#{idx}].href")
|
|
22
|
+
validate_cta_variant(item["variant"], idx) if item.key?("variant")
|
|
23
|
+
validate_boolean(item["external"], "navigation.cta[#{idx}].external") if item.key?("external")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate_cta_max_count(cta)
|
|
28
|
+
return if cta.length <= 2
|
|
29
|
+
|
|
30
|
+
add_error(field: "navigation.cta", error: "maximum 2 CTAs allowed",
|
|
31
|
+
got: "#{cta.length} items", fix: "Remove extra CTA items to have at most 2")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def validate_cta_variant(variant, idx)
|
|
35
|
+
return if variant.nil? || %w[primary secondary].include?(variant)
|
|
36
|
+
|
|
37
|
+
add_error(field: "navigation.cta[#{idx}].variant", error: "must be 'primary' or 'secondary'",
|
|
38
|
+
got: variant, fix: "Change to 'primary' or 'secondary'")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
class Config
|
|
5
|
+
module Validators
|
|
6
|
+
module Section
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def validate_top_level
|
|
10
|
+
validate_string(@config["title"], "title")
|
|
11
|
+
validate_string(@config["description"], "description")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def validate_branding_section
|
|
15
|
+
branding = @config["branding"]
|
|
16
|
+
return unless branding
|
|
17
|
+
|
|
18
|
+
validate_file_path_or_url(branding["logo"], "branding.logo")
|
|
19
|
+
validate_file_path_or_url(branding["favicon"], "branding.favicon")
|
|
20
|
+
validate_boolean(branding["credits"], "branding.credits") if branding.key?("credits")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def validate_socials_section
|
|
24
|
+
socials = @config["socials"]
|
|
25
|
+
return unless socials
|
|
26
|
+
return add_hash_error("socials") unless socials.is_a?(Hash)
|
|
27
|
+
|
|
28
|
+
socials.each { |platform, url| validate_url(url, "socials.#{platform}") unless platform == "custom" }
|
|
29
|
+
validate_custom_socials(socials["custom"]) if socials.key?("custom")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def validate_custom_socials(custom)
|
|
33
|
+
return if custom.nil?
|
|
34
|
+
return add_array_error("socials.custom") unless custom.is_a?(Array)
|
|
35
|
+
|
|
36
|
+
custom.each_with_index do |item, index|
|
|
37
|
+
validate_string(item["icon"], "socials.custom[#{index}].icon")
|
|
38
|
+
validate_url(item["href"], "socials.custom[#{index}].href")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def validate_tabs_section
|
|
43
|
+
tabs = @config["tabs"]
|
|
44
|
+
return unless tabs
|
|
45
|
+
return add_array_error("tabs") unless tabs.is_a?(Array)
|
|
46
|
+
|
|
47
|
+
tabs.each_with_index do |tab, index|
|
|
48
|
+
validate_string(tab["text"], "tabs[#{index}].text")
|
|
49
|
+
validate_string(tab["href"], "tabs[#{index}].href")
|
|
50
|
+
validate_boolean(tab["external"], "tabs[#{index}].external") if tab.key?("external")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def validate_sidebar_setting
|
|
55
|
+
sidebar = @config["sidebar"]
|
|
56
|
+
return if sidebar.nil? || Config::SIDEBAR_MODES.include?(sidebar)
|
|
57
|
+
|
|
58
|
+
add_error(
|
|
59
|
+
field: "sidebar",
|
|
60
|
+
error: "must be one of: #{Config::SIDEBAR_MODES.join(', ')}",
|
|
61
|
+
got: sidebar.inspect,
|
|
62
|
+
fix: "Change to 'config', 'auto', or 'distributed'"
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_build_section
|
|
67
|
+
build = @config["build"]
|
|
68
|
+
return unless build
|
|
69
|
+
|
|
70
|
+
validate_string(build["output"], "build.output")
|
|
71
|
+
validate_no_slashes(build["output"], "build.output")
|
|
72
|
+
validate_string(build["base"], "build.base")
|
|
73
|
+
validate_starts_with_slash(build["base"], "build.base")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def validate_search_section
|
|
77
|
+
search = @config["search"]
|
|
78
|
+
return unless search
|
|
79
|
+
|
|
80
|
+
validate_boolean(search["enabled"], "search.enabled") if search.key?("enabled")
|
|
81
|
+
validate_string(search["placeholder"], "search.placeholder") if search.key?("placeholder")
|
|
82
|
+
validate_array(search["exclude"], "search.exclude") if search.key?("exclude")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_announcement_section
|
|
86
|
+
announcement = @config["announcement"]
|
|
87
|
+
return unless announcement.is_a?(Hash)
|
|
88
|
+
|
|
89
|
+
validate_string(announcement["text"], "announcement.text") if announcement.key?("text")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def validate_feedback_section
|
|
93
|
+
feedback = @config["feedback"]
|
|
94
|
+
return unless feedback.is_a?(Hash) && feedback["enabled"] == true
|
|
95
|
+
return if analytics_configured?
|
|
96
|
+
|
|
97
|
+
add_error(
|
|
98
|
+
field: "feedback.enabled",
|
|
99
|
+
error: "requires analytics to be configured",
|
|
100
|
+
got: "feedback enabled without analytics",
|
|
101
|
+
fix: "Configure analytics (google, plausible, fathom, or script) to collect feedback responses"
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def analytics_configured?
|
|
106
|
+
analytics = @config["analytics"]
|
|
107
|
+
return false unless analytics.is_a?(Hash)
|
|
108
|
+
|
|
109
|
+
analytics["google"] || analytics["plausible"] || analytics["fathom"] || analytics["script"]
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|