docyard 0.8.0 → 1.0.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 (189) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -1
  3. data/README.md +8 -253
  4. data/exe/docyard +6 -0
  5. data/lib/docyard/build/asset_bundler.rb +2 -2
  6. data/lib/docyard/build/file_copier.rb +12 -5
  7. data/lib/docyard/build/llms_txt_generator.rb +103 -0
  8. data/lib/docyard/build/sitemap_generator.rb +1 -1
  9. data/lib/docyard/build/static_generator.rb +115 -79
  10. data/lib/docyard/builder.rb +6 -2
  11. data/lib/docyard/cli.rb +14 -4
  12. data/lib/docyard/components/aliases.rb +12 -0
  13. data/lib/docyard/components/processors/abbreviation_processor.rb +72 -0
  14. data/lib/docyard/components/processors/accordion_processor.rb +81 -0
  15. data/lib/docyard/components/processors/badge_processor.rb +72 -0
  16. data/lib/docyard/components/processors/callout_processor.rb +9 -3
  17. data/lib/docyard/components/processors/cards_processor.rb +100 -0
  18. data/lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb +24 -0
  19. data/lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb +44 -0
  20. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +34 -3
  21. data/lib/docyard/components/processors/code_block_processor.rb +11 -24
  22. data/lib/docyard/components/processors/code_group_processor.rb +182 -0
  23. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +7 -1
  24. data/lib/docyard/components/processors/custom_anchor_processor.rb +42 -0
  25. data/lib/docyard/components/processors/file_tree_processor.rb +150 -0
  26. data/lib/docyard/components/processors/icon_processor.rb +8 -2
  27. data/lib/docyard/components/processors/image_caption_processor.rb +96 -0
  28. data/lib/docyard/components/processors/include_processor.rb +86 -0
  29. data/lib/docyard/components/processors/steps_processor.rb +89 -0
  30. data/lib/docyard/components/processors/tabs_processor.rb +9 -1
  31. data/lib/docyard/components/processors/tooltip_processor.rb +57 -0
  32. data/lib/docyard/components/processors/video_embed_processor.rb +207 -0
  33. data/lib/docyard/components/support/code_block/feature_extractor.rb +3 -1
  34. data/lib/docyard/components/support/code_block/icon_detector.rb +5 -12
  35. data/lib/docyard/components/support/code_block/line_number_resolver.rb +30 -0
  36. data/lib/docyard/components/support/code_detector.rb +2 -12
  37. data/lib/docyard/components/support/code_group/html_builder.rb +118 -0
  38. data/lib/docyard/components/support/markdown_code_block_helper.rb +56 -0
  39. data/lib/docyard/components/support/tabs/icon_detector.rb +6 -2
  40. data/lib/docyard/components/support/tabs/parser.rb +6 -23
  41. data/lib/docyard/config/analytics_resolver.rb +24 -0
  42. data/lib/docyard/config/branding_resolver.rb +84 -58
  43. data/lib/docyard/config/key_validator.rb +30 -0
  44. data/lib/docyard/config/logo_detector.rb +39 -0
  45. data/lib/docyard/config/schema.rb +39 -0
  46. data/lib/docyard/config/section.rb +21 -0
  47. data/lib/docyard/config/validation_helpers.rb +83 -0
  48. data/lib/docyard/config/validator.rb +45 -144
  49. data/lib/docyard/config/validators/navigation.rb +43 -0
  50. data/lib/docyard/config/validators/section.rb +114 -0
  51. data/lib/docyard/config.rb +45 -96
  52. data/lib/docyard/constants.rb +59 -0
  53. data/lib/docyard/{utils/errors.rb → errors.rb} +6 -0
  54. data/lib/docyard/initializer.rb +100 -49
  55. data/lib/docyard/navigation/page_navigation_builder.rb +65 -0
  56. data/lib/docyard/navigation/sidebar/auto_builder.rb +107 -0
  57. data/lib/docyard/navigation/sidebar/cache.rb +96 -0
  58. data/lib/docyard/navigation/sidebar/config_builder.rb +179 -0
  59. data/lib/docyard/navigation/sidebar/distributed_builder.rb +145 -0
  60. data/lib/docyard/navigation/sidebar/item.rb +6 -1
  61. data/lib/docyard/navigation/sidebar/local_config_loader.rb +69 -3
  62. data/lib/docyard/navigation/sidebar/renderer.rb +18 -3
  63. data/lib/docyard/navigation/sidebar_builder.rb +43 -81
  64. data/lib/docyard/rendering/branding_variables.rb +65 -0
  65. data/lib/docyard/rendering/icon_helpers.rb +14 -1
  66. data/lib/docyard/rendering/icons/devicons.rb +63 -0
  67. data/lib/docyard/rendering/icons.rb +26 -27
  68. data/lib/docyard/rendering/markdown.rb +20 -15
  69. data/lib/docyard/rendering/og_helpers.rb +36 -0
  70. data/lib/docyard/rendering/renderer.rb +87 -58
  71. data/lib/docyard/rendering/template_resolver.rb +14 -0
  72. data/lib/docyard/routing/fallback_resolver.rb +3 -3
  73. data/lib/docyard/search/build_indexer.rb +2 -2
  74. data/lib/docyard/search/dev_indexer.rb +36 -28
  75. data/lib/docyard/search/pagefind_support.rb +1 -1
  76. data/lib/docyard/server/asset_handler.rb +40 -15
  77. data/lib/docyard/server/dev_server.rb +90 -55
  78. data/lib/docyard/server/file_watcher.rb +68 -18
  79. data/lib/docyard/server/pagefind_handler.rb +1 -1
  80. data/lib/docyard/server/preview_server.rb +29 -33
  81. data/lib/docyard/server/rack_application.rb +38 -70
  82. data/lib/docyard/server/router.rb +11 -7
  83. data/lib/docyard/server/sse_server.rb +157 -0
  84. data/lib/docyard/server/static_file_app.rb +42 -0
  85. data/lib/docyard/templates/assets/css/components/abbreviation.css +86 -0
  86. data/lib/docyard/templates/assets/css/components/accordion.css +138 -0
  87. data/lib/docyard/templates/assets/css/components/badges.css +47 -0
  88. data/lib/docyard/templates/assets/css/components/banner.css +233 -0
  89. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +2 -1
  90. data/lib/docyard/templates/assets/css/components/callout.css +26 -6
  91. data/lib/docyard/templates/assets/css/components/cards.css +100 -0
  92. data/lib/docyard/templates/assets/css/components/code-block.css +14 -2
  93. data/lib/docyard/templates/assets/css/components/code-group.css +294 -0
  94. data/lib/docyard/templates/assets/css/components/feedback.css +126 -0
  95. data/lib/docyard/templates/assets/css/components/figure.css +22 -0
  96. data/lib/docyard/templates/assets/css/components/file-tree.css +125 -0
  97. data/lib/docyard/templates/assets/css/components/heading-anchor.css +21 -13
  98. data/lib/docyard/templates/assets/css/components/icon.css +5 -0
  99. data/lib/docyard/templates/assets/css/components/lightbox.css +65 -0
  100. data/lib/docyard/templates/assets/css/components/nav-menu.css +20 -4
  101. data/lib/docyard/templates/assets/css/components/navigation.css +32 -3
  102. data/lib/docyard/templates/assets/css/components/page-actions.css +131 -0
  103. data/lib/docyard/templates/assets/css/components/prev-next.css +20 -22
  104. data/lib/docyard/templates/assets/css/components/search.css +6 -10
  105. data/lib/docyard/templates/assets/css/components/steps.css +122 -0
  106. data/lib/docyard/templates/assets/css/components/tab-bar.css +7 -4
  107. data/lib/docyard/templates/assets/css/components/table-of-contents.css +57 -11
  108. data/lib/docyard/templates/assets/css/components/tabs.css +13 -5
  109. data/lib/docyard/templates/assets/css/components/theme-toggle.css +3 -1
  110. data/lib/docyard/templates/assets/css/components/tooltip.css +113 -0
  111. data/lib/docyard/templates/assets/css/components/video.css +41 -0
  112. data/lib/docyard/templates/assets/css/landing.css +82 -13
  113. data/lib/docyard/templates/assets/css/layout.css +17 -0
  114. data/lib/docyard/templates/assets/css/markdown.css +25 -3
  115. data/lib/docyard/templates/assets/css/variables.css +13 -1
  116. data/lib/docyard/templates/assets/js/components/abbreviation.js +85 -0
  117. data/lib/docyard/templates/assets/js/components/banner.js +81 -0
  118. data/lib/docyard/templates/assets/js/components/code-group.js +286 -0
  119. data/lib/docyard/templates/assets/js/components/copy-page.js +115 -0
  120. data/lib/docyard/templates/assets/js/components/feedback.js +66 -0
  121. data/lib/docyard/templates/assets/js/components/file-tree.js +39 -0
  122. data/lib/docyard/templates/assets/js/components/lightbox.js +72 -0
  123. data/lib/docyard/templates/assets/js/components/navigation.js +3 -3
  124. data/lib/docyard/templates/assets/js/components/search.js +3 -3
  125. data/lib/docyard/templates/assets/js/components/table-of-contents.js +12 -6
  126. data/lib/docyard/templates/assets/js/components/tabs.js +45 -22
  127. data/lib/docyard/templates/assets/js/components/tooltip.js +118 -0
  128. data/lib/docyard/templates/assets/js/hot-reload.js +44 -0
  129. data/lib/docyard/templates/errors/404.html.erb +114 -5
  130. data/lib/docyard/templates/errors/500.html.erb +173 -10
  131. data/lib/docyard/templates/init/_sidebar.yml +36 -0
  132. data/lib/docyard/templates/init/docyard.yml +36 -0
  133. data/lib/docyard/templates/init/pages/components.md +146 -0
  134. data/lib/docyard/templates/init/pages/getting-started.md +94 -0
  135. data/lib/docyard/templates/init/pages/index.md +22 -0
  136. data/lib/docyard/templates/layouts/default.html.erb +11 -0
  137. data/lib/docyard/templates/layouts/splash.html.erb +15 -1
  138. data/lib/docyard/templates/partials/_accordion.html.erb +9 -0
  139. data/lib/docyard/templates/partials/_analytics.html.erb +24 -0
  140. data/lib/docyard/templates/partials/_banner.html.erb +27 -0
  141. data/lib/docyard/templates/partials/_card.html.erb +23 -0
  142. data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
  143. data/lib/docyard/templates/partials/_feedback.html.erb +14 -0
  144. data/lib/docyard/templates/partials/_footer.html.erb +1 -1
  145. data/lib/docyard/templates/partials/_head.html.erb +79 -4
  146. data/lib/docyard/templates/partials/_icon_library.html.erb +8 -0
  147. data/lib/docyard/templates/partials/_nav_group.html.erb +6 -0
  148. data/lib/docyard/templates/partials/_nav_leaf.html.erb +3 -0
  149. data/lib/docyard/templates/partials/_page_actions.html.erb +21 -0
  150. data/lib/docyard/templates/partials/_scripts.html.erb +6 -3
  151. data/lib/docyard/templates/partials/_step.html.erb +14 -0
  152. data/lib/docyard/templates/partials/_tabs.html.erb +4 -1
  153. data/lib/docyard/utils/git_info.rb +157 -0
  154. data/lib/docyard/utils/hash_utils.rb +31 -0
  155. data/lib/docyard/utils/html_helpers.rb +8 -0
  156. data/lib/docyard/utils/logging.rb +44 -3
  157. data/lib/docyard/utils/path_resolver.rb +0 -10
  158. data/lib/docyard/utils/path_utils.rb +73 -0
  159. data/lib/docyard/version.rb +1 -1
  160. data/lib/docyard.rb +2 -2
  161. metadata +114 -47
  162. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -31
  163. data/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
  164. data/.github/pull_request_template.md +0 -14
  165. data/.github/workflows/ci.yml +0 -49
  166. data/.rubocop.yml +0 -42
  167. data/CODE_OF_CONDUCT.md +0 -132
  168. data/CONTRIBUTING.md +0 -55
  169. data/LICENSE.vscode-icons +0 -42
  170. data/Rakefile +0 -8
  171. data/lib/docyard/config/constants.rb +0 -31
  172. data/lib/docyard/navigation/sidebar/children_discoverer.rb +0 -51
  173. data/lib/docyard/navigation/sidebar/config_parser.rb +0 -208
  174. data/lib/docyard/navigation/sidebar/file_resolver.rb +0 -78
  175. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +0 -78
  176. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +0 -69
  177. data/lib/docyard/navigation/sidebar/metadata_reader.rb +0 -47
  178. data/lib/docyard/navigation/sidebar/path_prefixer.rb +0 -34
  179. data/lib/docyard/navigation/sidebar/sorter.rb +0 -21
  180. data/lib/docyard/navigation/sidebar/title_extractor.rb +0 -25
  181. data/lib/docyard/navigation/sidebar/tree_builder.rb +0 -139
  182. data/lib/docyard/rendering/icons/LICENSE.phosphor +0 -21
  183. data/lib/docyard/rendering/icons/file_types.rb +0 -79
  184. data/lib/docyard/rendering/icons/phosphor.rb +0 -90
  185. data/lib/docyard/rendering/language_mapping.rb +0 -52
  186. data/lib/docyard/templates/assets/js/reload.js +0 -98
  187. data/lib/docyard/templates/partials/_icon.html.erb +0 -1
  188. data/lib/docyard/templates/partials/_icon_file_extension.html.erb +0 -1
  189. data/sig/docyard.rbs +0 -4
@@ -0,0 +1,207 @@
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
+ width = validate_dimension(attrs["width"])
151
+ height = validate_dimension(attrs["height"])
152
+ return "" unless width || height
153
+
154
+ styles = []
155
+ styles << "max-width: #{width}px" if width
156
+ styles << "height: #{height}px" if height
157
+ " style=\"#{styles.join('; ')}\""
158
+ end
159
+
160
+ def validate_dimension(value)
161
+ return nil if value.nil? || value.to_s.empty?
162
+
163
+ int_value = value.to_s.to_i
164
+ return nil unless int_value.positive? && int_value <= 10_000
165
+
166
+ int_value
167
+ end
168
+
169
+ def build_iframe_attrs(url, attrs, provider)
170
+ iframe_attrs = [
171
+ "src=\"#{url}\"",
172
+ "title=\"#{escape_attr(attrs['title'] || default_title(provider))}\"",
173
+ "frameborder=\"0\""
174
+ ]
175
+
176
+ iframe_attrs << "allow=\"#{build_allow_attr(attrs)}\""
177
+ iframe_attrs << "allowfullscreen" unless attrs["nofullscreen"]
178
+
179
+ iframe_attrs
180
+ end
181
+
182
+ def build_allow_attr(attrs)
183
+ permissions = %w[encrypted-media picture-in-picture web-share]
184
+ permissions.unshift("autoplay") if attrs["autoplay"]
185
+ permissions << "fullscreen" unless attrs["nofullscreen"]
186
+ permissions.join("; ")
187
+ end
188
+
189
+ def default_title(provider)
190
+ case provider
191
+ when "youtube" then "YouTube video player"
192
+ when "vimeo" then "Vimeo video player"
193
+ else "Video player"
194
+ end
195
+ end
196
+
197
+ def escape_attr(text)
198
+ text.to_s
199
+ .gsub("&", "&amp;")
200
+ .gsub("<", "&lt;")
201
+ .gsub(">", "&gt;")
202
+ .gsub('"', "&quot;")
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "patterns"
4
+ require_relative "../../../rendering/icons"
4
5
 
5
6
  module Docyard
6
7
  module Components
@@ -18,7 +19,8 @@ module Docyard
18
19
  cleaned = markdown.gsub(CODE_FENCE_REGEX) do
19
20
  block_data = extract_block_data(Regexp.last_match)
20
21
  blocks << block_data
21
- "```#{block_data[:lang]}\n#{block_data[:cleaned_content]}```"
22
+ highlight_lang = Icons.highlight_language(block_data[:lang])
23
+ "```#{highlight_lang}\n#{block_data[:cleaned_content]}```"
22
24
  end
23
25
  { cleaned_markdown: cleaned, blocks: blocks }
24
26
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../../rendering/language_mapping"
3
+ require_relative "../../../rendering/icons"
4
4
 
5
5
  module Docyard
6
6
  module Components
@@ -22,20 +22,13 @@ module Docyard
22
22
  }
23
23
  end
24
24
 
25
- icon, icon_source = auto_detect_icon(language)
26
- { title: title, icon: icon, icon_source: icon_source }
25
+ { title: title, icon: language, icon_source: "language" }
27
26
  end
28
27
 
29
- def auto_detect_icon(language)
30
- return [nil, nil] if language.nil?
28
+ def render_icon(language)
29
+ return "" if language.nil? || language.to_s.empty?
31
30
 
32
- if LanguageMapping.terminal_language?(language)
33
- %w[terminal-window phosphor]
34
- elsif (ext = LanguageMapping.extension_for(language))
35
- [ext, "file-extension"]
36
- else
37
- %w[file phosphor]
38
- end
31
+ Icons.render_for_language(language)
39
32
  end
40
33
  end
41
34
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ module Support
6
+ module CodeBlock
7
+ module LineNumberResolver
8
+ module_function
9
+
10
+ def enabled?(option)
11
+ return false if option == ":no-line-numbers"
12
+
13
+ option&.start_with?(":line-numbers") || false
14
+ end
15
+
16
+ def start_line(option)
17
+ return 1 unless option&.include?("=")
18
+
19
+ option.split("=").last.to_i
20
+ end
21
+
22
+ def generate_numbers(code_text, start = 1)
23
+ line_count = [code_text.lines.count, 1].max
24
+ (start...(start + line_count)).to_a
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../rendering/language_mapping"
3
+ require_relative "../../rendering/icons"
4
4
 
5
5
  module Docyard
6
6
  module Components
@@ -20,7 +20,7 @@ module Docyard
20
20
  language = extract_language
21
21
  return nil unless language
22
22
 
23
- icon_for_language(language)
23
+ { language: language }
24
24
  end
25
25
 
26
26
  private
@@ -45,16 +45,6 @@ module Docyard
45
45
 
46
46
  lang_line.downcase
47
47
  end
48
-
49
- def icon_for_language(language)
50
- if LanguageMapping.terminal_language?(language)
51
- { icon: "terminal-window", source: "phosphor" }
52
- elsif (extension = LanguageMapping.extension_for(language))
53
- { icon: extension, source: "file-extension" }
54
- else
55
- { icon: "file", source: "phosphor" }
56
- end
57
- end
58
48
  end
59
49
  end
60
50
  end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../rendering/icons"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Support
8
+ module CodeGroup
9
+ class HtmlBuilder
10
+ def initialize(blocks, group_id)
11
+ @blocks = blocks
12
+ @group_id = group_id
13
+ end
14
+
15
+ def build
16
+ <<~HTML
17
+ <div class="docyard-code-group" data-code-group="#{@group_id}">
18
+ <div class="docyard-code-group__tabs-wrapper">
19
+ <div class="docyard-code-group__tabs-scroll-container">
20
+ <div role="tablist" aria-label="Code examples" class="docyard-code-group__tabs">
21
+ #{build_tabs}
22
+ <div class="docyard-code-group__indicator" aria-hidden="true"></div>
23
+ </div>
24
+ </div>
25
+ #{build_copy_button}
26
+ </div>
27
+ <div class="docyard-code-group__panels">
28
+ #{build_panels}
29
+ </div>
30
+ </div>
31
+ HTML
32
+ end
33
+
34
+ private
35
+
36
+ def build_tabs
37
+ @blocks.each_with_index.map do |block, index|
38
+ build_tab(block, index)
39
+ end.join("\n")
40
+ end
41
+
42
+ def build_tab(block, index)
43
+ selected = index.zero? ? "true" : "false"
44
+ tabindex = index.zero? ? "0" : "-1"
45
+ icon_html = render_icon(block[:lang])
46
+ <<~HTML.strip
47
+ <button
48
+ role="tab"
49
+ aria-selected="#{selected}"
50
+ aria-controls="cg-panel-#{@group_id}-#{index}"
51
+ id="cg-tab-#{@group_id}-#{index}"
52
+ class="docyard-code-group__tab"
53
+ tabindex="#{tabindex}"
54
+ data-label="#{escape_html(block[:label])}"
55
+ >#{icon_html}<span class="docyard-code-group__tab-label">#{escape_html(block[:label])}</span></button>
56
+ HTML
57
+ end
58
+
59
+ def render_icon(lang)
60
+ return "" if lang.nil? || lang.empty?
61
+
62
+ Icons.render_for_language(lang)
63
+ end
64
+
65
+ def build_copy_button
66
+ copy_icon = Icons.render("copy", "regular") || ""
67
+ <<~HTML.strip
68
+ <button class="docyard-code-group__copy" aria-label="Copy code to clipboard">
69
+ <span class="docyard-code-group__copy-icon">#{copy_icon}</span>
70
+ <span class="docyard-code-group__copy-text">Copy</span>
71
+ </button>
72
+ HTML
73
+ end
74
+
75
+ def build_panels
76
+ @blocks.each_with_index.map do |block, index|
77
+ build_panel(block, index)
78
+ end.join("\n")
79
+ end
80
+
81
+ def build_panel(block, index)
82
+ hidden = index.zero? ? "false" : "true"
83
+ code_text = escape_html_attribute(block[:code_text] || "")
84
+ <<~HTML.strip
85
+ <div
86
+ role="tabpanel"
87
+ id="cg-panel-#{@group_id}-#{index}"
88
+ aria-labelledby="cg-tab-#{@group_id}-#{index}"
89
+ aria-hidden="#{hidden}"
90
+ class="docyard-code-group__panel"
91
+ tabindex="0"
92
+ data-code="#{code_text}"
93
+ >#{block[:content]}</div>
94
+ HTML
95
+ end
96
+
97
+ def escape_html(text)
98
+ text.to_s
99
+ .gsub("&", "&amp;")
100
+ .gsub("<", "&lt;")
101
+ .gsub(">", "&gt;")
102
+ .gsub('"', "&quot;")
103
+ end
104
+
105
+ def escape_html_attribute(text)
106
+ text.to_s
107
+ .gsub("&", "&amp;")
108
+ .gsub("<", "&lt;")
109
+ .gsub(">", "&gt;")
110
+ .gsub('"', "&quot;")
111
+ .gsub("'", "&#39;")
112
+ .gsub("\n", "&#10;")
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ 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,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../code_detector"
4
+ require_relative "../../../rendering/icons"
4
5
 
5
6
  module Docyard
6
7
  module Components
@@ -41,10 +42,13 @@ module Docyard
41
42
  detected = CodeDetector.detect(tab_content)
42
43
  return nil unless detected
43
44
 
45
+ language = detected[:language]
46
+ return nil unless Icons.devicon?(language)
47
+
44
48
  {
45
49
  name: tab_name,
46
- icon: detected[:icon],
47
- icon_source: detected[:source]
50
+ icon: language,
51
+ icon_source: "language"
48
52
  }
49
53
  end
50
54
 
@@ -3,6 +3,7 @@
3
3
  require_relative "../code_block/feature_extractor"
4
4
  require_relative "../code_block/icon_detector"
5
5
  require_relative "../code_block/line_wrapper"
6
+ require_relative "../code_block/line_number_resolver"
6
7
  require_relative "icon_detector"
7
8
  require_relative "../../../rendering/icons"
8
9
  require_relative "../../../rendering/renderer"
@@ -21,6 +22,7 @@ module Docyard
21
22
  CodeBlockFeatureExtractor = CodeBlock::FeatureExtractor
22
23
  CodeBlockIconDetector = CodeBlock::IconDetector
23
24
  CodeBlockLineWrapper = CodeBlock::LineWrapper
25
+ LineNumbers = CodeBlock::LineNumberResolver
24
26
 
25
27
  def self.parse(content)
26
28
  new(content).parse
@@ -120,15 +122,15 @@ module Docyard
120
122
  focus_lines: block_data[:focus_lines] || {},
121
123
  error_lines: block_data[:error_lines] || {},
122
124
  warning_lines: block_data[:warning_lines] || {},
123
- start_line: extract_start_line(block_data[:option])
125
+ start_line: LineNumbers.start_line(block_data[:option])
124
126
  }
125
127
  CodeBlockLineWrapper.wrap_code_block(html, wrapper_data)
126
128
  end
127
129
 
128
130
  def build_full_locals(processed_html, code_text, block_data)
129
131
  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
+ show_line_numbers = LineNumbers.enabled?(block_data[:option])
133
+ start_line = LineNumbers.start_line(block_data[:option])
132
134
 
133
135
  base_locals(processed_html, code_text, show_line_numbers, start_line)
134
136
  .merge(feature_locals(block_data))
@@ -141,7 +143,7 @@ module Docyard
141
143
  code_text: escape_html_attribute(code_text),
142
144
  copy_icon: Icons.render("copy", "regular") || "",
143
145
  show_line_numbers: show_line_numbers,
144
- line_numbers: show_line_numbers ? generate_line_numbers(code_text, start_line) : [],
146
+ line_numbers: show_line_numbers ? LineNumbers.generate_numbers(code_text, start_line) : [],
145
147
  start_line: start_line
146
148
  }
147
149
  end
@@ -164,25 +166,6 @@ module Docyard
164
166
  }
165
167
  end
166
168
 
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
169
  def extract_code_text(html)
187
170
  text = html.gsub(/<[^>]+>/, "")
188
171
  text = CGI.unescapeHTML(text)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module AnalyticsResolver
5
+ def analytics_options
6
+ analytics = config.analytics
7
+ {
8
+ analytics_google: analytics.google,
9
+ analytics_plausible: analytics.plausible,
10
+ analytics_fathom: analytics.fathom,
11
+ analytics_script: analytics.script,
12
+ has_analytics: any_analytics_configured?(analytics)
13
+ }
14
+ end
15
+
16
+ private
17
+
18
+ def any_analytics_configured?(analytics)
19
+ [analytics.google, analytics.plausible, analytics.fathom, analytics.script].any? do |value|
20
+ value.is_a?(String) && !value.strip.empty?
21
+ end
22
+ end
23
+ end
24
+ end