docyard 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/CHANGELOG.md +34 -1
  4. data/lib/docyard/build/asset_bundler.rb +22 -7
  5. data/lib/docyard/build/file_copier.rb +49 -27
  6. data/lib/docyard/build/sitemap_generator.rb +6 -6
  7. data/lib/docyard/build/static_generator.rb +82 -50
  8. data/lib/docyard/builder.rb +20 -10
  9. data/lib/docyard/cli.rb +6 -3
  10. data/lib/docyard/components/aliases.rb +29 -0
  11. data/lib/docyard/components/processors/callout_processor.rb +124 -0
  12. data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +106 -0
  13. data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +79 -0
  14. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +78 -0
  15. data/lib/docyard/components/processors/code_block_processor.rb +175 -0
  16. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +127 -0
  17. data/lib/docyard/components/processors/heading_anchor_processor.rb +39 -0
  18. data/lib/docyard/components/processors/icon_processor.rb +53 -0
  19. data/lib/docyard/components/processors/table_of_contents_processor.rb +68 -0
  20. data/lib/docyard/components/processors/table_wrapper_processor.rb +22 -0
  21. data/lib/docyard/components/processors/tabs_processor.rb +48 -0
  22. data/lib/docyard/components/support/code_block/feature_extractor.rb +117 -0
  23. data/lib/docyard/components/support/code_block/icon_detector.rb +44 -0
  24. data/lib/docyard/components/support/code_block/line_parser.rb +84 -0
  25. data/lib/docyard/components/support/code_block/line_wrapper.rb +50 -0
  26. data/lib/docyard/components/support/code_block/patterns.rb +55 -0
  27. data/lib/docyard/components/support/code_detector.rb +61 -0
  28. data/lib/docyard/components/support/tabs/icon_detector.rb +62 -0
  29. data/lib/docyard/components/support/tabs/parser.rb +195 -0
  30. data/lib/docyard/components/support/tabs/range_finder.rb +46 -0
  31. data/lib/docyard/config/branding_resolver.rb +183 -0
  32. data/lib/docyard/{constants.rb → config/constants.rb} +7 -4
  33. data/lib/docyard/config/validator.rb +122 -99
  34. data/lib/docyard/config.rb +38 -36
  35. data/lib/docyard/initializer.rb +15 -76
  36. data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
  37. data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +6 -3
  38. data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
  39. data/lib/docyard/navigation/sidebar/config_parser.rb +208 -0
  40. data/lib/docyard/navigation/sidebar/file_resolver.rb +78 -0
  41. data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +2 -1
  42. data/lib/docyard/navigation/sidebar/item.rb +96 -0
  43. data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
  44. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +69 -0
  45. data/lib/docyard/navigation/sidebar/metadata_reader.rb +47 -0
  46. data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
  47. data/lib/docyard/navigation/sidebar/renderer.rb +144 -0
  48. data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
  49. data/lib/docyard/navigation/sidebar/tree_builder.rb +139 -0
  50. data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
  51. data/lib/docyard/navigation/sidebar_builder.rb +159 -0
  52. data/lib/docyard/rendering/icon_helpers.rb +13 -0
  53. data/lib/docyard/{icons → rendering/icons}/phosphor.rb +26 -1
  54. data/lib/docyard/{markdown.rb → rendering/markdown.rb} +19 -13
  55. data/lib/docyard/rendering/renderer.rb +163 -0
  56. data/lib/docyard/rendering/template_resolver.rb +172 -0
  57. data/lib/docyard/routing/fallback_resolver.rb +92 -0
  58. data/lib/docyard/search/build_indexer.rb +74 -0
  59. data/lib/docyard/search/dev_indexer.rb +155 -0
  60. data/lib/docyard/search/pagefind_support.rb +33 -0
  61. data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +24 -19
  62. data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
  63. data/lib/docyard/server/pagefind_handler.rb +63 -0
  64. data/lib/docyard/{preview_server.rb → server/preview_server.rb} +2 -2
  65. data/lib/docyard/server/rack_application.rb +192 -0
  66. data/lib/docyard/server/resolution_result.rb +29 -0
  67. data/lib/docyard/{router.rb → server/router.rb} +4 -4
  68. data/lib/docyard/templates/assets/css/code.css +18 -51
  69. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +143 -0
  70. data/lib/docyard/templates/assets/css/components/callout.css +67 -67
  71. data/lib/docyard/templates/assets/css/components/code-block.css +180 -282
  72. data/lib/docyard/templates/assets/css/components/heading-anchor.css +28 -15
  73. data/lib/docyard/templates/assets/css/components/icon.css +0 -1
  74. data/lib/docyard/templates/assets/css/components/logo.css +0 -2
  75. data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
  76. data/lib/docyard/templates/assets/css/components/navigation.css +186 -167
  77. data/lib/docyard/templates/assets/css/components/prev-next.css +76 -47
  78. data/lib/docyard/templates/assets/css/components/search.css +561 -0
  79. data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
  80. data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
  81. data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
  82. data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
  83. data/lib/docyard/templates/assets/css/landing.css +815 -0
  84. data/lib/docyard/templates/assets/css/layout.css +503 -87
  85. data/lib/docyard/templates/assets/css/main.css +1 -3
  86. data/lib/docyard/templates/assets/css/markdown.css +111 -93
  87. data/lib/docyard/templates/assets/css/reset.css +0 -3
  88. data/lib/docyard/templates/assets/css/typography.css +43 -41
  89. data/lib/docyard/templates/assets/css/variables.css +268 -208
  90. data/lib/docyard/templates/assets/favicon.svg +7 -8
  91. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
  92. data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
  93. data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
  94. data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
  95. data/lib/docyard/templates/assets/js/components/search.js +610 -0
  96. data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
  97. data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
  98. data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
  99. data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
  100. data/lib/docyard/templates/assets/js/theme.js +0 -3
  101. data/lib/docyard/templates/assets/logo-dark.svg +8 -2
  102. data/lib/docyard/templates/assets/logo.svg +7 -4
  103. data/lib/docyard/templates/config/docyard.yml.erb +37 -34
  104. data/lib/docyard/templates/errors/404.html.erb +1 -1
  105. data/lib/docyard/templates/errors/500.html.erb +1 -1
  106. data/lib/docyard/templates/layouts/default.html.erb +19 -56
  107. data/lib/docyard/templates/layouts/splash.html.erb +176 -0
  108. data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
  109. data/lib/docyard/templates/partials/_code_block.html.erb +6 -4
  110. data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
  111. data/lib/docyard/templates/partials/_features.html.erb +15 -0
  112. data/lib/docyard/templates/partials/_footer.html.erb +42 -0
  113. data/lib/docyard/templates/partials/_head.html.erb +22 -0
  114. data/lib/docyard/templates/partials/_header.html.erb +49 -0
  115. data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
  116. data/lib/docyard/templates/partials/_hero.html.erb +27 -0
  117. data/lib/docyard/templates/partials/_nav_group.html.erb +25 -11
  118. data/lib/docyard/templates/partials/_nav_leaf.html.erb +1 -1
  119. data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
  120. data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
  121. data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
  122. data/lib/docyard/templates/partials/_prev_next.html.erb +9 -3
  123. data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
  124. data/lib/docyard/templates/partials/_search_modal.html.erb +41 -0
  125. data/lib/docyard/templates/partials/_search_trigger.html.erb +18 -0
  126. data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
  127. data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
  128. data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
  129. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
  130. data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
  131. data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
  132. data/lib/docyard/utils/html_helpers.rb +14 -0
  133. data/lib/docyard/utils/path_resolver.rb +2 -1
  134. data/lib/docyard/utils/url_helpers.rb +20 -0
  135. data/lib/docyard/version.rb +1 -1
  136. data/lib/docyard.rb +22 -15
  137. metadata +89 -50
  138. data/lib/docyard/components/callout_processor.rb +0 -121
  139. data/lib/docyard/components/code_block_diff_preprocessor.rb +0 -104
  140. data/lib/docyard/components/code_block_feature_extractor.rb +0 -113
  141. data/lib/docyard/components/code_block_focus_preprocessor.rb +0 -77
  142. data/lib/docyard/components/code_block_icon_detector.rb +0 -40
  143. data/lib/docyard/components/code_block_line_wrapper.rb +0 -46
  144. data/lib/docyard/components/code_block_options_preprocessor.rb +0 -76
  145. data/lib/docyard/components/code_block_patterns.rb +0 -51
  146. data/lib/docyard/components/code_block_processor.rb +0 -176
  147. data/lib/docyard/components/code_detector.rb +0 -59
  148. data/lib/docyard/components/code_line_parser.rb +0 -80
  149. data/lib/docyard/components/code_snippet_import_preprocessor.rb +0 -125
  150. data/lib/docyard/components/heading_anchor_processor.rb +0 -34
  151. data/lib/docyard/components/icon_detector.rb +0 -57
  152. data/lib/docyard/components/icon_processor.rb +0 -51
  153. data/lib/docyard/components/table_of_contents_processor.rb +0 -64
  154. data/lib/docyard/components/table_wrapper_processor.rb +0 -18
  155. data/lib/docyard/components/tabs_parser.rb +0 -191
  156. data/lib/docyard/components/tabs_processor.rb +0 -44
  157. data/lib/docyard/components/tabs_range_finder.rb +0 -42
  158. data/lib/docyard/rack_application.rb +0 -172
  159. data/lib/docyard/renderer.rb +0 -120
  160. data/lib/docyard/routing/resolution_result.rb +0 -31
  161. data/lib/docyard/sidebar/config_parser.rb +0 -180
  162. data/lib/docyard/sidebar/item.rb +0 -58
  163. data/lib/docyard/sidebar/renderer.rb +0 -137
  164. data/lib/docyard/sidebar/tree_builder.rb +0 -59
  165. data/lib/docyard/sidebar_builder.rb +0 -102
  166. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
  167. data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
  168. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
  169. data/lib/docyard/templates/markdown/index.md.erb +0 -82
  170. /data/lib/docyard/{sidebar → navigation/sidebar}/title_extractor.rb +0 -0
  171. /data/lib/docyard/{icons → rendering/icons}/LICENSE.phosphor +0 -0
  172. /data/lib/docyard/{icons → rendering/icons}/file_types.rb +0 -0
  173. /data/lib/docyard/{icons.rb → rendering/icons.rb} +0 -0
  174. /data/lib/docyard/{language_mapping.rb → rendering/language_mapping.rb} +0 -0
  175. /data/lib/docyard/{file_watcher.rb → server/file_watcher.rb} +0 -0
  176. /data/lib/docyard/{errors.rb → utils/errors.rb} +0 -0
  177. /data/lib/docyard/{logging.rb → utils/logging.rb} +0 -0
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../renderer"
4
- require_relative "base_processor"
5
- require_relative "tabs_parser"
6
- require "securerandom"
7
-
8
- module Docyard
9
- module Components
10
- class TabsProcessor < BaseProcessor
11
- self.priority = 15
12
-
13
- def preprocess(content)
14
- return content unless content.include?(":::tabs")
15
-
16
- content.gsub(/^:::[ \t]*tabs[ \t]*\n(.*?)^:::[ \t]*$/m) do
17
- process_tabs_block(Regexp.last_match(1))
18
- end
19
- end
20
-
21
- private
22
-
23
- def process_tabs_block(tabs_content)
24
- tabs = TabsParser.parse(tabs_content)
25
- return "" if tabs.empty?
26
-
27
- wrap_in_nomarkdown(render_tabs(tabs))
28
- end
29
-
30
- def render_tabs(tabs)
31
- Renderer.new.render_partial(
32
- "_tabs", {
33
- tabs: tabs,
34
- group_id: SecureRandom.hex(4)
35
- }
36
- )
37
- end
38
-
39
- def wrap_in_nomarkdown(html)
40
- "{::nomarkdown}\n#{html}\n{:/nomarkdown}"
41
- end
42
- end
43
- end
44
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Docyard
4
- module Components
5
- module TabsRangeFinder
6
- module_function
7
-
8
- def find_ranges(html)
9
- ranges = []
10
- start_pattern = '<div class="docyard-tabs"'
11
-
12
- pos = 0
13
- while (start_pos = html.index(start_pattern, pos))
14
- end_pos = find_matching_close_div(html, start_pos)
15
- ranges << (start_pos...end_pos) if end_pos
16
- pos = end_pos || (start_pos + 1)
17
- end
18
- ranges
19
- end
20
-
21
- def find_matching_close_div(html, start_pos)
22
- depth = 0
23
- pos = start_pos
24
-
25
- while pos < html.length
26
- if html[pos, 4] == "<div"
27
- depth += 1
28
- pos += 4
29
- elsif html[pos, 6] == "</div>"
30
- depth -= 1
31
- return pos + 6 if depth.zero?
32
-
33
- pos += 6
34
- else
35
- pos += 1
36
- end
37
- end
38
- nil
39
- end
40
- end
41
- end
42
- end
@@ -1,172 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "rack"
5
- require_relative "sidebar_builder"
6
- require_relative "prev_next_builder"
7
- require_relative "constants"
8
-
9
- module Docyard
10
- class RackApplication
11
- def initialize(docs_path:, file_watcher:, config: nil)
12
- @docs_path = docs_path
13
- @file_watcher = file_watcher
14
- @config = config
15
- @router = Router.new(docs_path: docs_path)
16
- @renderer = Renderer.new(base_url: config&.build&.base_url || "/", config: config)
17
- @asset_handler = AssetHandler.new
18
- end
19
-
20
- def call(env)
21
- handle_request(env)
22
- end
23
-
24
- private
25
-
26
- attr_reader :docs_path, :file_watcher, :config, :router, :renderer, :asset_handler
27
-
28
- def handle_request(env)
29
- path = env["PATH_INFO"]
30
-
31
- return handle_reload_check(env) if path == Constants::RELOAD_ENDPOINT
32
- return asset_handler.serve(path) if path.start_with?(Constants::ASSETS_PREFIX)
33
-
34
- handle_documentation_request(path)
35
- rescue StandardError => e
36
- handle_error(e)
37
- end
38
-
39
- def handle_documentation_request(path)
40
- result = router.resolve(path)
41
-
42
- if result.found?
43
- render_documentation_page(result.file_path, path)
44
- else
45
- render_not_found_page
46
- end
47
- end
48
-
49
- def render_documentation_page(file_path, current_path)
50
- sidebar_builder = build_sidebar_instance(current_path)
51
-
52
- html = renderer.render_file(
53
- file_path,
54
- sidebar_html: sidebar_builder.to_html,
55
- prev_next_html: build_prev_next(sidebar_builder, current_path, file_path),
56
- branding: branding_options
57
- )
58
-
59
- [Constants::STATUS_OK, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
60
- end
61
-
62
- def render_not_found_page
63
- html = renderer.render_not_found
64
- [Constants::STATUS_NOT_FOUND, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
65
- end
66
-
67
- def build_sidebar_instance(current_path)
68
- SidebarBuilder.new(
69
- docs_path: docs_path,
70
- current_path: current_path,
71
- config: config
72
- )
73
- end
74
-
75
- def build_prev_next(sidebar_builder, current_path, file_path)
76
- markdown_content = File.read(file_path)
77
- markdown = Markdown.new(markdown_content)
78
-
79
- PrevNextBuilder.new(
80
- sidebar_tree: sidebar_builder.tree,
81
- current_path: current_path,
82
- frontmatter: markdown.frontmatter,
83
- config: navigation_config
84
- ).to_html
85
- end
86
-
87
- def navigation_config
88
- return {} unless config
89
-
90
- config.navigation&.footer || {}
91
- end
92
-
93
- def branding_options
94
- return default_branding unless config
95
-
96
- default_branding.merge(config_branding_options)
97
- end
98
-
99
- def default_branding
100
- {
101
- site_title: Constants::DEFAULT_SITE_TITLE,
102
- site_description: "",
103
- logo: Constants::DEFAULT_LOGO_PATH,
104
- logo_dark: Constants::DEFAULT_LOGO_DARK_PATH,
105
- favicon: nil,
106
- display_logo: true,
107
- display_title: true
108
- }
109
- end
110
-
111
- def config_branding_options
112
- site = config.site
113
- branding = config.branding
114
-
115
- {
116
- site_title: site.title || Constants::DEFAULT_SITE_TITLE,
117
- site_description: site.description || "",
118
- logo: resolve_logo(branding.logo, branding.logo_dark),
119
- logo_dark: resolve_logo_dark(branding.logo, branding.logo_dark),
120
- favicon: branding.favicon
121
- }.merge(appearance_options(branding.appearance))
122
- end
123
-
124
- def appearance_options(appearance)
125
- appearance ||= {}
126
- {
127
- display_logo: appearance["logo"] != false,
128
- display_title: appearance["title"] != false
129
- }
130
- end
131
-
132
- def resolve_logo(logo, logo_dark)
133
- logo || logo_dark || Constants::DEFAULT_LOGO_PATH
134
- end
135
-
136
- def resolve_logo_dark(logo, logo_dark)
137
- logo_dark || logo || Constants::DEFAULT_LOGO_DARK_PATH
138
- end
139
-
140
- def handle_reload_check(env)
141
- since = parse_since_timestamp(env)
142
- reload_needed = file_watcher.changed_since?(since)
143
-
144
- build_reload_response(reload_needed)
145
- rescue StandardError => e
146
- log_reload_error(e)
147
- build_reload_response(false)
148
- end
149
-
150
- def parse_since_timestamp(env)
151
- query = Rack::Utils.parse_query(env["QUERY_STRING"])
152
- query["since"] ? Time.at(query["since"].to_f) : Time.now
153
- end
154
-
155
- def log_reload_error(error)
156
- Docyard.logger.error "Reload check error: #{error.message}"
157
- Docyard.logger.debug error.backtrace.join("\n")
158
- end
159
-
160
- def build_reload_response(reload_needed)
161
- response_body = { reload: reload_needed, timestamp: Time.now.to_f }.to_json
162
- [Constants::STATUS_OK, { "Content-Type" => Constants::CONTENT_TYPE_JSON }, [response_body]]
163
- end
164
-
165
- def handle_error(error)
166
- Docyard.logger.error "Request error: #{error.message}"
167
- Docyard.logger.debug error.backtrace.join("\n")
168
- [Constants::STATUS_INTERNAL_ERROR, { "Content-Type" => Constants::CONTENT_TYPE_HTML },
169
- [renderer.render_server_error(error)]]
170
- end
171
- end
172
- end
@@ -1,120 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "erb"
4
- require_relative "constants"
5
-
6
- module Docyard
7
- class Renderer
8
- LAYOUTS_PATH = File.join(__dir__, "templates", "layouts")
9
- ERRORS_PATH = File.join(__dir__, "templates", "errors")
10
- PARTIALS_PATH = File.join(__dir__, "templates", "partials")
11
-
12
- attr_reader :layout_path, :base_url, :config
13
-
14
- def initialize(layout: "default", base_url: "/", config: nil)
15
- @layout_path = File.join(LAYOUTS_PATH, "#{layout}.html.erb")
16
- @base_url = normalize_base_url(base_url)
17
- @config = config
18
- end
19
-
20
- def render_file(file_path, sidebar_html: "", prev_next_html: "", branding: {})
21
- markdown_content = File.read(file_path)
22
- markdown = Markdown.new(markdown_content, config: config)
23
-
24
- html_content = strip_md_from_links(markdown.html)
25
- toc = markdown.toc
26
-
27
- render(
28
- content: html_content,
29
- page_title: markdown.title || Constants::DEFAULT_SITE_TITLE,
30
- navigation: {
31
- sidebar_html: sidebar_html,
32
- prev_next_html: prev_next_html,
33
- toc: toc
34
- },
35
- branding: branding
36
- )
37
- end
38
-
39
- def render(content:, page_title: Constants::DEFAULT_SITE_TITLE, navigation: {}, branding: {})
40
- template = File.read(layout_path)
41
-
42
- sidebar_html = navigation[:sidebar_html] || ""
43
- prev_next_html = navigation[:prev_next_html] || ""
44
- toc = navigation[:toc] || []
45
-
46
- assign_content_variables(content, page_title, sidebar_html, prev_next_html, toc)
47
- assign_branding_variables(branding)
48
-
49
- ERB.new(template).result(binding)
50
- end
51
-
52
- def render_not_found
53
- render_error_template(404)
54
- end
55
-
56
- def render_server_error(error)
57
- @error_message = error.message
58
- @backtrace = error.backtrace.join("\n")
59
- render_error_template(500)
60
- end
61
-
62
- def render_error_template(status)
63
- error_template_path = File.join(ERRORS_PATH, "#{status}.html.erb")
64
- template = File.read(error_template_path)
65
- ERB.new(template).result(binding)
66
- end
67
-
68
- def render_partial(name, locals = {})
69
- partial_path = File.join(PARTIALS_PATH, "#{name}.html.erb")
70
- template = File.read(partial_path)
71
-
72
- locals.each { |key, value| instance_variable_set("@#{key}", value) }
73
-
74
- ERB.new(template).result(binding)
75
- end
76
-
77
- def asset_path(path)
78
- return path if path.nil? || path.start_with?("http://", "https://")
79
-
80
- "#{base_url}#{path}"
81
- end
82
-
83
- def link_path(path)
84
- return path if path.nil? || path.start_with?("http://", "https://")
85
-
86
- "#{base_url.chomp('/')}#{path}"
87
- end
88
-
89
- private
90
-
91
- def normalize_base_url(url)
92
- return "/" if url.nil? || url.empty?
93
-
94
- url = "/#{url}" unless url.start_with?("/")
95
- url.end_with?("/") ? url : "#{url}/"
96
- end
97
-
98
- def assign_content_variables(content, page_title, sidebar_html, prev_next_html, toc)
99
- @content = content
100
- @page_title = page_title
101
- @sidebar_html = sidebar_html
102
- @prev_next_html = prev_next_html
103
- @toc = toc
104
- end
105
-
106
- def assign_branding_variables(branding)
107
- @site_title = branding[:site_title] || Constants::DEFAULT_SITE_TITLE
108
- @site_description = branding[:site_description] || ""
109
- @logo = branding[:logo] || Constants::DEFAULT_LOGO_PATH
110
- @logo_dark = branding[:logo_dark]
111
- @favicon = branding[:favicon] || Constants::DEFAULT_FAVICON_PATH
112
- @display_logo = branding[:display_logo].nil? || branding[:display_logo]
113
- @display_title = branding[:display_title].nil? || branding[:display_title]
114
- end
115
-
116
- def strip_md_from_links(html)
117
- html.gsub(/href="([^"]+)\.md"/, 'href="\1"')
118
- end
119
- end
120
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Docyard
4
- module Routing
5
- class ResolutionResult
6
- attr_reader :file_path, :status
7
-
8
- def self.found(file_path)
9
- new(file_path: file_path, status: :found)
10
- end
11
-
12
- def self.not_found
13
- new(file_path: nil, status: :not_found)
14
- end
15
-
16
- def initialize(file_path:, status:)
17
- @file_path = file_path
18
- @status = status
19
- freeze
20
- end
21
-
22
- def found?
23
- status == :found
24
- end
25
-
26
- def not_found?
27
- status == :not_found
28
- end
29
- end
30
- end
31
- end
@@ -1,180 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "item"
4
- require_relative "title_extractor"
5
-
6
- module Docyard
7
- module Sidebar
8
- class ConfigParser
9
- attr_reader :config_items, :docs_path, :current_path, :title_extractor
10
-
11
- def initialize(config_items, docs_path:, current_path: "/", title_extractor: TitleExtractor.new)
12
- @config_items = config_items || []
13
- @docs_path = docs_path
14
- @current_path = Utils::PathResolver.normalize(current_path)
15
- @title_extractor = title_extractor
16
- end
17
-
18
- def parse
19
- parse_items(config_items)
20
- end
21
-
22
- private
23
-
24
- def parse_items(items, base_path = "")
25
- items.map do |item_config|
26
- parse_item(item_config, base_path)
27
- end.compact
28
- end
29
-
30
- def parse_item(item_config, base_path)
31
- case item_config
32
- when String
33
- resolve_file_item(item_config, base_path)
34
- when Hash
35
- parse_hash_item(item_config, base_path)
36
- end
37
- end
38
-
39
- def parse_hash_item(item_config, base_path)
40
- return parse_link_item(item_config) if link_item?(item_config)
41
- return parse_nested_item(item_config, base_path) if nested_item?(item_config)
42
- return resolve_file_item(item_config.keys.first, base_path, {}) if nil_value_item?(item_config)
43
-
44
- slug = item_config.keys.first
45
- options = item_config.values.first || {}
46
- resolve_file_item(slug, base_path, options)
47
- end
48
-
49
- def link_item?(config)
50
- config.key?("link") || config.key?(:link)
51
- end
52
-
53
- def nested_item?(config)
54
- config.size == 1 && config.values.first.is_a?(Hash)
55
- end
56
-
57
- def nil_value_item?(config)
58
- config.size == 1 && config.values.first.nil?
59
- end
60
-
61
- def parse_link_item(config)
62
- link = config["link"] || config[:link]
63
- text = config["text"] || config[:text]
64
- icon = config["icon"] || config[:icon]
65
- target = config["target"] || config[:target] || "_blank"
66
-
67
- Item.new(
68
- text: text,
69
- link: link,
70
- path: link,
71
- icon: icon,
72
- target: target,
73
- type: :external
74
- )
75
- end
76
-
77
- def parse_nested_item(item_config, base_path)
78
- slug = item_config.keys.first.to_s
79
- options = item_config.values.first || {}
80
- nested_items = extract_nested_items(options)
81
-
82
- dir_path = File.join(docs_path, base_path, slug)
83
-
84
- if File.directory?(dir_path)
85
- build_directory_item(slug, options, nested_items, base_path)
86
- elsif nested_items.any?
87
- build_file_with_children_item(slug, options, nested_items, base_path)
88
- else
89
- resolve_file_item(slug, base_path, options)
90
- end
91
- end
92
-
93
- def extract_nested_items(options)
94
- options["items"] || options[:items] || []
95
- end
96
-
97
- def extract_common_options(options)
98
- {
99
- text: options["text"] || options[:text],
100
- icon: options["icon"] || options[:icon],
101
- collapsed: options["collapsed"] || options[:collapsed] || false
102
- }
103
- end
104
-
105
- def build_directory_item(slug, options, nested_items, base_path)
106
- common_opts = extract_common_options(options)
107
- new_base_path = File.join(base_path, slug)
108
- parsed_items = parse_items(nested_items, new_base_path)
109
-
110
- Item.new(
111
- slug: slug,
112
- text: common_opts[:text] || Utils::TextFormatter.titleize(slug),
113
- icon: common_opts[:icon],
114
- collapsed: common_opts[:collapsed],
115
- items: parsed_items,
116
- type: :directory
117
- )
118
- end
119
-
120
- def build_file_with_children_item(slug, options, nested_items, base_path)
121
- common_opts = extract_common_options(options)
122
- file_path = File.join(docs_path, base_path, "#{slug}.md")
123
- url_path = Utils::PathResolver.to_url(File.join(base_path, slug))
124
- resolved_text = common_opts[:text] || extract_file_title(file_path, slug)
125
-
126
- Item.new(
127
- slug: slug,
128
- text: resolved_text,
129
- path: url_path,
130
- icon: common_opts[:icon],
131
- collapsed: common_opts[:collapsed],
132
- items: parse_items(nested_items, base_path),
133
- active: current_path == url_path,
134
- type: :file
135
- )
136
- end
137
-
138
- def extract_file_title(file_path, slug)
139
- File.exist?(file_path) ? title_extractor.extract(file_path) : Utils::TextFormatter.titleize(slug)
140
- end
141
-
142
- def resolve_file_item(slug, base_path, options = {})
143
- slug_str = slug.to_s
144
- options ||= {}
145
-
146
- file_path = File.join(docs_path, base_path, "#{slug_str}.md")
147
- url_path = Utils::PathResolver.to_url(File.join(base_path, slug_str))
148
-
149
- frontmatter = extract_frontmatter_metadata(file_path)
150
- text = resolve_item_text(slug_str, file_path, options, frontmatter[:text])
151
- icon = resolve_item_icon(options, frontmatter[:icon])
152
- final_path = options["link"] || options[:link] || url_path
153
-
154
- Item.new(
155
- slug: slug_str, text: text, path: final_path, icon: icon,
156
- active: current_path == final_path, type: :file
157
- )
158
- end
159
-
160
- def extract_frontmatter_metadata(file_path)
161
- return { text: nil, icon: nil } unless File.exist?(file_path)
162
-
163
- markdown = Markdown.new(File.read(file_path))
164
- {
165
- text: markdown.sidebar_text || markdown.title,
166
- icon: markdown.sidebar_icon
167
- }
168
- end
169
-
170
- def resolve_item_text(slug, file_path, options, frontmatter_text)
171
- text = options["text"] || options[:text] || frontmatter_text
172
- text || extract_file_title(file_path, slug)
173
- end
174
-
175
- def resolve_item_icon(options, frontmatter_icon)
176
- options["icon"] || options[:icon] || frontmatter_icon
177
- end
178
- end
179
- end
180
- end
@@ -1,58 +0,0 @@
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
7
-
8
- def initialize(**options)
9
- @slug = options[:slug]
10
- @text = options[:text]
11
- @icon = options[:icon]
12
- @link = options[:link]
13
- @target = options[:target] || "_self"
14
- @collapsed = options[:collapsed] || false
15
- @items = options[:items] || []
16
- @path = options[:path] || options[:link]
17
- @active = options[:active] || false
18
- @type = options[:type] || :file
19
- end
20
-
21
- def external?
22
- return false if path.nil?
23
-
24
- path.start_with?("http://", "https://")
25
- end
26
-
27
- def children?
28
- items.any?
29
- end
30
-
31
- def title
32
- text
33
- end
34
-
35
- def children
36
- items
37
- end
38
-
39
- def collapsible?
40
- children?
41
- end
42
-
43
- def to_h
44
- {
45
- title: title,
46
- path: path,
47
- icon: icon,
48
- active: active,
49
- type: type,
50
- collapsed: collapsed,
51
- collapsible: collapsible?,
52
- target: target,
53
- children: children.map(&:to_h)
54
- }
55
- end
56
- end
57
- end
58
- end