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
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../code_detector"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Support
8
+ module Tabs
9
+ class IconDetector
10
+ MANUAL_ICON_PATTERN = /^:([a-z0-9-]+):\s*(.+)$/i
11
+ CodeDetector = Support::CodeDetector
12
+
13
+ def self.detect(tab_name, tab_content)
14
+ new(tab_name, tab_content).detect
15
+ end
16
+
17
+ def initialize(tab_name, tab_content)
18
+ @tab_name = tab_name
19
+ @tab_content = tab_content
20
+ end
21
+
22
+ def detect
23
+ manual_icon || auto_detected_icon || no_icon
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :tab_name, :tab_content
29
+
30
+ def manual_icon
31
+ return nil unless tab_name.match(MANUAL_ICON_PATTERN)
32
+
33
+ {
34
+ name: Regexp.last_match(2).strip,
35
+ icon: Regexp.last_match(1),
36
+ icon_source: "phosphor"
37
+ }
38
+ end
39
+
40
+ def auto_detected_icon
41
+ detected = CodeDetector.detect(tab_content)
42
+ return nil unless detected
43
+
44
+ {
45
+ name: tab_name,
46
+ icon: detected[:icon],
47
+ icon_source: detected[:source]
48
+ }
49
+ end
50
+
51
+ def no_icon
52
+ {
53
+ name: tab_name,
54
+ icon: nil,
55
+ icon_source: nil
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../code_block/feature_extractor"
4
+ require_relative "../code_block/icon_detector"
5
+ require_relative "../code_block/line_wrapper"
6
+ require_relative "icon_detector"
7
+ require_relative "../../../rendering/icons"
8
+ require_relative "../../../rendering/renderer"
9
+ require "kramdown"
10
+ require "kramdown-parser-gfm"
11
+ require "cgi"
12
+
13
+ module Docyard
14
+ module Components
15
+ module Support
16
+ module Tabs
17
+ class Parser
18
+ include Utils::HtmlHelpers
19
+
20
+ IconDetector = Tabs::IconDetector
21
+ CodeBlockFeatureExtractor = CodeBlock::FeatureExtractor
22
+ CodeBlockIconDetector = CodeBlock::IconDetector
23
+ CodeBlockLineWrapper = CodeBlock::LineWrapper
24
+
25
+ def self.parse(content)
26
+ new(content).parse
27
+ end
28
+
29
+ def initialize(content)
30
+ @content = content
31
+ end
32
+
33
+ def parse
34
+ sections.filter_map { |section| parse_section(section) }
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :content
40
+
41
+ def sections
42
+ content.split(/^==[ \t]+/)
43
+ end
44
+
45
+ def parse_section(section)
46
+ return nil if section.strip.empty?
47
+
48
+ tab_name, tab_content = extract_tab_parts(section)
49
+ return nil if tab_name.nil? || tab_name.empty?
50
+
51
+ build_tab_data(tab_name, tab_content)
52
+ end
53
+
54
+ def extract_tab_parts(section)
55
+ parts = section.split("\n", 2)
56
+ [parts[0]&.strip, parts[1]&.strip || ""]
57
+ end
58
+
59
+ def build_tab_data(tab_name, tab_content)
60
+ icon_data = IconDetector.detect(tab_name, tab_content)
61
+
62
+ extracted = CodeBlockFeatureExtractor.process_markdown(tab_content)
63
+ rendered_content = render_markdown(extracted[:cleaned_markdown])
64
+ enhanced_content = enhance_code_blocks(rendered_content, extracted[:blocks])
65
+
66
+ {
67
+ name: icon_data[:name],
68
+ content: enhanced_content,
69
+ icon: icon_data[:icon],
70
+ icon_source: icon_data[:icon_source]
71
+ }
72
+ end
73
+
74
+ def render_markdown(markdown_content)
75
+ return "" if markdown_content.empty?
76
+
77
+ Kramdown::Document.new(
78
+ markdown_content,
79
+ input: "GFM",
80
+ hard_wrap: false,
81
+ syntax_highlighter: "rouge"
82
+ ).to_html
83
+ end
84
+
85
+ def enhance_code_blocks(html, blocks)
86
+ return html unless html.include?('<div class="highlight">')
87
+
88
+ block_index = 0
89
+ html.gsub(%r{<div class="highlight">(.*?)</div>}m) do
90
+ block_data = blocks[block_index] || {}
91
+ block_index += 1
92
+ render_enhanced_code_block(Regexp.last_match, block_data)
93
+ end
94
+ end
95
+
96
+ def render_enhanced_code_block(match, block_data)
97
+ original_html = match[0]
98
+ inner_html = match[1]
99
+ code_text = extract_code_text(inner_html)
100
+
101
+ processed_html = if needs_line_wrapping?(block_data)
102
+ wrap_code_block_lines(original_html, block_data)
103
+ else
104
+ original_html
105
+ end
106
+
107
+ Renderer.new.render_partial("_code_block", build_full_locals(processed_html, code_text, block_data))
108
+ end
109
+
110
+ def needs_line_wrapping?(block_data)
111
+ %i[highlights diff_lines focus_lines error_lines warning_lines].any? do |key|
112
+ block_data[key]&.any?
113
+ end
114
+ end
115
+
116
+ def wrap_code_block_lines(html, block_data)
117
+ wrapper_data = {
118
+ highlights: block_data[:highlights] || [],
119
+ diff_lines: block_data[:diff_lines] || {},
120
+ focus_lines: block_data[:focus_lines] || {},
121
+ error_lines: block_data[:error_lines] || {},
122
+ warning_lines: block_data[:warning_lines] || {},
123
+ start_line: extract_start_line(block_data[:option])
124
+ }
125
+ CodeBlockLineWrapper.wrap_code_block(html, wrapper_data)
126
+ end
127
+
128
+ def build_full_locals(processed_html, code_text, block_data)
129
+ title_data = CodeBlockIconDetector.detect(block_data[:title], block_data[:lang])
130
+ show_line_numbers = line_numbers_enabled?(block_data[:option])
131
+ start_line = extract_start_line(block_data[:option])
132
+
133
+ base_locals(processed_html, code_text, show_line_numbers, start_line)
134
+ .merge(feature_locals(block_data))
135
+ .merge(title_locals(title_data))
136
+ end
137
+
138
+ def base_locals(processed_html, code_text, show_line_numbers, start_line)
139
+ {
140
+ code_block_html: processed_html,
141
+ code_text: escape_html_attribute(code_text),
142
+ copy_icon: Icons.render("copy", "regular") || "",
143
+ show_line_numbers: show_line_numbers,
144
+ line_numbers: show_line_numbers ? generate_line_numbers(code_text, start_line) : [],
145
+ start_line: start_line
146
+ }
147
+ end
148
+
149
+ def feature_locals(block_data)
150
+ {
151
+ highlights: block_data[:highlights] || [],
152
+ diff_lines: block_data[:diff_lines] || {},
153
+ focus_lines: block_data[:focus_lines] || {},
154
+ error_lines: block_data[:error_lines] || {},
155
+ warning_lines: block_data[:warning_lines] || {}
156
+ }
157
+ end
158
+
159
+ def title_locals(title_data)
160
+ {
161
+ title: title_data[:title],
162
+ icon: title_data[:icon],
163
+ icon_source: title_data[:icon_source]
164
+ }
165
+ end
166
+
167
+ def line_numbers_enabled?(block_option)
168
+ return false if block_option == ":no-line-numbers"
169
+ return true if block_option&.start_with?(":line-numbers")
170
+
171
+ false
172
+ end
173
+
174
+ def extract_start_line(block_option)
175
+ return 1 unless block_option&.include?("=")
176
+
177
+ block_option.split("=").last.to_i
178
+ end
179
+
180
+ def generate_line_numbers(code_text, start_line)
181
+ line_count = code_text.lines.count
182
+ line_count = 1 if line_count.zero?
183
+ (start_line...(start_line + line_count)).to_a
184
+ end
185
+
186
+ def extract_code_text(html)
187
+ text = html.gsub(/<[^>]+>/, "")
188
+ text = CGI.unescapeHTML(text)
189
+ text.strip
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ module Support
6
+ module Tabs
7
+ module RangeFinder
8
+ module_function
9
+
10
+ def find_ranges(html)
11
+ ranges = []
12
+ start_pattern = '<div class="docyard-tabs"'
13
+
14
+ pos = 0
15
+ while (start_pos = html.index(start_pattern, pos))
16
+ end_pos = find_matching_close_div(html, start_pos)
17
+ ranges << (start_pos...end_pos) if end_pos
18
+ pos = end_pos || (start_pos + 1)
19
+ end
20
+ ranges
21
+ end
22
+
23
+ def find_matching_close_div(html, start_pos)
24
+ depth = 0
25
+ pos = start_pos
26
+
27
+ while pos < html.length
28
+ if html[pos, 4] == "<div"
29
+ depth += 1
30
+ pos += 4
31
+ elsif html[pos, 6] == "</div>"
32
+ depth -= 1
33
+ return pos + 6 if depth.zero?
34
+
35
+ pos += 6
36
+ else
37
+ pos += 1
38
+ end
39
+ end
40
+ nil
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class BrandingResolver
5
+ def initialize(config)
6
+ @config = config
7
+ end
8
+
9
+ SOCIAL_ICON_MAP = {
10
+ "x" => "x-logo", "twitter" => "x-logo", "discord" => "discord-logo",
11
+ "linkedin" => "linkedin-logo", "youtube" => "youtube-logo", "instagram" => "instagram-logo",
12
+ "facebook" => "facebook-logo", "tiktok" => "tiktok-logo", "twitch" => "twitch-logo",
13
+ "reddit" => "reddit-logo", "mastodon" => "mastodon-logo", "threads" => "threads-logo",
14
+ "pinterest" => "pinterest-logo", "medium" => "medium-logo", "slack" => "slack-logo",
15
+ "gitlab" => "gitlab-logo"
16
+ }.freeze
17
+
18
+ def resolve
19
+ return default_branding unless config
20
+
21
+ default_branding.merge(config_branding_options)
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :config
27
+
28
+ def default_branding
29
+ {
30
+ site_title: Constants::DEFAULT_SITE_TITLE,
31
+ site_description: "",
32
+ logo: Constants::DEFAULT_LOGO_PATH,
33
+ logo_dark: Constants::DEFAULT_LOGO_DARK_PATH,
34
+ favicon: nil,
35
+ credits: true,
36
+ social: []
37
+ }
38
+ end
39
+
40
+ def config_branding_options
41
+ site_options
42
+ .merge(logo_options)
43
+ .merge(search_options)
44
+ .merge(credits_options)
45
+ .merge(social_options)
46
+ .merge(navigation_options)
47
+ .merge(tabs_options)
48
+ end
49
+
50
+ def site_options
51
+ {
52
+ site_title: config.title || Constants::DEFAULT_SITE_TITLE,
53
+ site_description: config.description || "",
54
+ favicon: config.branding.favicon || auto_detect_favicon
55
+ }
56
+ end
57
+
58
+ def logo_options
59
+ branding = config.branding
60
+ logo = branding.logo || auto_detect_logo
61
+ has_custom_logo = !logo.nil?
62
+ {
63
+ logo: logo || Constants::DEFAULT_LOGO_PATH,
64
+ logo_dark: detect_dark_logo(logo) || Constants::DEFAULT_LOGO_DARK_PATH,
65
+ has_custom_logo: has_custom_logo
66
+ }
67
+ end
68
+
69
+ def auto_detect_logo
70
+ detect_public_file("logo", %w[svg png])
71
+ end
72
+
73
+ def auto_detect_favicon
74
+ detect_public_file("favicon", %w[ico svg png])
75
+ end
76
+
77
+ def detect_public_file(name, extensions)
78
+ extensions.each do |ext|
79
+ path = File.join(Constants::PUBLIC_DIR, "#{name}.#{ext}")
80
+ return "#{name}.#{ext}" if File.exist?(path)
81
+ end
82
+ nil
83
+ end
84
+
85
+ def detect_dark_logo(logo)
86
+ return nil unless logo
87
+
88
+ ext = File.extname(logo)
89
+ base = File.basename(logo, ext)
90
+ dark_filename = "#{base}-dark#{ext}"
91
+
92
+ if File.absolute_path?(logo)
93
+ dark_path = File.join(File.dirname(logo), dark_filename)
94
+ File.exist?(dark_path) ? dark_path : logo
95
+ else
96
+ dark_path = File.join("docs/public", dark_filename)
97
+ File.exist?(dark_path) ? dark_filename : logo
98
+ end
99
+ end
100
+
101
+ def search_options
102
+ {
103
+ search_enabled: config.search.enabled != false,
104
+ search_placeholder: config.search.placeholder || "Search..."
105
+ }
106
+ end
107
+
108
+ def credits_options
109
+ {
110
+ credits: config.branding.credits != false,
111
+ copyright: config.branding.copyright
112
+ }
113
+ end
114
+
115
+ def social_options
116
+ socials = config.socials || {}
117
+ {
118
+ social: normalize_social_links(socials)
119
+ }
120
+ end
121
+
122
+ def normalize_social_links(socials)
123
+ return [] unless socials.is_a?(Hash) && socials.any?
124
+
125
+ socials.filter_map { |platform, url| build_social_link(platform.to_s, url) }
126
+ end
127
+
128
+ def build_social_link(platform, url)
129
+ return if platform == "custom" || !valid_url?(url)
130
+
131
+ { platform: platform, url: url, icon: SOCIAL_ICON_MAP[platform] || platform }
132
+ end
133
+
134
+ def valid_url?(url)
135
+ url.is_a?(String) && !url.strip.empty?
136
+ end
137
+
138
+ def navigation_options
139
+ cta_items = config.navigation.cta || []
140
+ {
141
+ header_ctas: normalize_cta_items(cta_items)
142
+ }
143
+ end
144
+
145
+ def normalize_cta_items(items)
146
+ return [] unless items.is_a?(Array)
147
+
148
+ items.first(2).filter_map do |item|
149
+ next unless item.is_a?(Hash) && item["text"] && item["href"]
150
+
151
+ {
152
+ text: item["text"],
153
+ href: item["href"],
154
+ variant: item["variant"] || "primary",
155
+ external: item["external"] == true
156
+ }
157
+ end
158
+ end
159
+
160
+ def tabs_options
161
+ tab_items = config.tabs || []
162
+ {
163
+ tabs: normalize_tab_items(tab_items),
164
+ has_tabs: tab_items.any?
165
+ }
166
+ end
167
+
168
+ def normalize_tab_items(items)
169
+ return [] unless items.is_a?(Array)
170
+
171
+ items.filter_map do |item|
172
+ next unless item.is_a?(Hash) && item["text"] && item["href"]
173
+
174
+ {
175
+ text: item["text"],
176
+ href: item["href"],
177
+ icon: item["icon"],
178
+ external: item["external"] == true
179
+ }
180
+ end
181
+ end
182
+ end
183
+ end
@@ -8,7 +8,9 @@ module Docyard
8
8
  CONTENT_TYPE_JS = "application/javascript; charset=utf-8"
9
9
 
10
10
  RELOAD_ENDPOINT = "/_docyard/reload"
11
- ASSETS_PREFIX = "/assets/"
11
+ DOCYARD_ASSETS_PREFIX = "/_docyard/"
12
+ PAGEFIND_PREFIX = "/pagefind/"
13
+ PUBLIC_DIR = "docs/public"
12
14
 
13
15
  INDEX_FILE = "index"
14
16
  INDEX_TITLE = "Home"
@@ -17,12 +19,13 @@ module Docyard
17
19
  HTML_EXTENSION = ".html"
18
20
 
19
21
  STATUS_OK = 200
22
+ STATUS_REDIRECT = 302
20
23
  STATUS_NOT_FOUND = 404
21
24
  STATUS_INTERNAL_ERROR = 500
22
25
 
23
26
  DEFAULT_SITE_TITLE = "Documentation"
24
- DEFAULT_LOGO_PATH = "assets/logo.svg"
25
- DEFAULT_LOGO_DARK_PATH = "assets/logo-dark.svg"
26
- DEFAULT_FAVICON_PATH = "assets/favicon.svg"
27
+ DEFAULT_LOGO_PATH = "_docyard/logo.svg"
28
+ DEFAULT_LOGO_DARK_PATH = "_docyard/logo-dark.svg"
29
+ DEFAULT_FAVICON_PATH = "_docyard/favicon.svg"
27
30
  end
28
31
  end