docyard 0.9.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/README.md +8 -253
  4. data/exe/docyard +6 -0
  5. data/lib/docyard/build/asset_bundler.rb +24 -2
  6. data/lib/docyard/build/error_page_generator.rb +33 -0
  7. data/lib/docyard/build/file_copier.rb +12 -5
  8. data/lib/docyard/build/file_writer.rb +19 -0
  9. data/lib/docyard/build/llms_txt_generator.rb +103 -0
  10. data/lib/docyard/build/root_fallback_generator.rb +66 -0
  11. data/lib/docyard/build/sitemap_generator.rb +1 -1
  12. data/lib/docyard/build/static_generator.rb +119 -81
  13. data/lib/docyard/builder.rb +6 -2
  14. data/lib/docyard/cli.rb +14 -4
  15. data/lib/docyard/components/processors/callout_processor.rb +1 -1
  16. data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
  17. data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
  18. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +11 -1
  19. data/lib/docyard/components/processors/code_block_processor.rb +5 -24
  20. data/lib/docyard/components/processors/code_group_processor.rb +6 -22
  21. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +1 -0
  22. data/lib/docyard/components/processors/file_tree_processor.rb +1 -2
  23. data/lib/docyard/components/processors/icon_processor.rb +8 -2
  24. data/lib/docyard/components/processors/include_processor.rb +10 -10
  25. data/lib/docyard/components/processors/video_embed_processor.rb +14 -3
  26. data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
  27. data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
  28. data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
  29. data/lib/docyard/components/support/code_detector.rb +2 -12
  30. data/lib/docyard/components/support/code_group/html_builder.rb +2 -6
  31. data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
  32. data/lib/docyard/components/support/tabs/parser.rb +6 -23
  33. data/lib/docyard/config/analytics_resolver.rb +24 -0
  34. data/lib/docyard/config/branding_resolver.rb +58 -27
  35. data/lib/docyard/config/key_validator.rb +30 -0
  36. data/lib/docyard/config/logo_detector.rb +8 -8
  37. data/lib/docyard/config/schema.rb +39 -0
  38. data/lib/docyard/config/section.rb +21 -0
  39. data/lib/docyard/config/validation_helpers.rb +83 -0
  40. data/lib/docyard/config/validator.rb +45 -144
  41. data/lib/docyard/config/validators/navigation.rb +43 -0
  42. data/lib/docyard/config/validators/section.rb +114 -0
  43. data/lib/docyard/config.rb +46 -102
  44. data/lib/docyard/constants.rb +59 -0
  45. data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
  46. data/lib/docyard/initializer.rb +100 -49
  47. data/lib/docyard/navigation/breadcrumb_builder.rb +45 -6
  48. data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
  49. data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
  50. data/lib/docyard/navigation/sidebar/cache.rb +96 -0
  51. data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
  52. data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
  53. data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
  54. data/lib/docyard/navigation/sidebar/renderer.rb +12 -1
  55. data/lib/docyard/navigation/sidebar_builder.rb +43 -81
  56. data/lib/docyard/rendering/branding_variables.rb +65 -0
  57. data/lib/docyard/rendering/icon_helpers.rb +14 -1
  58. data/lib/docyard/rendering/icons/devicons.rb +63 -0
  59. data/lib/docyard/rendering/icons.rb +26 -27
  60. data/lib/docyard/rendering/markdown.rb +5 -23
  61. data/lib/docyard/rendering/og_helpers.rb +36 -0
  62. data/lib/docyard/rendering/renderer.rb +96 -61
  63. data/lib/docyard/rendering/template_resolver.rb +14 -0
  64. data/lib/docyard/routing/fallback_resolver.rb +3 -3
  65. data/lib/docyard/search/build_indexer.rb +2 -2
  66. data/lib/docyard/search/dev_indexer.rb +36 -28
  67. data/lib/docyard/search/pagefind_support.rb +1 -1
  68. data/lib/docyard/server/asset_handler.rb +39 -15
  69. data/lib/docyard/server/dev_server.rb +90 -55
  70. data/lib/docyard/server/file_watcher.rb +68 -18
  71. data/lib/docyard/server/pagefind_handler.rb +1 -1
  72. data/lib/docyard/server/preview_server.rb +29 -33
  73. data/lib/docyard/server/rack_application.rb +39 -71
  74. data/lib/docyard/server/router.rb +11 -7
  75. data/lib/docyard/server/sse_server.rb +157 -0
  76. data/lib/docyard/server/static_file_app.rb +42 -0
  77. data/lib/docyard/templates/assets/css/components/banner.css +31 -0
  78. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
  79. data/lib/docyard/templates/assets/css/components/callout.css +26 -6
  80. data/lib/docyard/templates/assets/css/components/code-block.css +4 -2
  81. data/lib/docyard/templates/assets/css/components/code-group.css +20 -7
  82. data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
  83. data/lib/docyard/templates/assets/css/components/file-tree.css +5 -4
  84. data/lib/docyard/templates/assets/css/components/heading-anchor.css +2 -2
  85. data/lib/docyard/templates/assets/css/components/icon.css +5 -0
  86. data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
  87. data/lib/docyard/templates/assets/css/components/navigation.css +25 -3
  88. data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
  89. data/lib/docyard/templates/assets/css/components/prev-next.css +14 -7
  90. data/lib/docyard/templates/assets/css/components/search.css +6 -10
  91. data/lib/docyard/templates/assets/css/components/tab-bar.css +9 -6
  92. data/lib/docyard/templates/assets/css/components/table-of-contents.css +63 -17
  93. data/lib/docyard/templates/assets/css/components/tabs.css +12 -4
  94. data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
  95. data/lib/docyard/templates/assets/css/landing.css +82 -13
  96. data/lib/docyard/templates/assets/css/layout.css +32 -16
  97. data/lib/docyard/templates/assets/css/markdown.css +22 -2
  98. data/lib/docyard/templates/assets/css/variables.css +14 -1
  99. data/lib/docyard/templates/assets/js/components/code-group.js +4 -1
  100. data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
  101. data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
  102. data/lib/docyard/templates/assets/js/components/file-tree.js +5 -5
  103. data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
  104. data/lib/docyard/templates/assets/js/components/search.js +3 -3
  105. data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
  106. data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
  107. data/lib/docyard/templates/assets/js/components/tooltip.js +4 -4
  108. data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
  109. data/lib/docyard/templates/errors/404.html.erb +125 -5
  110. data/lib/docyard/templates/errors/500.html.erb +184 -10
  111. data/lib/docyard/templates/errors/redirect.html.erb +12 -0
  112. data/lib/docyard/templates/init/_sidebar.yml +36 -0
  113. data/lib/docyard/templates/init/docyard.yml +36 -0
  114. data/lib/docyard/templates/init/pages/components.md +146 -0
  115. data/lib/docyard/templates/init/pages/getting-started.md +94 -0
  116. data/lib/docyard/templates/init/pages/index.md +22 -0
  117. data/lib/docyard/templates/layouts/default.html.erb +10 -0
  118. data/lib/docyard/templates/layouts/splash.html.erb +14 -1
  119. data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
  120. data/lib/docyard/templates/partials/_banner.html.erb +1 -1
  121. data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
  122. data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
  123. data/lib/docyard/templates/partials/_footer.html.erb +1 -1
  124. data/lib/docyard/templates/partials/_head.html.erb +80 -5
  125. data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
  126. data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
  127. data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
  128. data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
  129. data/lib/docyard/utils/git_info.rb +157 -0
  130. data/lib/docyard/utils/hash_utils.rb +31 -0
  131. data/lib/docyard/utils/html_helpers.rb +8 -0
  132. data/lib/docyard/utils/logging.rb +44 -3
  133. data/lib/docyard/utils/path_resolver.rb +0 -10
  134. data/lib/docyard/utils/path_utils.rb +73 -0
  135. data/lib/docyard/version.rb +1 -1
  136. data/lib/docyard.rb +2 -2
  137. metadata +81 -47
  138. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  139. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  140. data/.github/pull_request_template.md +0 -14
  141. data/.github/workflows/ci.yml +0 -49
  142. data/.rubocop.yml +0 -42
  143. data/CODE_OF_CONDUCT.md +0 -132
  144. data/CONTRIBUTING.md +0 -55
  145. data/LICENSE.vscode-icons +0 -42
  146. data/Rakefile +0 -8
  147. data/lib/docyard/config/constants.rb +0 -31
  148. data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
  149. data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
  150. data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -90
  151. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
  152. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -71
  153. data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -51
  154. data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
  155. data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
  156. data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
  157. data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -140
  158. data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
  159. data/lib/docyard/rendering/icons/file_types.rb +0 -79
  160. data/lib/docyard/rendering/icons/phosphor.rb +0 -93
  161. data/lib/docyard/rendering/language_mapping.rb +0 -52
  162. data/lib/docyard/templates/assets/js/reload.js +0 -98
  163. data/lib/docyard/templates/partials/_icon.html.erb +0 -1
  164. data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
  165. data/sig/docyard.rbs +0 -4
@@ -1,36 +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
- },
33
- "announcement" => nil
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?" }
34
33
  }.freeze
35
34
 
36
35
  attr_reader :data, :file_path
@@ -50,87 +49,50 @@ module Docyard
50
49
  File.exist?(file_path)
51
50
  end
52
51
 
53
- def title
54
- data["title"]
55
- end
56
-
57
- def description
58
- data["description"]
59
- end
60
-
61
- def branding
62
- @branding ||= ConfigSection.new(data["branding"])
63
- end
64
-
65
- def socials
66
- data["socials"]
67
- end
68
-
69
- def tabs
70
- data["tabs"]
71
- end
72
-
73
- def build
74
- @build ||= ConfigSection.new(data["build"])
75
- end
76
-
77
- def search
78
- @search ||= ConfigSection.new(data["search"])
79
- end
80
-
81
- def navigation
82
- @navigation ||= ConfigSection.new(data["navigation"])
83
- 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"]
62
+
63
+ def sidebar_config? = sidebar == "config"
64
+ def sidebar_auto? = sidebar == "auto"
65
+ def sidebar_distributed? = sidebar == "distributed"
66
+
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"])
84
74
 
85
75
  def announcement
86
- @announcement ||= data["announcement"] ? ConfigSection.new(data["announcement"]) : nil
76
+ @announcement ||= data["announcement"] ? Section.new(data["announcement"]) : nil
87
77
  end
88
78
 
89
79
  private
90
80
 
91
81
  def load_config_data
92
- if file_exists?
93
- load_and_merge_config
94
- else
95
- deep_dup(DEFAULT_CONFIG)
96
- end
82
+ file_exists? ? load_and_merge_config : Utils::HashUtils.deep_dup(DEFAULT_CONFIG)
97
83
  end
98
84
 
99
85
  def load_and_merge_config
100
86
  yaml_content = YAML.load_file(file_path)
101
- deep_merge(deep_dup(DEFAULT_CONFIG), yaml_content || {})
87
+ Utils::HashUtils.deep_merge(Utils::HashUtils.deep_dup(DEFAULT_CONFIG), yaml_content || {})
102
88
  rescue Psych::SyntaxError => e
103
89
  raise ConfigError, build_yaml_error_message(e)
104
90
  rescue StandardError => e
105
91
  raise ConfigError, "Error loading docyard.yml: #{e.message}"
106
92
  end
107
93
 
108
- def deep_merge(hash1, hash2)
109
- hash1.merge(hash2) do |_key, v1, v2|
110
- if v2.nil?
111
- v1
112
- elsif v1.is_a?(Hash) && v2.is_a?(Hash)
113
- deep_merge(v1, v2)
114
- else
115
- v2
116
- end
117
- end
118
- end
119
-
120
- def deep_dup(hash)
121
- hash.transform_values do |value|
122
- case value
123
- when Hash then deep_dup(value)
124
- when Array then value.map { |v| v.is_a?(Hash) ? deep_dup(v) : v }
125
- else value
126
- end
127
- end
128
- end
129
-
130
94
  def build_yaml_error_message(error)
131
- message = "Invalid YAML in docyard.yml:\n\n"
132
- message += " #{error.message}\n\n"
133
- message += "Fix: Check YAML syntax"
95
+ message = "Invalid YAML in docyard.yml:\n\n #{error.message}\n\nFix: Check YAML syntax"
134
96
  message += " at line #{error.line}" if error.respond_to?(:line)
135
97
  message
136
98
  end
@@ -139,22 +101,4 @@ module Docyard
139
101
  Validator.new(data).validate!
140
102
  end
141
103
  end
142
-
143
- class ConfigSection
144
- def initialize(data)
145
- @data = data || {}
146
- end
147
-
148
- def method_missing(method, *args)
149
- return @data[method.to_s] if args.empty?
150
-
151
- super
152
- end
153
-
154
- def respond_to_missing?(method, include_private = false)
155
- @data.key?(method.to_s) || super
156
- end
157
- end
158
-
159
- class ConfigError < StandardError; end
160
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
@@ -76,9 +76,10 @@ module Docyard
76
76
  def search_in_ancestors(node, path, title, href)
77
77
  return unless node[:children]&.any?
78
78
 
79
- effective_href = href == "/" ? derive_section_path(node) : href
80
- return unless path_is_ancestor?(effective_href)
79
+ section_path = derive_section_path(node, href)
80
+ return unless path_is_ancestor?(section_path)
81
81
 
82
+ effective_href = resolve_section_href(node, href, section_path)
82
83
  result = find_breadcrumb_path(
83
84
  node[:children],
84
85
  path + [Item.new(title: title, href: effective_href, current: false)]
@@ -86,14 +87,52 @@ module Docyard
86
87
  result.any? ? result : nil
87
88
  end
88
89
 
89
- def derive_section_path(node)
90
- first_child = node[:children]&.first
90
+ def derive_section_path(node, href)
91
+ return href if section_has_index?(href)
92
+
93
+ derive_section_path_from_children(node[:children])
94
+ end
95
+
96
+ def derive_section_path_from_children(children)
97
+ first_child = children&.first
91
98
  return nil unless first_child
92
99
 
93
100
  child_path = first_child[:path]
94
- return nil if child_path.nil? || child_path.empty?
101
+ return nil unless navigable_path?(child_path)
102
+
103
+ parent_dir = File.dirname(child_path)
104
+ root_parent?(parent_dir) ? child_path : parent_dir
105
+ end
106
+
107
+ def root_parent?(parent_dir)
108
+ parent_dir == "/" || parent_dir.empty?
109
+ end
110
+
111
+ def resolve_section_href(node, href, section_path)
112
+ return href if section_has_index?(href)
113
+
114
+ find_first_navigable_child(node[:children]) || section_path
115
+ end
116
+
117
+ def section_has_index?(path)
118
+ path && !path.empty? && path != "/"
119
+ end
120
+
121
+ def find_first_navigable_child(children)
122
+ return nil unless children&.any?
123
+
124
+ children.each do |child|
125
+ return child[:path] if navigable_path?(child[:path])
126
+
127
+ nested = find_first_navigable_child(child[:children])
128
+ return nested if nested
129
+ end
130
+
131
+ nil
132
+ end
95
133
 
96
- File.dirname(child_path)
134
+ def navigable_path?(path)
135
+ path && !path.empty? && path != "/"
97
136
  end
98
137
 
99
138
  def search_in_children(node, path)
@@ -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