docyard 0.9.0 → 1.0.1

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 (165) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/README.md +8 -253
  4. data/exe/docyard +6 -0
  5. data/lib/docyard/build/asset_bundler.rb +24 -2
  6. data/lib/docyard/build/error_page_generator.rb +33 -0
  7. data/lib/docyard/build/file_copier.rb +12 -5
  8. data/lib/docyard/build/file_writer.rb +19 -0
  9. data/lib/docyard/build/llms_txt_generator.rb +103 -0
  10. data/lib/docyard/build/root_fallback_generator.rb +66 -0
  11. data/lib/docyard/build/sitemap_generator.rb +1 -1
  12. data/lib/docyard/build/static_generator.rb +119 -81
  13. data/lib/docyard/builder.rb +6 -2
  14. data/lib/docyard/cli.rb +14 -4
  15. data/lib/docyard/components/processors/callout_processor.rb +1 -1
  16. data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
  17. data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
  18. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +11 -1
  19. data/lib/docyard/components/processors/code_block_processor.rb +5 -24
  20. data/lib/docyard/components/processors/code_group_processor.rb +6 -22
  21. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +1 -0
  22. data/lib/docyard/components/processors/file_tree_processor.rb +1 -2
  23. data/lib/docyard/components/processors/icon_processor.rb +8 -2
  24. data/lib/docyard/components/processors/include_processor.rb +10 -10
  25. data/lib/docyard/components/processors/video_embed_processor.rb +14 -3
  26. data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
  27. data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
  28. data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
  29. data/lib/docyard/components/support/code_detector.rb +2 -12
  30. data/lib/docyard/components/support/code_group/html_builder.rb +2 -6
  31. data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
  32. data/lib/docyard/components/support/tabs/parser.rb +6 -23
  33. data/lib/docyard/config/analytics_resolver.rb +24 -0
  34. data/lib/docyard/config/branding_resolver.rb +58 -27
  35. data/lib/docyard/config/key_validator.rb +30 -0
  36. data/lib/docyard/config/logo_detector.rb +8 -8
  37. data/lib/docyard/config/schema.rb +39 -0
  38. data/lib/docyard/config/section.rb +21 -0
  39. data/lib/docyard/config/validation_helpers.rb +83 -0
  40. data/lib/docyard/config/validator.rb +45 -144
  41. data/lib/docyard/config/validators/navigation.rb +43 -0
  42. data/lib/docyard/config/validators/section.rb +114 -0
  43. data/lib/docyard/config.rb +46 -102
  44. data/lib/docyard/constants.rb +59 -0
  45. data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
  46. data/lib/docyard/initializer.rb +100 -49
  47. data/lib/docyard/navigation/breadcrumb_builder.rb +45 -6
  48. data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
  49. data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
  50. data/lib/docyard/navigation/sidebar/cache.rb +96 -0
  51. data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
  52. data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
  53. data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
  54. data/lib/docyard/navigation/sidebar/renderer.rb +12 -1
  55. data/lib/docyard/navigation/sidebar_builder.rb +43 -81
  56. data/lib/docyard/rendering/branding_variables.rb +65 -0
  57. data/lib/docyard/rendering/icon_helpers.rb +14 -1
  58. data/lib/docyard/rendering/icons/devicons.rb +63 -0
  59. data/lib/docyard/rendering/icons.rb +26 -27
  60. data/lib/docyard/rendering/markdown.rb +5 -23
  61. data/lib/docyard/rendering/og_helpers.rb +36 -0
  62. data/lib/docyard/rendering/renderer.rb +96 -61
  63. data/lib/docyard/rendering/template_resolver.rb +14 -0
  64. data/lib/docyard/routing/fallback_resolver.rb +3 -3
  65. data/lib/docyard/search/build_indexer.rb +2 -2
  66. data/lib/docyard/search/dev_indexer.rb +36 -28
  67. data/lib/docyard/search/pagefind_support.rb +1 -1
  68. data/lib/docyard/server/asset_handler.rb +39 -15
  69. data/lib/docyard/server/dev_server.rb +90 -55
  70. data/lib/docyard/server/file_watcher.rb +68 -18
  71. data/lib/docyard/server/pagefind_handler.rb +1 -1
  72. data/lib/docyard/server/preview_server.rb +29 -33
  73. data/lib/docyard/server/rack_application.rb +39 -71
  74. data/lib/docyard/server/router.rb +11 -7
  75. data/lib/docyard/server/sse_server.rb +157 -0
  76. data/lib/docyard/server/static_file_app.rb +42 -0
  77. data/lib/docyard/templates/assets/css/components/banner.css +31 -0
  78. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
  79. data/lib/docyard/templates/assets/css/components/callout.css +26 -6
  80. data/lib/docyard/templates/assets/css/components/code-block.css +4 -2
  81. data/lib/docyard/templates/assets/css/components/code-group.css +20 -7
  82. data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
  83. data/lib/docyard/templates/assets/css/components/file-tree.css +5 -4
  84. data/lib/docyard/templates/assets/css/components/heading-anchor.css +2 -2
  85. data/lib/docyard/templates/assets/css/components/icon.css +5 -0
  86. data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
  87. data/lib/docyard/templates/assets/css/components/navigation.css +25 -3
  88. data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
  89. data/lib/docyard/templates/assets/css/components/prev-next.css +14 -7
  90. data/lib/docyard/templates/assets/css/components/search.css +6 -10
  91. data/lib/docyard/templates/assets/css/components/tab-bar.css +9 -6
  92. data/lib/docyard/templates/assets/css/components/table-of-contents.css +63 -17
  93. data/lib/docyard/templates/assets/css/components/tabs.css +12 -4
  94. data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
  95. data/lib/docyard/templates/assets/css/landing.css +82 -13
  96. data/lib/docyard/templates/assets/css/layout.css +32 -16
  97. data/lib/docyard/templates/assets/css/markdown.css +22 -2
  98. data/lib/docyard/templates/assets/css/variables.css +14 -1
  99. data/lib/docyard/templates/assets/js/components/code-group.js +4 -1
  100. data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
  101. data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
  102. data/lib/docyard/templates/assets/js/components/file-tree.js +5 -5
  103. data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
  104. data/lib/docyard/templates/assets/js/components/search.js +3 -3
  105. data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
  106. data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
  107. data/lib/docyard/templates/assets/js/components/tooltip.js +4 -4
  108. data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
  109. data/lib/docyard/templates/errors/404.html.erb +125 -5
  110. data/lib/docyard/templates/errors/500.html.erb +184 -10
  111. data/lib/docyard/templates/errors/redirect.html.erb +12 -0
  112. data/lib/docyard/templates/init/_sidebar.yml +36 -0
  113. data/lib/docyard/templates/init/docyard.yml +36 -0
  114. data/lib/docyard/templates/init/pages/components.md +146 -0
  115. data/lib/docyard/templates/init/pages/getting-started.md +94 -0
  116. data/lib/docyard/templates/init/pages/index.md +22 -0
  117. data/lib/docyard/templates/layouts/default.html.erb +10 -0
  118. data/lib/docyard/templates/layouts/splash.html.erb +14 -1
  119. data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
  120. data/lib/docyard/templates/partials/_banner.html.erb +1 -1
  121. data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
  122. data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
  123. data/lib/docyard/templates/partials/_footer.html.erb +1 -1
  124. data/lib/docyard/templates/partials/_head.html.erb +80 -5
  125. data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
  126. data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
  127. data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
  128. data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
  129. data/lib/docyard/utils/git_info.rb +157 -0
  130. data/lib/docyard/utils/hash_utils.rb +31 -0
  131. data/lib/docyard/utils/html_helpers.rb +8 -0
  132. data/lib/docyard/utils/logging.rb +44 -3
  133. data/lib/docyard/utils/path_resolver.rb +0 -10
  134. data/lib/docyard/utils/path_utils.rb +73 -0
  135. data/lib/docyard/version.rb +1 -1
  136. data/lib/docyard.rb +2 -2
  137. metadata +81 -47
  138. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  139. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  140. data/.github/pull_request_template.md +0 -14
  141. data/.github/workflows/ci.yml +0 -49
  142. data/.rubocop.yml +0 -42
  143. data/CODE_OF_CONDUCT.md +0 -132
  144. data/CONTRIBUTING.md +0 -55
  145. data/LICENSE.vscode-icons +0 -42
  146. data/Rakefile +0 -8
  147. data/lib/docyard/config/constants.rb +0 -31
  148. data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
  149. data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
  150. data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -90
  151. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
  152. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -71
  153. data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -51
  154. data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
  155. data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
  156. data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
  157. data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -140
  158. data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
  159. data/lib/docyard/rendering/icons/file_types.rb +0 -79
  160. data/lib/docyard/rendering/icons/phosphor.rb +0 -93
  161. data/lib/docyard/rendering/language_mapping.rb +0 -52
  162. data/lib/docyard/templates/assets/js/reload.js +0 -98
  163. data/lib/docyard/templates/partials/_icon.html.erb +0 -1
  164. data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
  165. data/sig/docyard.rbs +0 -4
data/LICENSE.vscode-icons DELETED
@@ -1,42 +0,0 @@
1
- File Type Icons from VSCode Icons
2
- =====================================
3
-
4
- The file type icons embedded in lib/docyard/icons/file_types.rb are from the VSCode Icons project:
5
- https://github.com/vscode-icons/vscode-icons
6
-
7
- License: Creative Commons Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)
8
- https://creativecommons.org/licenses/by-sa/4.0/
9
-
10
- Copyright (c) 2016 Roberto Huertas
11
-
12
- Icons included:
13
- - JavaScript (.js)
14
- - TypeScript (.ts)
15
- - JSX (.jsx)
16
- - TSX (.tsx)
17
- - Python (.py)
18
- - Ruby (.rb)
19
- - HTML (.html)
20
- - CSS (.css)
21
- - JSON (.json)
22
- - YAML (.yaml)
23
- - TOML (.toml)
24
- - Go (.go)
25
- - Rust (.rs)
26
- - PHP (.php)
27
- - SQL (.sql)
28
- - MySQL (.mysql)
29
- - PostgreSQL (.pgsql)
30
- - GraphQL (.graphql)
31
- - Vue (.vue)
32
- - Svelte (.svelte)
33
- - Protobuf (.proto)
34
-
35
- These icons are used under the terms of the CC BY-SA 4.0 license.
36
- You are free to:
37
- - Share — copy and redistribute the material in any medium or format
38
- - Adapt — remix, transform, and build upon the material for any purpose, even commercially
39
-
40
- Under the following terms:
41
- - Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made
42
- - ShareAlike — If you remix, transform, or build upon the material, you must distribute your contributions under the same license
data/Rakefile DELETED
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
5
-
6
- RSpec::Core::RakeTask.new(:spec)
7
-
8
- task default: :spec
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Docyard
4
- module Constants
5
- CONTENT_TYPE_HTML = "text/html; charset=utf-8"
6
- CONTENT_TYPE_JSON = "application/json; charset=utf-8"
7
- CONTENT_TYPE_CSS = "text/css; charset=utf-8"
8
- CONTENT_TYPE_JS = "application/javascript; charset=utf-8"
9
-
10
- RELOAD_ENDPOINT = "/_docyard/reload"
11
- DOCYARD_ASSETS_PREFIX = "/_docyard/"
12
- PAGEFIND_PREFIX = "/pagefind/"
13
- PUBLIC_DIR = "docs/public"
14
-
15
- INDEX_FILE = "index"
16
- INDEX_TITLE = "Home"
17
-
18
- MARKDOWN_EXTENSION = ".md"
19
- HTML_EXTENSION = ".html"
20
-
21
- STATUS_OK = 200
22
- STATUS_REDIRECT = 302
23
- STATUS_NOT_FOUND = 404
24
- STATUS_INTERNAL_ERROR = 500
25
-
26
- DEFAULT_SITE_TITLE = "Documentation"
27
- DEFAULT_LOGO_PATH = "_docyard/logo.svg"
28
- DEFAULT_LOGO_DARK_PATH = "_docyard/logo-dark.svg"
29
- DEFAULT_FAVICON_PATH = "_docyard/favicon.svg"
30
- end
31
- end
@@ -1,51 +0,0 @@
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
@@ -1,208 +0,0 @@
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
@@ -1,90 +0,0 @@
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
- paths = resolve_paths(slug, base_path, options)
62
- frontmatter = metadata_extractor.extract_frontmatter_metadata(paths[:file])
63
-
64
- build_context_hash(slug, paths, options, frontmatter)
65
- end
66
-
67
- def resolve_paths(slug, base_path, options)
68
- file_path = File.join(docs_path, base_path, "#{slug}.md")
69
- url_path = Utils::PathResolver.to_url(File.join(base_path, slug))
70
- final_path = options["link"] || options[:link] || url_path
71
-
72
- { file: file_path, final: final_path }
73
- end
74
-
75
- def build_context_hash(slug, paths, options, frontmatter)
76
- {
77
- slug: slug,
78
- text: metadata_extractor.resolve_item_text(slug, paths[:file], options, frontmatter[:text]),
79
- path: paths[:final],
80
- icon: metadata_extractor.resolve_item_icon(options, frontmatter[:icon]),
81
- badge: frontmatter[:badge],
82
- badge_type: frontmatter[:badge_type],
83
- active: current_path == paths[:final],
84
- type: :file,
85
- section: false
86
- }
87
- end
88
- end
89
- end
90
- end
@@ -1,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Docyard
4
- module Sidebar
5
- class FileSystemScanner
6
- attr_reader :docs_path
7
-
8
- def initialize(docs_path)
9
- @docs_path = docs_path
10
- end
11
-
12
- def scan
13
- return [] unless File.directory?(docs_path)
14
-
15
- scan_directory(docs_path, "")
16
- end
17
-
18
- private
19
-
20
- def scan_directory(base_path, relative_path)
21
- full_path = File.join(base_path, relative_path)
22
- return [] unless File.directory?(full_path)
23
-
24
- entries = sorted_entries(full_path, relative_path)
25
- entries.map { |entry| build_item(entry, base_path, relative_path, full_path) }.compact
26
- end
27
-
28
- def sorted_entries(full_path, relative_path)
29
- Dir.children(full_path)
30
- .reject { |entry| hidden_or_ignored?(entry, relative_path) }
31
- .sort_by { |entry| sort_key(entry) }
32
- end
33
-
34
- def build_item(entry, base_path, relative_path, full_path)
35
- entry_full_path = File.join(full_path, entry)
36
- entry_relative_path = build_relative_path(relative_path, entry)
37
-
38
- if File.directory?(entry_full_path)
39
- build_directory_item(entry, entry_relative_path, base_path)
40
- elsif entry.end_with?(".md")
41
- build_file_item(entry, entry_relative_path)
42
- end
43
- end
44
-
45
- def build_relative_path(relative_path, entry)
46
- relative_path.empty? ? entry : File.join(relative_path, entry)
47
- end
48
-
49
- def build_directory_item(entry, entry_relative_path, base_path)
50
- {
51
- type: :directory,
52
- name: entry,
53
- path: entry_relative_path,
54
- children: scan_directory(base_path, entry_relative_path)
55
- }
56
- end
57
-
58
- def build_file_item(entry, entry_relative_path)
59
- {
60
- type: :file,
61
- name: entry.delete_suffix(".md"),
62
- path: entry_relative_path
63
- }
64
- end
65
-
66
- def hidden_or_ignored?(entry, relative_path)
67
- entry.start_with?(".") ||
68
- entry.start_with?("_") ||
69
- (entry == "index.md" && relative_path.empty?) ||
70
- (entry == "public" && relative_path.empty?)
71
- end
72
-
73
- def sort_key(entry)
74
- entry.downcase
75
- end
76
- end
77
- end
78
- end
@@ -1,71 +0,0 @@
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
@@ -1,51 +0,0 @@
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
@@ -1,34 +0,0 @@
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,21 +0,0 @@
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