docyard 0.6.0 → 0.8.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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/CHANGELOG.md +34 -1
  4. data/lib/docyard/build/asset_bundler.rb +22 -7
  5. data/lib/docyard/build/file_copier.rb +49 -27
  6. data/lib/docyard/build/sitemap_generator.rb +6 -6
  7. data/lib/docyard/build/static_generator.rb +82 -50
  8. data/lib/docyard/builder.rb +20 -10
  9. data/lib/docyard/cli.rb +6 -3
  10. data/lib/docyard/components/aliases.rb +29 -0
  11. data/lib/docyard/components/processors/callout_processor.rb +124 -0
  12. data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +106 -0
  13. data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +79 -0
  14. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +78 -0
  15. data/lib/docyard/components/processors/code_block_processor.rb +175 -0
  16. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +127 -0
  17. data/lib/docyard/components/processors/heading_anchor_processor.rb +39 -0
  18. data/lib/docyard/components/processors/icon_processor.rb +53 -0
  19. data/lib/docyard/components/processors/table_of_contents_processor.rb +68 -0
  20. data/lib/docyard/components/processors/table_wrapper_processor.rb +22 -0
  21. data/lib/docyard/components/processors/tabs_processor.rb +48 -0
  22. data/lib/docyard/components/support/code_block/feature_extractor.rb +117 -0
  23. data/lib/docyard/components/support/code_block/icon_detector.rb +44 -0
  24. data/lib/docyard/components/support/code_block/line_parser.rb +84 -0
  25. data/lib/docyard/components/support/code_block/line_wrapper.rb +50 -0
  26. data/lib/docyard/components/support/code_block/patterns.rb +55 -0
  27. data/lib/docyard/components/support/code_detector.rb +61 -0
  28. data/lib/docyard/components/support/tabs/icon_detector.rb +62 -0
  29. data/lib/docyard/components/support/tabs/parser.rb +195 -0
  30. data/lib/docyard/components/support/tabs/range_finder.rb +46 -0
  31. data/lib/docyard/config/branding_resolver.rb +183 -0
  32. data/lib/docyard/{constants.rb → config/constants.rb} +7 -4
  33. data/lib/docyard/config/validator.rb +122 -99
  34. data/lib/docyard/config.rb +38 -36
  35. data/lib/docyard/initializer.rb +15 -76
  36. data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
  37. data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +6 -3
  38. data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
  39. data/lib/docyard/navigation/sidebar/config_parser.rb +208 -0
  40. data/lib/docyard/navigation/sidebar/file_resolver.rb +78 -0
  41. data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +2 -1
  42. data/lib/docyard/navigation/sidebar/item.rb +96 -0
  43. data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
  44. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +69 -0
  45. data/lib/docyard/navigation/sidebar/metadata_reader.rb +47 -0
  46. data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
  47. data/lib/docyard/navigation/sidebar/renderer.rb +144 -0
  48. data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
  49. data/lib/docyard/navigation/sidebar/tree_builder.rb +139 -0
  50. data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
  51. data/lib/docyard/navigation/sidebar_builder.rb +159 -0
  52. data/lib/docyard/rendering/icon_helpers.rb +13 -0
  53. data/lib/docyard/{icons → rendering/icons}/phosphor.rb +26 -1
  54. data/lib/docyard/{markdown.rb → rendering/markdown.rb} +19 -13
  55. data/lib/docyard/rendering/renderer.rb +163 -0
  56. data/lib/docyard/rendering/template_resolver.rb +172 -0
  57. data/lib/docyard/routing/fallback_resolver.rb +92 -0
  58. data/lib/docyard/search/build_indexer.rb +74 -0
  59. data/lib/docyard/search/dev_indexer.rb +155 -0
  60. data/lib/docyard/search/pagefind_support.rb +33 -0
  61. data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +24 -19
  62. data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
  63. data/lib/docyard/server/pagefind_handler.rb +63 -0
  64. data/lib/docyard/{preview_server.rb → server/preview_server.rb} +2 -2
  65. data/lib/docyard/server/rack_application.rb +192 -0
  66. data/lib/docyard/server/resolution_result.rb +29 -0
  67. data/lib/docyard/{router.rb → server/router.rb} +4 -4
  68. data/lib/docyard/templates/assets/css/code.css +18 -51
  69. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +143 -0
  70. data/lib/docyard/templates/assets/css/components/callout.css +67 -67
  71. data/lib/docyard/templates/assets/css/components/code-block.css +180 -282
  72. data/lib/docyard/templates/assets/css/components/heading-anchor.css +28 -15
  73. data/lib/docyard/templates/assets/css/components/icon.css +0 -1
  74. data/lib/docyard/templates/assets/css/components/logo.css +0 -2
  75. data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
  76. data/lib/docyard/templates/assets/css/components/navigation.css +186 -167
  77. data/lib/docyard/templates/assets/css/components/prev-next.css +76 -47
  78. data/lib/docyard/templates/assets/css/components/search.css +561 -0
  79. data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
  80. data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
  81. data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
  82. data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
  83. data/lib/docyard/templates/assets/css/landing.css +815 -0
  84. data/lib/docyard/templates/assets/css/layout.css +503 -87
  85. data/lib/docyard/templates/assets/css/main.css +1 -3
  86. data/lib/docyard/templates/assets/css/markdown.css +111 -93
  87. data/lib/docyard/templates/assets/css/reset.css +0 -3
  88. data/lib/docyard/templates/assets/css/typography.css +43 -41
  89. data/lib/docyard/templates/assets/css/variables.css +268 -208
  90. data/lib/docyard/templates/assets/favicon.svg +7 -8
  91. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
  92. data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
  93. data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
  94. data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
  95. data/lib/docyard/templates/assets/js/components/search.js +610 -0
  96. data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
  97. data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
  98. data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
  99. data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
  100. data/lib/docyard/templates/assets/js/theme.js +0 -3
  101. data/lib/docyard/templates/assets/logo-dark.svg +8 -2
  102. data/lib/docyard/templates/assets/logo.svg +7 -4
  103. data/lib/docyard/templates/config/docyard.yml.erb +37 -34
  104. data/lib/docyard/templates/errors/404.html.erb +1 -1
  105. data/lib/docyard/templates/errors/500.html.erb +1 -1
  106. data/lib/docyard/templates/layouts/default.html.erb +19 -56
  107. data/lib/docyard/templates/layouts/splash.html.erb +176 -0
  108. data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
  109. data/lib/docyard/templates/partials/_code_block.html.erb +6 -4
  110. data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
  111. data/lib/docyard/templates/partials/_features.html.erb +15 -0
  112. data/lib/docyard/templates/partials/_footer.html.erb +42 -0
  113. data/lib/docyard/templates/partials/_head.html.erb +22 -0
  114. data/lib/docyard/templates/partials/_header.html.erb +49 -0
  115. data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
  116. data/lib/docyard/templates/partials/_hero.html.erb +27 -0
  117. data/lib/docyard/templates/partials/_nav_group.html.erb +25 -11
  118. data/lib/docyard/templates/partials/_nav_leaf.html.erb +1 -1
  119. data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
  120. data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
  121. data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
  122. data/lib/docyard/templates/partials/_prev_next.html.erb +9 -3
  123. data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
  124. data/lib/docyard/templates/partials/_search_modal.html.erb +41 -0
  125. data/lib/docyard/templates/partials/_search_trigger.html.erb +18 -0
  126. data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
  127. data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
  128. data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
  129. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
  130. data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
  131. data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
  132. data/lib/docyard/utils/html_helpers.rb +14 -0
  133. data/lib/docyard/utils/path_resolver.rb +2 -1
  134. data/lib/docyard/utils/url_helpers.rb +20 -0
  135. data/lib/docyard/version.rb +1 -1
  136. data/lib/docyard.rb +22 -15
  137. metadata +89 -50
  138. data/lib/docyard/components/callout_processor.rb +0 -121
  139. data/lib/docyard/components/code_block_diff_preprocessor.rb +0 -104
  140. data/lib/docyard/components/code_block_feature_extractor.rb +0 -113
  141. data/lib/docyard/components/code_block_focus_preprocessor.rb +0 -77
  142. data/lib/docyard/components/code_block_icon_detector.rb +0 -40
  143. data/lib/docyard/components/code_block_line_wrapper.rb +0 -46
  144. data/lib/docyard/components/code_block_options_preprocessor.rb +0 -76
  145. data/lib/docyard/components/code_block_patterns.rb +0 -51
  146. data/lib/docyard/components/code_block_processor.rb +0 -176
  147. data/lib/docyard/components/code_detector.rb +0 -59
  148. data/lib/docyard/components/code_line_parser.rb +0 -80
  149. data/lib/docyard/components/code_snippet_import_preprocessor.rb +0 -125
  150. data/lib/docyard/components/heading_anchor_processor.rb +0 -34
  151. data/lib/docyard/components/icon_detector.rb +0 -57
  152. data/lib/docyard/components/icon_processor.rb +0 -51
  153. data/lib/docyard/components/table_of_contents_processor.rb +0 -64
  154. data/lib/docyard/components/table_wrapper_processor.rb +0 -18
  155. data/lib/docyard/components/tabs_parser.rb +0 -191
  156. data/lib/docyard/components/tabs_processor.rb +0 -44
  157. data/lib/docyard/components/tabs_range_finder.rb +0 -42
  158. data/lib/docyard/rack_application.rb +0 -172
  159. data/lib/docyard/renderer.rb +0 -120
  160. data/lib/docyard/routing/resolution_result.rb +0 -31
  161. data/lib/docyard/sidebar/config_parser.rb +0 -180
  162. data/lib/docyard/sidebar/item.rb +0 -58
  163. data/lib/docyard/sidebar/renderer.rb +0 -137
  164. data/lib/docyard/sidebar/tree_builder.rb +0 -59
  165. data/lib/docyard/sidebar_builder.rb +0 -102
  166. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
  167. data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
  168. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
  169. data/lib/docyard/templates/markdown/index.md.erb +0 -82
  170. /data/lib/docyard/{sidebar → navigation/sidebar}/title_extractor.rb +0 -0
  171. /data/lib/docyard/{icons → rendering/icons}/LICENSE.phosphor +0 -0
  172. /data/lib/docyard/{icons → rendering/icons}/file_types.rb +0 -0
  173. /data/lib/docyard/{icons.rb → rendering/icons.rb} +0 -0
  174. /data/lib/docyard/{language_mapping.rb → rendering/language_mapping.rb} +0 -0
  175. /data/lib/docyard/{file_watcher.rb → server/file_watcher.rb} +0 -0
  176. /data/lib/docyard/{errors.rb → utils/errors.rb} +0 -0
  177. /data/lib/docyard/{logging.rb → utils/logging.rb} +0 -0
@@ -9,21 +9,22 @@ module Docyard
9
9
  end
10
10
 
11
11
  def validate!
12
- validate_site_section
12
+ validate_top_level
13
13
  validate_branding_section
14
+ validate_socials_section
15
+ validate_tabs_section
14
16
  validate_build_section
15
- validate_markdown_section
17
+ validate_search_section
18
+ validate_navigation_section
16
19
 
17
20
  raise ConfigError, format_errors if @errors.any?
18
21
  end
19
22
 
20
23
  private
21
24
 
22
- def validate_site_section
23
- site = @config["site"]
24
-
25
- validate_string(site["title"], "site.title")
26
- validate_string(site["description"], "site.description")
25
+ def validate_top_level
26
+ validate_string(@config["title"], "title")
27
+ validate_string(@config["description"], "description")
27
28
  end
28
29
 
29
30
  def validate_branding_section
@@ -31,127 +32,141 @@ module Docyard
31
32
  return unless branding
32
33
 
33
34
  validate_file_path_or_url(branding["logo"], "branding.logo")
34
- validate_file_path_or_url(branding["logo_dark"], "branding.logo_dark")
35
35
  validate_file_path_or_url(branding["favicon"], "branding.favicon")
36
+ validate_boolean(branding["credits"], "branding.credits") if branding.key?("credits")
37
+ end
38
+
39
+ def validate_socials_section
40
+ socials = @config["socials"]
41
+ return unless socials
42
+ return add_hash_error("socials") unless socials.is_a?(Hash)
43
+
44
+ socials.each { |platform, url| validate_url(url, "socials.#{platform}") unless platform == "custom" }
45
+ validate_custom_socials(socials["custom"]) if socials.key?("custom")
46
+ end
47
+
48
+ def validate_custom_socials(custom)
49
+ return if custom.nil?
50
+ return add_array_error("socials.custom") unless custom.is_a?(Array)
36
51
 
37
- appearance = branding["appearance"] || {}
38
- validate_boolean(appearance["logo"], "branding.appearance.logo")
39
- validate_boolean(appearance["title"], "branding.appearance.title")
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
56
+ end
57
+
58
+ def validate_tabs_section
59
+ tabs = @config["tabs"]
60
+ return unless tabs
61
+ return add_array_error("tabs") unless tabs.is_a?(Array)
62
+
63
+ tabs.each_with_index do |tab, index|
64
+ validate_string(tab["text"], "tabs[#{index}].text")
65
+ validate_string(tab["href"], "tabs[#{index}].href")
66
+ validate_boolean(tab["external"], "tabs[#{index}].external") if tab.key?("external")
67
+ end
40
68
  end
41
69
 
42
70
  def validate_build_section
43
71
  build = @config["build"]
72
+ return unless build
44
73
 
45
- validate_string(build["output_dir"], "build.output_dir")
46
- validate_no_slashes(build["output_dir"], "build.output_dir")
47
- validate_string(build["base_url"], "build.base_url")
48
- validate_starts_with_slash(build["base_url"], "build.base_url")
49
- validate_boolean(build["clean"], "build.clean")
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")
50
78
  end
51
79
 
52
- def validate_markdown_section
53
- markdown = @config["markdown"]
54
- return unless markdown
80
+ def validate_search_section
81
+ search = @config["search"]
82
+ return unless search
55
83
 
56
- validate_boolean(markdown["lineNumbers"], "markdown.lineNumbers") if markdown.key?("lineNumbers")
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")
57
87
  end
58
88
 
59
- def validate_string(value, field_name)
60
- return if value.nil?
61
- return if value.is_a?(String)
89
+ def validate_navigation_section
90
+ cta = @config.dig("navigation", "cta")
91
+ return if cta.nil?
92
+ return add_array_error("navigation.cta") unless cta.is_a?(Array)
62
93
 
63
- add_error(
64
- field: field_name,
65
- error: "must be a string",
66
- got: value.class.name,
67
- fix: "Change to a string value"
68
- )
94
+ validate_cta_max_count(cta)
95
+ validate_cta_items(cta)
69
96
  end
70
97
 
71
- def validate_boolean(value, field_name)
72
- return if [true, false].include?(value)
73
-
74
- add_error(
75
- field: field_name,
76
- error: "must be true or false",
77
- got: value.inspect,
78
- fix: "Change to true or false"
79
- )
98
+ def validate_cta_items(cta)
99
+ cta.each_with_index do |item, idx|
100
+ validate_string(item["text"], "navigation.cta[#{idx}].text")
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")
104
+ end
80
105
  end
81
106
 
82
- def validate_file_path(value, field_name)
83
- return if value.nil?
84
- return add_file_path_type_error(value, field_name) unless value.is_a?(String)
107
+ def validate_cta_max_count(cta)
108
+ return if cta.length <= 2
85
109
 
86
- file_path = if File.absolute_path?(value)
87
- value
88
- else
89
- File.join("docs", value)
90
- end
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
91
113
 
92
- return if File.exist?(file_path)
114
+ def validate_cta_variant(variant, idx)
115
+ return if variant.nil? || %w[primary secondary].include?(variant)
93
116
 
94
- add_file_not_found_error(value, field_name)
117
+ add_error(field: "navigation.cta[#{idx}].variant", error: "must be 'primary' or 'secondary'",
118
+ got: variant, fix: "Change to 'primary' or 'secondary'")
95
119
  end
96
120
 
97
- def add_file_path_type_error(value, field_name)
98
- add_error(
99
- field: field_name,
100
- error: "must be a file path (string)",
101
- got: value.class.name,
102
- fix: "Change to a string file path"
103
- )
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")
104
125
  end
105
126
 
106
- def add_file_not_found_error(value, field_name)
107
- add_error(
108
- field: field_name,
109
- error: "file not found",
110
- got: value,
111
- fix: "Place the file in docs/ directory and use a relative path (e.g., 'assets/logo.svg')"
112
- )
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")
113
131
  end
114
132
 
115
- def validate_no_slashes(value, field_name)
116
- return if value.nil?
117
- return unless value.is_a?(String)
118
- return unless value.include?("/") || value.include?("\\")
133
+ def validate_url(value, field_name)
134
+ return if value.nil? || value.is_a?(String)
119
135
 
120
- add_error(
121
- field: field_name,
122
- error: "cannot contain slashes",
123
- got: value,
124
- fix: "Use a simple directory name like 'dist' or '_site'"
125
- )
136
+ add_error(field: field_name, error: "must be a URL string",
137
+ got: value.class.name, fix: "Change to a URL string")
126
138
  end
127
139
 
128
- def validate_starts_with_slash(value, field_name)
129
- return if value.nil?
130
- return if value.start_with?("/")
140
+ def validate_array(value, field_name)
141
+ return if value.nil? || value.is_a?(Array)
131
142
 
132
- add_error(
133
- field: field_name,
134
- error: "must start with /",
135
- got: value,
136
- fix: "Change to '/#{value}'"
137
- )
143
+ add_array_error(field_name)
138
144
  end
139
145
 
140
146
  def validate_file_path_or_url(value, field_name)
141
147
  return if value.nil?
142
- return add_file_path_type_error(value, field_name) unless value.is_a?(String)
143
-
148
+ return add_type_error(field_name, "file path or URL (string)", value.class.name) unless value.is_a?(String)
144
149
  return if url?(value)
145
150
 
146
- file_path = if File.absolute_path?(value)
147
- value
148
- else
149
- File.join("docs", value)
150
- end
151
-
151
+ file_path = File.absolute_path?(value) ? value : File.join("docs/public", value)
152
152
  return if File.exist?(file_path)
153
153
 
154
- add_file_not_found_error(value, field_name)
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}'")
155
170
  end
156
171
 
157
172
  def url?(value)
@@ -162,17 +177,25 @@ module Docyard
162
177
  @errors << error_data
163
178
  end
164
179
 
165
- def format_errors
166
- message = "Error in docyard.yml:\n\n"
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
167
183
 
168
- @errors.each do |err|
169
- message += " Field: #{err[:field]}\n"
170
- message += " Error: #{err[:error]}\n"
171
- message += " Got: #{err[:got]}\n"
172
- message += " Fix: #{err[:fix]}\n\n"
173
- end
184
+ def add_hash_error(field)
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")
187
+ end
174
188
 
175
- message.chomp
189
+ def add_array_error(field)
190
+ value = field.split(".").reduce(@config) { |h, k| h&.[](k) }
191
+ add_error(field: field, error: "must be an array", got: value.class.name, fix: "Change to an array")
192
+ end
193
+
194
+ def format_errors
195
+ errors_text = @errors.map do |err|
196
+ " Field: #{err[:field]}\n Error: #{err[:error]}\n Got: #{err[:got]}\n Fix: #{err[:fix]}"
197
+ end.join("\n\n")
198
+ "Error in docyard.yml:\n\n#{errors_text}"
176
199
  end
177
200
  end
178
201
  end
@@ -2,41 +2,33 @@
2
2
 
3
3
  require "yaml"
4
4
  require_relative "config/validator"
5
- require_relative "constants"
5
+ require_relative "config/constants"
6
6
 
7
7
  module Docyard
8
8
  class Config
9
9
  DEFAULT_CONFIG = {
10
- "site" => {
11
- "title" => Constants::DEFAULT_SITE_TITLE,
12
- "description" => ""
13
- },
10
+ "title" => Constants::DEFAULT_SITE_TITLE,
11
+ "description" => "",
14
12
  "branding" => {
15
13
  "logo" => nil,
16
- "logo_dark" => nil,
17
14
  "favicon" => nil,
18
- "appearance" => {
19
- "logo" => true,
20
- "title" => true
21
- }
15
+ "credits" => true,
16
+ "copyright" => nil
22
17
  },
18
+ "socials" => {},
19
+ "tabs" => [],
23
20
  "build" => {
24
- "output_dir" => "dist",
25
- "base_url" => "/",
26
- "clean" => true
21
+ "output" => "dist",
22
+ "base" => "/"
27
23
  },
28
- "sidebar" => {
29
- "items" => []
24
+ "search" => {
25
+ "enabled" => true,
26
+ "placeholder" => "Search...",
27
+ "exclude" => []
30
28
  },
31
29
  "navigation" => {
32
- "footer" => {
33
- "enabled" => true,
34
- "prev_text" => "Previous",
35
- "next_text" => "Next"
36
- }
37
- },
38
- "markdown" => {
39
- "lineNumbers" => false
30
+ "cta" => [],
31
+ "breadcrumbs" => true
40
32
  }
41
33
  }.freeze
42
34
 
@@ -57,30 +49,38 @@ module Docyard
57
49
  File.exist?(file_path)
58
50
  end
59
51
 
60
- def site
61
- @site ||= ConfigSection.new(data["site"])
52
+ def title
53
+ data["title"]
54
+ end
55
+
56
+ def description
57
+ data["description"]
62
58
  end
63
59
 
64
60
  def branding
65
61
  @branding ||= ConfigSection.new(data["branding"])
66
62
  end
67
63
 
64
+ def socials
65
+ data["socials"]
66
+ end
67
+
68
+ def tabs
69
+ data["tabs"]
70
+ end
71
+
68
72
  def build
69
73
  @build ||= ConfigSection.new(data["build"])
70
74
  end
71
75
 
72
- def sidebar
73
- @sidebar ||= ConfigSection.new(data["sidebar"])
76
+ def search
77
+ @search ||= ConfigSection.new(data["search"])
74
78
  end
75
79
 
76
80
  def navigation
77
81
  @navigation ||= ConfigSection.new(data["navigation"])
78
82
  end
79
83
 
80
- def markdown
81
- @markdown ||= ConfigSection.new(data["markdown"])
82
- end
83
-
84
84
  private
85
85
 
86
86
  def load_config_data
@@ -113,7 +113,13 @@ module Docyard
113
113
  end
114
114
 
115
115
  def deep_dup(hash)
116
- hash.transform_values { |value| value.is_a?(Hash) ? deep_dup(value) : value }
116
+ hash.transform_values do |value|
117
+ case value
118
+ when Hash then deep_dup(value)
119
+ when Array then value.map { |v| v.is_a?(Hash) ? deep_dup(v) : v }
120
+ else value
121
+ end
122
+ end
117
123
  end
118
124
 
119
125
  def build_yaml_error_message(error)
@@ -134,10 +140,6 @@ module Docyard
134
140
  @data = data || {}
135
141
  end
136
142
 
137
- def appearance
138
- @data["appearance"]
139
- end
140
-
141
143
  def method_missing(method, *args)
142
144
  return @data[method.to_s] if args.empty?
143
145
 
@@ -5,16 +5,8 @@ require "fileutils"
5
5
  module Docyard
6
6
  class Initializer
7
7
  DOCS_DIR = "docs"
8
- TEMPLATE_DIR = File.join(__dir__, "templates", "markdown")
9
8
  CONFIG_TEMPLATE_DIR = File.join(__dir__, "templates", "config")
10
9
 
11
- TEMPLATES = {
12
- "index.md" => "index.md.erb",
13
- "getting-started/installation.md" => "getting-started/installation.md.erb",
14
- "guides/markdown-features.md" => "guides/markdown-features.md.erb",
15
- "guides/configuration.md" => "guides/configuration.md.erb"
16
- }.freeze
17
-
18
10
  def initialize(path = ".")
19
11
  @path = path
20
12
  @docs_path = File.join(@path, DOCS_DIR)
@@ -39,23 +31,22 @@ module Docyard
39
31
 
40
32
  def create_structure
41
33
  FileUtils.mkdir_p(@docs_path)
42
-
43
- TEMPLATES.each do |output_name, template_name|
44
- copy_template(template_name, output_name)
45
- end
46
-
34
+ create_index_file
47
35
  create_example_config
48
36
  end
49
37
 
50
- def copy_template(template_name, output_name)
51
- template_path = File.join(TEMPLATE_DIR, template_name)
52
- output_path = File.join(@docs_path, output_name)
38
+ def create_index_file
39
+ index_path = File.join(@docs_path, "index.md")
40
+ content = <<~MARKDOWN
41
+ ---
42
+ title: Welcome
43
+ ---
53
44
 
54
- output_dir = File.dirname(output_path)
55
- FileUtils.mkdir_p(output_dir) unless File.directory?(output_dir)
45
+ # Welcome to Your Documentation
56
46
 
57
- content = File.read(template_path)
58
- File.write(output_path, content)
47
+ Start writing your documentation here.
48
+ MARKDOWN
49
+ File.write(index_path, content)
59
50
  end
60
51
 
61
52
  def create_example_config
@@ -81,16 +72,16 @@ module Docyard
81
72
 
82
73
  def print_banner
83
74
  puts ""
84
- puts "┌─────────────────────────────────────────────────────────────┐"
85
- puts "│ ✓ Docyard initialized successfully │"
86
- puts "└─────────────────────────────────────────────────────────────┘"
75
+ puts "Docyard initialized successfully"
87
76
  puts ""
88
77
  end
89
78
 
90
79
  def print_created_files
91
80
  puts "Created files:"
92
81
  puts ""
93
- print_file_tree
82
+ puts " docs/"
83
+ puts " index.md"
84
+ puts " docyard.yml"
94
85
  puts ""
95
86
  end
96
87
 
@@ -99,62 +90,10 @@ module Docyard
99
90
  puts ""
100
91
  puts " Start development server:"
101
92
  puts " docyard serve"
102
- puts " → http://localhost:4200"
103
93
  puts ""
104
94
  puts " Build for production:"
105
95
  puts " docyard build"
106
96
  puts ""
107
- puts " Preview production build:"
108
- puts " docyard preview"
109
- puts ""
110
- end
111
-
112
- def print_file_tree
113
- puts " ├── docs/"
114
-
115
- grouped_files = TEMPLATES.keys.group_by { |file| File.dirname(file) }
116
- sorted_dirs = grouped_files.keys.sort
117
-
118
- sorted_dirs.each_with_index do |dir, dir_idx|
119
- print_directory_group(dir, grouped_files[dir], dir_idx == sorted_dirs.length - 1)
120
- end
121
-
122
- puts " └── docyard.yml"
123
- end
124
-
125
- def print_directory_group(dir, files, is_last_dir)
126
- sorted_files = files.sort
127
-
128
- if dir == "."
129
- print_root_files(sorted_files, is_last_dir)
130
- else
131
- print_subdirectory(dir, sorted_files, is_last_dir)
132
- end
133
- end
134
-
135
- def print_root_files(files, is_last_dir)
136
- files.each_with_index do |file, idx|
137
- is_last = idx == files.length - 1 && is_last_dir
138
- prefix = is_last ? " │ └──" : " │ ├──"
139
- puts "#{prefix} #{file}"
140
- end
141
- end
142
-
143
- def print_subdirectory(dir, files, is_last_dir)
144
- dir_prefix = is_last_dir ? " │ └──" : " │ ├──"
145
- puts "#{dir_prefix} #{dir}/"
146
-
147
- files.each_with_index do |file, idx|
148
- print_subdirectory_file(file, idx, files.length, is_last_dir)
149
- end
150
- end
151
-
152
- def print_subdirectory_file(file, idx, total, is_last_dir)
153
- is_last_file = idx == total - 1
154
- file_prefix = is_last_dir ? " │ " : " │ │ "
155
- file_prefix += is_last_file ? "└──" : "├──"
156
- basename = File.basename(file)
157
- puts "#{file_prefix} #{basename}"
158
97
  end
159
98
  end
160
99
  end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class BreadcrumbBuilder
5
+ MAX_VISIBLE_ITEMS = 3
6
+
7
+ Item = Struct.new(:title, :href, :current, keyword_init: true)
8
+
9
+ attr_reader :sidebar_tree, :current_path
10
+
11
+ def initialize(sidebar_tree:, current_path:)
12
+ @sidebar_tree = sidebar_tree || []
13
+ @current_path = normalize_path(current_path)
14
+ end
15
+
16
+ def items
17
+ @items ||= build_items
18
+ end
19
+
20
+ def truncated?
21
+ full_path_items.length > MAX_VISIBLE_ITEMS
22
+ end
23
+
24
+ def should_show?
25
+ items.any? && !root_page?
26
+ end
27
+
28
+ private
29
+
30
+ def build_items
31
+ return [] if full_path_items.empty?
32
+
33
+ if truncated?
34
+ truncated_items
35
+ else
36
+ full_path_items
37
+ end
38
+ end
39
+
40
+ def truncated_items
41
+ path = full_path_items
42
+ [
43
+ Item.new(title: "...", href: nil, current: false),
44
+ path[-2],
45
+ path[-1]
46
+ ].compact
47
+ end
48
+
49
+ def full_path_items
50
+ @full_path_items ||= find_breadcrumb_path(sidebar_tree, [])
51
+ end
52
+
53
+ def find_breadcrumb_path(nodes, path)
54
+ nodes.each do |node|
55
+ result = process_node(node, path)
56
+ return result if result
57
+ end
58
+
59
+ []
60
+ end
61
+
62
+ def process_node(node, path)
63
+ node_path = normalize_path(node[:path])
64
+ node_title = truncate_title(node[:title] || "")
65
+
66
+ return build_current_item(path, node_title, node_path) if exact_match?(node_path)
67
+
68
+ search_in_ancestors(node, path, node_title, node_path) ||
69
+ search_in_children(node, path)
70
+ end
71
+
72
+ def build_current_item(path, title, href)
73
+ path + [Item.new(title: title, href: href, current: true)]
74
+ end
75
+
76
+ def search_in_ancestors(node, path, title, href)
77
+ return unless node[:children]&.any?
78
+
79
+ effective_href = href == "/" ? derive_section_path(node) : href
80
+ return unless path_is_ancestor?(effective_href)
81
+
82
+ result = find_breadcrumb_path(
83
+ node[:children],
84
+ path + [Item.new(title: title, href: effective_href, current: false)]
85
+ )
86
+ result.any? ? result : nil
87
+ end
88
+
89
+ def derive_section_path(node)
90
+ first_child = node[:children]&.first
91
+ return nil unless first_child
92
+
93
+ child_path = first_child[:path]
94
+ return nil if child_path.nil? || child_path.empty?
95
+
96
+ File.dirname(child_path)
97
+ end
98
+
99
+ def search_in_children(node, path)
100
+ return unless node[:children]&.any?
101
+
102
+ result = find_breadcrumb_path(node[:children], path)
103
+ result.any? ? result : nil
104
+ end
105
+
106
+ def exact_match?(node_path)
107
+ normalize_path(node_path) == current_path
108
+ end
109
+
110
+ def path_is_ancestor?(node_path)
111
+ return false if node_path.nil? || node_path.empty? || node_path == "/"
112
+
113
+ normalized = normalize_path(node_path)
114
+ current_path.start_with?("#{normalized}/") || current_path == normalized
115
+ end
116
+
117
+ def normalize_path(path)
118
+ return "/" if path.nil? || path.empty?
119
+
120
+ path.chomp("/")
121
+ end
122
+
123
+ def truncate_title(title)
124
+ return title if title.length <= 30
125
+
126
+ "#{title[0, 27]}..."
127
+ end
128
+
129
+ def root_page?
130
+ current_path == "/" || current_path.empty?
131
+ end
132
+ end
133
+ end