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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -1
- data/lib/docyard/build/asset_bundler.rb +7 -33
- data/lib/docyard/build/file_copier.rb +7 -15
- data/lib/docyard/build/llms_txt_generator.rb +0 -2
- data/lib/docyard/build/sitemap_generator.rb +1 -1
- data/lib/docyard/build/static_generator.rb +30 -32
- data/lib/docyard/build/step_runner.rb +88 -0
- data/lib/docyard/build/validator.rb +98 -0
- data/lib/docyard/builder.rb +82 -55
- data/lib/docyard/cli.rb +36 -4
- data/lib/docyard/components/aliases.rb +0 -4
- data/lib/docyard/components/processors/callout_processor.rb +1 -1
- data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +1 -1
- data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +1 -1
- data/lib/docyard/components/processors/code_block_options_preprocessor.rb +2 -2
- data/lib/docyard/components/processors/code_group_processor.rb +1 -1
- data/lib/docyard/components/processors/icon_processor.rb +2 -2
- data/lib/docyard/components/processors/tabs_processor.rb +1 -1
- data/lib/docyard/config/schema/definition.rb +29 -0
- data/lib/docyard/config/schema/sections.rb +63 -0
- data/lib/docyard/config/schema/simple_sections.rb +78 -0
- data/lib/docyard/config/schema.rb +28 -31
- data/lib/docyard/config/type_validators.rb +121 -0
- data/lib/docyard/config/validator.rb +136 -61
- data/lib/docyard/config.rb +1 -13
- data/lib/docyard/diagnostic.rb +89 -0
- data/lib/docyard/diagnostic_context.rb +48 -0
- data/lib/docyard/doctor/code_block_checker.rb +136 -0
- data/lib/docyard/doctor/component_checker.rb +49 -0
- data/lib/docyard/doctor/component_checkers/abbreviation_checker.rb +74 -0
- data/lib/docyard/doctor/component_checkers/badge_checker.rb +71 -0
- data/lib/docyard/doctor/component_checkers/base.rb +111 -0
- data/lib/docyard/doctor/component_checkers/callout_checker.rb +34 -0
- data/lib/docyard/doctor/component_checkers/cards_checker.rb +57 -0
- data/lib/docyard/doctor/component_checkers/code_group_checker.rb +47 -0
- data/lib/docyard/doctor/component_checkers/details_checker.rb +51 -0
- data/lib/docyard/doctor/component_checkers/icon_checker.rb +36 -0
- data/lib/docyard/doctor/component_checkers/image_attrs_checker.rb +46 -0
- data/lib/docyard/doctor/component_checkers/space_after_colons_checker.rb +45 -0
- data/lib/docyard/doctor/component_checkers/steps_checker.rb +35 -0
- data/lib/docyard/doctor/component_checkers/tabs_checker.rb +35 -0
- data/lib/docyard/doctor/component_checkers/tooltip_checker.rb +67 -0
- data/lib/docyard/doctor/component_checkers/unknown_type_checker.rb +34 -0
- data/lib/docyard/doctor/config_checker.rb +19 -0
- data/lib/docyard/doctor/config_fixer.rb +87 -0
- data/lib/docyard/doctor/content_checker.rb +164 -0
- data/lib/docyard/doctor/file_scanner.rb +113 -0
- data/lib/docyard/doctor/image_checker.rb +103 -0
- data/lib/docyard/doctor/link_checker.rb +91 -0
- data/lib/docyard/doctor/markdown_fixer.rb +62 -0
- data/lib/docyard/doctor/orphan_checker.rb +82 -0
- data/lib/docyard/doctor/reporter.rb +152 -0
- data/lib/docyard/doctor/sidebar_checker.rb +127 -0
- data/lib/docyard/doctor/sidebar_fixer.rb +47 -0
- data/lib/docyard/doctor.rb +178 -0
- data/lib/docyard/editor_launcher.rb +119 -0
- data/lib/docyard/errors.rb +0 -49
- data/lib/docyard/initializer.rb +32 -39
- data/lib/docyard/navigation/sidebar/local_config_loader.rb +44 -21
- data/lib/docyard/rendering/icon_helpers.rb +1 -3
- data/lib/docyard/search/build_indexer.rb +39 -24
- data/lib/docyard/search/dev_indexer.rb +9 -23
- data/lib/docyard/server/dev_server.rb +55 -13
- data/lib/docyard/server/error_overlay.rb +73 -0
- data/lib/docyard/server/file_watcher.rb +0 -1
- data/lib/docyard/server/page_diagnostics.rb +27 -0
- data/lib/docyard/server/preview_server.rb +17 -13
- data/lib/docyard/server/rack_application.rb +64 -3
- data/lib/docyard/server/resolution_result.rb +0 -4
- data/lib/docyard/templates/assets/css/error-overlay.css +669 -0
- data/lib/docyard/templates/assets/css/variables.css +1 -1
- data/lib/docyard/templates/assets/fonts/Inter-Variable.woff2 +0 -0
- data/lib/docyard/templates/assets/js/components/relative-time.js +42 -0
- data/lib/docyard/templates/assets/js/error-overlay.js +547 -0
- data/lib/docyard/templates/assets/js/hot-reload.js +35 -7
- data/lib/docyard/templates/errors/404.html.erb +1 -1
- data/lib/docyard/templates/errors/500.html.erb +1 -1
- data/lib/docyard/templates/partials/_head.html.erb +1 -1
- data/lib/docyard/templates/partials/_page_actions.html.erb +1 -1
- data/lib/docyard/ui.rb +80 -0
- data/lib/docyard/utils/logging.rb +5 -1
- data/lib/docyard/utils/text_formatter.rb +0 -6
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +4 -0
- metadata +47 -25
- data/lib/docyard/config/key_validator.rb +0 -30
- data/lib/docyard/config/validation_helpers.rb +0 -83
- data/lib/docyard/config/validators/navigation.rb +0 -43
- data/lib/docyard/config/validators/section.rb +0 -114
- data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "doctor/config_checker"
|
|
4
|
+
require_relative "doctor/sidebar_checker"
|
|
5
|
+
require_relative "doctor/content_checker"
|
|
6
|
+
require_relative "doctor/component_checker"
|
|
7
|
+
require_relative "doctor/code_block_checker"
|
|
8
|
+
require_relative "doctor/link_checker"
|
|
9
|
+
require_relative "doctor/image_checker"
|
|
10
|
+
require_relative "doctor/orphan_checker"
|
|
11
|
+
require_relative "doctor/file_scanner"
|
|
12
|
+
require_relative "doctor/config_fixer"
|
|
13
|
+
require_relative "doctor/sidebar_fixer"
|
|
14
|
+
require_relative "doctor/markdown_fixer"
|
|
15
|
+
require_relative "doctor/reporter"
|
|
16
|
+
|
|
17
|
+
module Docyard
|
|
18
|
+
class Doctor
|
|
19
|
+
attr_reader :config, :docs_path, :fix
|
|
20
|
+
|
|
21
|
+
def initialize(fix: false)
|
|
22
|
+
@fix = fix
|
|
23
|
+
@config = load_config_safely
|
|
24
|
+
@docs_path = config&.source || "docs"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run
|
|
28
|
+
fix ? run_with_fix : run_check_only
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def run_check_only
|
|
34
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
35
|
+
diagnostics, stats = collect_diagnostics
|
|
36
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
37
|
+
|
|
38
|
+
reporter = Reporter.new(diagnostics, stats, duration: duration)
|
|
39
|
+
reporter.print
|
|
40
|
+
reporter.exit_code
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def run_with_fix
|
|
44
|
+
config_fixer = ConfigFixer.new
|
|
45
|
+
sidebar_fixer = SidebarFixer.new(docs_path)
|
|
46
|
+
markdown_fixer = MarkdownFixer.new(docs_path)
|
|
47
|
+
|
|
48
|
+
run_config_fix_loop(config_fixer)
|
|
49
|
+
run_sidebar_fix(sidebar_fixer)
|
|
50
|
+
run_markdown_fix(markdown_fixer)
|
|
51
|
+
|
|
52
|
+
print_fix_results(config_fixer, sidebar_fixer, markdown_fixer)
|
|
53
|
+
|
|
54
|
+
diagnostics_after, _stats_after = collect_diagnostics
|
|
55
|
+
remaining_errors = diagnostics_after.count(&:error?)
|
|
56
|
+
|
|
57
|
+
print_remaining_issues(diagnostics_after) if remaining_errors.positive?
|
|
58
|
+
remaining_errors.positive? ? 1 : 0
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def run_config_fix_loop(fixer)
|
|
62
|
+
max_iterations = 10
|
|
63
|
+
max_iterations.times do
|
|
64
|
+
reload_config
|
|
65
|
+
diagnostics, _stats = collect_diagnostics
|
|
66
|
+
config_diagnostics = diagnostics.select { |d| d.category == :CONFIG && d.fixable? }
|
|
67
|
+
break if config_diagnostics.empty?
|
|
68
|
+
|
|
69
|
+
count_before = fixer.fixed_count
|
|
70
|
+
fixer.fix(config_diagnostics)
|
|
71
|
+
break if fixer.fixed_count == count_before # no progress made
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def run_sidebar_fix(fixer)
|
|
76
|
+
diagnostics, _stats = collect_diagnostics
|
|
77
|
+
sidebar_diagnostics = diagnostics.select { |d| d.category == :SIDEBAR && d.fixable? }
|
|
78
|
+
fixer.fix(sidebar_diagnostics)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def run_markdown_fix(fixer)
|
|
82
|
+
diagnostics, _stats = collect_diagnostics
|
|
83
|
+
component_diagnostics = diagnostics.select { |d| d.category == :COMPONENT && d.fixable? }
|
|
84
|
+
fixer.fix(component_diagnostics)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def reload_config
|
|
88
|
+
@config = load_config_safely
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def print_fix_results(config_fixer, sidebar_fixer, markdown_fixer)
|
|
92
|
+
print_fix_header
|
|
93
|
+
fixers = [
|
|
94
|
+
[config_fixer, "docyard.yml"],
|
|
95
|
+
[sidebar_fixer, "_sidebar.yml"],
|
|
96
|
+
[markdown_fixer, "markdown files"]
|
|
97
|
+
]
|
|
98
|
+
total_fixed = fixers.sum { |f, _| f.fixed_count }
|
|
99
|
+
|
|
100
|
+
total_fixed.positive? ? print_all_fixes(fixers, total_fixed) : print_no_fixes
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def print_all_fixes(fixers, total_fixed)
|
|
104
|
+
fixers.each { |fixer, name| print_fixer_results(fixer, name) }
|
|
105
|
+
puts " #{UI.success("Fixed #{total_fixed} issue(s) total")}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def print_fixer_results(fixer, name)
|
|
109
|
+
return unless fixer.fixed_count.positive?
|
|
110
|
+
|
|
111
|
+
puts " Fixed #{fixer.fixed_count} issue(s) in #{name}:"
|
|
112
|
+
print_fixed_issues(fixer) if fixer.respond_to?(:fixed_issues)
|
|
113
|
+
puts
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def print_fixed_issues(fixer)
|
|
117
|
+
fixer.fixed_issues.each { |d| puts " #{UI.dim(d.location)}: #{describe_fix(d)}" }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def print_no_fixes
|
|
121
|
+
puts " #{UI.yellow('No issues were auto-fixed.')}"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def print_fix_header
|
|
125
|
+
puts
|
|
126
|
+
puts " #{UI.bold('Docyard')} v#{VERSION}"
|
|
127
|
+
puts
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def describe_fix(diagnostic)
|
|
131
|
+
case diagnostic.fix[:type]
|
|
132
|
+
when :rename then "renamed '#{diagnostic.fix[:from]}' to '#{diagnostic.fix[:to]}'"
|
|
133
|
+
when :replace then "changed to #{diagnostic.fix[:value].inspect}"
|
|
134
|
+
when :line_replace then "replaced '#{diagnostic.fix[:from]}' with '#{diagnostic.fix[:to]}'"
|
|
135
|
+
else "fixed"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def print_remaining_issues(diagnostics)
|
|
140
|
+
puts
|
|
141
|
+
puts " Remaining issues:"
|
|
142
|
+
diagnostics.reject(&:fixable?).each { |d| puts " #{d.location}: #{d.message}" }
|
|
143
|
+
puts
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def load_config_safely
|
|
147
|
+
Config.load(Dir.pwd)
|
|
148
|
+
rescue ConfigError
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def collect_diagnostics
|
|
153
|
+
file_scanner = FileScanner.new(docs_path)
|
|
154
|
+
scanner_diagnostics = file_scanner.scan
|
|
155
|
+
|
|
156
|
+
diagnostics = [
|
|
157
|
+
collect_config_and_sidebar_diagnostics,
|
|
158
|
+
scanner_diagnostics,
|
|
159
|
+
config ? OrphanChecker.new(docs_path, config).check : []
|
|
160
|
+
].flatten
|
|
161
|
+
|
|
162
|
+
stats = {
|
|
163
|
+
files: file_scanner.files_scanned,
|
|
164
|
+
links: file_scanner.links_checked,
|
|
165
|
+
images: file_scanner.images_checked
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
[diagnostics, stats]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def collect_config_and_sidebar_diagnostics
|
|
172
|
+
diagnostics = []
|
|
173
|
+
diagnostics.concat(ConfigChecker.new(config).check) if config
|
|
174
|
+
diagnostics.concat(SidebarChecker.new(docs_path).check)
|
|
175
|
+
diagnostics
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module EditorLauncher
|
|
5
|
+
EDITORS = {
|
|
6
|
+
vscode: {
|
|
7
|
+
patterns: ["code", "Code", "Visual Studio Code"],
|
|
8
|
+
command: ->(f, l) { ["code", "--goto", "#{f}:#{l}"] }
|
|
9
|
+
},
|
|
10
|
+
cursor: { patterns: %w[cursor Cursor], command: ->(f, l) { ["cursor", "--goto", "#{f}:#{l}"] } },
|
|
11
|
+
zed: { patterns: %w[zed Zed], command: ->(f, l) { ["zed", "#{f}:#{l}"] } },
|
|
12
|
+
webstorm: { patterns: %w[webstorm idea], command: ->(f, l) { ["webstorm", "--line", l.to_s, f] } },
|
|
13
|
+
rubymine: { patterns: %w[rubymine mine], command: ->(f, l) { ["rubymine", "--line", l.to_s, f] } },
|
|
14
|
+
vim: { patterns: %w[vim nvim], command: ->(f, l) { [detect_vim, "+#{l}", f] } },
|
|
15
|
+
emacs: { patterns: %w[emacs], command: ->(f, l) { ["emacs", "+#{l}", f] } }
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
def available?
|
|
20
|
+
!detect.nil?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def detect
|
|
24
|
+
detect_from_env || detect_from_process
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def open(file, line = 1)
|
|
28
|
+
editor = detect
|
|
29
|
+
return false unless editor
|
|
30
|
+
|
|
31
|
+
command = EDITORS[editor][:command].call(file, line)
|
|
32
|
+
spawn(*command, %i[out err] => File::NULL)
|
|
33
|
+
true
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
Docyard.logger.warn("Failed to open editor: #{e.message}")
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def detect_from_env
|
|
42
|
+
%i[VISUAL EDITOR].each do |var|
|
|
43
|
+
editor_cmd = ENV.fetch(var.to_s, nil)
|
|
44
|
+
next unless editor_cmd
|
|
45
|
+
|
|
46
|
+
EDITORS.each do |name, config|
|
|
47
|
+
return name if config[:patterns].any? { |p| editor_cmd.include?(p) }
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def detect_from_process
|
|
54
|
+
return detect_windows_apps if Gem.win_platform?
|
|
55
|
+
return nil unless command_exists?("pgrep")
|
|
56
|
+
|
|
57
|
+
EDITORS.each do |name, config|
|
|
58
|
+
config[:patterns].each do |pattern|
|
|
59
|
+
return name if process_running?(pattern)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
detect_macos_apps
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def process_running?(pattern)
|
|
67
|
+
system("pgrep", "-x", pattern, out: File::NULL, err: File::NULL)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def detect_macos_apps
|
|
71
|
+
return nil unless RUBY_PLATFORM.include?("darwin")
|
|
72
|
+
|
|
73
|
+
macos_apps = {
|
|
74
|
+
vscode: "Visual Studio Code",
|
|
75
|
+
cursor: "Cursor",
|
|
76
|
+
zed: "Zed"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
macos_apps.each do |editor, app_name|
|
|
80
|
+
return editor if macos_app_running?(app_name)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def macos_app_running?(app_name)
|
|
87
|
+
system("pgrep", "-f", app_name, out: File::NULL, err: File::NULL)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def detect_windows_apps
|
|
91
|
+
windows_apps = {
|
|
92
|
+
vscode: "Code.exe",
|
|
93
|
+
cursor: "Cursor.exe",
|
|
94
|
+
zed: "zed.exe",
|
|
95
|
+
webstorm: "webstorm64.exe",
|
|
96
|
+
rubymine: "rubymine64.exe"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
windows_apps.each do |editor, exe|
|
|
100
|
+
return editor if windows_process_running?(exe)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def windows_process_running?(exe)
|
|
107
|
+
system("tasklist /FI \"IMAGENAME eq #{exe}\" 2>NUL | find /I \"#{exe}\" >NUL")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def command_exists?(cmd)
|
|
111
|
+
system("which", cmd, out: File::NULL, err: File::NULL)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def detect_vim
|
|
115
|
+
command_exists?("nvim") ? "nvim" : "vim"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/docyard/errors.rb
CHANGED
|
@@ -7,54 +7,5 @@ module Docyard
|
|
|
7
7
|
|
|
8
8
|
class SidebarConfigError < Error; end
|
|
9
9
|
|
|
10
|
-
class FileNotFoundError < Error
|
|
11
|
-
attr_reader :path
|
|
12
|
-
|
|
13
|
-
def initialize(path)
|
|
14
|
-
@path = path
|
|
15
|
-
super("File not found: #{path}")
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
class InvalidPathError < Error; end
|
|
20
|
-
|
|
21
|
-
class MarkdownParseError < Error
|
|
22
|
-
attr_reader :file_path, :original_error
|
|
23
|
-
|
|
24
|
-
def initialize(file_path, original_error)
|
|
25
|
-
@file_path = file_path
|
|
26
|
-
@original_error = original_error
|
|
27
|
-
super("Failed to parse markdown file #{file_path}: #{original_error.message}")
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
class TemplateRenderError < Error
|
|
32
|
-
attr_reader :template_path, :original_error
|
|
33
|
-
|
|
34
|
-
def initialize(template_path, original_error)
|
|
35
|
-
@template_path = template_path
|
|
36
|
-
@original_error = original_error
|
|
37
|
-
super("Failed to render template #{template_path}: #{original_error.message}")
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
class ReloadCheckError < Error
|
|
42
|
-
attr_reader :original_error
|
|
43
|
-
|
|
44
|
-
def initialize(original_error)
|
|
45
|
-
@original_error = original_error
|
|
46
|
-
super("Reload check failed: #{original_error.message}")
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
class AssetNotFoundError < Error
|
|
51
|
-
attr_reader :asset_path
|
|
52
|
-
|
|
53
|
-
def initialize(asset_path)
|
|
54
|
-
@asset_path = asset_path
|
|
55
|
-
super("Asset not found: #{asset_path}")
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
10
|
class BuildError < Error; end
|
|
60
11
|
end
|
data/lib/docyard/initializer.rb
CHANGED
|
@@ -47,21 +47,23 @@ module Docyard
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def user_confirms_overwrite?
|
|
50
|
-
|
|
50
|
+
puts
|
|
51
|
+
print " Overwrite existing files? [y/N] "
|
|
51
52
|
response = $stdin.gets&.strip&.downcase
|
|
52
53
|
%w[y yes].include?(response)
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
def print_existing_files_warning
|
|
56
|
-
puts
|
|
57
|
-
puts "
|
|
58
|
-
puts "
|
|
59
|
-
puts "
|
|
57
|
+
puts
|
|
58
|
+
puts " #{UI.yellow('Warning:')} Existing files found:"
|
|
59
|
+
puts " #{docs_path}/" if File.exist?(docs_path)
|
|
60
|
+
puts " #{config_path}" if File.exist?(config_path)
|
|
60
61
|
end
|
|
61
62
|
|
|
62
63
|
def print_abort_message
|
|
63
|
-
puts
|
|
64
|
-
puts "Aborted. Use
|
|
64
|
+
puts
|
|
65
|
+
puts " #{UI.yellow('Aborted.')} Use --force to overwrite existing files."
|
|
66
|
+
puts
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
def create_project_directory
|
|
@@ -103,48 +105,39 @@ module Docyard
|
|
|
103
105
|
end
|
|
104
106
|
|
|
105
107
|
def print_success
|
|
106
|
-
puts
|
|
107
|
-
puts "
|
|
108
|
-
puts
|
|
108
|
+
puts
|
|
109
|
+
puts " #{UI.bold('Docyard')} v#{VERSION}"
|
|
110
|
+
puts
|
|
111
|
+
puts " #{UI.success('Project initialized')}"
|
|
112
|
+
puts
|
|
109
113
|
print_created_structure
|
|
110
114
|
print_next_steps
|
|
111
115
|
end
|
|
112
116
|
|
|
113
|
-
def
|
|
114
|
-
"
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def print_created_structure
|
|
118
|
-
puts "Created:"
|
|
119
|
-
puts ""
|
|
117
|
+
def print_created_structure # rubocop:disable Metrics/AbcSize
|
|
118
|
+
puts " #{UI.bold('Created:')}"
|
|
120
119
|
if project_name
|
|
121
|
-
puts "
|
|
122
|
-
puts "
|
|
123
|
-
puts "
|
|
120
|
+
puts UI.dim(" #{project_name}/")
|
|
121
|
+
puts UI.dim(" docyard.yml")
|
|
122
|
+
puts UI.dim(" docs/")
|
|
124
123
|
else
|
|
125
|
-
puts "
|
|
126
|
-
puts "
|
|
124
|
+
puts UI.dim(" docyard.yml")
|
|
125
|
+
puts UI.dim(" docs/")
|
|
127
126
|
end
|
|
128
|
-
puts "
|
|
129
|
-
puts "
|
|
130
|
-
puts "
|
|
131
|
-
puts "
|
|
132
|
-
puts "
|
|
133
|
-
puts
|
|
127
|
+
puts UI.dim(" _sidebar.yml")
|
|
128
|
+
puts UI.dim(" index.md")
|
|
129
|
+
puts UI.dim(" getting-started.md")
|
|
130
|
+
puts UI.dim(" components.md")
|
|
131
|
+
puts UI.dim(" public/")
|
|
132
|
+
puts
|
|
134
133
|
end
|
|
135
134
|
|
|
136
135
|
def print_next_steps
|
|
137
|
-
puts "Next steps:"
|
|
138
|
-
puts ""
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
end
|
|
143
|
-
puts " Start the development server:"
|
|
144
|
-
puts " \e[1m$ docyard serve\e[0m"
|
|
145
|
-
puts ""
|
|
146
|
-
puts " Then open \e[4mhttp://localhost:4200\e[0m in your browser"
|
|
147
|
-
puts ""
|
|
136
|
+
puts " #{UI.bold('Next steps:')}"
|
|
137
|
+
puts " cd #{project_name}" if project_name
|
|
138
|
+
puts " docyard serve"
|
|
139
|
+
puts " Open #{UI.cyan('http://localhost:4200')}"
|
|
140
|
+
puts
|
|
148
141
|
end
|
|
149
142
|
end
|
|
150
143
|
end
|
|
@@ -7,10 +7,11 @@ module Docyard
|
|
|
7
7
|
class LocalConfigLoader
|
|
8
8
|
SIDEBAR_CONFIG_FILE = "_sidebar.yml"
|
|
9
9
|
|
|
10
|
-
attr_reader :docs_path
|
|
10
|
+
attr_reader :docs_path, :key_errors
|
|
11
11
|
|
|
12
|
-
def initialize(docs_path)
|
|
12
|
+
def initialize(docs_path, validate: true)
|
|
13
13
|
@docs_path = docs_path
|
|
14
|
+
@validate = validate
|
|
14
15
|
@key_errors = []
|
|
15
16
|
end
|
|
16
17
|
|
|
@@ -56,18 +57,18 @@ module Docyard
|
|
|
56
57
|
def validate_items(items, path_prefix: "")
|
|
57
58
|
return unless items.is_a?(Array)
|
|
58
59
|
|
|
59
|
-
items.
|
|
60
|
-
validate_item(item,
|
|
60
|
+
items.each do |item|
|
|
61
|
+
validate_item(item, path_prefix)
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
|
|
64
|
-
def validate_item(item,
|
|
65
|
+
def validate_item(item, path_prefix)
|
|
65
66
|
return unless item.is_a?(Hash)
|
|
66
67
|
|
|
67
68
|
if external_link?(item)
|
|
68
|
-
validate_external_link(item,
|
|
69
|
+
validate_external_link(item, path_prefix)
|
|
69
70
|
else
|
|
70
|
-
validate_sidebar_item(item,
|
|
71
|
+
validate_sidebar_item(item, path_prefix)
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
74
|
|
|
@@ -75,39 +76,61 @@ module Docyard
|
|
|
75
76
|
item.key?("link") || item.key?(:link)
|
|
76
77
|
end
|
|
77
78
|
|
|
78
|
-
def validate_external_link(item,
|
|
79
|
-
|
|
79
|
+
def validate_external_link(item, path_prefix)
|
|
80
|
+
link_text = item["text"] || item[:text] || item["link"] || item[:link]
|
|
81
|
+
context = build_context(path_prefix, link_text)
|
|
82
|
+
errors = Config::Schema.validate_keys(item, Config::Schema::SIDEBAR_EXTERNAL_LINK_KEYS, context: context)
|
|
80
83
|
@key_errors.concat(errors)
|
|
81
84
|
end
|
|
82
85
|
|
|
83
|
-
def validate_sidebar_item(item,
|
|
86
|
+
def validate_sidebar_item(item, path_prefix)
|
|
84
87
|
slug, options = extract_slug_and_options(item)
|
|
88
|
+
|
|
89
|
+
if slug.nil?
|
|
90
|
+
@key_errors << build_invalid_format_error(item, path_prefix)
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
|
|
85
94
|
return unless options.is_a?(Hash)
|
|
86
95
|
|
|
87
|
-
|
|
96
|
+
context = build_context(path_prefix, slug)
|
|
97
|
+
errors = Config::Schema.validate_keys(options, Config::Schema::SIDEBAR_ITEM_KEYS, context: context)
|
|
88
98
|
@key_errors.concat(errors)
|
|
89
|
-
validate_nested_items(options,
|
|
99
|
+
validate_nested_items(options, context)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def build_invalid_format_error(item, path_prefix)
|
|
103
|
+
item_desc = item.keys.first&.to_s || "item"
|
|
104
|
+
context = build_context(path_prefix, item_desc)
|
|
105
|
+
{
|
|
106
|
+
context: context,
|
|
107
|
+
message: "invalid format, expected slug-based key (e.g., 'page-name:' or '- page-name')"
|
|
108
|
+
}
|
|
90
109
|
end
|
|
91
110
|
|
|
92
111
|
def extract_slug_and_options(item)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
112
|
+
return [nil, item] if item.keys.size != 1
|
|
113
|
+
|
|
114
|
+
slug = item.keys.first
|
|
115
|
+
slug.is_a?(String) ? [slug, item[slug]] : [nil, item]
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def build_context(prefix, name)
|
|
119
|
+
return name.to_s if prefix.empty?
|
|
120
|
+
|
|
121
|
+
"#{prefix}.#{name}"
|
|
99
122
|
end
|
|
100
123
|
|
|
101
|
-
def validate_nested_items(options,
|
|
124
|
+
def validate_nested_items(options, context)
|
|
102
125
|
nested = options["items"] || options[:items]
|
|
103
126
|
return unless nested
|
|
104
127
|
|
|
105
|
-
|
|
106
|
-
validate_items(nested, path_prefix: nested_context)
|
|
128
|
+
validate_items(nested, path_prefix: context)
|
|
107
129
|
end
|
|
108
130
|
|
|
109
131
|
def report_key_errors
|
|
110
132
|
return if @key_errors.empty?
|
|
133
|
+
return unless @validate
|
|
111
134
|
|
|
112
135
|
messages = @key_errors.map { |e| "#{e[:context]}: #{e[:message]}" }
|
|
113
136
|
raise ConfigError, "Error in #{config_file_path}:\n#{messages.join("\n")}"
|
|
@@ -2,15 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Docyard
|
|
4
4
|
module IconHelpers
|
|
5
|
-
VALID_WEIGHTS = %w[regular bold fill light thin duotone].freeze
|
|
6
|
-
|
|
7
5
|
def icon(name, weight = "regular")
|
|
8
6
|
name = name.to_s
|
|
9
7
|
return name if name.strip.start_with?("<svg")
|
|
10
8
|
|
|
11
9
|
name = name.tr("_", "-")
|
|
12
10
|
weight = weight.to_s
|
|
13
|
-
weight = "regular" unless VALID_WEIGHTS.include?(weight)
|
|
11
|
+
weight = "regular" unless Icons::VALID_WEIGHTS.include?(weight)
|
|
14
12
|
weight_class = weight == "regular" ? "ph" : "ph-#{weight}"
|
|
15
13
|
%(<i class="#{weight_class} ph-#{name}" aria-hidden="true"></i>)
|
|
16
14
|
end
|
|
@@ -18,39 +18,26 @@ module Docyard
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def index
|
|
21
|
-
return 0 unless search_enabled?
|
|
22
|
-
|
|
23
|
-
log "Generating search index..."
|
|
24
|
-
|
|
25
|
-
unless pagefind_available?
|
|
26
|
-
warn_pagefind_missing
|
|
27
|
-
return 0
|
|
28
|
-
end
|
|
21
|
+
return [0, nil] unless search_enabled?
|
|
22
|
+
return [0, nil] unless pagefind_available?
|
|
29
23
|
|
|
30
24
|
run_pagefind
|
|
31
25
|
end
|
|
32
26
|
|
|
33
27
|
private
|
|
34
28
|
|
|
35
|
-
def warn_pagefind_missing
|
|
36
|
-
log_warning "[!] Search index skipped: Pagefind not found"
|
|
37
|
-
log_warning " Install with: npm install -g pagefind"
|
|
38
|
-
log_warning " Or run: npx pagefind --site #{output_dir}"
|
|
39
|
-
end
|
|
40
|
-
|
|
41
29
|
def run_pagefind
|
|
42
30
|
args = build_pagefind_args(output_dir)
|
|
43
|
-
log "Running: npx #{args.join(' ')}" if verbose
|
|
44
31
|
|
|
45
32
|
stdout, stderr, status = Open3.capture3(PAGEFIND_COMMAND, *args)
|
|
46
33
|
|
|
47
34
|
if status.success?
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
count = extract_page_count(stdout)
|
|
36
|
+
details = verbose ? collect_index_details : nil
|
|
37
|
+
[count, details]
|
|
51
38
|
else
|
|
52
|
-
|
|
53
|
-
0
|
|
39
|
+
Docyard.logger.warn("Search indexing failed: #{stderr}") if verbose
|
|
40
|
+
[0, nil]
|
|
54
41
|
end
|
|
55
42
|
end
|
|
56
43
|
|
|
@@ -62,12 +49,40 @@ module Docyard
|
|
|
62
49
|
end
|
|
63
50
|
end
|
|
64
51
|
|
|
65
|
-
def
|
|
66
|
-
|
|
52
|
+
def collect_index_details
|
|
53
|
+
indexed, excluded = classify_pages
|
|
54
|
+
format_index_details(indexed, excluded)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def classify_pages
|
|
58
|
+
indexed = []
|
|
59
|
+
excluded = []
|
|
60
|
+
|
|
61
|
+
Dir.glob(File.join(output_dir, "**", "index.html")).each do |file|
|
|
62
|
+
path = extract_page_path(file)
|
|
63
|
+
classify_page(File.read(file), path, indexed, excluded)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
[indexed, excluded]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def extract_page_path(file)
|
|
70
|
+
path = file.delete_prefix("#{output_dir}/").delete_suffix("/index.html")
|
|
71
|
+
path.empty? ? "/" : path
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def classify_page(content, path, indexed, excluded)
|
|
75
|
+
if content.include?("data-pagefind-body")
|
|
76
|
+
indexed << path
|
|
77
|
+
elsif content.include?("data-pagefind-ignore")
|
|
78
|
+
excluded << path
|
|
79
|
+
end
|
|
67
80
|
end
|
|
68
81
|
|
|
69
|
-
def
|
|
70
|
-
|
|
82
|
+
def format_index_details(indexed, excluded)
|
|
83
|
+
details = indexed.sort
|
|
84
|
+
excluded.sort.each { |path| details << "(excluded: #{path})" } if excluded.any?
|
|
85
|
+
details
|
|
71
86
|
end
|
|
72
87
|
end
|
|
73
88
|
end
|