docyard 0.8.0 → 1.0.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 (189) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -1
  3. data/README.md +8 -253
  4. data/exe/docyard +6 -0
  5. data/lib/docyard/build/asset_bundler.rb +2 -2
  6. data/lib/docyard/build/file_copier.rb +12 -5
  7. data/lib/docyard/build/llms_txt_generator.rb +103 -0
  8. data/lib/docyard/build/sitemap_generator.rb +1 -1
  9. data/lib/docyard/build/static_generator.rb +115 -79
  10. data/lib/docyard/builder.rb +6 -2
  11. data/lib/docyard/cli.rb +14 -4
  12. data/lib/docyard/components/aliases.rb +12 -0
  13. data/lib/docyard/components/processors/abbreviation_processor.rb +72 -0
  14. data/lib/docyard/components/processors/accordion_processor.rb +81 -0
  15. data/lib/docyard/components/processors/badge_processor.rb +72 -0
  16. data/lib/docyard/components/processors/callout_processor.rb +9 -3
  17. data/lib/docyard/components/processors/cards_processor.rb +100 -0
  18. data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
  19. data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
  20. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +34 -3
  21. data/lib/docyard/components/processors/code_block_processor.rb +11 -24
  22. data/lib/docyard/components/processors/code_group_processor.rb +182 -0
  23. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +7 -1
  24. data/lib/docyard/components/processors/custom_anchor_processor.rb +42 -0
  25. data/lib/docyard/components/processors/file_tree_processor.rb +150 -0
  26. data/lib/docyard/components/processors/icon_processor.rb +8 -2
  27. data/lib/docyard/components/processors/image_caption_processor.rb +96 -0
  28. data/lib/docyard/components/processors/include_processor.rb +86 -0
  29. data/lib/docyard/components/processors/steps_processor.rb +89 -0
  30. data/lib/docyard/components/processors/tabs_processor.rb +9 -1
  31. data/lib/docyard/components/processors/tooltip_processor.rb +57 -0
  32. data/lib/docyard/components/processors/video_embed_processor.rb +207 -0
  33. data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
  34. data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
  35. data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
  36. data/lib/docyard/components/support/code_detector.rb +2 -12
  37. data/lib/docyard/components/support/code_group/html_builder.rb +118 -0
  38. data/lib/docyard/components/support/markdown_code_block_helper.rb +56 -0
  39. data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
  40. data/lib/docyard/components/support/tabs/parser.rb +6 -23
  41. data/lib/docyard/config/analytics_resolver.rb +24 -0
  42. data/lib/docyard/config/branding_resolver.rb +84 -58
  43. data/lib/docyard/config/key_validator.rb +30 -0
  44. data/lib/docyard/config/logo_detector.rb +39 -0
  45. data/lib/docyard/config/schema.rb +39 -0
  46. data/lib/docyard/config/section.rb +21 -0
  47. data/lib/docyard/config/validation_helpers.rb +83 -0
  48. data/lib/docyard/config/validator.rb +45 -144
  49. data/lib/docyard/config/validators/navigation.rb +43 -0
  50. data/lib/docyard/config/validators/section.rb +114 -0
  51. data/lib/docyard/config.rb +45 -96
  52. data/lib/docyard/constants.rb +59 -0
  53. data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
  54. data/lib/docyard/initializer.rb +100 -49
  55. data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
  56. data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
  57. data/lib/docyard/navigation/sidebar/cache.rb +96 -0
  58. data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
  59. data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
  60. data/lib/docyard/navigation/sidebar/item.rb +6 -1
  61. data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
  62. data/lib/docyard/navigation/sidebar/renderer.rb +18 -3
  63. data/lib/docyard/navigation/sidebar_builder.rb +43 -81
  64. data/lib/docyard/rendering/branding_variables.rb +65 -0
  65. data/lib/docyard/rendering/icon_helpers.rb +14 -1
  66. data/lib/docyard/rendering/icons/devicons.rb +63 -0
  67. data/lib/docyard/rendering/icons.rb +26 -27
  68. data/lib/docyard/rendering/markdown.rb +20 -15
  69. data/lib/docyard/rendering/og_helpers.rb +36 -0
  70. data/lib/docyard/rendering/renderer.rb +87 -58
  71. data/lib/docyard/rendering/template_resolver.rb +14 -0
  72. data/lib/docyard/routing/fallback_resolver.rb +3 -3
  73. data/lib/docyard/search/build_indexer.rb +2 -2
  74. data/lib/docyard/search/dev_indexer.rb +36 -28
  75. data/lib/docyard/search/pagefind_support.rb +1 -1
  76. data/lib/docyard/server/asset_handler.rb +40 -15
  77. data/lib/docyard/server/dev_server.rb +90 -55
  78. data/lib/docyard/server/file_watcher.rb +68 -18
  79. data/lib/docyard/server/pagefind_handler.rb +1 -1
  80. data/lib/docyard/server/preview_server.rb +29 -33
  81. data/lib/docyard/server/rack_application.rb +38 -70
  82. data/lib/docyard/server/router.rb +11 -7
  83. data/lib/docyard/server/sse_server.rb +157 -0
  84. data/lib/docyard/server/static_file_app.rb +42 -0
  85. data/lib/docyard/templates/assets/css/components/abbreviation.css +86 -0
  86. data/lib/docyard/templates/assets/css/components/accordion.css +138 -0
  87. data/lib/docyard/templates/assets/css/components/badges.css +47 -0
  88. data/lib/docyard/templates/assets/css/components/banner.css +233 -0
  89. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
  90. data/lib/docyard/templates/assets/css/components/callout.css +26 -6
  91. data/lib/docyard/templates/assets/css/components/cards.css +100 -0
  92. data/lib/docyard/templates/assets/css/components/code-block.css +14 -2
  93. data/lib/docyard/templates/assets/css/components/code-group.css +294 -0
  94. data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
  95. data/lib/docyard/templates/assets/css/components/figure.css +22 -0
  96. data/lib/docyard/templates/assets/css/components/file-tree.css +125 -0
  97. data/lib/docyard/templates/assets/css/components/heading-anchor.css +21 -13
  98. data/lib/docyard/templates/assets/css/components/icon.css +5 -0
  99. data/lib/docyard/templates/assets/css/components/lightbox.css +65 -0
  100. data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
  101. data/lib/docyard/templates/assets/css/components/navigation.css +32 -3
  102. data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
  103. data/lib/docyard/templates/assets/css/components/prev-next.css +20 -22
  104. data/lib/docyard/templates/assets/css/components/search.css +6 -10
  105. data/lib/docyard/templates/assets/css/components/steps.css +122 -0
  106. data/lib/docyard/templates/assets/css/components/tab-bar.css +7 -4
  107. data/lib/docyard/templates/assets/css/components/table-of-contents.css +57 -11
  108. data/lib/docyard/templates/assets/css/components/tabs.css +13 -5
  109. data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
  110. data/lib/docyard/templates/assets/css/components/tooltip.css +113 -0
  111. data/lib/docyard/templates/assets/css/components/video.css +41 -0
  112. data/lib/docyard/templates/assets/css/landing.css +82 -13
  113. data/lib/docyard/templates/assets/css/layout.css +17 -0
  114. data/lib/docyard/templates/assets/css/markdown.css +25 -3
  115. data/lib/docyard/templates/assets/css/variables.css +13 -1
  116. data/lib/docyard/templates/assets/js/components/abbreviation.js +85 -0
  117. data/lib/docyard/templates/assets/js/components/banner.js +81 -0
  118. data/lib/docyard/templates/assets/js/components/code-group.js +286 -0
  119. data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
  120. data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
  121. data/lib/docyard/templates/assets/js/components/file-tree.js +39 -0
  122. data/lib/docyard/templates/assets/js/components/lightbox.js +72 -0
  123. data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
  124. data/lib/docyard/templates/assets/js/components/search.js +3 -3
  125. data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
  126. data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
  127. data/lib/docyard/templates/assets/js/components/tooltip.js +118 -0
  128. data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
  129. data/lib/docyard/templates/errors/404.html.erb +114 -5
  130. data/lib/docyard/templates/errors/500.html.erb +173 -10
  131. data/lib/docyard/templates/init/_sidebar.yml +36 -0
  132. data/lib/docyard/templates/init/docyard.yml +36 -0
  133. data/lib/docyard/templates/init/pages/components.md +146 -0
  134. data/lib/docyard/templates/init/pages/getting-started.md +94 -0
  135. data/lib/docyard/templates/init/pages/index.md +22 -0
  136. data/lib/docyard/templates/layouts/default.html.erb +11 -0
  137. data/lib/docyard/templates/layouts/splash.html.erb +15 -1
  138. data/lib/docyard/templates/partials/_accordion.html.erb +9 -0
  139. data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
  140. data/lib/docyard/templates/partials/_banner.html.erb +27 -0
  141. data/lib/docyard/templates/partials/_card.html.erb +23 -0
  142. data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
  143. data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
  144. data/lib/docyard/templates/partials/_footer.html.erb +1 -1
  145. data/lib/docyard/templates/partials/_head.html.erb +79 -4
  146. data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
  147. data/lib/docyard/templates/partials/_nav_group.html.erb +6 -0
  148. data/lib/docyard/templates/partials/_nav_leaf.html.erb +3 -0
  149. data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
  150. data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
  151. data/lib/docyard/templates/partials/_step.html.erb +14 -0
  152. data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
  153. data/lib/docyard/utils/git_info.rb +157 -0
  154. data/lib/docyard/utils/hash_utils.rb +31 -0
  155. data/lib/docyard/utils/html_helpers.rb +8 -0
  156. data/lib/docyard/utils/logging.rb +44 -3
  157. data/lib/docyard/utils/path_resolver.rb +0 -10
  158. data/lib/docyard/utils/path_utils.rb +73 -0
  159. data/lib/docyard/version.rb +1 -1
  160. data/lib/docyard.rb +2 -2
  161. metadata +114 -47
  162. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  163. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  164. data/.github/pull_request_template.md +0 -14
  165. data/.github/workflows/ci.yml +0 -49
  166. data/.rubocop.yml +0 -42
  167. data/CODE_OF_CONDUCT.md +0 -132
  168. data/CONTRIBUTING.md +0 -55
  169. data/LICENSE.vscode-icons +0 -42
  170. data/Rakefile +0 -8
  171. data/lib/docyard/config/constants.rb +0 -31
  172. data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
  173. data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
  174. data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -78
  175. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
  176. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -69
  177. data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -47
  178. data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
  179. data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
  180. data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
  181. data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -139
  182. data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
  183. data/lib/docyard/rendering/icons/file_types.rb +0 -79
  184. data/lib/docyard/rendering/icons/phosphor.rb +0 -90
  185. data/lib/docyard/rendering/language_mapping.rb +0 -52
  186. data/lib/docyard/templates/assets/js/reload.js +0 -98
  187. data/lib/docyard/templates/partials/_icon.html.erb +0 -1
  188. data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
  189. data/sig/docyard.rbs +0 -4
@@ -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
@@ -1,35 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "yaml"
4
+ require_relative "config/section"
5
+ require_relative "config/schema"
4
6
  require_relative "config/validator"
5
- require_relative "config/constants"
7
+ require_relative "constants"
8
+ require_relative "utils/hash_utils"
6
9
 
7
10
  module Docyard
8
11
  class Config
12
+ SIDEBAR_MODES = %w[config auto distributed].freeze
13
+
9
14
  DEFAULT_CONFIG = {
10
15
  "title" => Constants::DEFAULT_SITE_TITLE,
11
16
  "description" => "",
12
- "branding" => {
13
- "logo" => nil,
14
- "favicon" => nil,
15
- "credits" => true,
16
- "copyright" => nil
17
- },
17
+ "url" => nil,
18
+ "og_image" => nil,
19
+ "twitter" => nil,
20
+ "source" => "docs",
21
+ "branding" => { "logo" => nil, "favicon" => nil, "credits" => true, "copyright" => nil, "color" => nil },
18
22
  "socials" => {},
19
23
  "tabs" => [],
20
- "build" => {
21
- "output" => "dist",
22
- "base" => "/"
23
- },
24
- "search" => {
25
- "enabled" => true,
26
- "placeholder" => "Search...",
27
- "exclude" => []
28
- },
29
- "navigation" => {
30
- "cta" => [],
31
- "breadcrumbs" => true
32
- }
24
+ "sidebar" => "config",
25
+ "build" => { "output" => "dist", "base" => "/" },
26
+ "search" => { "enabled" => true, "placeholder" => "Search...", "exclude" => [] },
27
+ "navigation" => { "cta" => [], "breadcrumbs" => true },
28
+ "announcement" => nil,
29
+ "repo" => { "url" => nil, "branch" => "main", "edit_path" => nil, "edit_link" => true,
30
+ "last_updated" => true },
31
+ "analytics" => { "google" => nil, "plausible" => nil, "fathom" => nil, "script" => nil },
32
+ "feedback" => { "enabled" => false, "question" => "Was this page helpful?" }
33
33
  }.freeze
34
34
 
35
35
  attr_reader :data, :file_path
@@ -49,83 +49,50 @@ module Docyard
49
49
  File.exist?(file_path)
50
50
  end
51
51
 
52
- def title
53
- data["title"]
54
- end
52
+ def title = data["title"]
53
+ def description = data["description"]
54
+ def url = data["url"]
55
+ def og_image = data["og_image"]
56
+ def twitter = data["twitter"]
57
+ def source = data["source"]
58
+ def public_dir = File.join(source, "public")
59
+ def socials = data["socials"]
60
+ def tabs = data["tabs"]
61
+ def sidebar = data["sidebar"]
55
62
 
56
- def description
57
- data["description"]
58
- end
63
+ def sidebar_config? = sidebar == "config"
64
+ def sidebar_auto? = sidebar == "auto"
65
+ def sidebar_distributed? = sidebar == "distributed"
59
66
 
60
- def branding
61
- @branding ||= ConfigSection.new(data["branding"])
62
- end
67
+ def branding = @branding ||= Section.new(data["branding"])
68
+ def build = @build ||= Section.new(data["build"])
69
+ def search = @search ||= Section.new(data["search"])
70
+ def navigation = @navigation ||= Section.new(data["navigation"])
71
+ def repo = @repo ||= Section.new(data["repo"])
72
+ def analytics = @analytics ||= Section.new(data["analytics"])
73
+ def feedback = @feedback ||= Section.new(data["feedback"])
63
74
 
64
- def socials
65
- data["socials"]
66
- end
67
-
68
- def tabs
69
- data["tabs"]
70
- end
71
-
72
- def build
73
- @build ||= ConfigSection.new(data["build"])
74
- end
75
-
76
- def search
77
- @search ||= ConfigSection.new(data["search"])
78
- end
79
-
80
- def navigation
81
- @navigation ||= ConfigSection.new(data["navigation"])
75
+ def announcement
76
+ @announcement ||= data["announcement"] ? Section.new(data["announcement"]) : nil
82
77
  end
83
78
 
84
79
  private
85
80
 
86
81
  def load_config_data
87
- if file_exists?
88
- load_and_merge_config
89
- else
90
- deep_dup(DEFAULT_CONFIG)
91
- end
82
+ file_exists? ? load_and_merge_config : Utils::HashUtils.deep_dup(DEFAULT_CONFIG)
92
83
  end
93
84
 
94
85
  def load_and_merge_config
95
86
  yaml_content = YAML.load_file(file_path)
96
- deep_merge(deep_dup(DEFAULT_CONFIG), yaml_content || {})
87
+ Utils::HashUtils.deep_merge(Utils::HashUtils.deep_dup(DEFAULT_CONFIG), yaml_content || {})
97
88
  rescue Psych::SyntaxError => e
98
89
  raise ConfigError, build_yaml_error_message(e)
99
90
  rescue StandardError => e
100
91
  raise ConfigError, "Error loading docyard.yml: #{e.message}"
101
92
  end
102
93
 
103
- def deep_merge(hash1, hash2)
104
- hash1.merge(hash2) do |_key, v1, v2|
105
- if v2.nil?
106
- v1
107
- elsif v1.is_a?(Hash) && v2.is_a?(Hash)
108
- deep_merge(v1, v2)
109
- else
110
- v2
111
- end
112
- end
113
- end
114
-
115
- def deep_dup(hash)
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
123
- end
124
-
125
94
  def build_yaml_error_message(error)
126
- message = "Invalid YAML in docyard.yml:\n\n"
127
- message += " #{error.message}\n\n"
128
- message += "Fix: Check YAML syntax"
95
+ message = "Invalid YAML in docyard.yml:\n\n #{error.message}\n\nFix: Check YAML syntax"
129
96
  message += " at line #{error.line}" if error.respond_to?(:line)
130
97
  message
131
98
  end
@@ -134,22 +101,4 @@ module Docyard
134
101
  Validator.new(data).validate!
135
102
  end
136
103
  end
137
-
138
- class ConfigSection
139
- def initialize(data)
140
- @data = data || {}
141
- end
142
-
143
- def method_missing(method, *args)
144
- return @data[method.to_s] if args.empty?
145
-
146
- super
147
- end
148
-
149
- def respond_to_missing?(method, include_private = false)
150
- @data.key?(method.to_s) || super
151
- end
152
- end
153
-
154
- class ConfigError < StandardError; end
155
104
  end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Constants
5
+ CONTENT_TYPE_HTML = "text/html; charset=utf-8"
6
+
7
+ DOCYARD_ASSETS_PREFIX = "/_docyard/"
8
+ PAGEFIND_PREFIX = "/_docyard/pagefind/"
9
+
10
+ INDEX_FILE = "index"
11
+ MARKDOWN_EXTENSION = ".md"
12
+
13
+ STATUS_OK = 200
14
+ STATUS_REDIRECT = 302
15
+ STATUS_NOT_FOUND = 404
16
+ STATUS_INTERNAL_ERROR = 500
17
+
18
+ DEFAULT_SITE_TITLE = "Documentation"
19
+ DEFAULT_LOGO_PATH = "_docyard/logo.svg"
20
+ DEFAULT_LOGO_DARK_PATH = "_docyard/logo-dark.svg"
21
+ DEFAULT_FAVICON_PATH = "_docyard/favicon.svg"
22
+
23
+ SOCIAL_ICON_MAP = {
24
+ "github" => "github-logo",
25
+ "x" => "x-logo",
26
+ "twitter" => "x-logo",
27
+ "discord" => "discord-logo",
28
+ "slack" => "slack-logo",
29
+ "linkedin" => "linkedin-logo",
30
+ "youtube" => "youtube-logo",
31
+ "twitch" => "twitch-logo",
32
+ "instagram" => "instagram-logo",
33
+ "facebook" => "facebook-logo",
34
+ "tiktok" => "tiktok-logo",
35
+ "reddit" => "reddit-logo",
36
+ "mastodon" => "mastodon-logo",
37
+ "threads" => "threads-logo",
38
+ "pinterest" => "pinterest-logo",
39
+ "medium" => "medium-logo",
40
+ "gitlab" => "gitlab-logo",
41
+ "figma" => "figma-logo",
42
+ "dribbble" => "dribbble-logo",
43
+ "behance" => "behance-logo",
44
+ "codepen" => "codepen-logo",
45
+ "codesandbox" => "codesandbox-logo",
46
+ "notion" => "notion-logo",
47
+ "spotify" => "spotify-logo",
48
+ "soundcloud" => "soundcloud-logo",
49
+ "whatsapp" => "whatsapp-logo",
50
+ "telegram" => "telegram-logo",
51
+ "snapchat" => "snapchat-logo",
52
+ "patreon" => "patreon-logo",
53
+ "paypal" => "paypal-logo",
54
+ "stripe" => "stripe-logo",
55
+ "google-podcasts" => "google-podcasts-logo",
56
+ "apple-podcasts" => "apple-podcasts-logo"
57
+ }.freeze
58
+ end
59
+ end
@@ -3,6 +3,10 @@
3
3
  module Docyard
4
4
  class Error < StandardError; end
5
5
 
6
+ class ConfigError < Error; end
7
+
8
+ class SidebarConfigError < Error; end
9
+
6
10
  class FileNotFoundError < Error
7
11
  attr_reader :path
8
12
 
@@ -51,4 +55,6 @@ module Docyard
51
55
  super("Asset not found: #{asset_path}")
52
56
  end
53
57
  end
58
+
59
+ class BuildError < Error; end
54
60
  end
@@ -5,19 +5,21 @@ require "fileutils"
5
5
  module Docyard
6
6
  class Initializer
7
7
  DOCS_DIR = "docs"
8
- CONFIG_TEMPLATE_DIR = File.join(__dir__, "templates", "config")
8
+ TEMPLATES_DIR = File.join(__dir__, "templates", "init")
9
9
 
10
- def initialize(path = ".")
11
- @path = path
12
- @docs_path = File.join(@path, DOCS_DIR)
10
+ attr_reader :project_name, :project_path, :docs_path, :force
11
+
12
+ def initialize(project_name = nil, force: false)
13
+ @project_name = project_name
14
+ @project_path = project_name ? File.join(".", project_name) : "."
15
+ @docs_path = File.join(@project_path, DOCS_DIR)
16
+ @force = force
13
17
  end
14
18
 
15
- def run
16
- if already_initialized?
17
- print_already_exists_error
18
- return
19
- end
19
+ def run # rubocop:disable Naming/PredicateMethod
20
+ return false unless check_existing_files
20
21
 
22
+ create_project_directory if project_name
21
23
  create_structure
22
24
  print_success
23
25
  true
@@ -25,74 +27,123 @@ module Docyard
25
27
 
26
28
  private
27
29
 
28
- def already_initialized?
29
- File.exist?(@docs_path)
30
+ def check_existing_files # rubocop:disable Naming/PredicateMethod
31
+ return true if force
32
+ return true unless files_exist?
33
+
34
+ print_existing_files_warning
35
+ return true if user_confirms_overwrite?
36
+
37
+ print_abort_message
38
+ false
30
39
  end
31
40
 
32
- def create_structure
33
- FileUtils.mkdir_p(@docs_path)
34
- create_index_file
35
- create_example_config
41
+ def files_exist?
42
+ File.exist?(docs_path) || File.exist?(config_path)
36
43
  end
37
44
 
38
- def create_index_file
39
- index_path = File.join(@docs_path, "index.md")
40
- content = <<~MARKDOWN
41
- ---
42
- title: Welcome
43
- ---
45
+ def config_path
46
+ File.join(project_path, "docyard.yml")
47
+ end
44
48
 
45
- # Welcome to Your Documentation
49
+ def user_confirms_overwrite?
50
+ print "\nOverwrite existing files? [y/N] "
51
+ response = $stdin.gets&.strip&.downcase
52
+ %w[y yes].include?(response)
53
+ end
46
54
 
47
- Start writing your documentation here.
48
- MARKDOWN
49
- File.write(index_path, content)
55
+ def print_existing_files_warning
56
+ puts ""
57
+ puts "\e[33mWarning:\e[0m Existing files found:"
58
+ puts " - #{docs_path}/" if File.exist?(docs_path)
59
+ puts " - #{config_path}" if File.exist?(config_path)
50
60
  end
51
61
 
52
- def create_example_config
53
- config_path = File.join(@path, "docyard.yml")
54
- return if File.exist?(config_path)
62
+ def print_abort_message
63
+ puts ""
64
+ puts "Aborted. Use \e[1m--force\e[0m to overwrite existing files."
65
+ end
55
66
 
56
- template_path = File.join(CONFIG_TEMPLATE_DIR, "docyard.yml.erb")
57
- config_content = File.read(template_path)
67
+ def create_project_directory
68
+ FileUtils.mkdir_p(project_path)
69
+ end
58
70
 
59
- File.write(config_path, config_content)
71
+ def create_structure
72
+ FileUtils.mkdir_p(docs_path)
73
+ FileUtils.mkdir_p(File.join(docs_path, "public"))
74
+ create_config_file
75
+ create_sidebar_file
76
+ create_starter_pages
60
77
  end
61
78
 
62
- def print_already_exists_error
63
- puts "Error: #{DOCS_DIR}/ folder already exists"
64
- puts " Remove it first or run docyard in a different directory"
79
+ def create_config_file
80
+ template = File.read(File.join(TEMPLATES_DIR, "docyard.yml"))
81
+ content = template.gsub("{{PROJECT_NAME}}", display_name)
82
+ File.write(config_path, content)
65
83
  end
66
84
 
67
- def print_success
68
- print_banner
69
- print_created_files
70
- print_next_steps
85
+ def create_sidebar_file
86
+ template = File.read(File.join(TEMPLATES_DIR, "_sidebar.yml"))
87
+ File.write(File.join(docs_path, "_sidebar.yml"), template)
88
+ end
89
+
90
+ def create_starter_pages
91
+ pages_dir = File.join(TEMPLATES_DIR, "pages")
92
+ Dir.glob(File.join(pages_dir, "*.md")).each do |template_path|
93
+ filename = File.basename(template_path)
94
+ content = File.read(template_path).gsub("{{PROJECT_NAME}}", display_name)
95
+ File.write(File.join(docs_path, filename), content)
96
+ end
97
+ end
98
+
99
+ def display_name
100
+ return "My Documentation" unless project_name
101
+
102
+ project_name.split(/[-_]/).map(&:capitalize).join(" ")
71
103
  end
72
104
 
73
- def print_banner
105
+ def print_success
74
106
  puts ""
75
- puts "Docyard initialized successfully"
107
+ puts "\e[32m#{success_icon} Docyard project initialized successfully!\e[0m"
76
108
  puts ""
109
+ print_created_structure
110
+ print_next_steps
111
+ end
112
+
113
+ def success_icon
114
+ "\u2714"
77
115
  end
78
116
 
79
- def print_created_files
80
- puts "Created files:"
117
+ def print_created_structure
118
+ puts "Created:"
81
119
  puts ""
82
- puts " docs/"
83
- puts " index.md"
84
- puts " docyard.yml"
120
+ if project_name
121
+ puts " \e[1m#{project_name}/\e[0m"
122
+ puts " \u251C\u2500\u2500 docyard.yml"
123
+ puts " \u2514\u2500\u2500 docs/"
124
+ else
125
+ puts " \e[1mdocyard.yml\e[0m"
126
+ puts " \e[1mdocs/\e[0m"
127
+ end
128
+ puts " \u251C\u2500\u2500 _sidebar.yml"
129
+ puts " \u251C\u2500\u2500 index.md"
130
+ puts " \u251C\u2500\u2500 getting-started.md"
131
+ puts " \u251C\u2500\u2500 components.md"
132
+ puts " \u2514\u2500\u2500 public/"
85
133
  puts ""
86
134
  end
87
135
 
88
136
  def print_next_steps
89
137
  puts "Next steps:"
90
138
  puts ""
91
- puts " Start development server:"
92
- puts " docyard serve"
139
+ if project_name
140
+ puts " \e[1mcd #{project_name}\e[0m"
141
+ puts ""
142
+ end
143
+ puts " Start the development server:"
144
+ puts " \e[1m$ docyard serve\e[0m"
93
145
  puts ""
94
- puts " Build for production:"
95
- puts " docyard build"
146
+ puts " Then open \e[4mhttp://localhost:4200\e[0m in your browser"
96
147
  puts ""
97
148
  end
98
149
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "sidebar_builder"
4
+ require_relative "prev_next_builder"
5
+ require_relative "breadcrumb_builder"
6
+
7
+ module Docyard
8
+ module Navigation
9
+ class PageNavigationBuilder
10
+ def initialize(docs_path:, config:, sidebar_cache: nil)
11
+ @docs_path = docs_path
12
+ @config = config
13
+ @sidebar_cache = sidebar_cache
14
+ end
15
+
16
+ def build(current_path:, markdown:, header_ctas: [], show_sidebar: true)
17
+ return empty_navigation unless show_sidebar
18
+
19
+ sidebar_builder = build_sidebar(current_path, header_ctas)
20
+ {
21
+ sidebar_html: sidebar_builder.to_html,
22
+ prev_next_html: build_prev_next(sidebar_builder, current_path, markdown),
23
+ breadcrumbs: build_breadcrumbs(sidebar_builder.tree, current_path)
24
+ }
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :docs_path, :config, :sidebar_cache
30
+
31
+ def empty_navigation
32
+ { sidebar_html: "", prev_next_html: "", breadcrumbs: nil }
33
+ end
34
+
35
+ def build_sidebar(current_path, header_ctas)
36
+ SidebarBuilder.new(
37
+ docs_path: docs_path,
38
+ current_path: current_path,
39
+ config: config,
40
+ header_ctas: header_ctas,
41
+ sidebar_cache: sidebar_cache
42
+ )
43
+ end
44
+
45
+ def build_prev_next(sidebar_builder, current_path, markdown)
46
+ PrevNextBuilder.new(
47
+ sidebar_tree: sidebar_builder.tree,
48
+ current_path: current_path,
49
+ frontmatter: markdown.frontmatter,
50
+ config: {}
51
+ ).to_html
52
+ end
53
+
54
+ def build_breadcrumbs(sidebar_tree, current_path)
55
+ return nil unless breadcrumbs_enabled?
56
+
57
+ BreadcrumbBuilder.new(sidebar_tree: sidebar_tree, current_path: current_path)
58
+ end
59
+
60
+ def breadcrumbs_enabled?
61
+ config&.navigation&.breadcrumbs != false
62
+ end
63
+ end
64
+ end
65
+ end