docyard 0.7.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -1
- data/CHANGELOG.md +20 -1
- data/lib/docyard/build/asset_bundler.rb +22 -7
- data/lib/docyard/build/file_copier.rb +49 -27
- data/lib/docyard/build/sitemap_generator.rb +6 -6
- data/lib/docyard/build/static_generator.rb +85 -12
- data/lib/docyard/builder.rb +6 -6
- data/lib/docyard/config/branding_resolver.rb +126 -17
- data/lib/docyard/config/constants.rb +6 -4
- data/lib/docyard/config/validator.rb +122 -99
- data/lib/docyard/config.rb +36 -43
- data/lib/docyard/initializer.rb +15 -76
- data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
- data/lib/docyard/navigation/prev_next_builder.rb +4 -1
- data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
- data/lib/docyard/navigation/sidebar/config_parser.rb +136 -108
- data/lib/docyard/navigation/sidebar/file_resolver.rb +78 -0
- data/lib/docyard/navigation/sidebar/file_system_scanner.rb +2 -1
- data/lib/docyard/navigation/sidebar/item.rb +45 -7
- data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
- data/lib/docyard/navigation/sidebar/metadata_extractor.rb +69 -0
- data/lib/docyard/navigation/sidebar/metadata_reader.rb +47 -0
- data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
- data/lib/docyard/navigation/sidebar/renderer.rb +55 -37
- data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
- data/lib/docyard/navigation/sidebar/tree_builder.rb +99 -26
- data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
- data/lib/docyard/navigation/sidebar_builder.rb +105 -36
- data/lib/docyard/rendering/icon_helpers.rb +13 -0
- data/lib/docyard/rendering/icons/phosphor.rb +23 -1
- data/lib/docyard/rendering/markdown.rb +5 -0
- data/lib/docyard/rendering/renderer.rb +74 -34
- data/lib/docyard/rendering/template_resolver.rb +172 -0
- data/lib/docyard/routing/fallback_resolver.rb +92 -0
- data/lib/docyard/search/build_indexer.rb +1 -1
- data/lib/docyard/search/dev_indexer.rb +51 -6
- data/lib/docyard/search/pagefind_support.rb +2 -0
- data/lib/docyard/server/asset_handler.rb +24 -19
- data/lib/docyard/server/pagefind_handler.rb +63 -0
- data/lib/docyard/server/preview_server.rb +1 -1
- data/lib/docyard/server/rack_application.rb +81 -64
- data/lib/docyard/templates/assets/css/code.css +18 -51
- data/lib/docyard/templates/assets/css/components/breadcrumbs.css +143 -0
- data/lib/docyard/templates/assets/css/components/callout.css +67 -67
- data/lib/docyard/templates/assets/css/components/code-block.css +180 -282
- data/lib/docyard/templates/assets/css/components/heading-anchor.css +28 -15
- data/lib/docyard/templates/assets/css/components/icon.css +0 -1
- data/lib/docyard/templates/assets/css/components/logo.css +0 -2
- data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
- data/lib/docyard/templates/assets/css/components/navigation.css +186 -167
- data/lib/docyard/templates/assets/css/components/prev-next.css +76 -47
- data/lib/docyard/templates/assets/css/components/search.css +186 -174
- data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
- data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
- data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
- data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
- data/lib/docyard/templates/assets/css/landing.css +815 -0
- data/lib/docyard/templates/assets/css/layout.css +489 -87
- data/lib/docyard/templates/assets/css/main.css +1 -3
- data/lib/docyard/templates/assets/css/markdown.css +111 -93
- data/lib/docyard/templates/assets/css/reset.css +0 -3
- data/lib/docyard/templates/assets/css/typography.css +43 -41
- data/lib/docyard/templates/assets/css/variables.css +268 -208
- data/lib/docyard/templates/assets/favicon.svg +7 -8
- data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
- data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
- data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
- data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
- data/lib/docyard/templates/assets/js/components/search.js +0 -75
- data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
- data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
- data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
- data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
- data/lib/docyard/templates/assets/js/theme.js +0 -3
- data/lib/docyard/templates/assets/logo-dark.svg +8 -2
- data/lib/docyard/templates/assets/logo.svg +7 -4
- data/lib/docyard/templates/config/docyard.yml.erb +37 -34
- data/lib/docyard/templates/errors/404.html.erb +1 -1
- data/lib/docyard/templates/errors/500.html.erb +1 -1
- data/lib/docyard/templates/layouts/default.html.erb +18 -67
- data/lib/docyard/templates/layouts/splash.html.erb +176 -0
- data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
- data/lib/docyard/templates/partials/_code_block.html.erb +5 -3
- data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
- data/lib/docyard/templates/partials/_features.html.erb +15 -0
- data/lib/docyard/templates/partials/_footer.html.erb +42 -0
- data/lib/docyard/templates/partials/_head.html.erb +22 -0
- data/lib/docyard/templates/partials/_header.html.erb +49 -0
- data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
- data/lib/docyard/templates/partials/_hero.html.erb +27 -0
- data/lib/docyard/templates/partials/_nav_group.html.erb +25 -11
- data/lib/docyard/templates/partials/_nav_leaf.html.erb +1 -1
- data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
- data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
- data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
- data/lib/docyard/templates/partials/_prev_next.html.erb +8 -2
- data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
- data/lib/docyard/templates/partials/_search_modal.html.erb +2 -6
- data/lib/docyard/templates/partials/_search_trigger.html.erb +2 -6
- data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
- data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
- data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
- data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
- data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
- data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
- data/lib/docyard/version.rb +1 -1
- metadata +33 -5
- data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
- data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
- data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
- data/lib/docyard/templates/markdown/index.md.erb +0 -82
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
groups << { section: false, items: current_non_section_items } if current_non_section_items.any?
|
|
71
|
+
groups
|
|
67
72
|
end
|
|
68
73
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
@@ -111,14 +116,27 @@ module Docyard
|
|
|
111
116
|
)
|
|
112
117
|
end
|
|
113
118
|
|
|
119
|
+
def render_nested_section(item)
|
|
120
|
+
children_html = render_tree(item[:children])
|
|
121
|
+
render_partial(
|
|
122
|
+
:nav_nested_section,
|
|
123
|
+
title: item[:title],
|
|
124
|
+
icon: item[:icon],
|
|
125
|
+
children_html: children_html
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
114
129
|
def render_group_item(item)
|
|
115
130
|
children_html = render_tree(item[:children])
|
|
116
131
|
render_partial(
|
|
117
132
|
:nav_group,
|
|
118
133
|
title: item[:title],
|
|
134
|
+
path: item[:path],
|
|
135
|
+
active: item[:active],
|
|
119
136
|
children_html: children_html,
|
|
120
137
|
icon: item[:icon],
|
|
121
|
-
collapsed: item[:collapsed]
|
|
138
|
+
collapsed: item[:collapsed],
|
|
139
|
+
has_index: item[:has_index]
|
|
122
140
|
)
|
|
123
141
|
end
|
|
124
142
|
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,138 @@
|
|
|
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
|
-
|
|
39
|
+
dir_context = build_directory_context(dir_path)
|
|
40
|
+
children = build_directory_children(item, dir_path, depth)
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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], children: [] }
|
|
63
136
|
end
|
|
64
137
|
end
|
|
65
138
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Sidebar
|
|
5
|
+
class TreeFilter
|
|
6
|
+
def initialize(tree, tab_path)
|
|
7
|
+
@tree = tree
|
|
8
|
+
@tab_path = tab_path
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def filter
|
|
12
|
+
@tree.filter_map { |item| filter_item(item) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def filter_item(item)
|
|
18
|
+
children = item[:children] || []
|
|
19
|
+
|
|
20
|
+
if children.any?
|
|
21
|
+
filter_parent_item(item, children)
|
|
22
|
+
else
|
|
23
|
+
filter_leaf_item(item)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def filter_parent_item(item, children)
|
|
28
|
+
filtered_children = self.class.new(children, @tab_path).filter
|
|
29
|
+
has_matching_content = filtered_children.any? { |c| !external_item?(c) }
|
|
30
|
+
|
|
31
|
+
return nil if !has_matching_content && !item_matches_path?(item[:path])
|
|
32
|
+
|
|
33
|
+
item.merge(children: filtered_children)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def filter_leaf_item(item)
|
|
37
|
+
return item if external_item?(item)
|
|
38
|
+
return nil unless item_matches_path?(item[:path])
|
|
39
|
+
|
|
40
|
+
item
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def external_item?(item)
|
|
44
|
+
item[:type] == :external || item[:path]&.start_with?("http")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def item_matches_path?(item_path)
|
|
48
|
+
return false if item_path.nil?
|
|
49
|
+
|
|
50
|
+
normalized_path = item_path.chomp("/")
|
|
51
|
+
normalized_path == @tab_path || normalized_path.start_with?("#{@tab_path}/")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|