docyard 0.7.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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/CHANGELOG.md +20 -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 +85 -12
  8. data/lib/docyard/builder.rb +6 -6
  9. data/lib/docyard/config/branding_resolver.rb +126 -17
  10. data/lib/docyard/config/constants.rb +6 -4
  11. data/lib/docyard/config/validator.rb +122 -99
  12. data/lib/docyard/config.rb +36 -43
  13. data/lib/docyard/initializer.rb +15 -76
  14. data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
  15. data/lib/docyard/navigation/prev_next_builder.rb +4 -1
  16. data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
  17. data/lib/docyard/navigation/sidebar/config_parser.rb +136 -108
  18. data/lib/docyard/navigation/sidebar/file_resolver.rb +78 -0
  19. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +2 -1
  20. data/lib/docyard/navigation/sidebar/item.rb +45 -7
  21. data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
  22. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +69 -0
  23. data/lib/docyard/navigation/sidebar/metadata_reader.rb +47 -0
  24. data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
  25. data/lib/docyard/navigation/sidebar/renderer.rb +55 -37
  26. data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
  27. data/lib/docyard/navigation/sidebar/tree_builder.rb +99 -26
  28. data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
  29. data/lib/docyard/navigation/sidebar_builder.rb +105 -36
  30. data/lib/docyard/rendering/icon_helpers.rb +13 -0
  31. data/lib/docyard/rendering/icons/phosphor.rb +23 -1
  32. data/lib/docyard/rendering/markdown.rb +5 -0
  33. data/lib/docyard/rendering/renderer.rb +74 -34
  34. data/lib/docyard/rendering/template_resolver.rb +172 -0
  35. data/lib/docyard/routing/fallback_resolver.rb +92 -0
  36. data/lib/docyard/search/build_indexer.rb +1 -1
  37. data/lib/docyard/search/dev_indexer.rb +51 -6
  38. data/lib/docyard/search/pagefind_support.rb +2 -0
  39. data/lib/docyard/server/asset_handler.rb +24 -19
  40. data/lib/docyard/server/pagefind_handler.rb +63 -0
  41. data/lib/docyard/server/preview_server.rb +1 -1
  42. data/lib/docyard/server/rack_application.rb +81 -64
  43. data/lib/docyard/templates/assets/css/code.css +18 -51
  44. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +143 -0
  45. data/lib/docyard/templates/assets/css/components/callout.css +67 -67
  46. data/lib/docyard/templates/assets/css/components/code-block.css +180 -282
  47. data/lib/docyard/templates/assets/css/components/heading-anchor.css +28 -15
  48. data/lib/docyard/templates/assets/css/components/icon.css +0 -1
  49. data/lib/docyard/templates/assets/css/components/logo.css +0 -2
  50. data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
  51. data/lib/docyard/templates/assets/css/components/navigation.css +186 -167
  52. data/lib/docyard/templates/assets/css/components/prev-next.css +76 -47
  53. data/lib/docyard/templates/assets/css/components/search.css +186 -174
  54. data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
  55. data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
  56. data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
  57. data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
  58. data/lib/docyard/templates/assets/css/landing.css +815 -0
  59. data/lib/docyard/templates/assets/css/layout.css +489 -87
  60. data/lib/docyard/templates/assets/css/main.css +1 -3
  61. data/lib/docyard/templates/assets/css/markdown.css +111 -93
  62. data/lib/docyard/templates/assets/css/reset.css +0 -3
  63. data/lib/docyard/templates/assets/css/typography.css +43 -41
  64. data/lib/docyard/templates/assets/css/variables.css +268 -208
  65. data/lib/docyard/templates/assets/favicon.svg +7 -8
  66. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
  67. data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
  68. data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
  69. data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
  70. data/lib/docyard/templates/assets/js/components/search.js +0 -75
  71. data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
  72. data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
  73. data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
  74. data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
  75. data/lib/docyard/templates/assets/js/theme.js +0 -3
  76. data/lib/docyard/templates/assets/logo-dark.svg +8 -2
  77. data/lib/docyard/templates/assets/logo.svg +7 -4
  78. data/lib/docyard/templates/config/docyard.yml.erb +37 -34
  79. data/lib/docyard/templates/errors/404.html.erb +1 -1
  80. data/lib/docyard/templates/errors/500.html.erb +1 -1
  81. data/lib/docyard/templates/layouts/default.html.erb +18 -67
  82. data/lib/docyard/templates/layouts/splash.html.erb +176 -0
  83. data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
  84. data/lib/docyard/templates/partials/_code_block.html.erb +5 -3
  85. data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
  86. data/lib/docyard/templates/partials/_features.html.erb +15 -0
  87. data/lib/docyard/templates/partials/_footer.html.erb +42 -0
  88. data/lib/docyard/templates/partials/_head.html.erb +22 -0
  89. data/lib/docyard/templates/partials/_header.html.erb +49 -0
  90. data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
  91. data/lib/docyard/templates/partials/_hero.html.erb +27 -0
  92. data/lib/docyard/templates/partials/_nav_group.html.erb +25 -11
  93. data/lib/docyard/templates/partials/_nav_leaf.html.erb +1 -1
  94. data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
  95. data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
  96. data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
  97. data/lib/docyard/templates/partials/_prev_next.html.erb +8 -2
  98. data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
  99. data/lib/docyard/templates/partials/_search_modal.html.erb +2 -6
  100. data/lib/docyard/templates/partials/_search_trigger.html.erb +2 -6
  101. data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
  102. data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
  103. data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
  104. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
  105. data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
  106. data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
  107. data/lib/docyard/version.rb +1 -1
  108. metadata +33 -5
  109. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
  110. data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
  111. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
  112. data/lib/docyard/templates/markdown/index.md.erb +0 -82
@@ -5,19 +5,23 @@ require_relative "sidebar/title_extractor"
5
5
  require_relative "sidebar/tree_builder"
6
6
  require_relative "sidebar/renderer"
7
7
  require_relative "sidebar/config_parser"
8
+ require_relative "sidebar/local_config_loader"
9
+ require_relative "sidebar/path_prefixer"
10
+ require_relative "sidebar/tree_filter"
8
11
 
9
12
  module Docyard
10
13
  class SidebarBuilder
11
- attr_reader :docs_path, :current_path, :config
14
+ attr_reader :docs_path, :current_path, :config, :header_ctas
12
15
 
13
- def initialize(docs_path:, current_path: "/", config: nil)
16
+ def initialize(docs_path:, current_path: "/", config: nil, header_ctas: [])
14
17
  @docs_path = docs_path
15
18
  @current_path = current_path
16
19
  @config = config
20
+ @header_ctas = header_ctas
17
21
  end
18
22
 
19
23
  def tree
20
- @tree ||= build_tree
24
+ @tree ||= build_scoped_tree
21
25
  end
22
26
 
23
27
  def to_html
@@ -26,65 +30,130 @@ module Docyard
26
30
 
27
31
  private
28
32
 
29
- def build_tree
30
- if config_sidebar_items?
31
- build_tree_from_config
33
+ def build_scoped_tree
34
+ active_tab = find_active_tab
35
+ return build_tree_for_path(docs_path) unless active_tab
36
+
37
+ build_tree_for_tab(active_tab)
38
+ end
39
+
40
+ def build_tree_for_tab(tab)
41
+ tab_path = tab["href"]&.chomp("/")
42
+ return build_tree_for_path(docs_path) if empty_tab_path?(tab_path)
43
+
44
+ scoped_docs_path = resolve_scoped_path(tab_path)
45
+ build_scoped_or_filtered_tree(scoped_docs_path, tab_path)
46
+ end
47
+
48
+ def empty_tab_path?(tab_path)
49
+ tab_path.nil? || tab_path.empty? || tab_path == "/"
50
+ end
51
+
52
+ def resolve_scoped_path(tab_path)
53
+ tab_folder = tab_path.sub(%r{^/}, "")
54
+ File.join(docs_path, tab_folder)
55
+ end
56
+
57
+ def build_scoped_or_filtered_tree(scoped_docs_path, tab_path)
58
+ if scoped_sidebar_available?(scoped_docs_path)
59
+ build_tree_for_path(scoped_docs_path, base_url_prefix: tab_path)
32
60
  else
33
- build_tree_from_filesystem
61
+ Sidebar::TreeFilter.new(build_tree_for_path(docs_path), tab_path).filter
34
62
  end
35
63
  end
36
64
 
37
- def build_tree_from_config
38
- config_parser.parse.map(&:to_h)
65
+ def scoped_sidebar_available?(path)
66
+ File.directory?(path) && Sidebar::LocalConfigLoader.new(path).config_file_exists?
39
67
  end
40
68
 
41
- def build_tree_from_filesystem
42
- file_items = scanner.scan
43
- tree_builder.build(file_items)
69
+ def build_tree_for_path(path, base_url_prefix: "")
70
+ config_items = Sidebar::LocalConfigLoader.new(path).load
71
+ tree = build_tree(config_items, path, base_url_prefix)
72
+ maybe_prepend_overview(tree, path, base_url_prefix)
44
73
  end
45
74
 
46
- def config_sidebar_items?
47
- config_sidebar_items&.any?
75
+ def build_tree(config_items, path, base_url_prefix)
76
+ if config_items&.any?
77
+ build_tree_from_config(config_items, path, base_url_prefix)
78
+ else
79
+ build_tree_from_filesystem(path, base_url_prefix)
80
+ end
48
81
  end
49
82
 
50
- def config_sidebar_items
51
- return [] unless config
83
+ def maybe_prepend_overview(tree, path, base_url_prefix)
84
+ return tree if skip_overview?(tree, path, base_url_prefix)
52
85
 
53
- config.sidebar&.items || []
86
+ [build_overview_item(base_url_prefix)] + tree
54
87
  end
55
88
 
56
- def config_parser
57
- @config_parser ||= Sidebar::ConfigParser.new(
58
- config_sidebar_items,
59
- docs_path: docs_path,
60
- current_path: current_path
61
- )
89
+ def skip_overview?(tree, path, base_url_prefix)
90
+ base_url_prefix.empty? ||
91
+ tree.first&.dig(:section) ||
92
+ !File.file?(File.join(path, "index.md")) ||
93
+ tree.any? { |item| item[:path] == base_url_prefix }
62
94
  end
63
95
 
64
- def scanner
65
- @scanner ||= Sidebar::FileSystemScanner.new(docs_path)
96
+ def build_overview_item(base_url_prefix)
97
+ {
98
+ title: "Overview", path: base_url_prefix, icon: nil,
99
+ active: current_path == base_url_prefix, type: :file,
100
+ collapsed: false, collapsible: false, target: "_self",
101
+ has_index: false, section: false, children: []
102
+ }
66
103
  end
67
104
 
68
- def tree_builder
69
- @tree_builder ||= Sidebar::TreeBuilder.new(
70
- docs_path: docs_path,
71
- current_path: current_path
72
- )
105
+ def build_tree_from_config(items, path, base_url_prefix)
106
+ tree = Sidebar::ConfigParser.new(
107
+ items, docs_path: path, current_path: current_path_relative_to(base_url_prefix)
108
+ ).parse.map(&:to_h)
109
+
110
+ Sidebar::PathPrefixer.new(tree, base_url_prefix).prefix
111
+ end
112
+
113
+ def build_tree_from_filesystem(path, base_url_prefix)
114
+ file_items = Sidebar::FileSystemScanner.new(path).scan
115
+ tree = Sidebar::TreeBuilder.new(
116
+ docs_path: path, current_path: current_path_relative_to(base_url_prefix)
117
+ ).build(file_items)
118
+
119
+ Sidebar::PathPrefixer.new(tree, base_url_prefix).prefix
120
+ end
121
+
122
+ def current_path_relative_to(prefix)
123
+ return current_path if prefix.empty?
124
+ return current_path unless current_path.start_with?(prefix)
125
+
126
+ relative = current_path.sub(prefix, "")
127
+ relative.empty? ? "/" : relative
73
128
  end
74
129
 
75
130
  def renderer
76
131
  @renderer ||= Sidebar::Renderer.new(
77
- site_title: extract_site_title,
78
- base_url: extract_base_url
132
+ site_title: config&.title || "Documentation",
133
+ base_url: config&.build&.base || "/",
134
+ header_ctas: header_ctas
79
135
  )
80
136
  end
81
137
 
82
- def extract_base_url
83
- config&.build&.base_url || "/"
138
+ def tabs_configured?
139
+ tabs = config&.tabs
140
+ tabs.is_a?(Array) && tabs.any?
141
+ end
142
+
143
+ def find_active_tab
144
+ return nil unless tabs_configured?
145
+
146
+ normalized_current = current_path.chomp("/")
147
+ config.tabs.find { |tab| tab_matches_current?(tab, normalized_current) }
84
148
  end
85
149
 
86
- def extract_site_title
87
- config&.site&.title || "Documentation"
150
+ def tab_matches_current?(tab, normalized_current)
151
+ return false if tab["external"]
152
+
153
+ tab_href = tab["href"]&.chomp("/")
154
+ return false if tab_href.nil?
155
+
156
+ normalized_current == tab_href || normalized_current.start_with?("#{tab_href}/")
88
157
  end
89
158
  end
90
159
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module IconHelpers
5
+ def icon(name, weight = "regular")
6
+ Icons.render(name.to_s.tr("_", "-"), weight) || ""
7
+ end
8
+
9
+ def icon_file_extension(extension)
10
+ Icons.render_file_extension(extension) || ""
11
+ end
12
+ end
13
+ end
@@ -39,7 +39,29 @@ module Docyard
39
39
  "list-dashes" => '<path d="M88,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H96A8,8,0,0,1,88,64Zm128,56H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Zm0,64H96a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM56,56H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Zm0,64H40a8,8,0,0,0,0,16H56a8,8,0,0,0,0-16Z"/>',
40
40
  "magnifying-glass" => '<path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"/>',
41
41
  "command" => '<path d="M180,144H160V112h20a36,36,0,1,0-36-36V96H112V76a36,36,0,1,0-36,36H96v32H76a36,36,0,1,0,36,36V160h32v20a36,36,0,1,0,36-36ZM160,76a20,20,0,1,1,20,20H160ZM56,76a20,20,0,0,1,40,0V96H76A20,20,0,0,1,56,76ZM96,180a20,20,0,1,1-20-20H96Zm16-68h32v32H112Zm68,88a20,20,0,0,1-20-20V160h20a20,20,0,0,1,0,40Z"/>',
42
- "hash" => '<path d="M224,88H175.4l8.47-46.57a8,8,0,0,0-15.74-2.86l-9,49.43H111.4l8.47-46.57a8,8,0,0,0-15.74-2.86L95.14,88H48a8,8,0,0,0,0,16H92.23L83.5,152H32a8,8,0,0,0,0,16H80.6l-8.47,46.57a8,8,0,0,0,6.44,9.3A7.79,7.79,0,0,0,80,224a8,8,0,0,0,7.86-6.57l9-49.43H144.6l-8.47,46.57a8,8,0,0,0,6.44,9.3,7.79,7.79,0,0,0,1.43.13,8,8,0,0,0,7.86-6.57l9-49.43H208a8,8,0,0,0,0-16H163.77l8.73-48H224a8,8,0,0,0,0-16Zm-68.5,64H107.77l8.73-48h47.73Z"/>'
42
+ "hash" => '<path d="M224,88H175.4l8.47-46.57a8,8,0,0,0-15.74-2.86l-9,49.43H111.4l8.47-46.57a8,8,0,0,0-15.74-2.86L95.14,88H48a8,8,0,0,0,0,16H92.23L83.5,152H32a8,8,0,0,0,0,16H80.6l-8.47,46.57a8,8,0,0,0,6.44,9.3A7.79,7.79,0,0,0,80,224a8,8,0,0,0,7.86-6.57l9-49.43H144.6l-8.47,46.57a8,8,0,0,0,6.44,9.3,7.79,7.79,0,0,0,1.43.13,8,8,0,0,0,7.86-6.57l9-49.43H208a8,8,0,0,0,0-16H163.77l8.73-48H224a8,8,0,0,0,0-16Zm-68.5,64H107.77l8.73-48h47.73Z"/>',
43
+ "table" => '<path d="M224,48H32a8,8,0,0,0-8,8V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A8,8,0,0,0,224,48ZM40,112H80v32H40Zm56,0H216v32H96Zm120-48v32H40V64ZM40,160H80v32H40Zm176,32H96V160H216v32Z"/>',
44
+ "caret-down" => '<path d="M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z"/>',
45
+ "sidebar" => '<path d="M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40ZM40,56H80V200H40ZM216,200H96V56H216V200Z"/>',
46
+ "link-simple" => '<path d="M165.66,90.34a8,8,0,0,1,0,11.32l-64,64a8,8,0,0,1-11.32-11.32l64-64A8,8,0,0,1,165.66,90.34ZM215.6,40.4a56,56,0,0,0-79.2,0L106.34,70.45a8,8,0,0,0,11.32,11.32l30.06-30a40,40,0,0,1,56.57,56.56l-30.07,30.06a8,8,0,0,0,11.31,11.32L215.6,119.6a56,56,0,0,0,0-79.2ZM138.34,174.22l-30.06,30.06a40,40,0,1,1-56.56-56.56l30.05-30.06a8,8,0,0,0-11.32-11.32L40.4,136.4a56,56,0,0,0,79.2,79.2l30.06-30.07a8,8,0,0,0-11.32-11.31Z"/>',
47
+ "copyright" => '<path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216ZM96,128a32,32,0,0,0,57.6,19.2,8,8,0,0,1,12.8,9.61,48,48,0,1,1,0-57.62,8,8,0,0,1-12.8,9.61A32,32,0,0,0,96,128Z"/>',
48
+ "equals" => '<path d="M224,160a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,160ZM40,104H216a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16Z"/>',
49
+ "x-logo" => '<path d="M214.75,211.71l-62.6-98.38,61.77-67.95a8,8,0,0,0-11.84-10.76L143.24,99.34,102.75,35.71A8,8,0,0,0,96,32H48a8,8,0,0,0-6.75,12.3l62.6,98.37-61.77,68a8,8,0,1,0,11.84,10.76l58.84-64.72,40.49,63.63A8,8,0,0,0,160,224h48a8,8,0,0,0,6.75-12.29ZM164.39,208,62.57,48h29L193.43,208Z"/>',
50
+ "discord-logo" => '<path d="M104,140a12,12,0,1,1-12-12A12,12,0,0,1,104,140Zm60-12a12,12,0,1,0,12,12A12,12,0,0,0,164,128Zm74.45,64.9-67,29.71a16.17,16.17,0,0,1-21.71-9.1l-8.11-22q-6.72.45-13.63.46t-13.63-.46l-8.11,22a16.18,16.18,0,0,1-21.71,9.1l-67-29.71a15.93,15.93,0,0,1-9.06-18.51L38,58A16.07,16.07,0,0,1,51,46.14l36.06-5.93a16.22,16.22,0,0,1,18.26,11.88l3.26,12.84Q118.11,64,128,64t19.4.93l3.26-12.84a16.21,16.21,0,0,1,18.26-11.88L205,46.14A16.07,16.07,0,0,1,218,58l29.53,116.38A15.93,15.93,0,0,1,238.45,192.9ZM232,178.28,202.47,62s0,0-.08,0L166.33,56a.17.17,0,0,0-.17,0l-2.83,11.14c5,.94,10,2.06,14.83,3.42A8,8,0,0,1,176,86.31a8.09,8.09,0,0,1-2.16-.3A172.25,172.25,0,0,0,128,80a172.25,172.25,0,0,0-45.84,6,8,8,0,1,1-4.32-15.4c4.82-1.36,9.78-2.48,14.82-3.42L89.83,56s0,0-.12,0h0L53.61,61.93a.17.17,0,0,0-.09,0L24,178.33,91,208a.23.23,0,0,0,.22,0L98,189.72a173.2,173.2,0,0,1-20.14-4.32A8,8,0,0,1,82.16,170,171.85,171.85,0,0,0,128,176a171.85,171.85,0,0,0,45.84-6,8,8,0,0,1,4.32,15.41A173.2,173.2,0,0,1,158,189.72L164.75,208a.22.22,0,0,0,.21,0Z"/>',
51
+ "linkedin-logo" => '<path d="M216,24H40A16,16,0,0,0,24,40V216a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V40A16,16,0,0,0,216,24Zm0,192H40V40H216V216ZM96,112v64a8,8,0,0,1-16,0V112a8,8,0,0,1,16,0Zm88,28v36a8,8,0,0,1-16,0V140a20,20,0,0,0-40,0v36a8,8,0,0,1-16,0V112a8,8,0,0,1,15.79-1.78A36,36,0,0,1,184,140ZM100,84A12,12,0,1,1,88,72,12,12,0,0,1,100,84Z"/>',
52
+ "youtube-logo" => '<path d="M164.44,121.34l-48-32A8,8,0,0,0,104,96v64a8,8,0,0,0,12.44,6.66l48-32a8,8,0,0,0,0-13.32ZM120,145.05V111l25.58,17ZM234.33,69.52a24,24,0,0,0-14.49-16.4C185.56,39.88,131,40,128,40s-57.56-.12-91.84,13.12a24,24,0,0,0-14.49,16.4C19.08,79.5,16,97.74,16,128s3.08,48.5,5.67,58.48a24,24,0,0,0,14.49,16.41C69,215.56,120.4,216,127.34,216h1.32c6.94,0,58.37-.44,91.18-13.11a24,24,0,0,0,14.49-16.41c2.59-10,5.67-28.22,5.67-58.48S236.92,79.5,234.33,69.52Zm-15.49,113a8,8,0,0,1-4.77,5.49c-31.65,12.22-85.48,12-86,12H128c-.54,0-54.33.2-86-12a8,8,0,0,1-4.77-5.49C34.8,173.39,32,156.57,32,128s2.8-45.39,5.16-54.47A8,8,0,0,1,41.93,68c30.52-11.79,81.66-12,85.85-12h.27c.54,0,54.38-.18,86,12a8,8,0,0,1,4.77,5.49C221.2,82.61,224,99.43,224,128S221.2,173.39,218.84,182.47Z"/>',
53
+ "twitter-logo" => '<path d="M214.75,211.71l-62.6-98.38,61.77-67.95a8,8,0,0,0-11.84-10.76L143.24,99.34,102.75,35.71A8,8,0,0,0,96,32H48a8,8,0,0,0-6.75,12.3l62.6,98.37-61.77,68a8,8,0,1,0,11.84,10.76l58.84-64.72,40.49,63.63A8,8,0,0,0,160,224h48a8,8,0,0,0,6.75-12.29ZM164.39,208,62.57,48h29L193.43,208Z"/>',
54
+ "instagram-logo" => '<path d="M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160ZM176,24H80A56.06,56.06,0,0,0,24,80v96a56.06,56.06,0,0,0,56,56h96a56.06,56.06,0,0,0,56-56V80A56.06,56.06,0,0,0,176,24Zm40,152a40,40,0,0,1-40,40H80a40,40,0,0,1-40-40V80A40,40,0,0,1,80,40h96a40,40,0,0,1,40,40ZM192,76a12,12,0,1,1-12-12A12,12,0,0,1,192,76Z"/>',
55
+ "facebook-logo" => '<path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm8,191.63V152h24a8,8,0,0,0,0-16H136V112a16,16,0,0,1,16-16h16a8,8,0,0,0,0-16H152a32,32,0,0,0-32,32v24H96a8,8,0,0,0,0,16h24v63.63a88,88,0,1,1,16,0Z"/>',
56
+ "tiktok-logo" => '<path d="M224,72a48.05,48.05,0,0,1-48-48,8,8,0,0,0-8-8H128a8,8,0,0,0-8,8V156a20,20,0,1,1-28.57-18.08A8,8,0,0,0,96,130.69V88a8,8,0,0,0-9.4-7.88C50.91,86.48,24,119.1,24,156a76,76,0,0,0,152,0V116.29A103.25,103.25,0,0,0,224,128a8,8,0,0,0,8-8V80A8,8,0,0,0,224,72Zm-8,39.64a87.19,87.19,0,0,1-43.33-16.15A8,8,0,0,0,160,102v54a60,60,0,0,1-120,0c0-25.9,16.64-49.13,40-57.6v27.67A36,36,0,1,0,136,156V32h24.5A64.14,64.14,0,0,0,216,87.5Z"/>',
57
+ "twitch-logo" => '<path d="M208,32H48A16,16,0,0,0,32,48V192a16,16,0,0,0,16,16H64v32a8,8,0,0,0,13.12,6.15L122.9,208h42.2a16,16,0,0,0,10.25-3.71l42.89-35.75A15.93,15.93,0,0,0,224,156.25V48A16,16,0,0,0,208,32Zm0,124.25L165.1,192H120a8,8,0,0,0-5.12,1.85L80,222.92V200a8,8,0,0,0-8-8H48V48H208ZM160,136V88a8,8,0,0,1,16,0v48a8,8,0,0,1-16,0Zm-48,0V88a8,8,0,0,1,16,0v48a8,8,0,0,1-16,0Z"/>',
58
+ "reddit-logo" => '<path d="M248,104a32,32,0,0,0-52.94-24.19c-16.75-8.9-36.76-14.28-57.66-15.53l5.19-31.17,17.72,2.72a24,24,0,1,0,2.87-15.74l-26-4a8,8,0,0,0-9.11,6.59L121.2,64.16c-21.84.94-42.82,6.38-60.26,15.65a32,32,0,0,0-42.59,47.74A59,59,0,0,0,16,144c0,21.93,12,42.35,33.91,57.49C70.88,216,98.61,224,128,224s57.12-8,78.09-22.51C228,186.35,240,165.93,240,144a59,59,0,0,0-2.35-16.45A32.16,32.16,0,0,0,248,104ZM184,24a8,8,0,1,1-8,8A8,8,0,0,1,184,24Zm40.13,93.78a8,8,0,0,0-3.29,10A43.58,43.58,0,0,1,224,144c0,16.53-9.59,32.27-27,44.33C178.67,201,154.17,208,128,208s-50.67-7-69-19.67C41.59,176.27,32,160.53,32,144a43.75,43.75,0,0,1,3.14-16.17,8,8,0,0,0-3.27-10A16,16,0,1,1,52.94,94.59a8,8,0,0,0,10.45,2.23l.36-.22C81.45,85.9,104.25,80,128,80h0c23.73,0,46.53,5.9,64.23,16.6l.42.25a8,8,0,0,0,10.39-2.26,16,16,0,1,1,21.07,23.19ZM88,144a16,16,0,1,1,16-16A16,16,0,0,1,88,144Zm96-16a16,16,0,1,1-16-16A16,16,0,0,1,184,128Zm-16.93,44.25a8,8,0,0,1-3.32,10.82,76.18,76.18,0,0,1-71.5,0,8,8,0,1,1,7.5-14.14,60.18,60.18,0,0,0,56.5,0A8,8,0,0,1,167.07,172.25Z"/>',
59
+ "mastodon-logo" => '<path d="M184,32H72A40,40,0,0,0,32,72V192a40,40,0,0,0,40,40h88a8,8,0,0,0,0-16H72a24,24,0,0,1-24-24v-8H184a40,40,0,0,0,40-40V72A40,40,0,0,0,184,32Zm24,112a24,24,0,0,1-24,24H48V72A24,24,0,0,1,72,48H184a24,24,0,0,1,24,24Zm-24-40v32a8,8,0,0,1-16,0V104a16,16,0,0,0-32,0v32a8,8,0,0,1-16,0V104a16,16,0,0,0-32,0v32a8,8,0,0,1-16,0V104a32,32,0,0,1,56-21.13A32,32,0,0,1,184,104Z"/>',
60
+ "threads-logo" => '<path d="M186.42,123.65a63.81,63.81,0,0,0-11.13-6.72c-4-29.89-24-39.31-33.1-42.07-19.78-6-42.51,1.19-52.85,16.7a8,8,0,0,0,13.32,8.88c6.37-9.56,22-14.16,34.89-10.27,9.95,3,16.82,10.3,20.15,21a81.05,81.05,0,0,0-15.29-1.43c-13.92,0-26.95,3.59-36.67,10.1C94.3,127.57,88,139,88,152c0,20.58,15.86,35.52,37.71,35.52a48,48,0,0,0,34.35-14.81c6.44-6.7,14-18.36,15.61-37.1.38.26.74.53,1.1.8C186.88,144.05,192,154.68,192,168c0,19.36-20.34,48-64,48-26.73,0-45.48-8.65-57.34-26.44C60.93,175,56,154.26,56,128s4.93-47,14.66-61.56C82.52,48.65,101.27,40,128,40c32.93,0,54,13.25,64.53,40.52a8,8,0,1,0,14.93-5.75C194.68,41.56,167.2,24,128,24,96,24,72.19,35.29,57.34,57.56,45.83,74.83,40,98.52,40,128s5.83,53.17,17.34,70.44C72.19,220.71,96,232,128,232c30.07,0,48.9-11.48,59.4-21.1C200.3,199.08,208,183,208,168,208,149.66,200.54,134.32,186.42,123.65Zm-37.89,38a31.94,31.94,0,0,1-22.82,9.9c-10.81,0-21.71-6-21.71-19.52,0-12.63,12-26.21,38.41-26.21A63.88,63.88,0,0,1,160,128.24C160,142.32,156,153.86,148.53,161.62Z"/>',
61
+ "pinterest-logo" => '<path d="M224,112c0,22.57-7.9,43.2-22.23,58.11C188.39,184,170.25,192,152,192c-17.88,0-29.82-5.86-37.43-12l-10.78,45.82A8,8,0,0,1,96,232a8.24,8.24,0,0,1-1.84-.21,8,8,0,0,1-6-9.62l32-136a8,8,0,0,1,15.58,3.66l-16.9,71.8C122,166,131.3,176,152,176c27.53,0,56-23.94,56-64A72,72,0,1,0,73.63,148a8,8,0,0,1-13.85,8A88,88,0,1,1,224,112Z"/>',
62
+ "medium-logo" => '<path d="M72,64a64,64,0,1,0,64,64A64.07,64.07,0,0,0,72,64Zm0,112a48,48,0,1,1,48-48A48.05,48.05,0,0,1,72,176ZM184,64c-5.68,0-16.4,2.76-24.32,21.25C154.73,96.8,152,112,152,128s2.73,31.2,7.68,42.75C167.6,189.24,178.32,192,184,192s16.4-2.76,24.32-21.25C213.27,159.2,216,144,216,128s-2.73-31.2-7.68-42.75C200.4,66.76,189.68,64,184,64Zm0,112c-5.64,0-16-18.22-16-48s10.36-48,16-48,16,18.22,16,48S189.64,176,184,176ZM248,72V184a8,8,0,0,1-16,0V72a8,8,0,0,1,16,0Z"/>',
63
+ "slack-logo" => '<path d="M221.13,128A32,32,0,0,0,184,76.31V56a32,32,0,0,0-56-21.13A32,32,0,0,0,76.31,72H56a32,32,0,0,0-21.13,56A32,32,0,0,0,72,179.69V200a32,32,0,0,0,56,21.13A32,32,0,0,0,179.69,184H200a32,32,0,0,0,21.13-56ZM72,152a16,16,0,1,1-16-16H72Zm48,48a16,16,0,0,1-32,0V152a16,16,0,0,1,16-16h16Zm0-80H56a16,16,0,0,1,0-32h48a16,16,0,0,1,16,16Zm0-48H104a16,16,0,1,1,16-16Zm16-16a16,16,0,0,1,32,0v48a16,16,0,0,1-16,16H136Zm16,160a16,16,0,0,1-16-16V184h16a16,16,0,0,1,0,32Zm48-48H152a16,16,0,0,1-16-16V136h64a16,16,0,0,1,0,32Zm0-48H184V104a16,16,0,1,1,16,16Z"/>',
64
+ "gitlab-logo" => '<path d="M230.15,117.1,210.25,41a11.94,11.94,0,0,0-22.79-1.11L169.78,88H86.22L68.54,39.87A11.94,11.94,0,0,0,45.75,41L25.85,117.1a57.19,57.19,0,0,0,22,61l73.27,51.76a11.91,11.91,0,0,0,13.74,0l73.27-51.76A57.19,57.19,0,0,0,230.15,117.1ZM58,57.5,73.13,98.76A8,8,0,0,0,80.64,104h94.72a8,8,0,0,0,7.51-5.24L198,57.5l13.07,50L128,166.21,44.9,107.5ZM40.68,124.11,114.13,176,93.41,190.65,57.09,165A41.06,41.06,0,0,1,40.68,124.11Zm87.32,91-20.73-14.65L128,185.8l20.73,14.64ZM198.91,165l-36.32,25.66L141.87,176l73.45-51.9A41.06,41.06,0,0,1,198.91,165Z"/>'
43
65
  },
44
66
  "bold" => {
45
67
  "heart" => '<path d="M178,36c-20.09,0-37.92,7.93-50,21.56C115.92,43.93,98.09,36,78,36a66.08,66.08,0,0,0-66,66c0,72.34,105.81,130.14,110.31,132.57a12,12,0,0,0,11.38,0C138.19,232.14,244,174.34,244,102A66.08,66.08,0,0,0,178,36Zm-5.49,142.36A328.69,328.69,0,0,1,128,210.16a328.69,328.69,0,0,1-44.51-31.8C61.82,159.77,36,131.42,36,102A42,42,0,0,1,78,60c17.8,0,32.7,9.4,38.89,24.54a12,12,0,0,0,22.22,0C145.3,69.4,160.2,60,178,60a42,42,0,0,1,42,42C220,131.42,194.18,159.77,172.51,178.36Z"/>'
@@ -62,6 +62,10 @@ module Docyard
62
62
  frontmatter.dig("sidebar", "collapsed")
63
63
  end
64
64
 
65
+ def sidebar_order
66
+ frontmatter.dig("sidebar", "order")
67
+ end
68
+
65
69
  def toc
66
70
  @context[:toc] || []
67
71
  end
@@ -91,6 +95,7 @@ module Docyard
91
95
  input: "GFM",
92
96
  hard_wrap: false,
93
97
  syntax_highlighter: "rouge",
98
+ syntax_highlighter_opts: { guess_lang: true },
94
99
  parse_block_html: true
95
100
  ).to_html
96
101
 
@@ -2,51 +2,52 @@
2
2
 
3
3
  require "erb"
4
4
  require_relative "../config/constants"
5
+ require_relative "icon_helpers"
5
6
 
6
7
  module Docyard
7
8
  class Renderer
8
9
  include Utils::UrlHelpers
10
+ include IconHelpers
9
11
 
10
12
  LAYOUTS_PATH = File.join(__dir__, "../templates", "layouts")
11
13
  ERRORS_PATH = File.join(__dir__, "../templates", "errors")
12
14
  PARTIALS_PATH = File.join(__dir__, "../templates", "partials")
15
+ DEFAULT_LAYOUT = "default"
13
16
 
14
- attr_reader :layout_path, :base_url, :config
17
+ attr_reader :base_url, :config
15
18
 
16
- def initialize(layout: "default", base_url: "/", config: nil)
17
- @layout_path = File.join(LAYOUTS_PATH, "#{layout}.html.erb")
19
+ def initialize(base_url: "/", config: nil)
18
20
  @base_url = normalize_base_url(base_url)
19
21
  @config = config
20
22
  end
21
23
 
22
- def render_file(file_path, sidebar_html: "", prev_next_html: "", branding: {})
23
- markdown_content = File.read(file_path)
24
- markdown = Markdown.new(markdown_content, config: config)
25
-
26
- html_content = strip_md_from_links(markdown.html)
27
- toc = markdown.toc
24
+ def render_file(file_path, sidebar_html: "", prev_next_html: "", breadcrumbs: nil, branding: {},
25
+ template_options: {}, current_path: "/")
26
+ markdown = Markdown.new(File.read(file_path), config: config)
28
27
 
29
28
  render(
30
- content: html_content,
29
+ content: strip_md_from_links(markdown.html),
31
30
  page_title: markdown.title || Constants::DEFAULT_SITE_TITLE,
32
- navigation: {
33
- sidebar_html: sidebar_html,
34
- prev_next_html: prev_next_html,
35
- toc: toc
36
- },
37
- branding: branding
31
+ navigation: build_navigation(sidebar_html, prev_next_html, markdown.toc, breadcrumbs),
32
+ branding: branding,
33
+ template_options: template_options,
34
+ current_path: current_path
38
35
  )
39
36
  end
40
37
 
41
- def render(content:, page_title: Constants::DEFAULT_SITE_TITLE, navigation: {}, branding: {})
42
- template = File.read(layout_path)
38
+ def build_navigation(sidebar_html, prev_next_html, toc, breadcrumbs)
39
+ { sidebar_html: sidebar_html, prev_next_html: prev_next_html, toc: toc, breadcrumbs: breadcrumbs }
40
+ end
43
41
 
44
- sidebar_html = navigation[:sidebar_html] || ""
45
- prev_next_html = navigation[:prev_next_html] || ""
46
- toc = navigation[:toc] || []
42
+ def render(content:, page_title: Constants::DEFAULT_SITE_TITLE, navigation: {}, branding: {},
43
+ template_options: {}, current_path: "/")
44
+ layout = template_options[:template] || DEFAULT_LAYOUT
45
+ layout_path = File.join(LAYOUTS_PATH, "#{layout}.html.erb")
46
+ template = File.read(layout_path)
47
47
 
48
- assign_content_variables(content, page_title, sidebar_html, prev_next_html, toc)
49
- assign_branding_variables(branding)
48
+ assign_content_variables(content, page_title, navigation)
49
+ assign_branding_variables(branding, current_path)
50
+ assign_template_variables(template_options)
50
51
 
51
52
  ERB.new(template).result(binding)
52
53
  end
@@ -84,18 +85,20 @@ module Docyard
84
85
 
85
86
  private
86
87
 
87
- def assign_content_variables(content, page_title, sidebar_html, prev_next_html, toc)
88
+ def assign_content_variables(content, page_title, navigation)
88
89
  @content = content
89
90
  @page_title = page_title
90
- @sidebar_html = sidebar_html
91
- @prev_next_html = prev_next_html
92
- @toc = toc
91
+ @sidebar_html = navigation[:sidebar_html] || ""
92
+ @prev_next_html = navigation[:prev_next_html] || ""
93
+ @toc = navigation[:toc] || []
94
+ @breadcrumbs = navigation[:breadcrumbs]
93
95
  end
94
96
 
95
- def assign_branding_variables(branding)
97
+ def assign_branding_variables(branding, current_path = "/")
96
98
  assign_site_branding(branding)
97
- assign_display_options(branding)
98
99
  assign_search_options(branding)
100
+ assign_credits_and_social(branding)
101
+ assign_tabs(branding, current_path)
99
102
  end
100
103
 
101
104
  def assign_site_branding(branding)
@@ -104,11 +107,7 @@ module Docyard
104
107
  @logo = branding[:logo] || Constants::DEFAULT_LOGO_PATH
105
108
  @logo_dark = branding[:logo_dark]
106
109
  @favicon = branding[:favicon] || Constants::DEFAULT_FAVICON_PATH
107
- end
108
-
109
- def assign_display_options(branding)
110
- @display_logo = branding[:display_logo].nil? || branding[:display_logo]
111
- @display_title = branding[:display_title].nil? || branding[:display_title]
110
+ @has_custom_logo = branding[:has_custom_logo] || false
112
111
  end
113
112
 
114
113
  def assign_search_options(branding)
@@ -116,6 +115,47 @@ module Docyard
116
115
  @search_placeholder = branding[:search_placeholder] || "Search documentation..."
117
116
  end
118
117
 
118
+ def assign_credits_and_social(branding)
119
+ @credits = branding[:credits] != false
120
+ @copyright = branding[:copyright]
121
+ @social = branding[:social] || []
122
+ @header_ctas = branding[:header_ctas] || []
123
+ end
124
+
125
+ def assign_tabs(branding, current_path)
126
+ tabs = branding[:tabs] || []
127
+ @tabs = tabs.map { |tab| tab.merge(active: tab_active?(tab[:href], current_path)) }
128
+ @has_tabs = branding[:has_tabs] || false
129
+ @current_path = current_path
130
+ end
131
+
132
+ def tab_active?(tab_href, current_path)
133
+ return false if tab_href.nil? || current_path.nil?
134
+ return false if tab_href.start_with?("http://", "https://")
135
+
136
+ normalized_tab = tab_href.chomp("/")
137
+ normalized_current = current_path.chomp("/")
138
+
139
+ return true if normalized_tab == normalized_current
140
+
141
+ current_path.start_with?("#{normalized_tab}/")
142
+ end
143
+
144
+ def assign_template_variables(template_options)
145
+ @hero = template_options[:hero]
146
+ @features = template_options[:features]
147
+ @features_header = template_options[:features_header]
148
+ @show_sidebar = template_options.fetch(:show_sidebar, true)
149
+ @show_toc = template_options.fetch(:show_toc, true)
150
+ assign_footer_from_landing(template_options[:footer])
151
+ end
152
+
153
+ def assign_footer_from_landing(footer)
154
+ return unless footer
155
+
156
+ @footer_links = footer[:links]
157
+ end
158
+
119
159
  def strip_md_from_links(html)
120
160
  html.gsub(/href="([^"]+)\.md"/, 'href="\1"')
121
161
  end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class TemplateResolver
5
+ BACKGROUNDS = %w[grid glow mesh none].freeze
6
+ DEFAULT_BACKGROUND = "grid"
7
+
8
+ attr_reader :frontmatter, :site_config
9
+
10
+ def initialize(frontmatter, site_config = {})
11
+ @frontmatter = frontmatter || {}
12
+ @site_config = site_config || {}
13
+ end
14
+
15
+ def landing?
16
+ landing_config.any?
17
+ end
18
+
19
+ def template
20
+ landing? ? "splash" : "default"
21
+ end
22
+
23
+ def show_sidebar?
24
+ if landing?
25
+ landing_config.fetch("sidebar", false)
26
+ else
27
+ true
28
+ end
29
+ end
30
+
31
+ def show_toc?
32
+ return false if landing?
33
+
34
+ true
35
+ end
36
+
37
+ def hero_config
38
+ return nil unless landing?
39
+
40
+ hero = landing_config["hero"]
41
+ return nil unless hero.is_a?(Hash)
42
+
43
+ symbolize_hero(hero)
44
+ end
45
+
46
+ def features_config
47
+ return nil unless landing?
48
+
49
+ features = landing_config["features"]
50
+ return nil unless features.is_a?(Array)
51
+
52
+ features.map { |f| symbolize_feature(f) }
53
+ end
54
+
55
+ def features_header_config
56
+ return nil unless landing?
57
+
58
+ header = landing_config["features_header"]
59
+ return nil unless header.is_a?(Hash)
60
+
61
+ {
62
+ label: header["label"],
63
+ title: header["title"],
64
+ description: header["description"]
65
+ }.compact
66
+ end
67
+
68
+ def footer_config
69
+ return nil unless landing?
70
+
71
+ footer = landing_config["footer"]
72
+ return nil unless footer.is_a?(Hash)
73
+
74
+ {
75
+ links: normalize_footer_links(footer["links"])
76
+ }
77
+ end
78
+
79
+ def to_options
80
+ {
81
+ template: template,
82
+ landing: landing?,
83
+ show_sidebar: show_sidebar?,
84
+ show_toc: show_toc?,
85
+ hero: hero_config,
86
+ features: features_config,
87
+ features_header: features_header_config,
88
+ footer: footer_config
89
+ }
90
+ end
91
+
92
+ private
93
+
94
+ def normalize_footer_links(links)
95
+ return nil unless links.is_a?(Array)
96
+
97
+ links.map do |link|
98
+ next unless link.is_a?(Hash)
99
+
100
+ { text: link["text"], link: link["link"] }
101
+ end.compact
102
+ end
103
+
104
+ def landing_config
105
+ @landing_config ||= frontmatter["landing"] || site_config["landing"] || {}
106
+ end
107
+
108
+ def symbolize_hero(hero)
109
+ background = hero["background"]
110
+ validated_bg = BACKGROUNDS.include?(background) ? background : DEFAULT_BACKGROUND
111
+
112
+ {
113
+ background: validated_bg,
114
+ badge: hero["badge"],
115
+ name: hero["name"],
116
+ title: hero["title"],
117
+ tagline: hero["tagline"],
118
+ gradient: hero.fetch("gradient", true),
119
+ image: symbolize_image(hero["image"]),
120
+ actions: symbolize_actions(hero["actions"])
121
+ }.compact
122
+ end
123
+
124
+ def symbolize_image(image)
125
+ return nil unless image.is_a?(Hash)
126
+
127
+ if image["light"] || image["dark"]
128
+ {
129
+ light: image["light"],
130
+ dark: image["dark"],
131
+ alt: image["alt"]
132
+ }.compact
133
+ else
134
+ {
135
+ src: image["src"],
136
+ alt: image["alt"]
137
+ }.compact
138
+ end
139
+ end
140
+
141
+ def symbolize_actions(actions)
142
+ return nil unless actions.is_a?(Array)
143
+
144
+ actions.map do |action|
145
+ {
146
+ text: action["text"],
147
+ link: action["link"],
148
+ icon: action["icon"],
149
+ variant: action["variant"] || "primary",
150
+ target: action["target"],
151
+ rel: action["rel"]
152
+ }.compact
153
+ end
154
+ end
155
+
156
+ def symbolize_feature(feature)
157
+ return {} unless feature.is_a?(Hash)
158
+
159
+ {
160
+ title: feature["title"],
161
+ description: feature["description"],
162
+ icon: feature["icon"],
163
+ color: feature["color"],
164
+ link: feature["link"],
165
+ link_text: feature["link_text"],
166
+ size: feature["size"],
167
+ target: feature["target"],
168
+ rel: feature["rel"]
169
+ }.compact
170
+ end
171
+ end
172
+ end