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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "renderer"
4
- require_relative "utils/path_resolver"
3
+ require_relative "../rendering/renderer"
4
+ require_relative "../utils/path_resolver"
5
5
 
6
6
  module Docyard
7
7
  class PrevNextBuilder
@@ -124,7 +124,10 @@ module Docyard
124
124
  end
125
125
 
126
126
  def valid_navigation_item?(item)
127
- item[:type] == :file && item[:path] && !external_link?(item[:path])
127
+ return false unless item[:path]
128
+ return false if external_link?(item[:path])
129
+
130
+ item[:type] == :file || (item[:type] == :directory && item[:has_index])
128
131
  end
129
132
 
130
133
  def build_link(item)
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Sidebar
5
+ class ChildrenDiscoverer
6
+ attr_reader :docs_path
7
+
8
+ def initialize(docs_path:)
9
+ @docs_path = docs_path
10
+ end
11
+
12
+ def discover(relative_path, depth:, &item_builder)
13
+ full_path = File.join(docs_path, relative_path)
14
+ return [] unless File.directory?(full_path)
15
+
16
+ local_config = load_local_sidebar_config(full_path)
17
+ return yield(local_config, relative_path, depth) if local_config
18
+
19
+ discover_from_filesystem(full_path, relative_path, depth, &item_builder)
20
+ end
21
+
22
+ private
23
+
24
+ def load_local_sidebar_config(dir_path)
25
+ LocalConfigLoader.new(dir_path).load
26
+ end
27
+
28
+ def discover_from_filesystem(full_path, relative_path, depth, &item_builder)
29
+ entries = filtered_entries(full_path)
30
+ entries.map { |entry| build_entry(entry, full_path, relative_path, depth, &item_builder) }.compact
31
+ end
32
+
33
+ def filtered_entries(full_path)
34
+ Dir.children(full_path)
35
+ .reject { |e| e.start_with?(".") || e.start_with?("_") || e == "index.md" }
36
+ .sort
37
+ end
38
+
39
+ def build_entry(entry, full_path, relative_path, depth)
40
+ entry_path = File.join(full_path, entry)
41
+
42
+ if File.directory?(entry_path)
43
+ yield(:directory, entry, relative_path, depth)
44
+ elsif entry.end_with?(".md")
45
+ slug = entry.delete_suffix(".md")
46
+ yield(:file, slug, relative_path, depth)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "item"
4
+ require_relative "title_extractor"
5
+ require_relative "metadata_extractor"
6
+ require_relative "children_discoverer"
7
+ require_relative "file_resolver"
8
+
9
+ module Docyard
10
+ module Sidebar
11
+ class ConfigParser # rubocop:disable Metrics/ClassLength
12
+ attr_reader :config_items, :docs_path, :current_path, :metadata_extractor,
13
+ :children_discoverer, :file_resolver
14
+
15
+ def initialize(config_items, docs_path:, current_path: "/", title_extractor: TitleExtractor.new)
16
+ @config_items = config_items || []
17
+ @docs_path = docs_path
18
+ @current_path = Utils::PathResolver.normalize(current_path)
19
+ @metadata_extractor = MetadataExtractor.new(docs_path: docs_path, title_extractor: title_extractor)
20
+ @children_discoverer = ChildrenDiscoverer.new(docs_path: docs_path)
21
+ @file_resolver = FileResolver.new(
22
+ docs_path: docs_path, current_path: @current_path, metadata_extractor: metadata_extractor
23
+ )
24
+ end
25
+
26
+ def parse
27
+ parse_items(config_items, "", depth: 1)
28
+ end
29
+
30
+ private
31
+
32
+ def parse_items(items, base_path, depth:)
33
+ items.map { |item_config| parse_item(item_config, base_path, depth: depth) }.compact
34
+ end
35
+
36
+ def parse_item(item_config, base_path, depth:)
37
+ case item_config
38
+ when String then resolve_string_item(item_config, base_path, depth: depth)
39
+ when Hash then parse_hash_item(item_config, base_path, depth: depth)
40
+ end
41
+ end
42
+
43
+ def resolve_string_item(slug, base_path, depth:)
44
+ if File.directory?(File.join(docs_path, base_path, slug))
45
+ build_directory_item(slug, {}, [], base_path, depth: depth)
46
+ else
47
+ file_resolver.resolve(slug, base_path)
48
+ end
49
+ end
50
+
51
+ def parse_hash_item(item_config, base_path, depth:)
52
+ return file_resolver.build_link_item(item_config) if external_link?(item_config)
53
+
54
+ slug = item_config.keys.first
55
+ options = item_config.values.first
56
+
57
+ return file_resolver.resolve(slug, base_path, {}) if options.nil?
58
+ return parse_nested_item(slug, options, base_path, depth: depth) if options.is_a?(Hash)
59
+
60
+ file_resolver.resolve(slug, base_path, options)
61
+ end
62
+
63
+ def external_link?(config)
64
+ config.key?("link") || config.key?(:link)
65
+ end
66
+
67
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
68
+ def parse_nested_item(slug, options, base_path, depth:)
69
+ slug = slug.to_s
70
+ nested_items = options["items"] || options[:items] || []
71
+ dir_path = File.join(docs_path, base_path, slug)
72
+ is_virtual_group = (options["section"] == false || options[:section] == false) && nested_items.any?
73
+
74
+ if is_virtual_group
75
+ build_virtual_group_item(slug, options, nested_items, base_path, depth: depth)
76
+ elsif File.directory?(dir_path)
77
+ build_directory_item(slug, options, nested_items, base_path, depth: depth)
78
+ elsif nested_items.any?
79
+ build_file_with_children_item(slug, options, nested_items, base_path, depth: depth)
80
+ else
81
+ file_resolver.resolve(slug, base_path, options)
82
+ end
83
+ end
84
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
85
+
86
+ def build_virtual_group_item(slug, options, nested_items, base_path, depth:)
87
+ common_opts = metadata_extractor.extract_common_options(options)
88
+ parsed_items = parse_items(nested_items, base_path, depth: depth + 1)
89
+ is_collapsed = common_opts[:collapsed] != false && !active_child?(parsed_items)
90
+
91
+ Item.new(
92
+ slug: slug,
93
+ text: common_opts[:text] || Utils::TextFormatter.titleize(slug),
94
+ path: nil,
95
+ icon: common_opts[:icon],
96
+ collapsed: is_collapsed,
97
+ active: false,
98
+ items: parsed_items,
99
+ type: :directory,
100
+ section: false
101
+ )
102
+ end
103
+
104
+ def build_directory_item(slug, options, nested_items, base_path, depth:)
105
+ context = build_directory_context(slug, options, nested_items, base_path, depth)
106
+ context[:parsed_items] = prepend_intro_if_needed(context, depth)
107
+ create_directory_item(slug, context, depth)
108
+ end
109
+
110
+ def build_directory_context(slug, options, nested_items, base_path, depth)
111
+ new_base_path = File.join(base_path, slug)
112
+ {
113
+ common_opts: metadata_extractor.extract_common_options(options),
114
+ parsed_items: resolve_directory_children(nested_items, new_base_path, depth),
115
+ **build_index_info(new_base_path)
116
+ }
117
+ end
118
+
119
+ def build_index_info(base_path)
120
+ index_file_path = File.join(docs_path, base_path, "index.md")
121
+ has_index = File.file?(index_file_path)
122
+ url_path = has_index ? Utils::PathResolver.to_url(base_path) : nil
123
+
124
+ { index_file_path: index_file_path, has_index: has_index,
125
+ url_path: url_path, is_active: has_index && current_path == url_path }
126
+ end
127
+
128
+ def resolve_directory_children(nested_items, base_path, depth)
129
+ return parse_items(nested_items, base_path, depth: depth + 1) if nested_items.any?
130
+
131
+ auto_discover_children(base_path, depth: depth + 1)
132
+ end
133
+
134
+ def prepend_intro_if_needed(context, depth)
135
+ is_section = section_for_depth?(context[:common_opts][:section], depth)
136
+ return context[:parsed_items] unless is_section && context[:has_index]
137
+
138
+ [build_introduction_item(context[:index_file_path], context[:url_path])] + context[:parsed_items]
139
+ end
140
+
141
+ def create_directory_item(slug, context, depth)
142
+ is_section = section_for_depth?(context[:common_opts][:section], depth)
143
+ Item.new(
144
+ slug: slug,
145
+ text: context[:common_opts][:text] || Utils::TextFormatter.titleize(slug),
146
+ path: is_section ? nil : context[:url_path],
147
+ icon: context[:common_opts][:icon],
148
+ collapsed: is_section ? false : directory_collapsed?(context),
149
+ active: is_section ? false : context[:is_active],
150
+ has_index: is_section ? false : context[:has_index],
151
+ items: context[:parsed_items],
152
+ type: :directory,
153
+ section: is_section
154
+ )
155
+ end
156
+
157
+ def section_for_depth?(explicit_section, depth)
158
+ return explicit_section unless explicit_section.nil?
159
+
160
+ depth == 1
161
+ end
162
+
163
+ def directory_collapsed?(context)
164
+ return false if context[:is_active] || active_child?(context[:parsed_items])
165
+
166
+ context[:common_opts][:collapsed] != false
167
+ end
168
+
169
+ def build_introduction_item(index_file_path, url_path)
170
+ metadata = metadata_extractor.extract_index_metadata(index_file_path)
171
+ Item.new(
172
+ slug: "index", text: metadata[:sidebar_text] || "Overview",
173
+ path: url_path, icon: metadata[:icon], active: current_path == url_path, type: :file
174
+ )
175
+ end
176
+
177
+ def build_file_with_children_item(slug, options, nested_items, base_path, depth:)
178
+ file_resolver.build_file_with_children(
179
+ slug: slug, options: options, base_path: base_path,
180
+ parsed_items: parse_items(nested_items, base_path, depth: depth + 1),
181
+ depth: depth
182
+ )
183
+ end
184
+
185
+ def auto_discover_children(relative_path, depth:)
186
+ children_discoverer.discover(relative_path, depth: depth) do |config_or_type, *args|
187
+ if config_or_type.is_a?(Array)
188
+ parse_items(config_or_type, args[0], depth: args[1])
189
+ else
190
+ build_discovered_item(config_or_type, args[0], args[1], depth)
191
+ end
192
+ end
193
+ end
194
+
195
+ def build_discovered_item(type, slug, base_path, depth)
196
+ if type == :directory
197
+ build_directory_item(slug, {}, [], base_path, depth: depth)
198
+ else
199
+ file_resolver.resolve(slug, base_path)
200
+ end
201
+ end
202
+
203
+ def active_child?(items)
204
+ items.any? { |item| item.active || active_child?(item.items) }
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "item"
4
+
5
+ module Docyard
6
+ module Sidebar
7
+ class FileResolver
8
+ attr_reader :docs_path, :current_path, :metadata_extractor
9
+
10
+ def initialize(docs_path:, current_path:, metadata_extractor:)
11
+ @docs_path = docs_path
12
+ @current_path = current_path
13
+ @metadata_extractor = metadata_extractor
14
+ end
15
+
16
+ def resolve(slug, base_path, options = {})
17
+ context = build_context(slug.to_s, base_path, options || {})
18
+ Item.new(**context)
19
+ end
20
+
21
+ def build_link_item(config)
22
+ Item.new(
23
+ text: config["text"] || config[:text],
24
+ link: config["link"] || config[:link],
25
+ path: config["link"] || config[:link],
26
+ icon: config["icon"] || config[:icon],
27
+ target: config["target"] || config[:target] || "_blank",
28
+ type: :external,
29
+ section: false
30
+ )
31
+ end
32
+
33
+ def build_file_with_children(slug:, options:, base_path:, parsed_items:, depth: 1)
34
+ common_opts = metadata_extractor.extract_common_options(options)
35
+ file_path = File.join(docs_path, base_path, "#{slug}.md")
36
+ url_path = Utils::PathResolver.to_url(File.join(base_path, slug))
37
+ is_section = section_for_depth?(common_opts[:section], depth)
38
+
39
+ Item.new(
40
+ slug: slug,
41
+ text: common_opts[:text] || metadata_extractor.extract_file_title(file_path, slug),
42
+ path: is_section ? nil : url_path,
43
+ icon: common_opts[:icon],
44
+ collapsed: is_section ? false : common_opts[:collapsed],
45
+ items: parsed_items,
46
+ active: is_section ? false : current_path == url_path,
47
+ type: is_section ? :section : :file,
48
+ section: is_section
49
+ )
50
+ end
51
+
52
+ private
53
+
54
+ def section_for_depth?(explicit_section, depth)
55
+ return explicit_section unless explicit_section.nil?
56
+
57
+ depth == 1
58
+ end
59
+
60
+ def build_context(slug, base_path, options)
61
+ file_path = File.join(docs_path, base_path, "#{slug}.md")
62
+ url_path = Utils::PathResolver.to_url(File.join(base_path, slug))
63
+ frontmatter = metadata_extractor.extract_frontmatter_metadata(file_path)
64
+ final_path = options["link"] || options[:link] || url_path
65
+
66
+ {
67
+ slug: slug,
68
+ text: metadata_extractor.resolve_item_text(slug, file_path, options, frontmatter[:text]),
69
+ path: final_path,
70
+ icon: metadata_extractor.resolve_item_icon(options, frontmatter[:icon]),
71
+ active: current_path == final_path,
72
+ type: :file,
73
+ section: false
74
+ }
75
+ end
76
+ end
77
+ end
78
+ end
@@ -66,7 +66,8 @@ module Docyard
66
66
  def hidden_or_ignored?(entry, relative_path)
67
67
  entry.start_with?(".") ||
68
68
  entry.start_with?("_") ||
69
- (entry == "index.md" && relative_path.empty?)
69
+ (entry == "index.md" && relative_path.empty?) ||
70
+ (entry == "public" && relative_path.empty?)
70
71
  end
71
72
 
72
73
  def sort_key(entry)
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Sidebar
5
+ class Item
6
+ attr_reader :slug, :text, :icon, :link, :target, :collapsed, :items, :path, :active, :type, :has_index, :section
7
+
8
+ DEFAULTS = {
9
+ target: "_self",
10
+ collapsed: false,
11
+ items: [],
12
+ active: false,
13
+ type: :file,
14
+ has_index: false,
15
+ section: true
16
+ }.freeze
17
+
18
+ def initialize(**options)
19
+ assign_required_attributes(options)
20
+ assign_optional_attributes(options)
21
+ end
22
+
23
+ private
24
+
25
+ def assign_required_attributes(options)
26
+ @slug = options[:slug]
27
+ @text = options[:text]
28
+ @icon = options[:icon]
29
+ @link = options[:link]
30
+ end
31
+
32
+ def assign_optional_attributes(options)
33
+ assign_navigation_attributes(options)
34
+ assign_state_attributes(options)
35
+ end
36
+
37
+ def assign_navigation_attributes(options)
38
+ @target = options.fetch(:target, DEFAULTS[:target])
39
+ @path = options[:path] || options[:link]
40
+ @active = options.fetch(:active, DEFAULTS[:active])
41
+ @type = options.fetch(:type, DEFAULTS[:type])
42
+ end
43
+
44
+ def assign_state_attributes(options)
45
+ @collapsed = options.fetch(:collapsed, DEFAULTS[:collapsed])
46
+ @items = options.fetch(:items, DEFAULTS[:items])
47
+ @has_index = options.fetch(:has_index, DEFAULTS[:has_index])
48
+ @section = options.fetch(:section, DEFAULTS[:section])
49
+ end
50
+
51
+ public
52
+
53
+ def external?
54
+ return false if path.nil?
55
+
56
+ path.start_with?("http://", "https://")
57
+ end
58
+
59
+ def children?
60
+ items.any?
61
+ end
62
+
63
+ def title
64
+ text
65
+ end
66
+
67
+ def children
68
+ items
69
+ end
70
+
71
+ def collapsible?
72
+ children? && !section
73
+ end
74
+
75
+ def section?
76
+ section == true
77
+ end
78
+
79
+ def to_h
80
+ {
81
+ title: title,
82
+ path: path,
83
+ icon: icon,
84
+ active: active,
85
+ type: type,
86
+ collapsed: collapsed,
87
+ collapsible: collapsible?,
88
+ target: target,
89
+ has_index: has_index,
90
+ section: section,
91
+ children: children.map(&:to_h)
92
+ }
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Docyard
6
+ module Sidebar
7
+ class LocalConfigLoader
8
+ SIDEBAR_CONFIG_FILE = "_sidebar.yml"
9
+
10
+ attr_reader :docs_path
11
+
12
+ def initialize(docs_path)
13
+ @docs_path = docs_path
14
+ end
15
+
16
+ def load
17
+ return nil unless config_file_exists?
18
+
19
+ parse_config_file
20
+ end
21
+
22
+ def config_file_exists?
23
+ File.file?(config_file_path)
24
+ end
25
+
26
+ private
27
+
28
+ def config_file_path
29
+ File.join(docs_path, SIDEBAR_CONFIG_FILE)
30
+ end
31
+
32
+ def parse_config_file
33
+ content = YAML.load_file(config_file_path)
34
+ normalize_config(content)
35
+ rescue Psych::SyntaxError => e
36
+ warn "Warning: Invalid YAML in #{config_file_path}: #{e.message}"
37
+ nil
38
+ rescue StandardError => e
39
+ warn "Warning: Error reading #{config_file_path}: #{e.message}"
40
+ nil
41
+ end
42
+
43
+ def normalize_config(content)
44
+ return nil if content.nil?
45
+ return content if content.is_a?(Array)
46
+
47
+ content["items"] if content.is_a?(Hash)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Sidebar
5
+ class MetadataExtractor
6
+ attr_reader :docs_path, :title_extractor
7
+
8
+ def initialize(docs_path:, title_extractor:)
9
+ @docs_path = docs_path
10
+ @title_extractor = title_extractor
11
+ end
12
+
13
+ def extract_index_metadata(file_path)
14
+ return { sidebar_text: nil, icon: nil } unless File.file?(file_path)
15
+
16
+ markdown = Markdown.new(File.read(file_path))
17
+ {
18
+ sidebar_text: markdown.sidebar_text,
19
+ icon: markdown.sidebar_icon
20
+ }
21
+ rescue StandardError
22
+ { sidebar_text: nil, icon: nil }
23
+ end
24
+
25
+ def extract_frontmatter_metadata(file_path)
26
+ return { text: nil, icon: nil } unless File.exist?(file_path)
27
+
28
+ markdown = Markdown.new(File.read(file_path))
29
+ {
30
+ text: markdown.sidebar_text || markdown.title,
31
+ icon: markdown.sidebar_icon
32
+ }
33
+ end
34
+
35
+ def extract_file_title(file_path, slug)
36
+ File.exist?(file_path) ? title_extractor.extract(file_path) : Utils::TextFormatter.titleize(slug)
37
+ end
38
+
39
+ def extract_common_options(options)
40
+ collapsed_value = options["collapsed"]
41
+ collapsed_value = options[:collapsed] if collapsed_value.nil?
42
+ collapsible_value = options["collapsible"]
43
+ collapsible_value = options[:collapsible] if collapsible_value.nil?
44
+ collapsible_value = true if !collapsed_value.nil? && collapsible_value.nil?
45
+ {
46
+ text: options["text"] || options[:text],
47
+ icon: options["icon"] || options[:icon],
48
+ collapsed: collapsed_value,
49
+ section: section_from_collapsible(collapsible_value)
50
+ }
51
+ end
52
+
53
+ def section_from_collapsible(collapsible_value)
54
+ return nil if collapsible_value.nil?
55
+
56
+ collapsible_value != true
57
+ end
58
+
59
+ def resolve_item_text(slug, file_path, options, frontmatter_text)
60
+ text = options["text"] || options[:text] || frontmatter_text
61
+ text || extract_file_title(file_path, slug)
62
+ end
63
+
64
+ def resolve_item_icon(options, frontmatter_icon)
65
+ options["icon"] || options[:icon] || frontmatter_icon
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Sidebar
5
+ class MetadataReader
6
+ def extract_file_metadata(file_path)
7
+ return empty_file_metadata unless File.file?(file_path)
8
+
9
+ content = File.read(file_path)
10
+ markdown = Markdown.new(content)
11
+ {
12
+ title: markdown.sidebar_text || markdown.title,
13
+ icon: markdown.sidebar_icon,
14
+ collapsed: markdown.sidebar_collapsed,
15
+ order: markdown.sidebar_order
16
+ }
17
+ rescue StandardError
18
+ empty_file_metadata
19
+ end
20
+
21
+ def extract_index_metadata(file_path)
22
+ return empty_index_metadata unless File.file?(file_path)
23
+
24
+ content = File.read(file_path)
25
+ markdown = Markdown.new(content)
26
+ {
27
+ sidebar_text: markdown.sidebar_text,
28
+ icon: markdown.sidebar_icon,
29
+ collapsed: markdown.sidebar_collapsed,
30
+ order: markdown.sidebar_order
31
+ }
32
+ rescue StandardError
33
+ empty_index_metadata
34
+ end
35
+
36
+ private
37
+
38
+ def empty_file_metadata
39
+ { title: nil, icon: nil, collapsed: nil, order: nil }
40
+ end
41
+
42
+ def empty_index_metadata
43
+ { sidebar_text: nil, icon: nil, collapsed: nil, order: nil }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Sidebar
5
+ class PathPrefixer
6
+ def initialize(tree, prefix)
7
+ @tree = tree
8
+ @prefix = prefix
9
+ end
10
+
11
+ def prefix
12
+ return @tree if @prefix.empty?
13
+
14
+ @tree.map { |item| prefix_item(item) }
15
+ end
16
+
17
+ private
18
+
19
+ def prefix_item(item)
20
+ prefixed = item.dup
21
+ prefixed[:path] = prefixed_path(prefixed[:path])
22
+ prefixed[:children] = self.class.new(prefixed[:children], @prefix).prefix if prefixed[:children]&.any?
23
+ prefixed
24
+ end
25
+
26
+ def prefixed_path(path)
27
+ return path if path.nil? || path.start_with?("http")
28
+
29
+ path_without_slash = path.sub(%r{^/}, "")
30
+ path_without_slash.empty? ? @prefix : "#{@prefix}/#{path_without_slash}"
31
+ end
32
+ end
33
+ end
34
+ end