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.
- checksums.yaml +4 -4
- data/.rubocop.yml +5 -1
- data/CHANGELOG.md +34 -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 +82 -50
- data/lib/docyard/builder.rb +20 -10
- data/lib/docyard/cli.rb +6 -3
- data/lib/docyard/components/aliases.rb +29 -0
- data/lib/docyard/components/processors/callout_processor.rb +124 -0
- data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +106 -0
- data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +79 -0
- data/lib/docyard/components/processors/code_block_options_preprocessor.rb +78 -0
- data/lib/docyard/components/processors/code_block_processor.rb +175 -0
- data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +127 -0
- data/lib/docyard/components/processors/heading_anchor_processor.rb +39 -0
- data/lib/docyard/components/processors/icon_processor.rb +53 -0
- data/lib/docyard/components/processors/table_of_contents_processor.rb +68 -0
- data/lib/docyard/components/processors/table_wrapper_processor.rb +22 -0
- data/lib/docyard/components/processors/tabs_processor.rb +48 -0
- data/lib/docyard/components/support/code_block/feature_extractor.rb +117 -0
- data/lib/docyard/components/support/code_block/icon_detector.rb +44 -0
- data/lib/docyard/components/support/code_block/line_parser.rb +84 -0
- data/lib/docyard/components/support/code_block/line_wrapper.rb +50 -0
- data/lib/docyard/components/support/code_block/patterns.rb +55 -0
- data/lib/docyard/components/support/code_detector.rb +61 -0
- data/lib/docyard/components/support/tabs/icon_detector.rb +62 -0
- data/lib/docyard/components/support/tabs/parser.rb +195 -0
- data/lib/docyard/components/support/tabs/range_finder.rb +46 -0
- data/lib/docyard/config/branding_resolver.rb +183 -0
- data/lib/docyard/{constants.rb → config/constants.rb} +7 -4
- data/lib/docyard/config/validator.rb +122 -99
- data/lib/docyard/config.rb +38 -36
- data/lib/docyard/initializer.rb +15 -76
- data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
- data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +6 -3
- data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
- data/lib/docyard/navigation/sidebar/config_parser.rb +208 -0
- data/lib/docyard/navigation/sidebar/file_resolver.rb +78 -0
- data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +2 -1
- data/lib/docyard/navigation/sidebar/item.rb +96 -0
- 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 +144 -0
- data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
- data/lib/docyard/navigation/sidebar/tree_builder.rb +139 -0
- data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
- data/lib/docyard/navigation/sidebar_builder.rb +159 -0
- data/lib/docyard/rendering/icon_helpers.rb +13 -0
- data/lib/docyard/{icons → rendering/icons}/phosphor.rb +26 -1
- data/lib/docyard/{markdown.rb → rendering/markdown.rb} +19 -13
- data/lib/docyard/rendering/renderer.rb +163 -0
- 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 +74 -0
- data/lib/docyard/search/dev_indexer.rb +155 -0
- data/lib/docyard/search/pagefind_support.rb +33 -0
- data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +24 -19
- data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
- data/lib/docyard/server/pagefind_handler.rb +63 -0
- data/lib/docyard/{preview_server.rb → server/preview_server.rb} +2 -2
- data/lib/docyard/server/rack_application.rb +192 -0
- data/lib/docyard/server/resolution_result.rb +29 -0
- data/lib/docyard/{router.rb → server/router.rb} +4 -4
- 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 +561 -0
- 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 +503 -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 +610 -0
- 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 +19 -56
- 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 +6 -4
- 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 +9 -3
- data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
- data/lib/docyard/templates/partials/_search_modal.html.erb +41 -0
- data/lib/docyard/templates/partials/_search_trigger.html.erb +18 -0
- 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/utils/html_helpers.rb +14 -0
- data/lib/docyard/utils/path_resolver.rb +2 -1
- data/lib/docyard/utils/url_helpers.rb +20 -0
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +22 -15
- metadata +89 -50
- data/lib/docyard/components/callout_processor.rb +0 -121
- data/lib/docyard/components/code_block_diff_preprocessor.rb +0 -104
- data/lib/docyard/components/code_block_feature_extractor.rb +0 -113
- data/lib/docyard/components/code_block_focus_preprocessor.rb +0 -77
- data/lib/docyard/components/code_block_icon_detector.rb +0 -40
- data/lib/docyard/components/code_block_line_wrapper.rb +0 -46
- data/lib/docyard/components/code_block_options_preprocessor.rb +0 -76
- data/lib/docyard/components/code_block_patterns.rb +0 -51
- data/lib/docyard/components/code_block_processor.rb +0 -176
- data/lib/docyard/components/code_detector.rb +0 -59
- data/lib/docyard/components/code_line_parser.rb +0 -80
- data/lib/docyard/components/code_snippet_import_preprocessor.rb +0 -125
- data/lib/docyard/components/heading_anchor_processor.rb +0 -34
- data/lib/docyard/components/icon_detector.rb +0 -57
- data/lib/docyard/components/icon_processor.rb +0 -51
- data/lib/docyard/components/table_of_contents_processor.rb +0 -64
- data/lib/docyard/components/table_wrapper_processor.rb +0 -18
- data/lib/docyard/components/tabs_parser.rb +0 -191
- data/lib/docyard/components/tabs_processor.rb +0 -44
- data/lib/docyard/components/tabs_range_finder.rb +0 -42
- data/lib/docyard/rack_application.rb +0 -172
- data/lib/docyard/renderer.rb +0 -120
- data/lib/docyard/routing/resolution_result.rb +0 -31
- data/lib/docyard/sidebar/config_parser.rb +0 -180
- data/lib/docyard/sidebar/item.rb +0 -58
- data/lib/docyard/sidebar/renderer.rb +0 -137
- data/lib/docyard/sidebar/tree_builder.rb +0 -59
- data/lib/docyard/sidebar_builder.rb +0 -102
- 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
- /data/lib/docyard/{sidebar → navigation/sidebar}/title_extractor.rb +0 -0
- /data/lib/docyard/{icons → rendering/icons}/LICENSE.phosphor +0 -0
- /data/lib/docyard/{icons → rendering/icons}/file_types.rb +0 -0
- /data/lib/docyard/{icons.rb → rendering/icons.rb} +0 -0
- /data/lib/docyard/{language_mapping.rb → rendering/language_mapping.rb} +0 -0
- /data/lib/docyard/{file_watcher.rb → server/file_watcher.rb} +0 -0
- /data/lib/docyard/{errors.rb → utils/errors.rb} +0 -0
- /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
|
-
|
|
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
|