docyard 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/CHANGELOG.md +34 -1
  4. data/lib/docyard/build/asset_bundler.rb +22 -7
  5. data/lib/docyard/build/file_copier.rb +49 -27
  6. data/lib/docyard/build/sitemap_generator.rb +6 -6
  7. data/lib/docyard/build/static_generator.rb +82 -50
  8. data/lib/docyard/builder.rb +20 -10
  9. data/lib/docyard/cli.rb +6 -3
  10. data/lib/docyard/components/aliases.rb +29 -0
  11. data/lib/docyard/components/processors/callout_processor.rb +124 -0
  12. data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +106 -0
  13. data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +79 -0
  14. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +78 -0
  15. data/lib/docyard/components/processors/code_block_processor.rb +175 -0
  16. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +127 -0
  17. data/lib/docyard/components/processors/heading_anchor_processor.rb +39 -0
  18. data/lib/docyard/components/processors/icon_processor.rb +53 -0
  19. data/lib/docyard/components/processors/table_of_contents_processor.rb +68 -0
  20. data/lib/docyard/components/processors/table_wrapper_processor.rb +22 -0
  21. data/lib/docyard/components/processors/tabs_processor.rb +48 -0
  22. data/lib/docyard/components/support/code_block/feature_extractor.rb +117 -0
  23. data/lib/docyard/components/support/code_block/icon_detector.rb +44 -0
  24. data/lib/docyard/components/support/code_block/line_parser.rb +84 -0
  25. data/lib/docyard/components/support/code_block/line_wrapper.rb +50 -0
  26. data/lib/docyard/components/support/code_block/patterns.rb +55 -0
  27. data/lib/docyard/components/support/code_detector.rb +61 -0
  28. data/lib/docyard/components/support/tabs/icon_detector.rb +62 -0
  29. data/lib/docyard/components/support/tabs/parser.rb +195 -0
  30. data/lib/docyard/components/support/tabs/range_finder.rb +46 -0
  31. data/lib/docyard/config/branding_resolver.rb +183 -0
  32. data/lib/docyard/{constants.rb → config/constants.rb} +7 -4
  33. data/lib/docyard/config/validator.rb +122 -99
  34. data/lib/docyard/config.rb +38 -36
  35. data/lib/docyard/initializer.rb +15 -76
  36. data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
  37. data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +6 -3
  38. data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
  39. data/lib/docyard/navigation/sidebar/config_parser.rb +208 -0
  40. data/lib/docyard/navigation/sidebar/file_resolver.rb +78 -0
  41. data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +2 -1
  42. data/lib/docyard/navigation/sidebar/item.rb +96 -0
  43. data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
  44. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +69 -0
  45. data/lib/docyard/navigation/sidebar/metadata_reader.rb +47 -0
  46. data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
  47. data/lib/docyard/navigation/sidebar/renderer.rb +144 -0
  48. data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
  49. data/lib/docyard/navigation/sidebar/tree_builder.rb +139 -0
  50. data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
  51. data/lib/docyard/navigation/sidebar_builder.rb +159 -0
  52. data/lib/docyard/rendering/icon_helpers.rb +13 -0
  53. data/lib/docyard/{icons → rendering/icons}/phosphor.rb +26 -1
  54. data/lib/docyard/{markdown.rb → rendering/markdown.rb} +19 -13
  55. data/lib/docyard/rendering/renderer.rb +163 -0
  56. data/lib/docyard/rendering/template_resolver.rb +172 -0
  57. data/lib/docyard/routing/fallback_resolver.rb +92 -0
  58. data/lib/docyard/search/build_indexer.rb +74 -0
  59. data/lib/docyard/search/dev_indexer.rb +155 -0
  60. data/lib/docyard/search/pagefind_support.rb +33 -0
  61. data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +24 -19
  62. data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
  63. data/lib/docyard/server/pagefind_handler.rb +63 -0
  64. data/lib/docyard/{preview_server.rb → server/preview_server.rb} +2 -2
  65. data/lib/docyard/server/rack_application.rb +192 -0
  66. data/lib/docyard/server/resolution_result.rb +29 -0
  67. data/lib/docyard/{router.rb → server/router.rb} +4 -4
  68. data/lib/docyard/templates/assets/css/code.css +18 -51
  69. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +143 -0
  70. data/lib/docyard/templates/assets/css/components/callout.css +67 -67
  71. data/lib/docyard/templates/assets/css/components/code-block.css +180 -282
  72. data/lib/docyard/templates/assets/css/components/heading-anchor.css +28 -15
  73. data/lib/docyard/templates/assets/css/components/icon.css +0 -1
  74. data/lib/docyard/templates/assets/css/components/logo.css +0 -2
  75. data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
  76. data/lib/docyard/templates/assets/css/components/navigation.css +186 -167
  77. data/lib/docyard/templates/assets/css/components/prev-next.css +76 -47
  78. data/lib/docyard/templates/assets/css/components/search.css +561 -0
  79. data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
  80. data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
  81. data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
  82. data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
  83. data/lib/docyard/templates/assets/css/landing.css +815 -0
  84. data/lib/docyard/templates/assets/css/layout.css +503 -87
  85. data/lib/docyard/templates/assets/css/main.css +1 -3
  86. data/lib/docyard/templates/assets/css/markdown.css +111 -93
  87. data/lib/docyard/templates/assets/css/reset.css +0 -3
  88. data/lib/docyard/templates/assets/css/typography.css +43 -41
  89. data/lib/docyard/templates/assets/css/variables.css +268 -208
  90. data/lib/docyard/templates/assets/favicon.svg +7 -8
  91. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
  92. data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
  93. data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
  94. data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
  95. data/lib/docyard/templates/assets/js/components/search.js +610 -0
  96. data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
  97. data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
  98. data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
  99. data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
  100. data/lib/docyard/templates/assets/js/theme.js +0 -3
  101. data/lib/docyard/templates/assets/logo-dark.svg +8 -2
  102. data/lib/docyard/templates/assets/logo.svg +7 -4
  103. data/lib/docyard/templates/config/docyard.yml.erb +37 -34
  104. data/lib/docyard/templates/errors/404.html.erb +1 -1
  105. data/lib/docyard/templates/errors/500.html.erb +1 -1
  106. data/lib/docyard/templates/layouts/default.html.erb +19 -56
  107. data/lib/docyard/templates/layouts/splash.html.erb +176 -0
  108. data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
  109. data/lib/docyard/templates/partials/_code_block.html.erb +6 -4
  110. data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
  111. data/lib/docyard/templates/partials/_features.html.erb +15 -0
  112. data/lib/docyard/templates/partials/_footer.html.erb +42 -0
  113. data/lib/docyard/templates/partials/_head.html.erb +22 -0
  114. data/lib/docyard/templates/partials/_header.html.erb +49 -0
  115. data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
  116. data/lib/docyard/templates/partials/_hero.html.erb +27 -0
  117. data/lib/docyard/templates/partials/_nav_group.html.erb +25 -11
  118. data/lib/docyard/templates/partials/_nav_leaf.html.erb +1 -1
  119. data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
  120. data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
  121. data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
  122. data/lib/docyard/templates/partials/_prev_next.html.erb +9 -3
  123. data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
  124. data/lib/docyard/templates/partials/_search_modal.html.erb +41 -0
  125. data/lib/docyard/templates/partials/_search_trigger.html.erb +18 -0
  126. data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
  127. data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
  128. data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
  129. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
  130. data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
  131. data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
  132. data/lib/docyard/utils/html_helpers.rb +14 -0
  133. data/lib/docyard/utils/path_resolver.rb +2 -1
  134. data/lib/docyard/utils/url_helpers.rb +20 -0
  135. data/lib/docyard/version.rb +1 -1
  136. data/lib/docyard.rb +22 -15
  137. metadata +89 -50
  138. data/lib/docyard/components/callout_processor.rb +0 -121
  139. data/lib/docyard/components/code_block_diff_preprocessor.rb +0 -104
  140. data/lib/docyard/components/code_block_feature_extractor.rb +0 -113
  141. data/lib/docyard/components/code_block_focus_preprocessor.rb +0 -77
  142. data/lib/docyard/components/code_block_icon_detector.rb +0 -40
  143. data/lib/docyard/components/code_block_line_wrapper.rb +0 -46
  144. data/lib/docyard/components/code_block_options_preprocessor.rb +0 -76
  145. data/lib/docyard/components/code_block_patterns.rb +0 -51
  146. data/lib/docyard/components/code_block_processor.rb +0 -176
  147. data/lib/docyard/components/code_detector.rb +0 -59
  148. data/lib/docyard/components/code_line_parser.rb +0 -80
  149. data/lib/docyard/components/code_snippet_import_preprocessor.rb +0 -125
  150. data/lib/docyard/components/heading_anchor_processor.rb +0 -34
  151. data/lib/docyard/components/icon_detector.rb +0 -57
  152. data/lib/docyard/components/icon_processor.rb +0 -51
  153. data/lib/docyard/components/table_of_contents_processor.rb +0 -64
  154. data/lib/docyard/components/table_wrapper_processor.rb +0 -18
  155. data/lib/docyard/components/tabs_parser.rb +0 -191
  156. data/lib/docyard/components/tabs_processor.rb +0 -44
  157. data/lib/docyard/components/tabs_range_finder.rb +0 -42
  158. data/lib/docyard/rack_application.rb +0 -172
  159. data/lib/docyard/renderer.rb +0 -120
  160. data/lib/docyard/routing/resolution_result.rb +0 -31
  161. data/lib/docyard/sidebar/config_parser.rb +0 -180
  162. data/lib/docyard/sidebar/item.rb +0 -58
  163. data/lib/docyard/sidebar/renderer.rb +0 -137
  164. data/lib/docyard/sidebar/tree_builder.rb +0 -59
  165. data/lib/docyard/sidebar_builder.rb +0 -102
  166. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
  167. data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
  168. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
  169. data/lib/docyard/templates/markdown/index.md.erb +0 -82
  170. /data/lib/docyard/{sidebar → navigation/sidebar}/title_extractor.rb +0 -0
  171. /data/lib/docyard/{icons → rendering/icons}/LICENSE.phosphor +0 -0
  172. /data/lib/docyard/{icons → rendering/icons}/file_types.rb +0 -0
  173. /data/lib/docyard/{icons.rb → rendering/icons.rb} +0 -0
  174. /data/lib/docyard/{language_mapping.rb → rendering/language_mapping.rb} +0 -0
  175. /data/lib/docyard/{file_watcher.rb → server/file_watcher.rb} +0 -0
  176. /data/lib/docyard/{errors.rb → utils/errors.rb} +0 -0
  177. /data/lib/docyard/{logging.rb → utils/logging.rb} +0 -0
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rendering/icons"
4
+ require_relative "../../rendering/renderer"
5
+ require_relative "../base_processor"
6
+ require "kramdown"
7
+ require "kramdown-parser-gfm"
8
+
9
+ module Docyard
10
+ module Components
11
+ module Processors
12
+ class CalloutProcessor < BaseProcessor
13
+ self.priority = 10
14
+
15
+ CALLOUT_TYPES = {
16
+ "note" => { title: "Note", icon: "info", color: "note" },
17
+ "tip" => { title: "Tip", icon: "lightbulb", color: "tip" },
18
+ "important" => { title: "Important", icon: "warning-circle", color: "important" },
19
+ "warning" => { title: "Warning", icon: "warning", color: "warning" },
20
+ "danger" => { title: "Danger", icon: "siren", color: "danger" }
21
+ }.freeze
22
+
23
+ GITHUB_ALERT_TYPES = {
24
+ "NOTE" => "note",
25
+ "TIP" => "tip",
26
+ "IMPORTANT" => "important",
27
+ "WARNING" => "warning",
28
+ "CAUTION" => "danger"
29
+ }.freeze
30
+
31
+ def preprocess(markdown)
32
+ process_container_syntax(markdown)
33
+ end
34
+
35
+ def postprocess(html)
36
+ process_github_alerts(html)
37
+ end
38
+
39
+ private
40
+
41
+ def process_container_syntax(markdown)
42
+ markdown.gsub(/^:::[ \t]*(\w+)(?:[ \t]+([^\n]+?))?[ \t]*\n(.*?)^:::[ \t]*$/m) do
43
+ process_callout_match(Regexp.last_match(0), Regexp.last_match(1), Regexp.last_match(2),
44
+ Regexp.last_match(3))
45
+ end
46
+ end
47
+
48
+ def process_callout_match(original_match, type_raw, custom_title, content_markdown)
49
+ type = type_raw.downcase
50
+ return original_match unless CALLOUT_TYPES.key?(type)
51
+
52
+ config = CALLOUT_TYPES[type]
53
+ title = determine_title(custom_title, config[:title])
54
+ content_html = render_markdown_content(content_markdown.strip)
55
+
56
+ wrap_in_nomarkdown(render_callout_html(type, title, content_html, config[:icon]))
57
+ end
58
+
59
+ def determine_title(custom_title, default_title)
60
+ title = custom_title&.strip
61
+ title.nil? || title.empty? ? default_title : title
62
+ end
63
+
64
+ def render_markdown_content(content_markdown)
65
+ return "" if content_markdown.empty?
66
+
67
+ Kramdown::Document.new(
68
+ content_markdown,
69
+ input: "GFM",
70
+ hard_wrap: false,
71
+ syntax_highlighter: "rouge"
72
+ ).to_html
73
+ end
74
+
75
+ def wrap_in_nomarkdown(html)
76
+ "{::nomarkdown}\n#{html}\n{:/nomarkdown}"
77
+ end
78
+
79
+ def process_github_alerts(html)
80
+ github_alert_regex = %r{
81
+ <blockquote>\s*
82
+ <p>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*
83
+ (?:<br\s*/>)?\s*
84
+ (.*?)</p>
85
+ (.*?)
86
+ </blockquote>
87
+ }mx
88
+
89
+ html.gsub(github_alert_regex) do
90
+ process_github_alert_match(Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3))
91
+ end
92
+ end
93
+
94
+ def process_github_alert_match(alert_type, first_para, rest_content)
95
+ type = GITHUB_ALERT_TYPES[alert_type]
96
+ config = CALLOUT_TYPES[type]
97
+ content_html = combine_alert_content(first_para.strip, rest_content.strip)
98
+
99
+ render_callout_html(type, config[:title], content_html, config[:icon])
100
+ end
101
+
102
+ def combine_alert_content(first_para, rest_content)
103
+ return "<p>#{first_para}</p>" if rest_content.empty?
104
+
105
+ "<p>#{first_para}</p>#{rest_content}"
106
+ end
107
+
108
+ def render_callout_html(type, title, content_html, icon_name)
109
+ icon_svg = Icons.render(icon_name, "duotone") || ""
110
+ renderer = Renderer.new
111
+
112
+ renderer.render_partial(
113
+ "_callout", {
114
+ type: type,
115
+ title: title,
116
+ content_html: content_html,
117
+ icon_svg: icon_svg
118
+ }
119
+ )
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+ require_relative "../support/code_block/patterns"
5
+
6
+ module Docyard
7
+ module Components
8
+ module Processors
9
+ class CodeBlockDiffPreprocessor < BaseProcessor
10
+ include Support::CodeBlock::Patterns
11
+
12
+ self.priority = 6
13
+
14
+ CODE_BLOCK_REGEX = /^```(\w*).*?\n(.*?)^```/m
15
+ TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
16
+
17
+ def preprocess(content)
18
+ context[:code_block_diff_lines] ||= []
19
+ context[:code_block_error_lines] ||= []
20
+ context[:code_block_warning_lines] ||= []
21
+ @block_index = 0
22
+ @tabs_ranges = find_tabs_ranges(content)
23
+
24
+ content.gsub(CODE_BLOCK_REGEX) { |_| process_code_block(Regexp.last_match) }
25
+ end
26
+
27
+ private
28
+
29
+ def process_code_block(match)
30
+ return match[0] if inside_tabs?(match.begin(0))
31
+
32
+ result = extract_all_markers(match[2])
33
+ store_extracted_markers(result)
34
+ @block_index += 1
35
+ match[0].sub(match[2], result[:cleaned_content])
36
+ end
37
+
38
+ def store_extracted_markers(result)
39
+ context[:code_block_diff_lines][@block_index] = result[:diff_lines]
40
+ context[:code_block_error_lines][@block_index] = result[:error_lines]
41
+ context[:code_block_warning_lines][@block_index] = result[:warning_lines]
42
+ end
43
+
44
+ def extract_all_markers(code_content)
45
+ diff_info = extract_diff_lines(code_content)
46
+ error_info = extract_error_lines(diff_info[:cleaned_content])
47
+ warning_info = extract_warning_lines(error_info[:cleaned_content])
48
+
49
+ {
50
+ diff_lines: diff_info[:lines],
51
+ error_lines: error_info[:lines],
52
+ warning_lines: warning_info[:lines],
53
+ cleaned_content: warning_info[:cleaned_content]
54
+ }
55
+ end
56
+
57
+ def extract_diff_lines(code_content)
58
+ extract_marker_lines(code_content, DIFF_MARKER_PATTERN) do |match|
59
+ diff_type = match.captures.compact.first
60
+ diff_type == "++" ? :addition : :deletion
61
+ end
62
+ end
63
+
64
+ def extract_error_lines(code_content)
65
+ extract_marker_lines(code_content, ERROR_MARKER_PATTERN) { true }
66
+ end
67
+
68
+ def extract_warning_lines(code_content)
69
+ extract_marker_lines(code_content, WARNING_MARKER_PATTERN) { true }
70
+ end
71
+
72
+ def extract_marker_lines(code_content, pattern)
73
+ lines = code_content.lines
74
+ marker_lines = {}
75
+ cleaned_lines = []
76
+
77
+ lines.each_with_index do |line, index|
78
+ line_num = index + 1
79
+
80
+ if (match = line.match(pattern))
81
+ marker_lines[line_num] = yield(match)
82
+ cleaned_lines << line.gsub(pattern, "")
83
+ else
84
+ cleaned_lines << line
85
+ end
86
+ end
87
+
88
+ { lines: marker_lines, cleaned_content: cleaned_lines.join }
89
+ end
90
+
91
+ def inside_tabs?(position)
92
+ @tabs_ranges.any? { |range| range.cover?(position) }
93
+ end
94
+
95
+ def find_tabs_ranges(content)
96
+ ranges = []
97
+ content.scan(TABS_BLOCK_REGEX) do
98
+ match = Regexp.last_match
99
+ ranges << (match.begin(0)...match.end(0))
100
+ end
101
+ ranges
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Processors
8
+ class CodeBlockFocusPreprocessor < BaseProcessor
9
+ self.priority = 7
10
+
11
+ FOCUS_MARKER_PATTERN = %r{
12
+ (?:
13
+ //\s*\[!code\s+focus\] |
14
+ \#\s*\[!code\s+focus\] |
15
+ /\*\s*\[!code\s+focus\]\s*\*/ |
16
+ --\s*\[!code\s+focus\] |
17
+ <!--\s*\[!code\s+focus\]\s*--> |
18
+ ;\s*\[!code\s+focus\]
19
+ )[^\S\n]*
20
+ }x
21
+
22
+ CODE_BLOCK_REGEX = /^```(\w*).*?\n(.*?)^```/m
23
+ TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
24
+
25
+ def preprocess(content)
26
+ context[:code_block_focus_lines] ||= []
27
+ @block_index = 0
28
+ @tabs_ranges = find_tabs_ranges(content)
29
+
30
+ content.gsub(CODE_BLOCK_REGEX) { |_| process_code_block(Regexp.last_match) }
31
+ end
32
+
33
+ private
34
+
35
+ def process_code_block(match)
36
+ return match[0] if inside_tabs?(match.begin(0))
37
+
38
+ focus_info = extract_focus_lines(match[2])
39
+ context[:code_block_focus_lines][@block_index] = focus_info[:lines]
40
+ @block_index += 1
41
+ match[0].sub(match[2], focus_info[:cleaned_content])
42
+ end
43
+
44
+ def extract_focus_lines(code_content)
45
+ lines = code_content.lines
46
+ focus_lines = {}
47
+ cleaned_lines = []
48
+
49
+ lines.each_with_index do |line, index|
50
+ line_num = index + 1
51
+
52
+ if line.match?(FOCUS_MARKER_PATTERN)
53
+ focus_lines[line_num] = true
54
+ cleaned_line = line.gsub(FOCUS_MARKER_PATTERN, "")
55
+ cleaned_lines << cleaned_line
56
+ else
57
+ cleaned_lines << line
58
+ end
59
+ end
60
+
61
+ { lines: focus_lines, cleaned_content: cleaned_lines.join }
62
+ end
63
+
64
+ def inside_tabs?(position)
65
+ @tabs_ranges.any? { |range| range.cover?(position) }
66
+ end
67
+
68
+ def find_tabs_ranges(content)
69
+ ranges = []
70
+ content.scan(TABS_BLOCK_REGEX) do
71
+ match = Regexp.last_match
72
+ ranges << (match.begin(0)...match.end(0))
73
+ end
74
+ ranges
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Processors
8
+ class CodeBlockOptionsPreprocessor < BaseProcessor
9
+ self.priority = 5
10
+
11
+ CODE_FENCE_REGEX = /^```(\w+)(?:\s*\[([^\]]+)\])?(:\S+)?(?:\s*\{([^}\n]+)\})?/
12
+ TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
13
+
14
+ def preprocess(content)
15
+ context[:code_block_options] ||= []
16
+ @tabs_ranges = find_tabs_ranges(content)
17
+
18
+ process_code_fences(content)
19
+ end
20
+
21
+ private
22
+
23
+ def process_code_fences(content)
24
+ result = +""
25
+ last_end = 0
26
+
27
+ content.scan(CODE_FENCE_REGEX) do
28
+ match = Regexp.last_match
29
+ result << content[last_end...match.begin(0)]
30
+ result << process_fence_match(match)
31
+ last_end = match.end(0)
32
+ end
33
+
34
+ result << content[last_end..]
35
+ end
36
+
37
+ def process_fence_match(match)
38
+ store_code_block_options(match) unless inside_tabs?(match.begin(0))
39
+ "```#{match[1]}"
40
+ end
41
+
42
+ def store_code_block_options(match)
43
+ context[:code_block_options] << {
44
+ lang: match[1],
45
+ title: match[2],
46
+ option: match[3],
47
+ highlights: parse_highlights(match[4])
48
+ }
49
+ end
50
+
51
+ def inside_tabs?(position)
52
+ @tabs_ranges.any? { |range| range.cover?(position) }
53
+ end
54
+
55
+ def find_tabs_ranges(content)
56
+ ranges = []
57
+ content.scan(TABS_BLOCK_REGEX) do
58
+ match = Regexp.last_match
59
+ ranges << (match.begin(0)...match.end(0))
60
+ end
61
+ ranges
62
+ end
63
+
64
+ def parse_highlights(highlights_str)
65
+ return [] if highlights_str.nil? || highlights_str.strip.empty?
66
+
67
+ highlights_str.split(",").flat_map { |part| parse_highlight_part(part.strip) }.uniq.sort
68
+ end
69
+
70
+ def parse_highlight_part(part)
71
+ return (part.split("-")[0].to_i..part.split("-")[1].to_i).to_a if part.include?("-")
72
+
73
+ [part.to_i]
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rendering/icons"
4
+ require_relative "../../rendering/language_mapping"
5
+ require_relative "../../rendering/renderer"
6
+ require_relative "../base_processor"
7
+ require_relative "../support/code_block/icon_detector"
8
+ require_relative "../support/code_block/line_wrapper"
9
+ require_relative "../support/tabs/range_finder"
10
+
11
+ module Docyard
12
+ module Components
13
+ module Processors
14
+ class CodeBlockProcessor < BaseProcessor
15
+ include Support::CodeBlock::LineWrapper
16
+ include Utils::HtmlHelpers
17
+
18
+ self.priority = 20
19
+
20
+ CodeBlockIconDetector = Support::CodeBlock::IconDetector
21
+ TabsRangeFinder = Support::Tabs::RangeFinder
22
+
23
+ def postprocess(html)
24
+ return html unless html.include?('<div class="highlight">')
25
+
26
+ initialize_postprocess_state(html)
27
+ process_all_highlight_blocks(html)
28
+ end
29
+
30
+ private
31
+
32
+ def initialize_postprocess_state(html)
33
+ @block_index = 0
34
+ @options = context[:code_block_options] || []
35
+ @diff_lines = context[:code_block_diff_lines] || []
36
+ @focus_lines = context[:code_block_focus_lines] || []
37
+ @error_lines = context[:code_block_error_lines] || []
38
+ @warning_lines = context[:code_block_warning_lines] || []
39
+ @global_line_numbers = context.dig(:config, "markdown", "lineNumbers") || false
40
+ @tabs_ranges = TabsRangeFinder.find_ranges(html)
41
+ end
42
+
43
+ def process_all_highlight_blocks(html)
44
+ result = +""
45
+ last_end = 0
46
+
47
+ html.scan(%r{<div class="highlight">(.*?)</div>}m) do
48
+ match = Regexp.last_match
49
+ result << html[last_end...match.begin(0)]
50
+ result << process_highlight_match(match)
51
+ last_end = match.end(0)
52
+ end
53
+
54
+ result << html[last_end..]
55
+ end
56
+
57
+ def process_highlight_match(match)
58
+ if inside_tabs?(match.begin(0))
59
+ match[0]
60
+ else
61
+ processed = process_code_block(match[0], match[1])
62
+ @block_index += 1
63
+ processed
64
+ end
65
+ end
66
+
67
+ def process_code_block(original_html, inner_html)
68
+ block_data = extract_block_data(inner_html)
69
+ processed_html = process_html_for_highlighting(original_html, block_data)
70
+
71
+ render_code_block_with_copy(block_data.merge(html: processed_html))
72
+ end
73
+
74
+ def extract_block_data(inner_html)
75
+ opts = current_block_options
76
+ code_text = extract_code_text(inner_html)
77
+ start_line = extract_start_line(opts[:option])
78
+ show_line_numbers = determine_line_numbers(opts[:option])
79
+ title_data = CodeBlockIconDetector.detect(opts[:title], opts[:lang])
80
+
81
+ build_block_data(code_text, opts, show_line_numbers, start_line, title_data)
82
+ end
83
+
84
+ def current_block_options
85
+ block_opts = @options[@block_index] || {}
86
+ {
87
+ option: block_opts[:option],
88
+ title: block_opts[:title],
89
+ lang: block_opts[:lang],
90
+ highlights: block_opts[:highlights] || []
91
+ }
92
+ end
93
+
94
+ def build_block_data(code_text, opts, show_line_numbers, start_line, title_data)
95
+ {
96
+ text: code_text,
97
+ highlights: opts[:highlights],
98
+ diff_lines: @diff_lines[@block_index] || {},
99
+ focus_lines: @focus_lines[@block_index] || {},
100
+ error_lines: @error_lines[@block_index] || {},
101
+ warning_lines: @warning_lines[@block_index] || {},
102
+ show_line_numbers: show_line_numbers,
103
+ line_numbers: show_line_numbers ? generate_line_numbers(code_text, start_line) : [],
104
+ start_line: start_line,
105
+ title: title_data[:title],
106
+ icon: title_data[:icon],
107
+ icon_source: title_data[:icon_source]
108
+ }
109
+ end
110
+
111
+ def process_html_for_highlighting(original_html, block_data)
112
+ needs_wrapping = block_data[:highlights].any? || block_data[:diff_lines].any? ||
113
+ block_data[:focus_lines].any? || block_data[:error_lines].any? ||
114
+ block_data[:warning_lines].any?
115
+ return original_html unless needs_wrapping
116
+
117
+ wrap_code_block(original_html, block_data)
118
+ end
119
+
120
+ def determine_line_numbers(block_option)
121
+ return false if block_option == ":no-line-numbers"
122
+ return true if block_option&.start_with?(":line-numbers")
123
+
124
+ @global_line_numbers
125
+ end
126
+
127
+ def extract_start_line(block_option)
128
+ return 1 unless block_option&.include?("=")
129
+
130
+ block_option.split("=").last.to_i
131
+ end
132
+
133
+ def generate_line_numbers(code_text, start_line)
134
+ line_count = code_text.lines.count
135
+ line_count = 1 if line_count.zero?
136
+ (start_line...(start_line + line_count)).to_a
137
+ end
138
+
139
+ def inside_tabs?(position)
140
+ @tabs_ranges.any? { |range| range.cover?(position) }
141
+ end
142
+
143
+ def extract_code_text(html)
144
+ text = html.gsub(/<[^>]+>/, "")
145
+ text = CGI.unescapeHTML(text)
146
+ text.strip
147
+ end
148
+
149
+ def render_code_block_with_copy(block_data)
150
+ Renderer.new.render_partial("_code_block", template_locals(block_data))
151
+ end
152
+
153
+ def template_locals(block_data)
154
+ base_locals(block_data).merge(line_feature_locals(block_data)).merge(title_locals(block_data))
155
+ end
156
+
157
+ def base_locals(block_data)
158
+ { code_block_html: block_data[:html], code_text: escape_html_attribute(block_data[:text]),
159
+ copy_icon: Icons.render("copy", "regular") || "", show_line_numbers: block_data[:show_line_numbers],
160
+ line_numbers: block_data[:line_numbers], start_line: block_data[:start_line] }
161
+ end
162
+
163
+ def line_feature_locals(block_data)
164
+ { highlights: block_data[:highlights], diff_lines: block_data[:diff_lines],
165
+ focus_lines: block_data[:focus_lines], error_lines: block_data[:error_lines],
166
+ warning_lines: block_data[:warning_lines] }
167
+ end
168
+
169
+ def title_locals(block_data)
170
+ { title: block_data[:title], icon: block_data[:icon], icon_source: block_data[:icon_source] }
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Processors
8
+ class CodeSnippetImportPreprocessor < BaseProcessor
9
+ EXTENSION_MAP = {
10
+ "rb" => "ruby",
11
+ "js" => "javascript",
12
+ "ts" => "typescript",
13
+ "py" => "python",
14
+ "yml" => "yaml",
15
+ "md" => "markdown",
16
+ "sh" => "bash",
17
+ "zsh" => "bash",
18
+ "jsx" => "jsx",
19
+ "tsx" => "tsx"
20
+ }.freeze
21
+
22
+ IMPORT_PATTERN = %r{^<<<\s+@/([^\s{#]+)(?:#([\w-]+))?(?:\{([^}]+)\})?\s*$}
23
+
24
+ self.priority = 1
25
+
26
+ def preprocess(content)
27
+ @docs_root = context[:docs_root] || "docs"
28
+ content.gsub(IMPORT_PATTERN) { |_| process_import(Regexp.last_match) }
29
+ end
30
+
31
+ private
32
+
33
+ def process_import(match)
34
+ filepath = match[1]
35
+ region = match[2]
36
+ options = match[3]
37
+
38
+ file_content = read_file(filepath)
39
+ return import_error(filepath, "File not found") unless file_content
40
+
41
+ file_content = extract_region(file_content, region) if region
42
+ return import_error(filepath, "Region '#{region}' not found") unless file_content
43
+
44
+ build_code_block(file_content, filepath, options)
45
+ end
46
+
47
+ def read_file(filepath)
48
+ full_path = File.join(@docs_root, filepath)
49
+ return nil unless File.exist?(full_path)
50
+
51
+ File.read(full_path)
52
+ end
53
+
54
+ def extract_region(content, region_name)
55
+ region_start = %r{^[ \t]*(?://|#|/\*)\s*#region\s+#{Regexp.escape(region_name)}\b.*$}
56
+ region_end = %r{^[ \t]*(?://|#|/\*|\*/)\s*#endregion\s*#{Regexp.escape(region_name)}?\b.*$}
57
+
58
+ lines = content.lines
59
+ start_index = lines.find_index { |line| line.match?(region_start) }
60
+ return nil unless start_index
61
+
62
+ end_index = lines[(start_index + 1)..].find_index { |line| line.match?(region_end) }
63
+ return nil unless end_index
64
+
65
+ end_index += start_index + 1
66
+ lines[(start_index + 1)...end_index].join
67
+ end
68
+
69
+ def build_code_block(content, filepath, options)
70
+ lang = detect_language(filepath)
71
+ highlights = nil
72
+
73
+ if options
74
+ parsed = parse_options(options)
75
+ lang = parsed[:lang] if parsed[:lang]
76
+ highlights = parsed[:highlights] if parsed[:highlights]
77
+ end
78
+
79
+ content = extract_line_range(content, highlights) if highlights&.include?("-") && !highlights.include?(",")
80
+
81
+ meta = build_meta_string(highlights, filepath)
82
+
83
+ "```#{lang}#{meta}\n#{content.chomp}\n```"
84
+ end
85
+
86
+ def parse_options(options)
87
+ parts = options.strip.split(/\s+/)
88
+ result = { highlights: nil, lang: nil }
89
+
90
+ parts.each do |part|
91
+ if part.match?(/^[\d,-]+$/)
92
+ result[:highlights] = part
93
+ else
94
+ result[:lang] = part
95
+ end
96
+ end
97
+
98
+ result
99
+ end
100
+
101
+ def extract_line_range(content, range_str)
102
+ return content unless range_str&.match?(/^\d+-\d+$/)
103
+
104
+ start_line, end_line = range_str.split("-").map(&:to_i)
105
+ lines = content.lines
106
+ lines[(start_line - 1)..(end_line - 1)]&.join || content
107
+ end
108
+
109
+ def build_meta_string(highlights, filepath)
110
+ parts = []
111
+ parts << " [#{File.basename(filepath)}]" if filepath
112
+ parts << " {#{highlights}}" if highlights && !highlights.match?(/^\d+-\d+$/)
113
+ parts.join
114
+ end
115
+
116
+ def detect_language(filepath)
117
+ ext = File.extname(filepath).delete_prefix(".")
118
+ EXTENSION_MAP[ext] || ext
119
+ end
120
+
121
+ def import_error(filepath, message)
122
+ "```\nError importing #{filepath}: #{message}\n```"
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end