docyard 0.9.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 (159) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  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/processors/callout_processor.rb +1 -1
  13. data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
  14. data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
  15. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +11 -1
  16. data/lib/docyard/components/processors/code_block_processor.rb +5 -24
  17. data/lib/docyard/components/processors/code_group_processor.rb +6 -22
  18. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +1 -0
  19. data/lib/docyard/components/processors/file_tree_processor.rb +1 -2
  20. data/lib/docyard/components/processors/icon_processor.rb +8 -2
  21. data/lib/docyard/components/processors/include_processor.rb +10 -10
  22. data/lib/docyard/components/processors/video_embed_processor.rb +14 -3
  23. data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
  24. data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
  25. data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
  26. data/lib/docyard/components/support/code_detector.rb +2 -12
  27. data/lib/docyard/components/support/code_group/html_builder.rb +2 -6
  28. data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
  29. data/lib/docyard/components/support/tabs/parser.rb +6 -23
  30. data/lib/docyard/config/analytics_resolver.rb +24 -0
  31. data/lib/docyard/config/branding_resolver.rb +58 -27
  32. data/lib/docyard/config/key_validator.rb +30 -0
  33. data/lib/docyard/config/logo_detector.rb +8 -8
  34. data/lib/docyard/config/schema.rb +39 -0
  35. data/lib/docyard/config/section.rb +21 -0
  36. data/lib/docyard/config/validation_helpers.rb +83 -0
  37. data/lib/docyard/config/validator.rb +45 -144
  38. data/lib/docyard/config/validators/navigation.rb +43 -0
  39. data/lib/docyard/config/validators/section.rb +114 -0
  40. data/lib/docyard/config.rb +46 -102
  41. data/lib/docyard/constants.rb +59 -0
  42. data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
  43. data/lib/docyard/initializer.rb +100 -49
  44. data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
  45. data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
  46. data/lib/docyard/navigation/sidebar/cache.rb +96 -0
  47. data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
  48. data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
  49. data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
  50. data/lib/docyard/navigation/sidebar/renderer.rb +12 -1
  51. data/lib/docyard/navigation/sidebar_builder.rb +43 -81
  52. data/lib/docyard/rendering/branding_variables.rb +65 -0
  53. data/lib/docyard/rendering/icon_helpers.rb +14 -1
  54. data/lib/docyard/rendering/icons/devicons.rb +63 -0
  55. data/lib/docyard/rendering/icons.rb +26 -27
  56. data/lib/docyard/rendering/markdown.rb +5 -23
  57. data/lib/docyard/rendering/og_helpers.rb +36 -0
  58. data/lib/docyard/rendering/renderer.rb +87 -59
  59. data/lib/docyard/rendering/template_resolver.rb +14 -0
  60. data/lib/docyard/routing/fallback_resolver.rb +3 -3
  61. data/lib/docyard/search/build_indexer.rb +2 -2
  62. data/lib/docyard/search/dev_indexer.rb +36 -28
  63. data/lib/docyard/search/pagefind_support.rb +1 -1
  64. data/lib/docyard/server/asset_handler.rb +39 -15
  65. data/lib/docyard/server/dev_server.rb +90 -55
  66. data/lib/docyard/server/file_watcher.rb +68 -18
  67. data/lib/docyard/server/pagefind_handler.rb +1 -1
  68. data/lib/docyard/server/preview_server.rb +29 -33
  69. data/lib/docyard/server/rack_application.rb +38 -70
  70. data/lib/docyard/server/router.rb +11 -7
  71. data/lib/docyard/server/sse_server.rb +157 -0
  72. data/lib/docyard/server/static_file_app.rb +42 -0
  73. data/lib/docyard/templates/assets/css/components/banner.css +31 -0
  74. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
  75. data/lib/docyard/templates/assets/css/components/callout.css +26 -6
  76. data/lib/docyard/templates/assets/css/components/code-block.css +4 -2
  77. data/lib/docyard/templates/assets/css/components/code-group.css +20 -7
  78. data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
  79. data/lib/docyard/templates/assets/css/components/file-tree.css +5 -4
  80. data/lib/docyard/templates/assets/css/components/icon.css +5 -0
  81. data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
  82. data/lib/docyard/templates/assets/css/components/navigation.css +25 -3
  83. data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
  84. data/lib/docyard/templates/assets/css/components/prev-next.css +14 -7
  85. data/lib/docyard/templates/assets/css/components/search.css +6 -10
  86. data/lib/docyard/templates/assets/css/components/tab-bar.css +7 -4
  87. data/lib/docyard/templates/assets/css/components/table-of-contents.css +57 -11
  88. data/lib/docyard/templates/assets/css/components/tabs.css +12 -4
  89. data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
  90. data/lib/docyard/templates/assets/css/landing.css +82 -13
  91. data/lib/docyard/templates/assets/css/layout.css +17 -0
  92. data/lib/docyard/templates/assets/css/markdown.css +22 -2
  93. data/lib/docyard/templates/assets/css/variables.css +13 -1
  94. data/lib/docyard/templates/assets/js/components/code-group.js +4 -1
  95. data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
  96. data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
  97. data/lib/docyard/templates/assets/js/components/file-tree.js +5 -5
  98. data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
  99. data/lib/docyard/templates/assets/js/components/search.js +3 -3
  100. data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
  101. data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
  102. data/lib/docyard/templates/assets/js/components/tooltip.js +4 -4
  103. data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
  104. data/lib/docyard/templates/errors/404.html.erb +114 -5
  105. data/lib/docyard/templates/errors/500.html.erb +173 -10
  106. data/lib/docyard/templates/init/_sidebar.yml +36 -0
  107. data/lib/docyard/templates/init/docyard.yml +36 -0
  108. data/lib/docyard/templates/init/pages/components.md +146 -0
  109. data/lib/docyard/templates/init/pages/getting-started.md +94 -0
  110. data/lib/docyard/templates/init/pages/index.md +22 -0
  111. data/lib/docyard/templates/layouts/default.html.erb +10 -0
  112. data/lib/docyard/templates/layouts/splash.html.erb +14 -1
  113. data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
  114. data/lib/docyard/templates/partials/_banner.html.erb +1 -1
  115. data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
  116. data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
  117. data/lib/docyard/templates/partials/_footer.html.erb +1 -1
  118. data/lib/docyard/templates/partials/_head.html.erb +79 -4
  119. data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
  120. data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
  121. data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
  122. data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
  123. data/lib/docyard/utils/git_info.rb +157 -0
  124. data/lib/docyard/utils/hash_utils.rb +31 -0
  125. data/lib/docyard/utils/html_helpers.rb +8 -0
  126. data/lib/docyard/utils/logging.rb +44 -3
  127. data/lib/docyard/utils/path_resolver.rb +0 -10
  128. data/lib/docyard/utils/path_utils.rb +73 -0
  129. data/lib/docyard/version.rb +1 -1
  130. data/lib/docyard.rb +2 -2
  131. metadata +77 -47
  132. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  133. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  134. data/.github/pull_request_template.md +0 -14
  135. data/.github/workflows/ci.yml +0 -49
  136. data/.rubocop.yml +0 -42
  137. data/CODE_OF_CONDUCT.md +0 -132
  138. data/CONTRIBUTING.md +0 -55
  139. data/LICENSE.vscode-icons +0 -42
  140. data/Rakefile +0 -8
  141. data/lib/docyard/config/constants.rb +0 -31
  142. data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
  143. data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
  144. data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -90
  145. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
  146. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -71
  147. data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -51
  148. data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
  149. data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
  150. data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
  151. data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -140
  152. data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
  153. data/lib/docyard/rendering/icons/file_types.rb +0 -79
  154. data/lib/docyard/rendering/icons/phosphor.rb +0 -93
  155. data/lib/docyard/rendering/language_mapping.rb +0 -52
  156. data/lib/docyard/templates/assets/js/reload.js +0 -98
  157. data/lib/docyard/templates/partials/_icon.html.erb +0 -1
  158. data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
  159. data/sig/docyard.rbs +0 -4
@@ -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
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "item"
4
+
5
+ module Docyard
6
+ module Sidebar
7
+ class AutoBuilder
8
+ attr_reader :docs_path, :current_path
9
+
10
+ def initialize(docs_path, current_path: "/")
11
+ @docs_path = docs_path
12
+ @current_path = Utils::PathResolver.normalize(current_path)
13
+ end
14
+
15
+ def build
16
+ return [] unless File.directory?(docs_path)
17
+
18
+ scan_directory("").map(&:to_h)
19
+ end
20
+
21
+ private
22
+
23
+ def scan_directory(relative_path, depth: 1)
24
+ full_path = File.join(docs_path, relative_path)
25
+ return [] unless File.directory?(full_path)
26
+
27
+ entries = sorted_entries(full_path, relative_path)
28
+ entries.map { |entry| build_item(entry, relative_path, depth) }.compact
29
+ end
30
+
31
+ def sorted_entries(full_path, relative_path)
32
+ Dir.children(full_path)
33
+ .reject { |entry| ignored_entry?(entry, relative_path) }
34
+ .sort_by(&:downcase)
35
+ end
36
+
37
+ def build_item(entry, relative_path, depth)
38
+ entry_relative_path = build_relative_path(relative_path, entry)
39
+ entry_full_path = File.join(docs_path, entry_relative_path)
40
+
41
+ if File.directory?(entry_full_path)
42
+ build_directory_item(entry, entry_relative_path, depth)
43
+ elsif entry.end_with?(".md")
44
+ build_file_item(entry, entry_relative_path)
45
+ end
46
+ end
47
+
48
+ def build_relative_path(relative_path, entry)
49
+ relative_path.empty? ? entry : File.join(relative_path, entry)
50
+ end
51
+
52
+ def build_directory_item(name, relative_path, depth)
53
+ children = scan_directory(relative_path, depth: depth + 1)
54
+ return nil if children.empty?
55
+
56
+ url_path = "/#{relative_path}"
57
+ has_index = File.file?(File.join(docs_path, relative_path, "index.md"))
58
+
59
+ Item.new(
60
+ slug: name,
61
+ text: Utils::TextFormatter.titleize(name),
62
+ path: has_index ? url_path : nil,
63
+ type: :directory,
64
+ section: depth == 1,
65
+ collapsed: depth > 1 && !child_active?(children),
66
+ has_index: has_index,
67
+ active: has_index && current_path == url_path,
68
+ items: children
69
+ )
70
+ end
71
+
72
+ def build_file_item(filename, relative_path)
73
+ slug = filename.delete_suffix(".md")
74
+ url_path = "/#{relative_path.delete_suffix('.md')}"
75
+
76
+ Item.new(
77
+ slug: slug,
78
+ text: Utils::TextFormatter.titleize(slug),
79
+ path: url_path,
80
+ type: :file,
81
+ section: false,
82
+ active: current_path == url_path,
83
+ items: []
84
+ )
85
+ end
86
+
87
+ def ignored_entry?(entry, relative_path)
88
+ entry.start_with?(".") ||
89
+ entry.start_with?("_") ||
90
+ root_index?(entry, relative_path) ||
91
+ public_folder?(entry, relative_path)
92
+ end
93
+
94
+ def root_index?(entry, relative_path)
95
+ entry == "index.md" && relative_path.empty?
96
+ end
97
+
98
+ def public_folder?(entry, relative_path)
99
+ entry == "public" && relative_path.empty?
100
+ end
101
+
102
+ def child_active?(children)
103
+ children.any? { |child| child.active || child_active?(child.items) }
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "config_builder"
4
+ require_relative "auto_builder"
5
+ require_relative "distributed_builder"
6
+ require_relative "local_config_loader"
7
+
8
+ module Docyard
9
+ module Sidebar
10
+ class Cache
11
+ attr_reader :docs_path, :config, :tree, :built_at
12
+
13
+ def initialize(docs_path:, config:)
14
+ @docs_path = docs_path
15
+ @config = config
16
+ @tree = nil
17
+ @built_at = nil
18
+ end
19
+
20
+ def build
21
+ @tree = build_tree
22
+ @built_at = Time.now
23
+ @tree
24
+ end
25
+
26
+ def get(current_path: "/")
27
+ return nil unless @tree
28
+
29
+ mark_active_items(@tree, current_path)
30
+ end
31
+
32
+ def invalidate
33
+ @tree = nil
34
+ @built_at = nil
35
+ end
36
+
37
+ def valid?
38
+ !@tree.nil?
39
+ end
40
+
41
+ private
42
+
43
+ def build_tree
44
+ case config.sidebar
45
+ when "auto"
46
+ AutoBuilder.new(docs_path, current_path: "/").build
47
+ when "distributed"
48
+ DistributedBuilder.new(docs_path, current_path: "/").build
49
+ else
50
+ build_config_tree
51
+ end
52
+ end
53
+
54
+ def build_config_tree
55
+ config_items = LocalConfigLoader.new(docs_path).load
56
+ return [] unless config_items
57
+
58
+ ConfigBuilder.new(config_items, current_path: "/").build
59
+ end
60
+
61
+ def mark_active_items(items, current_path)
62
+ deep_copy_with_active(items, current_path)
63
+ end
64
+
65
+ def deep_copy_with_active(items, current_path)
66
+ items.map do |item|
67
+ copied = item.dup
68
+ copied[:active] = path_matches?(copied[:path], current_path)
69
+ copied[:children] = deep_copy_with_active(item[:children] || [], current_path)
70
+ copied[:collapsed] = determine_collapsed_for_copy(copied)
71
+ copied
72
+ end
73
+ end
74
+
75
+ def path_matches?(item_path, current_path)
76
+ return false if item_path.nil?
77
+
78
+ normalized_item = Utils::PathResolver.normalize(item_path)
79
+ normalized_current = Utils::PathResolver.normalize(current_path)
80
+ normalized_item == normalized_current
81
+ end
82
+
83
+ def determine_collapsed_for_copy(item)
84
+ return false if item[:section]
85
+ return false if item[:active]
86
+ return false if child_active?(item[:children] || [])
87
+
88
+ item[:collapsed]
89
+ end
90
+
91
+ def child_active?(children)
92
+ children.any? { |child| child[:active] || child_active?(child[:children] || []) }
93
+ end
94
+ end
95
+ end
96
+ end