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
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../diagnostic_context"
4
+
5
+ module Docyard
6
+ class Doctor
7
+ class LinkChecker
8
+ MARKDOWN_LINK_REGEX = /\[([^\]]*)\]\(([^)]+)\)/
9
+ INTERNAL_LINK_REGEX = %r{^/[^/]}
10
+ IMAGE_EXTENSIONS = %w[.png .jpg .jpeg .gif .svg .webp .ico .bmp].freeze
11
+ CODE_FENCE_REGEX = /^(`{3,}|~{3,})/
12
+ LINKS_DOCS_URL = nil
13
+
14
+ attr_reader :docs_path, :links_checked
15
+
16
+ def initialize(docs_path)
17
+ @docs_path = docs_path
18
+ @links_checked = 0
19
+ end
20
+
21
+ def check_file(content, file_path)
22
+ relative_file = file_path.delete_prefix("#{docs_path}/")
23
+ diagnostics = []
24
+
25
+ each_line_outside_code_blocks(content) do |line, line_number|
26
+ diagnostics.concat(check_line_for_links(line, line_number, relative_file))
27
+ end
28
+
29
+ diagnostics
30
+ end
31
+
32
+ private
33
+
34
+ def each_line_outside_code_blocks(content)
35
+ in_code_block = false
36
+
37
+ content.each_line.with_index(1) do |line, line_number|
38
+ in_code_block = !in_code_block if line.match?(CODE_FENCE_REGEX)
39
+ yield(line, line_number) unless in_code_block
40
+ end
41
+ end
42
+
43
+ def check_line_for_links(line, line_number, relative_file)
44
+ line.scan(MARKDOWN_LINK_REGEX).filter_map do |_text, url|
45
+ next unless internal_link?(url)
46
+ next if image_path?(url)
47
+
48
+ @links_checked += 1
49
+ target_path = url.split("#").first
50
+ next if file_exists?(target_path)
51
+
52
+ build_diagnostic(relative_file, line_number, target_path)
53
+ end
54
+ end
55
+
56
+ def build_diagnostic(file, line, target)
57
+ full_path = File.join(docs_path, file)
58
+ source_context = DiagnosticContext.extract_source_context(full_path, line)
59
+
60
+ Diagnostic.new(
61
+ severity: :warning,
62
+ category: :LINK,
63
+ code: "LINK_BROKEN",
64
+ message: "Broken link to '#{target}'",
65
+ file: file,
66
+ line: line,
67
+ field: target,
68
+ doc_url: LINKS_DOCS_URL,
69
+ source_context: source_context
70
+ )
71
+ end
72
+
73
+ def internal_link?(url)
74
+ url.match?(INTERNAL_LINK_REGEX)
75
+ end
76
+
77
+ def image_path?(url)
78
+ IMAGE_EXTENSIONS.any? { |ext| url.downcase.end_with?(ext) }
79
+ end
80
+
81
+ def file_exists?(url_path)
82
+ clean_path = url_path.chomp("/")
83
+ [
84
+ File.join(docs_path, "#{clean_path}.md"),
85
+ File.join(docs_path, clean_path, "index.md"),
86
+ File.join(docs_path, "#{clean_path}.html")
87
+ ].any? { |f| File.exist?(f) }
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class Doctor
5
+ class MarkdownFixer
6
+ attr_reader :fixed_issues
7
+
8
+ def initialize(docs_path)
9
+ @docs_path = docs_path
10
+ @fixed_issues = []
11
+ end
12
+
13
+ def fixed_count
14
+ @fixed_issues.size
15
+ end
16
+
17
+ def fix(diagnostics)
18
+ fixable = diagnostics.select(&:fixable?)
19
+ return if fixable.empty?
20
+
21
+ fixable.group_by(&:file).each do |file, file_diagnostics|
22
+ fix_file(file, file_diagnostics)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def fix_file(relative_file, diagnostics)
29
+ file_path = File.join(@docs_path, relative_file)
30
+ return unless File.exist?(file_path)
31
+
32
+ lines = File.readlines(file_path)
33
+ apply_all_fixes(lines, diagnostics)
34
+ File.write(file_path, lines.join)
35
+ end
36
+
37
+ def apply_all_fixes(lines, diagnostics)
38
+ diagnostics.group_by(&:line).each do |line_number, line_diagnostics|
39
+ next unless valid_line_number?(line_number, lines.size)
40
+
41
+ line_diagnostics.each { |d| apply_fix(lines, line_number, d) }
42
+ end
43
+ end
44
+
45
+ def valid_line_number?(line_number, total_lines)
46
+ line_number&.positive? && line_number <= total_lines
47
+ end
48
+
49
+ def apply_fix(lines, line_number, diagnostic)
50
+ fix = diagnostic.fix
51
+ return unless fix[:type] == :line_replace
52
+
53
+ index = line_number - 1
54
+ original_line = lines[index]
55
+ return unless original_line.include?(fix[:from])
56
+
57
+ lines[index] = original_line.sub(fix[:from], fix[:to])
58
+ @fixed_issues << diagnostic
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class Doctor
5
+ class OrphanChecker
6
+ attr_reader :docs_path, :config
7
+
8
+ def initialize(docs_path, config)
9
+ @docs_path = docs_path
10
+ @config = config
11
+ end
12
+
13
+ def check
14
+ return [] if auto_sidebar?
15
+
16
+ sidebar_pages = collect_sidebar_pages
17
+ return [] unless sidebar_pages
18
+
19
+ all_pages = collect_all_pages
20
+ orphans = all_pages - sidebar_pages
21
+
22
+ orphans.map { |file| build_diagnostic(file) }
23
+ end
24
+
25
+ private
26
+
27
+ def build_diagnostic(file)
28
+ Diagnostic.new(
29
+ severity: :warning,
30
+ category: :ORPHAN,
31
+ code: "ORPHAN_PAGE",
32
+ message: "not linked from sidebar",
33
+ file: file
34
+ )
35
+ end
36
+
37
+ def auto_sidebar?
38
+ config.sidebar == "auto"
39
+ end
40
+
41
+ def collect_all_pages
42
+ Dir.glob(File.join(docs_path, "**", "*.md"))
43
+ .map { |f| f.delete_prefix("#{docs_path}/") }
44
+ .reject { |f| f.start_with?("_") || f == "index.md" }
45
+ end
46
+
47
+ def collect_sidebar_pages
48
+ config_items = load_sidebar_config
49
+ return nil unless config_items
50
+
51
+ sidebar_tree = Sidebar::ConfigBuilder.new(config_items, current_path: "/").build
52
+ extract_paths_from_tree(sidebar_tree)
53
+ rescue StandardError
54
+ nil
55
+ end
56
+
57
+ def load_sidebar_config
58
+ Sidebar::LocalConfigLoader.new(docs_path, validate: false).load
59
+ end
60
+
61
+ def extract_paths_from_tree(items, collected = [])
62
+ items.each do |item|
63
+ path = item[:path]
64
+ collected << path_to_file(path) if path && !item[:link]
65
+ extract_paths_from_tree(item[:children] || [], collected)
66
+ end
67
+ collected
68
+ end
69
+
70
+ def path_to_file(url_path)
71
+ clean_path = url_path.delete_prefix("/").chomp("/")
72
+ return "index.md" if clean_path.empty?
73
+
74
+ if File.exist?(File.join(docs_path, clean_path, "index.md"))
75
+ "#{clean_path}/index.md"
76
+ else
77
+ "#{clean_path}.md"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class Doctor
5
+ class Reporter
6
+ CATEGORY_LABELS = {
7
+ CONFIG: "Configuration",
8
+ SIDEBAR: "Sidebar",
9
+ CONTENT: "Content",
10
+ COMPONENT: "Components",
11
+ SYNTAX: "Syntax",
12
+ LINK: "Broken links",
13
+ IMAGE: "Missing images",
14
+ ORPHAN: "Orphan pages"
15
+ }.freeze
16
+
17
+ CATEGORY_ORDER = %i[CONFIG SIDEBAR CONTENT COMPONENT LINK IMAGE ORPHAN SYNTAX].freeze
18
+
19
+ attr_reader :diagnostics, :stats, :fixed, :duration
20
+
21
+ def initialize(diagnostics, stats = {}, fixed: false, duration: nil)
22
+ @diagnostics = diagnostics
23
+ @stats = stats
24
+ @fixed = fixed
25
+ @duration = duration
26
+ end
27
+
28
+ def print
29
+ print_header
30
+ print_categories
31
+ print_summary
32
+ end
33
+
34
+ def exit_code
35
+ error_count.positive? ? 1 : 0
36
+ end
37
+
38
+ private
39
+
40
+ def print_header
41
+ puts
42
+ puts " #{UI.bold('Docyard')} v#{VERSION}"
43
+ puts
44
+ puts " Checking docs..."
45
+ puts
46
+ end
47
+
48
+ def print_categories
49
+ CATEGORY_ORDER.each do |category|
50
+ print_category(category)
51
+ end
52
+ end
53
+
54
+ def print_category(category)
55
+ items = diagnostics_for(category)
56
+ return if items.empty?
57
+
58
+ print_category_header(category, items)
59
+ items.each { |d| puts format_diagnostic(d) }
60
+ puts
61
+ end
62
+
63
+ def diagnostics_for(category)
64
+ diagnostics.select { |d| d.category == category }
65
+ end
66
+
67
+ def print_category_header(category, items)
68
+ label = CATEGORY_LABELS[category] || category.to_s
69
+ counts = issue_counts(items.count(&:error?), items.count(&:warning?))
70
+ puts " #{UI.bold(label.ljust(28))} #{counts}"
71
+ end
72
+
73
+ def format_diagnostic(diagnostic)
74
+ prefix = diagnostic.error? ? UI.red("error") : UI.yellow("warn ")
75
+ location = diagnostic.location&.ljust(26) || (" " * 26)
76
+ suffix = diagnostic.fixable? ? " #{UI.cyan('[fixable]')}" : ""
77
+ " #{prefix} #{location} #{diagnostic.message}#{suffix}"
78
+ end
79
+
80
+ def print_summary
81
+ puts " #{stats_summary}"
82
+
83
+ if error_count.zero? && warning_count.zero?
84
+ puts " #{UI.success('No issues found')}"
85
+ else
86
+ puts " #{build_issue_summary}"
87
+ print_fixable_hint
88
+ end
89
+
90
+ puts " #{format_duration}" if duration
91
+ puts
92
+ end
93
+
94
+ def format_duration
95
+ if duration < 1
96
+ UI.dim("Finished in #{(duration * 1000).round}ms")
97
+ else
98
+ UI.dim("Finished in #{duration.round(2)}s")
99
+ end
100
+ end
101
+
102
+ def print_fixable_hint
103
+ return if fixed
104
+
105
+ fixable = diagnostics.count(&:fixable?)
106
+ return if fixable.zero?
107
+
108
+ puts
109
+ puts " #{UI.cyan("Run with --fix to auto-fix #{pluralize(fixable, 'issue')}.")}"
110
+ end
111
+
112
+ def issue_counts(errors, warnings)
113
+ parts = []
114
+ parts << UI.red(pluralize(errors, "error")) if errors.positive?
115
+ parts << UI.yellow(pluralize(warnings, "warning")) if warnings.positive?
116
+ parts.join(", ")
117
+ end
118
+
119
+ def stats_summary
120
+ stats_parts = [
121
+ stat_part(:files, "file"),
122
+ stat_part(:links, "link"),
123
+ stat_part(:images, "image")
124
+ ].compact
125
+ "Checked #{stats_parts.join(', ')}"
126
+ end
127
+
128
+ def stat_part(key, word)
129
+ pluralize(stats[key], word) if stats[key]
130
+ end
131
+
132
+ def build_issue_summary
133
+ parts = []
134
+ parts << UI.red(pluralize(error_count, "error")) if error_count.positive?
135
+ parts << UI.yellow(pluralize(warning_count, "warning")) if warning_count.positive?
136
+ "Found #{parts.join(', ')}"
137
+ end
138
+
139
+ def pluralize(count, word)
140
+ count == 1 ? "#{count} #{word}" : "#{count} #{word}s"
141
+ end
142
+
143
+ def error_count
144
+ diagnostics.count(&:error?)
145
+ end
146
+
147
+ def warning_count
148
+ diagnostics.count(&:warning?)
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../diagnostic_context"
4
+
5
+ module Docyard
6
+ class Doctor
7
+ class SidebarChecker
8
+ SIDEBAR_DOCS_URL = "https://docyard.dev/customize/sidebar/"
9
+
10
+ attr_reader :docs_path
11
+
12
+ def initialize(docs_path)
13
+ @docs_path = docs_path
14
+ @sidebar_path = File.join(docs_path, "_sidebar.yml")
15
+ end
16
+
17
+ def check
18
+ loader = Sidebar::LocalConfigLoader.new(docs_path, validate: false)
19
+ items = loader.load
20
+ diagnostics = convert_key_errors(loader.key_errors)
21
+ diagnostics.concat(check_missing_files(items)) if items
22
+ diagnostics
23
+ end
24
+
25
+ private
26
+
27
+ def convert_key_errors(key_errors)
28
+ key_errors.map do |error|
29
+ Diagnostic.new(
30
+ severity: :error,
31
+ category: :SIDEBAR,
32
+ code: "SIDEBAR_VALIDATION",
33
+ field: "_sidebar.yml: #{error[:context]}",
34
+ message: error[:message],
35
+ fix: error[:fix]
36
+ )
37
+ end
38
+ end
39
+
40
+ def check_missing_files(items, path_prefix: "")
41
+ diagnostics = []
42
+ items.each do |item|
43
+ diagnostics.concat(check_item_file(item, path_prefix))
44
+ end
45
+ diagnostics
46
+ end
47
+
48
+ def check_item_file(item, path_prefix)
49
+ diagnostics = []
50
+ slug, options = extract_slug_and_options(item)
51
+ return diagnostics unless slug
52
+
53
+ if requires_file?(options)
54
+ file_path = build_file_path(path_prefix, slug, options)
55
+ diagnostics << build_missing_file_diagnostic(slug, path_prefix, file_path) unless file_exists?(file_path)
56
+ end
57
+
58
+ if options.is_a?(Hash) && options["items"]
59
+ nested_prefix = options["group"] ? path_prefix : File.join(path_prefix, slug)
60
+ diagnostics.concat(check_missing_files(options["items"], path_prefix: nested_prefix))
61
+ end
62
+
63
+ diagnostics
64
+ end
65
+
66
+ def requires_file?(options)
67
+ return true unless options.is_a?(Hash)
68
+ return true unless options["items"]
69
+
70
+ options["index"]
71
+ end
72
+
73
+ def extract_slug_and_options(item)
74
+ return [item, nil] if item.is_a?(String)
75
+ return [nil, nil] unless item.is_a?(Hash)
76
+ return [nil, nil] if external_link?(item)
77
+ return [nil, nil] if item.keys.size != 1
78
+
79
+ slug = item.keys.first
80
+ slug.is_a?(String) ? [slug, item[slug]] : [nil, nil]
81
+ end
82
+
83
+ def build_file_path(path_prefix, slug, options)
84
+ base = File.join(path_prefix, slug)
85
+ options.is_a?(Hash) && options["index"] ? File.join(base, "index") : base
86
+ end
87
+
88
+ def file_exists?(relative_path)
89
+ md_path = File.join(docs_path, "#{relative_path}.md")
90
+ index_path = File.join(docs_path, relative_path, "index.md")
91
+ File.exist?(md_path) || File.exist?(index_path)
92
+ end
93
+
94
+ def build_missing_file_diagnostic(slug, path_prefix, file_path)
95
+ context = path_prefix.empty? ? slug : "#{path_prefix}/#{slug}"
96
+ line = find_sidebar_line(slug)
97
+ source_context = DiagnosticContext.extract_source_context(@sidebar_path, line) if line
98
+
99
+ Diagnostic.new(
100
+ severity: :error,
101
+ category: :SIDEBAR,
102
+ code: "SIDEBAR_MISSING_FILE",
103
+ file: "_sidebar.yml",
104
+ line: line,
105
+ field: context,
106
+ message: "references missing file '#{file_path}.md'",
107
+ doc_url: SIDEBAR_DOCS_URL,
108
+ source_context: source_context
109
+ )
110
+ end
111
+
112
+ def find_sidebar_line(slug)
113
+ return nil unless File.exist?(@sidebar_path)
114
+
115
+ lines = File.readlines(@sidebar_path)
116
+ lines.each_with_index do |line, index|
117
+ return index + 1 if line.include?("- #{slug}") || line.include?("#{slug}:")
118
+ end
119
+ nil
120
+ end
121
+
122
+ def external_link?(item)
123
+ item.key?("link") || item.key?(:link)
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class Doctor
5
+ class SidebarFixer
6
+ attr_reader :fixed_issues
7
+
8
+ def initialize(docs_path)
9
+ @sidebar_path = File.join(docs_path, "_sidebar.yml")
10
+ @fixed_issues = []
11
+ end
12
+
13
+ def fix(issues)
14
+ fixable = issues.select(&:fixable?)
15
+ return if fixable.empty?
16
+ return unless File.exist?(@sidebar_path)
17
+
18
+ lines = File.readlines(@sidebar_path)
19
+ fixable.each { |issue| attempt_fix(lines, issue) }
20
+ File.write(@sidebar_path, lines.join)
21
+ end
22
+
23
+ def fixed_count
24
+ @fixed_issues.size
25
+ end
26
+
27
+ private
28
+
29
+ def attempt_fix(lines, issue)
30
+ return unless issue.fix[:type] == :rename
31
+
32
+ fix_rename(lines, issue)
33
+ end
34
+
35
+ def fix_rename(lines, issue)
36
+ from_key = issue.fix[:from]
37
+ to_key = issue.fix[:to]
38
+
39
+ index = lines.find_index { |line| line =~ /^(\s*)#{Regexp.escape(from_key)}:/ }
40
+ return unless index
41
+
42
+ lines[index] = lines[index].sub(/#{Regexp.escape(from_key)}:/, "#{to_key}:")
43
+ @fixed_issues << issue
44
+ end
45
+ end
46
+ end
47
+ end