docyard 0.8.0 → 0.9.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -1
  3. data/lib/docyard/components/aliases.rb +12 -0
  4. data/lib/docyard/components/processors/abbreviation_processor.rb +72 -0
  5. data/lib/docyard/components/processors/accordion_processor.rb +81 -0
  6. data/lib/docyard/components/processors/badge_processor.rb +72 -0
  7. data/lib/docyard/components/processors/callout_processor.rb +8 -2
  8. data/lib/docyard/components/processors/cards_processor.rb +100 -0
  9. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +23 -2
  10. data/lib/docyard/components/processors/code_block_processor.rb +6 -0
  11. data/lib/docyard/components/processors/code_group_processor.rb +198 -0
  12. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +6 -1
  13. data/lib/docyard/components/processors/custom_anchor_processor.rb +42 -0
  14. data/lib/docyard/components/processors/file_tree_processor.rb +151 -0
  15. data/lib/docyard/components/processors/image_caption_processor.rb +96 -0
  16. data/lib/docyard/components/processors/include_processor.rb +86 -0
  17. data/lib/docyard/components/processors/steps_processor.rb +89 -0
  18. data/lib/docyard/components/processors/tabs_processor.rb +9 -1
  19. data/lib/docyard/components/processors/tooltip_processor.rb +57 -0
  20. data/lib/docyard/components/processors/video_embed_processor.rb +196 -0
  21. data/lib/docyard/components/support/code_group/html_builder.rb +122 -0
  22. data/lib/docyard/components/support/markdown_code_block_helper.rb +56 -0
  23. data/lib/docyard/config/branding_resolver.rb +30 -35
  24. data/lib/docyard/config/logo_detector.rb +39 -0
  25. data/lib/docyard/config.rb +6 -1
  26. data/lib/docyard/navigation/sidebar/file_resolver.rb +16 -4
  27. data/lib/docyard/navigation/sidebar/item.rb +6 -1
  28. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +4 -2
  29. data/lib/docyard/navigation/sidebar/metadata_reader.rb +8 -4
  30. data/lib/docyard/navigation/sidebar/renderer.rb +6 -2
  31. data/lib/docyard/navigation/sidebar/tree_builder.rb +2 -1
  32. data/lib/docyard/rendering/icons/phosphor.rb +3 -0
  33. data/lib/docyard/rendering/markdown.rb +24 -1
  34. data/lib/docyard/rendering/renderer.rb +2 -1
  35. data/lib/docyard/server/asset_handler.rb +1 -0
  36. data/lib/docyard/templates/assets/css/components/abbreviation.css +86 -0
  37. data/lib/docyard/templates/assets/css/components/accordion.css +138 -0
  38. data/lib/docyard/templates/assets/css/components/badges.css +47 -0
  39. data/lib/docyard/templates/assets/css/components/banner.css +202 -0
  40. data/lib/docyard/templates/assets/css/components/cards.css +100 -0
  41. data/lib/docyard/templates/assets/css/components/code-block.css +10 -0
  42. data/lib/docyard/templates/assets/css/components/code-group.css +281 -0
  43. data/lib/docyard/templates/assets/css/components/figure.css +22 -0
  44. data/lib/docyard/templates/assets/css/components/file-tree.css +124 -0
  45. data/lib/docyard/templates/assets/css/components/heading-anchor.css +21 -13
  46. data/lib/docyard/templates/assets/css/components/lightbox.css +65 -0
  47. data/lib/docyard/templates/assets/css/components/navigation.css +7 -0
  48. data/lib/docyard/templates/assets/css/components/prev-next.css +9 -18
  49. data/lib/docyard/templates/assets/css/components/steps.css +122 -0
  50. data/lib/docyard/templates/assets/css/components/tabs.css +1 -1
  51. data/lib/docyard/templates/assets/css/components/tooltip.css +113 -0
  52. data/lib/docyard/templates/assets/css/components/video.css +41 -0
  53. data/lib/docyard/templates/assets/css/markdown.css +5 -3
  54. data/lib/docyard/templates/assets/js/components/abbreviation.js +85 -0
  55. data/lib/docyard/templates/assets/js/components/banner.js +81 -0
  56. data/lib/docyard/templates/assets/js/components/code-group.js +283 -0
  57. data/lib/docyard/templates/assets/js/components/file-tree.js +39 -0
  58. data/lib/docyard/templates/assets/js/components/lightbox.js +72 -0
  59. data/lib/docyard/templates/assets/js/components/tooltip.js +118 -0
  60. data/lib/docyard/templates/layouts/default.html.erb +1 -0
  61. data/lib/docyard/templates/layouts/splash.html.erb +1 -0
  62. data/lib/docyard/templates/partials/_accordion.html.erb +9 -0
  63. data/lib/docyard/templates/partials/_banner.html.erb +27 -0
  64. data/lib/docyard/templates/partials/_card.html.erb +23 -0
  65. data/lib/docyard/templates/partials/_nav_group.html.erb +6 -0
  66. data/lib/docyard/templates/partials/_nav_leaf.html.erb +3 -0
  67. data/lib/docyard/templates/partials/_step.html.erb +14 -0
  68. data/lib/docyard/version.rb +1 -1
  69. metadata +38 -1
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../../rendering/renderer"
4
4
  require_relative "../base_processor"
5
+ require_relative "../support/markdown_code_block_helper"
5
6
  require_relative "../support/tabs/parser"
6
7
  require "securerandom"
7
8
 
@@ -9,6 +10,8 @@ module Docyard
9
10
  module Components
10
11
  module Processors
11
12
  class TabsProcessor < BaseProcessor
13
+ include Support::MarkdownCodeBlockHelper
14
+
12
15
  self.priority = 15
13
16
 
14
17
  TabsParser = Support::Tabs::Parser
@@ -16,8 +19,13 @@ module Docyard
16
19
  def preprocess(content)
17
20
  return content unless content.include?(":::tabs")
18
21
 
22
+ @code_block_ranges = find_code_block_ranges(content)
23
+
19
24
  content.gsub(/^:::[ \t]*tabs[ \t]*\n(.*?)^:::[ \t]*$/m) do
20
- process_tabs_block(Regexp.last_match(1))
25
+ match = Regexp.last_match
26
+ next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
27
+
28
+ process_tabs_block(match[1])
21
29
  end
22
30
  end
23
31
 
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+ require_relative "../support/markdown_code_block_helper"
5
+
6
+ module Docyard
7
+ module Components
8
+ module Processors
9
+ class TooltipProcessor < BaseProcessor
10
+ include Support::MarkdownCodeBlockHelper
11
+
12
+ TOOLTIP_PATTERN = /:tooltip\[([^\]]+)\]\{([^}]+)\}/
13
+ self.priority = 6
14
+
15
+ def preprocess(content)
16
+ process_outside_code_blocks(content) do |segment|
17
+ segment.gsub(TOOLTIP_PATTERN) do |_match|
18
+ term = ::Regexp.last_match(1)
19
+ attributes = parse_attributes(::Regexp.last_match(2))
20
+ build_tooltip_tag(term, attributes)
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def parse_attributes(attr_string)
28
+ attributes = {}
29
+ attr_string.scan(/(\w+)="([^"]*)"/) do |key, value|
30
+ attributes[key.to_sym] = value
31
+ end
32
+ attributes
33
+ end
34
+
35
+ def build_tooltip_tag(term, attributes)
36
+ description = escape_html(attributes[:description] || "")
37
+ link = attributes[:link]
38
+ link_text = attributes[:link_text] || "Learn more"
39
+
40
+ data_attrs = %(data-description="#{description}")
41
+ data_attrs += %( data-link="#{escape_html(link)}") if link
42
+ data_attrs += %( data-link-text="#{escape_html(link_text)}") if link
43
+
44
+ %(<span class="docyard-tooltip" #{data_attrs}>#{term}</span>)
45
+ end
46
+
47
+ def escape_html(text)
48
+ text.to_s
49
+ .gsub("&", "&amp;")
50
+ .gsub("<", "&lt;")
51
+ .gsub(">", "&gt;")
52
+ .gsub('"', "&quot;")
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+ require_relative "../support/markdown_code_block_helper"
5
+
6
+ module Docyard
7
+ module Components
8
+ module Processors
9
+ class VideoEmbedProcessor < BaseProcessor
10
+ include Support::MarkdownCodeBlockHelper
11
+
12
+ YOUTUBE_PATTERN = /::youtube\[([^\]]+)\](?:\{([^}]*)\})?/
13
+ VIMEO_PATTERN = /::vimeo\[([^\]]+)\](?:\{([^}]*)\})?/
14
+ VIDEO_PATTERN = /::video\[([^\]]+)\](?:\{([^}]*)\})?/
15
+
16
+ self.priority = 5
17
+
18
+ def preprocess(content)
19
+ process_outside_code_blocks(content) do |segment|
20
+ process_video_embeds(segment)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def process_video_embeds(content)
27
+ result = content.gsub(YOUTUBE_PATTERN) do
28
+ video_id = Regexp.last_match(1)
29
+ attrs_string = Regexp.last_match(2)
30
+ build_youtube_embed(video_id, parse_attributes(attrs_string))
31
+ end
32
+
33
+ result = result.gsub(VIMEO_PATTERN) do
34
+ video_id = Regexp.last_match(1)
35
+ attrs_string = Regexp.last_match(2)
36
+ build_vimeo_embed(video_id, parse_attributes(attrs_string))
37
+ end
38
+
39
+ result.gsub(VIDEO_PATTERN) do
40
+ src = Regexp.last_match(1)
41
+ attrs_string = Regexp.last_match(2)
42
+ build_native_video(src, parse_attributes(attrs_string))
43
+ end
44
+ end
45
+
46
+ def parse_attributes(attrs_string)
47
+ return {} if attrs_string.nil? || attrs_string.empty?
48
+
49
+ attrs = {}
50
+
51
+ attrs_string.scan(/(\w+)="([^"]*)"/) do |key, value|
52
+ attrs[key] = value
53
+ end
54
+
55
+ %w[autoplay loop muted nofullscreen controls playsinline].each do |flag|
56
+ attrs[flag] = true if attrs_string.match?(/\b#{flag}\b/) && !attrs.key?(flag)
57
+ end
58
+
59
+ attrs
60
+ end
61
+
62
+ def build_youtube_embed(video_id, attrs)
63
+ params = youtube_params(attrs)
64
+ url = "https://www.youtube-nocookie.com/embed/#{escape_attr(video_id)}"
65
+ url += "?#{params.join('&')}" unless params.empty?
66
+
67
+ build_iframe(url, attrs, "youtube")
68
+ end
69
+
70
+ def build_vimeo_embed(video_id, attrs)
71
+ params = vimeo_params(attrs)
72
+ url = "https://player.vimeo.com/video/#{escape_attr(video_id)}"
73
+ url += "?#{params.join('&')}" unless params.empty?
74
+
75
+ build_iframe(url, attrs, "vimeo")
76
+ end
77
+
78
+ def build_native_video(src, attrs)
79
+ wrapper_style = build_wrapper_style(attrs)
80
+ video_attrs = build_video_attrs(src, attrs)
81
+
82
+ "\n\n" \
83
+ "<div class=\"docyard-video docyard-video--native\"#{wrapper_style} markdown=\"0\">\n " \
84
+ "<video #{video_attrs.join(' ')}></video>\n" \
85
+ "</div>" \
86
+ "\n\n"
87
+ end
88
+
89
+ def build_video_attrs(src, attrs)
90
+ video_attrs = ["src=\"#{escape_attr(src)}\""]
91
+ video_attrs.concat(video_optional_attrs(attrs))
92
+ video_attrs.concat(video_boolean_attrs(attrs))
93
+ video_attrs
94
+ end
95
+
96
+ def video_optional_attrs(attrs)
97
+ result = []
98
+ result << "poster=\"#{escape_attr(attrs['poster'])}\"" if attrs["poster"]
99
+ result << "preload=\"#{escape_attr(attrs['preload'])}\"" if attrs["preload"]
100
+ result
101
+ end
102
+
103
+ def video_boolean_attrs(attrs)
104
+ result = []
105
+ result << "controls" unless controls_disabled?(attrs)
106
+ result << "autoplay" if attrs["autoplay"]
107
+ result << "muted" if attrs["muted"]
108
+ result << "loop" if attrs["loop"]
109
+ result << "playsinline" if attrs["playsinline"]
110
+ result
111
+ end
112
+
113
+ def controls_disabled?(attrs)
114
+ ["false", false].include?(attrs["controls"])
115
+ end
116
+
117
+ def youtube_params(attrs)
118
+ params = []
119
+ params << "autoplay=1" if attrs["autoplay"]
120
+ params << "loop=1" if attrs["loop"]
121
+ params << "mute=1" if attrs["muted"]
122
+ params << "controls=0" if ["false", false].include?(attrs["controls"])
123
+ params << "start=#{attrs['start']}" if attrs["start"]
124
+ params << "rel=0"
125
+ params
126
+ end
127
+
128
+ def vimeo_params(attrs)
129
+ params = []
130
+ params << "autoplay=1" if attrs["autoplay"]
131
+ params << "loop=1" if attrs["loop"]
132
+ params << "muted=1" if attrs["muted"]
133
+ params << "controls=0" if ["false", false].include?(attrs["controls"])
134
+ params << "dnt=1"
135
+ params
136
+ end
137
+
138
+ def build_iframe(url, attrs, provider)
139
+ wrapper_style = build_wrapper_style(attrs)
140
+ iframe_attrs = build_iframe_attrs(url, attrs, provider)
141
+
142
+ "\n\n" \
143
+ "<div class=\"docyard-video docyard-video--#{provider}\"#{wrapper_style} markdown=\"0\">\n " \
144
+ "<iframe #{iframe_attrs.join(' ')}></iframe>\n" \
145
+ "</div>" \
146
+ "\n\n"
147
+ end
148
+
149
+ def build_wrapper_style(attrs)
150
+ return "" unless attrs["width"] || attrs["height"]
151
+
152
+ styles = []
153
+ styles << "max-width: #{escape_attr(attrs['width'])}px" if attrs["width"]
154
+ styles << "height: #{escape_attr(attrs['height'])}px" if attrs["height"]
155
+ " style=\"#{styles.join('; ')}\""
156
+ end
157
+
158
+ def build_iframe_attrs(url, attrs, provider)
159
+ iframe_attrs = [
160
+ "src=\"#{url}\"",
161
+ "title=\"#{escape_attr(attrs['title'] || default_title(provider))}\"",
162
+ "frameborder=\"0\""
163
+ ]
164
+
165
+ iframe_attrs << "allow=\"#{build_allow_attr(attrs)}\""
166
+ iframe_attrs << "allowfullscreen" unless attrs["nofullscreen"]
167
+
168
+ iframe_attrs
169
+ end
170
+
171
+ def build_allow_attr(attrs)
172
+ permissions = %w[encrypted-media picture-in-picture web-share]
173
+ permissions.unshift("autoplay") if attrs["autoplay"]
174
+ permissions << "fullscreen" unless attrs["nofullscreen"]
175
+ permissions.join("; ")
176
+ end
177
+
178
+ def default_title(provider)
179
+ case provider
180
+ when "youtube" then "YouTube video player"
181
+ when "vimeo" then "Vimeo video player"
182
+ else "Video player"
183
+ end
184
+ end
185
+
186
+ def escape_attr(text)
187
+ text.to_s
188
+ .gsub("&", "&amp;")
189
+ .gsub("<", "&lt;")
190
+ .gsub(">", "&gt;")
191
+ .gsub('"', "&quot;")
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../code_block/icon_detector"
4
+ require_relative "../../../rendering/icons"
5
+
6
+ module Docyard
7
+ module Components
8
+ module Support
9
+ module CodeGroup
10
+ class HtmlBuilder
11
+ def initialize(blocks, group_id)
12
+ @blocks = blocks
13
+ @group_id = group_id
14
+ end
15
+
16
+ def build
17
+ <<~HTML
18
+ <div class="docyard-code-group" data-code-group="#{@group_id}">
19
+ <div class="docyard-code-group__tabs-wrapper">
20
+ <div class="docyard-code-group__tabs-scroll-container">
21
+ <div role="tablist" aria-label="Code examples" class="docyard-code-group__tabs">
22
+ #{build_tabs}
23
+ <div class="docyard-code-group__indicator" aria-hidden="true"></div>
24
+ </div>
25
+ </div>
26
+ #{build_copy_button}
27
+ </div>
28
+ <div class="docyard-code-group__panels">
29
+ #{build_panels}
30
+ </div>
31
+ </div>
32
+ HTML
33
+ end
34
+
35
+ private
36
+
37
+ def build_tabs
38
+ @blocks.each_with_index.map do |block, index|
39
+ build_tab(block, index)
40
+ end.join("\n")
41
+ end
42
+
43
+ def build_tab(block, index)
44
+ selected = index.zero? ? "true" : "false"
45
+ tabindex = index.zero? ? "0" : "-1"
46
+ icon_html = render_icon(block[:lang])
47
+ <<~HTML.strip
48
+ <button
49
+ role="tab"
50
+ aria-selected="#{selected}"
51
+ aria-controls="cg-panel-#{@group_id}-#{index}"
52
+ id="cg-tab-#{@group_id}-#{index}"
53
+ class="docyard-code-group__tab"
54
+ tabindex="#{tabindex}"
55
+ data-label="#{escape_html(block[:label])}"
56
+ >#{icon_html}#{escape_html(block[:label])}</button>
57
+ HTML
58
+ end
59
+
60
+ def render_icon(lang)
61
+ return "" if lang.nil? || lang.empty?
62
+
63
+ icon, icon_source = CodeBlock::IconDetector.auto_detect_icon(lang)
64
+ return "" unless icon && icon_source == "file-extension"
65
+
66
+ Icons.render_file_extension(icon) || ""
67
+ end
68
+
69
+ def build_copy_button
70
+ copy_icon = Icons.render("copy", "regular") || ""
71
+ <<~HTML.strip
72
+ <button class="docyard-code-group__copy" aria-label="Copy code to clipboard">
73
+ <span class="docyard-code-group__copy-icon">#{copy_icon}</span>
74
+ <span class="docyard-code-group__copy-text">Copy</span>
75
+ </button>
76
+ HTML
77
+ end
78
+
79
+ def build_panels
80
+ @blocks.each_with_index.map do |block, index|
81
+ build_panel(block, index)
82
+ end.join("\n")
83
+ end
84
+
85
+ def build_panel(block, index)
86
+ hidden = index.zero? ? "false" : "true"
87
+ code_text = escape_html_attribute(block[:code_text] || "")
88
+ <<~HTML.strip
89
+ <div
90
+ role="tabpanel"
91
+ id="cg-panel-#{@group_id}-#{index}"
92
+ aria-labelledby="cg-tab-#{@group_id}-#{index}"
93
+ aria-hidden="#{hidden}"
94
+ class="docyard-code-group__panel"
95
+ tabindex="0"
96
+ data-code="#{code_text}"
97
+ >#{block[:content]}</div>
98
+ HTML
99
+ end
100
+
101
+ def escape_html(text)
102
+ text.to_s
103
+ .gsub("&", "&amp;")
104
+ .gsub("<", "&lt;")
105
+ .gsub(">", "&gt;")
106
+ .gsub('"', "&quot;")
107
+ end
108
+
109
+ def escape_html_attribute(text)
110
+ text.to_s
111
+ .gsub("&", "&amp;")
112
+ .gsub("<", "&lt;")
113
+ .gsub(">", "&gt;")
114
+ .gsub('"', "&quot;")
115
+ .gsub("'", "&#39;")
116
+ .gsub("\n", "&#10;")
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ module Support
6
+ module MarkdownCodeBlockHelper
7
+ FENCED_CODE_BLOCK_REGEX = /^(`{3,}|~{3,})[^\n]*\n.*?^\1\s*$/m
8
+
9
+ def process_outside_code_blocks(content)
10
+ segments = split_by_code_blocks(content)
11
+
12
+ segments.map do |segment|
13
+ segment[:type] == :code ? segment[:content] : yield(segment[:content])
14
+ end.join
15
+ end
16
+
17
+ def find_code_block_ranges(content, exclude_language: nil)
18
+ ranges = []
19
+ content.scan(FENCED_CODE_BLOCK_REGEX) do
20
+ match = Regexp.last_match
21
+ next if exclude_language && match[0] =~ /\A[`~]{3,}#{exclude_language}\b/
22
+
23
+ ranges << (match.begin(0)...match.end(0))
24
+ end
25
+ ranges
26
+ end
27
+
28
+ def inside_code_block?(position, ranges)
29
+ ranges.any? { |range| range.cover?(position) }
30
+ end
31
+
32
+ private
33
+
34
+ def split_by_code_blocks(content)
35
+ segments = []
36
+ current_pos = 0
37
+
38
+ content.scan(FENCED_CODE_BLOCK_REGEX) do
39
+ match = Regexp.last_match
40
+ match_start = match.begin(0)
41
+ match_end = match.end(0)
42
+
43
+ segments << { type: :text, content: content[current_pos...match_start] } if match_start > current_pos
44
+
45
+ segments << { type: :code, content: content[match_start...match_end] }
46
+ current_pos = match_end
47
+ end
48
+
49
+ segments << { type: :text, content: content[current_pos..] } if current_pos < content.length
50
+
51
+ segments.empty? ? [{ type: :text, content: content }] : segments
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "logo_detector"
4
+
3
5
  module Docyard
4
6
  class BrandingResolver
5
7
  def initialize(config)
@@ -45,59 +47,28 @@ module Docyard
45
47
  .merge(social_options)
46
48
  .merge(navigation_options)
47
49
  .merge(tabs_options)
50
+ .merge(announcement_options)
48
51
  end
49
52
 
50
53
  def site_options
51
54
  {
52
55
  site_title: config.title || Constants::DEFAULT_SITE_TITLE,
53
56
  site_description: config.description || "",
54
- favicon: config.branding.favicon || auto_detect_favicon
57
+ favicon: config.branding.favicon || LogoDetector.auto_detect_favicon
55
58
  }
56
59
  end
57
60
 
58
61
  def logo_options
59
62
  branding = config.branding
60
- logo = branding.logo || auto_detect_logo
63
+ logo = branding.logo || LogoDetector.auto_detect_logo
61
64
  has_custom_logo = !logo.nil?
62
65
  {
63
66
  logo: logo || Constants::DEFAULT_LOGO_PATH,
64
- logo_dark: detect_dark_logo(logo) || Constants::DEFAULT_LOGO_DARK_PATH,
67
+ logo_dark: LogoDetector.detect_dark_logo(logo) || Constants::DEFAULT_LOGO_DARK_PATH,
65
68
  has_custom_logo: has_custom_logo
66
69
  }
67
70
  end
68
71
 
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
72
  def search_options
102
73
  {
103
74
  search_enabled: config.search.enabled != false,
@@ -179,5 +150,29 @@ module Docyard
179
150
  }
180
151
  end
181
152
  end
153
+
154
+ def announcement_options
155
+ announcement = config.announcement
156
+ return { announcement: nil } unless announcement
157
+
158
+ {
159
+ announcement: {
160
+ text: announcement.text,
161
+ link: announcement.link,
162
+ button: build_announcement_button(announcement),
163
+ dismissible: announcement.dismissible != false
164
+ }
165
+ }
166
+ end
167
+
168
+ def build_announcement_button(announcement)
169
+ button = announcement.button
170
+ return nil unless button.is_a?(Hash) && button["text"]
171
+
172
+ {
173
+ text: button["text"],
174
+ link: button["link"] || announcement.link
175
+ }
176
+ end
182
177
  end
183
178
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module LogoDetector
5
+ module_function
6
+
7
+ def auto_detect_logo
8
+ detect_public_file("logo", %w[svg png])
9
+ end
10
+
11
+ def auto_detect_favicon
12
+ detect_public_file("favicon", %w[ico svg png])
13
+ end
14
+
15
+ def detect_public_file(name, extensions)
16
+ extensions.each do |ext|
17
+ path = File.join(Constants::PUBLIC_DIR, "#{name}.#{ext}")
18
+ return "#{name}.#{ext}" if File.exist?(path)
19
+ end
20
+ nil
21
+ end
22
+
23
+ def detect_dark_logo(logo)
24
+ return nil unless logo
25
+
26
+ ext = File.extname(logo)
27
+ base = File.basename(logo, ext)
28
+ dark_filename = "#{base}-dark#{ext}"
29
+
30
+ if File.absolute_path?(logo)
31
+ dark_path = File.join(File.dirname(logo), dark_filename)
32
+ File.exist?(dark_path) ? dark_path : logo
33
+ else
34
+ dark_path = File.join("docs/public", dark_filename)
35
+ File.exist?(dark_path) ? dark_filename : logo
36
+ end
37
+ end
38
+ end
39
+ end
@@ -29,7 +29,8 @@ module Docyard
29
29
  "navigation" => {
30
30
  "cta" => [],
31
31
  "breadcrumbs" => true
32
- }
32
+ },
33
+ "announcement" => nil
33
34
  }.freeze
34
35
 
35
36
  attr_reader :data, :file_path
@@ -81,6 +82,10 @@ module Docyard
81
82
  @navigation ||= ConfigSection.new(data["navigation"])
82
83
  end
83
84
 
85
+ def announcement
86
+ @announcement ||= data["announcement"] ? ConfigSection.new(data["announcement"]) : nil
87
+ end
88
+
84
89
  private
85
90
 
86
91
  def load_config_data
@@ -58,17 +58,29 @@ module Docyard
58
58
  end
59
59
 
60
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)
61
68
  file_path = File.join(docs_path, base_path, "#{slug}.md")
62
69
  url_path = Utils::PathResolver.to_url(File.join(base_path, slug))
63
- frontmatter = metadata_extractor.extract_frontmatter_metadata(file_path)
64
70
  final_path = options["link"] || options[:link] || url_path
65
71
 
72
+ { file: file_path, final: final_path }
73
+ end
74
+
75
+ def build_context_hash(slug, paths, options, frontmatter)
66
76
  {
67
77
  slug: slug,
68
- text: metadata_extractor.resolve_item_text(slug, file_path, options, frontmatter[:text]),
69
- path: final_path,
78
+ text: metadata_extractor.resolve_item_text(slug, paths[:file], options, frontmatter[:text]),
79
+ path: paths[:final],
70
80
  icon: metadata_extractor.resolve_item_icon(options, frontmatter[:icon]),
71
- active: current_path == final_path,
81
+ badge: frontmatter[:badge],
82
+ badge_type: frontmatter[:badge_type],
83
+ active: current_path == paths[:final],
72
84
  type: :file,
73
85
  section: false
74
86
  }
@@ -3,7 +3,8 @@
3
3
  module Docyard
4
4
  module Sidebar
5
5
  class Item
6
- attr_reader :slug, :text, :icon, :link, :target, :collapsed, :items, :path, :active, :type, :has_index, :section
6
+ attr_reader :slug, :text, :icon, :link, :target, :collapsed, :items, :path, :active, :type, :has_index, :section,
7
+ :badge, :badge_type
7
8
 
8
9
  DEFAULTS = {
9
10
  target: "_self",
@@ -27,6 +28,8 @@ module Docyard
27
28
  @text = options[:text]
28
29
  @icon = options[:icon]
29
30
  @link = options[:link]
31
+ @badge = options[:badge]
32
+ @badge_type = options[:badge_type]
30
33
  end
31
34
 
32
35
  def assign_optional_attributes(options)
@@ -88,6 +91,8 @@ module Docyard
88
91
  target: target,
89
92
  has_index: has_index,
90
93
  section: section,
94
+ badge: badge,
95
+ badge_type: badge_type,
91
96
  children: children.map(&:to_h)
92
97
  }
93
98
  end