docyard 0.7.0 → 0.9.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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/CHANGELOG.md +43 -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 +85 -12
  8. data/lib/docyard/builder.rb +6 -6
  9. data/lib/docyard/components/aliases.rb +12 -0
  10. data/lib/docyard/components/processors/abbreviation_processor.rb +72 -0
  11. data/lib/docyard/components/processors/accordion_processor.rb +81 -0
  12. data/lib/docyard/components/processors/badge_processor.rb +72 -0
  13. data/lib/docyard/components/processors/callout_processor.rb +8 -2
  14. data/lib/docyard/components/processors/cards_processor.rb +100 -0
  15. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +23 -2
  16. data/lib/docyard/components/processors/code_block_processor.rb +6 -0
  17. data/lib/docyard/components/processors/code_group_processor.rb +198 -0
  18. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +6 -1
  19. data/lib/docyard/components/processors/custom_anchor_processor.rb +42 -0
  20. data/lib/docyard/components/processors/file_tree_processor.rb +151 -0
  21. data/lib/docyard/components/processors/image_caption_processor.rb +96 -0
  22. data/lib/docyard/components/processors/include_processor.rb +86 -0
  23. data/lib/docyard/components/processors/steps_processor.rb +89 -0
  24. data/lib/docyard/components/processors/tabs_processor.rb +9 -1
  25. data/lib/docyard/components/processors/tooltip_processor.rb +57 -0
  26. data/lib/docyard/components/processors/video_embed_processor.rb +196 -0
  27. data/lib/docyard/components/support/code_group/html_builder.rb +122 -0
  28. data/lib/docyard/components/support/markdown_code_block_helper.rb +56 -0
  29. data/lib/docyard/config/branding_resolver.rb +121 -17
  30. data/lib/docyard/config/constants.rb +6 -4
  31. data/lib/docyard/config/logo_detector.rb +39 -0
  32. data/lib/docyard/config/validator.rb +122 -99
  33. data/lib/docyard/config.rb +40 -42
  34. data/lib/docyard/initializer.rb +15 -76
  35. data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
  36. data/lib/docyard/navigation/prev_next_builder.rb +4 -1
  37. data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
  38. data/lib/docyard/navigation/sidebar/config_parser.rb +136 -108
  39. data/lib/docyard/navigation/sidebar/file_resolver.rb +90 -0
  40. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +2 -1
  41. data/lib/docyard/navigation/sidebar/item.rb +50 -7
  42. data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
  43. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +71 -0
  44. data/lib/docyard/navigation/sidebar/metadata_reader.rb +51 -0
  45. data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
  46. data/lib/docyard/navigation/sidebar/renderer.rb +60 -38
  47. data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
  48. data/lib/docyard/navigation/sidebar/tree_builder.rb +100 -26
  49. data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
  50. data/lib/docyard/navigation/sidebar_builder.rb +105 -36
  51. data/lib/docyard/rendering/icon_helpers.rb +13 -0
  52. data/lib/docyard/rendering/icons/phosphor.rb +26 -1
  53. data/lib/docyard/rendering/markdown.rb +29 -1
  54. data/lib/docyard/rendering/renderer.rb +75 -34
  55. data/lib/docyard/rendering/template_resolver.rb +172 -0
  56. data/lib/docyard/routing/fallback_resolver.rb +92 -0
  57. data/lib/docyard/search/build_indexer.rb +1 -1
  58. data/lib/docyard/search/dev_indexer.rb +51 -6
  59. data/lib/docyard/search/pagefind_support.rb +2 -0
  60. data/lib/docyard/server/asset_handler.rb +25 -19
  61. data/lib/docyard/server/pagefind_handler.rb +63 -0
  62. data/lib/docyard/server/preview_server.rb +1 -1
  63. data/lib/docyard/server/rack_application.rb +81 -64
  64. data/lib/docyard/templates/assets/css/code.css +18 -51
  65. data/lib/docyard/templates/assets/css/components/abbreviation.css +86 -0
  66. data/lib/docyard/templates/assets/css/components/accordion.css +138 -0
  67. data/lib/docyard/templates/assets/css/components/badges.css +47 -0
  68. data/lib/docyard/templates/assets/css/components/banner.css +202 -0
  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/cards.css +100 -0
  72. data/lib/docyard/templates/assets/css/components/code-block.css +190 -282
  73. data/lib/docyard/templates/assets/css/components/code-group.css +281 -0
  74. data/lib/docyard/templates/assets/css/components/figure.css +22 -0
  75. data/lib/docyard/templates/assets/css/components/file-tree.css +124 -0
  76. data/lib/docyard/templates/assets/css/components/heading-anchor.css +36 -15
  77. data/lib/docyard/templates/assets/css/components/icon.css +0 -1
  78. data/lib/docyard/templates/assets/css/components/lightbox.css +65 -0
  79. data/lib/docyard/templates/assets/css/components/logo.css +0 -2
  80. data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
  81. data/lib/docyard/templates/assets/css/components/navigation.css +193 -167
  82. data/lib/docyard/templates/assets/css/components/prev-next.css +68 -48
  83. data/lib/docyard/templates/assets/css/components/search.css +186 -174
  84. data/lib/docyard/templates/assets/css/components/steps.css +122 -0
  85. data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
  86. data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
  87. data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
  88. data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
  89. data/lib/docyard/templates/assets/css/components/tooltip.css +113 -0
  90. data/lib/docyard/templates/assets/css/components/video.css +41 -0
  91. data/lib/docyard/templates/assets/css/landing.css +815 -0
  92. data/lib/docyard/templates/assets/css/layout.css +489 -87
  93. data/lib/docyard/templates/assets/css/main.css +1 -3
  94. data/lib/docyard/templates/assets/css/markdown.css +113 -93
  95. data/lib/docyard/templates/assets/css/reset.css +0 -3
  96. data/lib/docyard/templates/assets/css/typography.css +43 -41
  97. data/lib/docyard/templates/assets/css/variables.css +268 -208
  98. data/lib/docyard/templates/assets/favicon.svg +7 -8
  99. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
  100. data/lib/docyard/templates/assets/js/components/abbreviation.js +85 -0
  101. data/lib/docyard/templates/assets/js/components/banner.js +81 -0
  102. data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
  103. data/lib/docyard/templates/assets/js/components/code-group.js +283 -0
  104. data/lib/docyard/templates/assets/js/components/file-tree.js +39 -0
  105. data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
  106. data/lib/docyard/templates/assets/js/components/lightbox.js +72 -0
  107. data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
  108. data/lib/docyard/templates/assets/js/components/search.js +0 -75
  109. data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
  110. data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
  111. data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
  112. data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
  113. data/lib/docyard/templates/assets/js/components/tooltip.js +118 -0
  114. data/lib/docyard/templates/assets/js/theme.js +0 -3
  115. data/lib/docyard/templates/assets/logo-dark.svg +8 -2
  116. data/lib/docyard/templates/assets/logo.svg +7 -4
  117. data/lib/docyard/templates/config/docyard.yml.erb +37 -34
  118. data/lib/docyard/templates/errors/404.html.erb +1 -1
  119. data/lib/docyard/templates/errors/500.html.erb +1 -1
  120. data/lib/docyard/templates/layouts/default.html.erb +19 -67
  121. data/lib/docyard/templates/layouts/splash.html.erb +177 -0
  122. data/lib/docyard/templates/partials/_accordion.html.erb +9 -0
  123. data/lib/docyard/templates/partials/_banner.html.erb +27 -0
  124. data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
  125. data/lib/docyard/templates/partials/_card.html.erb +23 -0
  126. data/lib/docyard/templates/partials/_code_block.html.erb +5 -3
  127. data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
  128. data/lib/docyard/templates/partials/_features.html.erb +15 -0
  129. data/lib/docyard/templates/partials/_footer.html.erb +42 -0
  130. data/lib/docyard/templates/partials/_head.html.erb +22 -0
  131. data/lib/docyard/templates/partials/_header.html.erb +49 -0
  132. data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
  133. data/lib/docyard/templates/partials/_hero.html.erb +27 -0
  134. data/lib/docyard/templates/partials/_nav_group.html.erb +31 -11
  135. data/lib/docyard/templates/partials/_nav_leaf.html.erb +4 -1
  136. data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
  137. data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
  138. data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
  139. data/lib/docyard/templates/partials/_prev_next.html.erb +8 -2
  140. data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
  141. data/lib/docyard/templates/partials/_search_modal.html.erb +2 -6
  142. data/lib/docyard/templates/partials/_search_trigger.html.erb +2 -6
  143. data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
  144. data/lib/docyard/templates/partials/_step.html.erb +14 -0
  145. data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
  146. data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
  147. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
  148. data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
  149. data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
  150. data/lib/docyard/version.rb +1 -1
  151. metadata +70 -5
  152. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
  153. data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
  154. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
  155. data/lib/docyard/templates/markdown/index.md.erb +0 -82
@@ -3,21 +3,56 @@
3
3
  module Docyard
4
4
  module Sidebar
5
5
  class Item
6
- attr_reader :slug, :text, :icon, :link, :target, :collapsed, :items, :path, :active, :type
6
+ attr_reader :slug, :text, :icon, :link, :target, :collapsed, :items, :path, :active, :type, :has_index, :section,
7
+ :badge, :badge_type
8
+
9
+ DEFAULTS = {
10
+ target: "_self",
11
+ collapsed: false,
12
+ items: [],
13
+ active: false,
14
+ type: :file,
15
+ has_index: false,
16
+ section: true
17
+ }.freeze
7
18
 
8
19
  def initialize(**options)
20
+ assign_required_attributes(options)
21
+ assign_optional_attributes(options)
22
+ end
23
+
24
+ private
25
+
26
+ def assign_required_attributes(options)
9
27
  @slug = options[:slug]
10
28
  @text = options[:text]
11
29
  @icon = options[:icon]
12
30
  @link = options[:link]
13
- @target = options[:target] || "_self"
14
- @collapsed = options[:collapsed] || false
15
- @items = options[:items] || []
31
+ @badge = options[:badge]
32
+ @badge_type = options[:badge_type]
33
+ end
34
+
35
+ def assign_optional_attributes(options)
36
+ assign_navigation_attributes(options)
37
+ assign_state_attributes(options)
38
+ end
39
+
40
+ def assign_navigation_attributes(options)
41
+ @target = options.fetch(:target, DEFAULTS[:target])
16
42
  @path = options[:path] || options[:link]
17
- @active = options[:active] || false
18
- @type = options[:type] || :file
43
+ @active = options.fetch(:active, DEFAULTS[:active])
44
+ @type = options.fetch(:type, DEFAULTS[:type])
45
+ end
46
+
47
+ def assign_state_attributes(options)
48
+ @collapsed = options.fetch(:collapsed, DEFAULTS[:collapsed])
49
+ @items = options.fetch(:items, DEFAULTS[:items])
50
+ @has_index = options.fetch(:has_index, DEFAULTS[:has_index])
51
+ @section = options.fetch(:section, DEFAULTS[:section])
19
52
  end
20
53
 
54
+ public
55
+
21
56
  def external?
22
57
  return false if path.nil?
23
58
 
@@ -37,7 +72,11 @@ module Docyard
37
72
  end
38
73
 
39
74
  def collapsible?
40
- children?
75
+ children? && !section
76
+ end
77
+
78
+ def section?
79
+ section == true
41
80
  end
42
81
 
43
82
  def to_h
@@ -50,6 +89,10 @@ module Docyard
50
89
  collapsed: collapsed,
51
90
  collapsible: collapsible?,
52
91
  target: target,
92
+ has_index: has_index,
93
+ section: section,
94
+ badge: badge,
95
+ badge_type: badge_type,
53
96
  children: children.map(&:to_h)
54
97
  }
55
98
  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,71 @@
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, badge: nil, badge_type: 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
+ badge: markdown.sidebar_badge,
33
+ badge_type: markdown.sidebar_badge_type
34
+ }
35
+ end
36
+
37
+ def extract_file_title(file_path, slug)
38
+ File.exist?(file_path) ? title_extractor.extract(file_path) : Utils::TextFormatter.titleize(slug)
39
+ end
40
+
41
+ def extract_common_options(options)
42
+ collapsed_value = options["collapsed"]
43
+ collapsed_value = options[:collapsed] if collapsed_value.nil?
44
+ collapsible_value = options["collapsible"]
45
+ collapsible_value = options[:collapsible] if collapsible_value.nil?
46
+ collapsible_value = true if !collapsed_value.nil? && collapsible_value.nil?
47
+ {
48
+ text: options["text"] || options[:text],
49
+ icon: options["icon"] || options[:icon],
50
+ collapsed: collapsed_value,
51
+ section: section_from_collapsible(collapsible_value)
52
+ }
53
+ end
54
+
55
+ def section_from_collapsible(collapsible_value)
56
+ return nil if collapsible_value.nil?
57
+
58
+ collapsible_value != true
59
+ end
60
+
61
+ def resolve_item_text(slug, file_path, options, frontmatter_text)
62
+ text = options["text"] || options[:text] || frontmatter_text
63
+ text || extract_file_title(file_path, slug)
64
+ end
65
+
66
+ def resolve_item_icon(options, frontmatter_icon)
67
+ options["icon"] || options[:icon] || frontmatter_icon
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,51 @@
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
+ badge: markdown.sidebar_badge,
17
+ badge_type: markdown.sidebar_badge_type
18
+ }
19
+ rescue StandardError
20
+ empty_file_metadata
21
+ end
22
+
23
+ def extract_index_metadata(file_path)
24
+ return empty_index_metadata unless File.file?(file_path)
25
+
26
+ content = File.read(file_path)
27
+ markdown = Markdown.new(content)
28
+ {
29
+ sidebar_text: markdown.sidebar_text,
30
+ icon: markdown.sidebar_icon,
31
+ collapsed: markdown.sidebar_collapsed,
32
+ order: markdown.sidebar_order,
33
+ badge: markdown.sidebar_badge,
34
+ badge_type: markdown.sidebar_badge_type
35
+ }
36
+ rescue StandardError
37
+ empty_index_metadata
38
+ end
39
+
40
+ private
41
+
42
+ def empty_file_metadata
43
+ { title: nil, icon: nil, collapsed: nil, order: nil, badge: nil, badge_type: nil }
44
+ end
45
+
46
+ def empty_index_metadata
47
+ { sidebar_text: nil, icon: nil, collapsed: nil, order: nil, badge: nil, badge_type: nil }
48
+ end
49
+ end
50
+ end
51
+ 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
@@ -1,28 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
+ require_relative "../../rendering/icon_helpers"
4
5
 
5
6
  module Docyard
6
7
  module Sidebar
7
8
  class Renderer
8
9
  include Utils::UrlHelpers
10
+ include IconHelpers
9
11
 
10
12
  PARTIALS_PATH = File.join(__dir__, "../../templates/partials")
11
13
 
12
- attr_reader :site_title, :base_url
14
+ attr_reader :site_title, :base_url, :header_ctas
13
15
 
14
- def initialize(site_title: "Documentation", base_url: "/")
16
+ def initialize(site_title: "Documentation", base_url: "/", header_ctas: [])
15
17
  @site_title = site_title
16
18
  @base_url = normalize_base_url(base_url)
19
+ @header_ctas = header_ctas
17
20
  end
18
21
 
19
22
  def render(tree)
20
23
  return "" if tree.empty?
21
24
 
22
25
  nav_content = render_tree_with_sections(tree)
23
- footer_html = render_partial(:sidebar_footer)
24
-
25
- render_partial(:sidebar, nav_content: nav_content, footer_html: footer_html)
26
+ render_partial(:sidebar, nav_content: nav_content, header_ctas: header_ctas)
26
27
  end
27
28
 
28
29
  private
@@ -37,50 +38,52 @@ module Docyard
37
38
  ERB.new(template).result(erb_binding)
38
39
  end
39
40
 
40
- def icon(name, weight = "regular")
41
- Icons.render(name.to_s.tr("_", "-"), weight) || ""
42
- end
43
-
44
41
  def render_tree_with_sections(items)
45
42
  filtered_items = items.reject { |item| item[:title]&.downcase == site_title.downcase }
46
- grouped_items = group_by_section(filtered_items)
47
-
48
- grouped_items.map do |section_name, section_items|
49
- render_section(section_name, section_items)
43
+ grouped = group_items_by_section(filtered_items)
44
+
45
+ grouped.map do |group|
46
+ if group[:section]
47
+ render_section(group[:item])
48
+ else
49
+ render_item_group(group[:items])
50
+ end
50
51
  end.join
51
52
  end
52
53
 
53
- def render_section(section_name, section_items)
54
- section_content = render_tree(section_items)
55
- render_partial(:nav_section, section_name: section_name, section_content: section_content)
56
- end
57
-
58
- def group_by_section(items)
59
- sections = {}
60
- root_items = []
54
+ def group_items_by_section(items)
55
+ groups = []
56
+ current_non_section_items = []
61
57
 
62
58
  items.each do |item|
63
- process_section_item(item, sections, root_items)
59
+ if item[:section]
60
+ if current_non_section_items.any?
61
+ groups << { section: false, items: current_non_section_items }
62
+ current_non_section_items = []
63
+ end
64
+ groups << { section: true, item: item }
65
+ else
66
+ current_non_section_items << item
67
+ end
64
68
  end
65
69
 
66
- build_section_result(sections, root_items)
70
+ groups << { section: false, items: current_non_section_items } if current_non_section_items.any?
71
+ groups
67
72
  end
68
73
 
69
- def process_section_item(item, sections, root_items)
70
- return if item[:title]&.downcase == site_title.downcase
71
-
72
- if item[:type] == :directory && !item[:children].empty?
73
- section_name = item[:title].upcase
74
- sections[section_name] = item[:children]
75
- else
76
- root_items << item
77
- end
74
+ def render_section(item)
75
+ section_content = render_tree(item[:children])
76
+ render_partial(:nav_section,
77
+ section_name: item[:title],
78
+ section_icon: item[:icon],
79
+ section_content: section_content)
78
80
  end
79
81
 
80
- def build_section_result(sections, root_items)
81
- result = {}
82
- result[nil] = root_items unless root_items.empty?
83
- result.merge!(sections)
82
+ def render_item_group(items)
83
+ render_partial(:nav_section,
84
+ section_name: nil,
85
+ section_icon: nil,
86
+ section_content: render_tree(items))
84
87
  end
85
88
 
86
89
  def render_tree(items)
@@ -93,6 +96,8 @@ module Docyard
93
96
  def render_item(item)
94
97
  item_content = if item[:children].empty?
95
98
  render_leaf_item(item)
99
+ elsif item[:section]
100
+ render_nested_section(item)
96
101
  else
97
102
  render_group_item(item)
98
103
  end
@@ -107,7 +112,19 @@ module Docyard
107
112
  title: item[:title],
108
113
  active: item[:active],
109
114
  icon: item[:icon],
110
- target: item[:target]
115
+ target: item[:target],
116
+ badge: item[:badge],
117
+ badge_type: item[:badge_type]
118
+ )
119
+ end
120
+
121
+ def render_nested_section(item)
122
+ children_html = render_tree(item[:children])
123
+ render_partial(
124
+ :nav_nested_section,
125
+ title: item[:title],
126
+ icon: item[:icon],
127
+ children_html: children_html
111
128
  )
112
129
  end
113
130
 
@@ -116,9 +133,14 @@ module Docyard
116
133
  render_partial(
117
134
  :nav_group,
118
135
  title: item[:title],
136
+ path: item[:path],
137
+ active: item[:active],
119
138
  children_html: children_html,
120
139
  icon: item[:icon],
121
- collapsed: item[:collapsed]
140
+ collapsed: item[:collapsed],
141
+ has_index: item[:has_index],
142
+ badge: item[:badge],
143
+ badge_type: item[:badge_type]
122
144
  )
123
145
  end
124
146
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Sidebar
5
+ module Sorter
6
+ module_function
7
+
8
+ def sort_by_order(items)
9
+ items.sort_by do |item|
10
+ order = item[:order]
11
+ title = item[:title]&.downcase || ""
12
+ if order.nil?
13
+ [1, title]
14
+ else
15
+ [0, order, title]
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,65 +1,139 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "sorter"
4
+ require_relative "local_config_loader"
5
+ require_relative "config_parser"
6
+ require_relative "metadata_reader"
7
+
3
8
  module Docyard
4
9
  module Sidebar
5
10
  class TreeBuilder
6
- attr_reader :docs_path, :current_path, :title_extractor
11
+ attr_reader :docs_path, :current_path, :title_extractor, :metadata_reader
7
12
 
8
13
  def initialize(docs_path:, current_path:, title_extractor: TitleExtractor.new)
9
14
  @docs_path = docs_path
10
15
  @current_path = Utils::PathResolver.normalize(current_path)
11
16
  @title_extractor = title_extractor
17
+ @metadata_reader = MetadataReader.new
12
18
  end
13
19
 
14
20
  def build(file_items)
15
- transform_items(file_items, "")
21
+ transform_items(file_items, "", depth: 1)
16
22
  end
17
23
 
18
24
  private
19
25
 
20
- def transform_items(items, relative_base)
21
- items.map do |item|
26
+ def transform_items(items, relative_base, depth:)
27
+ transformed = items.map do |item|
22
28
  if item[:type] == :directory
23
- transform_directory(item, relative_base)
29
+ transform_directory(item, relative_base, depth: depth)
24
30
  else
25
31
  transform_file(item, relative_base)
26
32
  end
27
33
  end
34
+ Sorter.sort_by_order(transformed)
28
35
  end
29
36
 
30
- def transform_directory(item, relative_base)
37
+ def transform_directory(item, relative_base, depth:)
31
38
  dir_path = File.join(relative_base, item[:name])
32
- children = transform_items(item[:children], dir_path)
39
+ dir_context = build_directory_context(dir_path)
40
+ children = build_directory_children(item, dir_path, depth)
33
41
 
34
- {
35
- title: Utils::TextFormatter.titleize(item[:name]),
36
- path: nil,
37
- active: false,
38
- type: :directory,
39
- collapsible: true,
40
- collapsed: !active_child?(children),
41
- children: children
42
- }
42
+ if depth == 1
43
+ build_section(item, children, dir_context)
44
+ else
45
+ build_collapsible_group(item, children, dir_context)
46
+ end
43
47
  end
44
48
 
45
- def active_child?(children)
46
- children.any? do |child|
47
- child[:active] || active_child?(child[:children] || [])
49
+ def build_directory_children(item, dir_path, depth)
50
+ full_dir_path = File.join(docs_path, dir_path)
51
+ local_config = LocalConfigLoader.new(full_dir_path).load
52
+
53
+ if local_config
54
+ build_children_from_config(local_config, dir_path)
55
+ else
56
+ transform_items(item[:children], dir_path, depth: depth + 1)
57
+ end
58
+ end
59
+
60
+ def build_children_from_config(config_items, base_path)
61
+ full_base_path = File.join(docs_path, base_path)
62
+ parser = ConfigParser.new(config_items, docs_path: full_base_path, current_path: current_path)
63
+ parser.parse.map(&:to_h)
64
+ end
65
+
66
+ def build_directory_context(dir_path)
67
+ index_file_path = File.join(docs_path, dir_path, "index.md")
68
+ has_index = File.file?(index_file_path)
69
+ { index_file_path: index_file_path, has_index: has_index,
70
+ url_path: has_index ? Utils::PathResolver.to_url(dir_path) : nil }
71
+ end
72
+
73
+ def build_section(item, children, context)
74
+ filtered_children = filter_index_from_children(children, context[:url_path])
75
+ metadata = context[:has_index] ? metadata_reader.extract_index_metadata(context[:index_file_path]) : {}
76
+
77
+ if context[:has_index]
78
+ overview = build_overview_item(metadata, context[:url_path])
79
+ filtered_children = [overview] + filtered_children
48
80
  end
81
+
82
+ build_section_hash(item, filtered_children, metadata)
83
+ end
84
+
85
+ def build_section_hash(item, children, metadata)
86
+ { title: Utils::TextFormatter.titleize(item[:name]), path: nil, icon: metadata[:icon],
87
+ active: false, type: :directory, section: true,
88
+ collapsed: false, has_index: false, order: metadata[:order], children: children }
89
+ end
90
+
91
+ def build_collapsible_group(item, children, context)
92
+ filtered_children = filter_index_from_children(children, context[:url_path])
93
+ metadata = context[:has_index] ? metadata_reader.extract_index_metadata(context[:index_file_path]) : {}
94
+ is_active = context[:has_index] && current_path == context[:url_path]
95
+
96
+ build_collapsible_hash(item, filtered_children, context, metadata, is_active)
97
+ end
98
+
99
+ def build_collapsible_hash(item, children, context, metadata, is_active)
100
+ { title: Utils::TextFormatter.titleize(item[:name]), path: context[:url_path],
101
+ icon: metadata[:icon], active: is_active, type: :directory, section: false,
102
+ collapsed: collapsible_collapsed?(children, is_active), has_index: context[:has_index],
103
+ order: metadata[:order], children: children }
104
+ end
105
+
106
+ def collapsible_collapsed?(children, is_active)
107
+ return false if is_active || active_child?(children)
108
+
109
+ true
110
+ end
111
+
112
+ def build_overview_item(metadata, url_path)
113
+ { title: metadata[:sidebar_text] || "Overview", path: url_path,
114
+ icon: metadata[:icon], active: current_path == url_path, type: :file, children: [] }
115
+ end
116
+
117
+ def filter_index_from_children(children, index_url_path)
118
+ return children unless index_url_path
119
+
120
+ children.reject { |child| child[:path] == index_url_path }
121
+ end
122
+
123
+ def active_child?(children)
124
+ children.any? { |child| child[:active] || active_child?(child[:children] || []) }
49
125
  end
50
126
 
51
127
  def transform_file(item, relative_base)
52
128
  file_path = File.join(relative_base, "#{item[:name]}#{Constants::MARKDOWN_EXTENSION}")
53
129
  full_file_path = File.join(docs_path, file_path)
54
130
  url_path = Utils::PathResolver.to_url(file_path.delete_suffix(Constants::MARKDOWN_EXTENSION))
131
+ metadata = metadata_reader.extract_file_metadata(full_file_path)
55
132
 
56
- {
57
- title: title_extractor.extract(full_file_path),
58
- path: url_path,
59
- active: current_path == url_path,
60
- type: :file,
61
- children: []
62
- }
133
+ { title: metadata[:title] || title_extractor.extract(full_file_path),
134
+ path: url_path, icon: metadata[:icon], active: current_path == url_path,
135
+ type: :file, order: metadata[:order], badge: metadata[:badge],
136
+ badge_type: metadata[:badge_type], children: [] }
63
137
  end
64
138
  end
65
139
  end