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,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rendering/renderer"
4
+ require_relative "../base_processor"
5
+
6
+ module Docyard
7
+ module Components
8
+ module Processors
9
+ class HeadingAnchorProcessor < BaseProcessor
10
+ self.priority = 30
11
+
12
+ def postprocess(html)
13
+ add_anchor_links(html)
14
+ end
15
+
16
+ private
17
+
18
+ def add_anchor_links(html)
19
+ html.gsub(%r{<(h[2-6])\s+id="([^"]+)">(.*?)</\1>}m) do |_match|
20
+ tag = Regexp.last_match(1)
21
+ id = Regexp.last_match(2)
22
+ content = Regexp.last_match(3)
23
+
24
+ anchor_html = render_anchor_link(id)
25
+
26
+ "<#{tag} id=\"#{id}\">#{content}#{anchor_html}</#{tag}>"
27
+ end
28
+ end
29
+
30
+ def render_anchor_link(id)
31
+ renderer = Renderer.new
32
+ renderer.render_partial("_heading_anchor", {
33
+ id: id
34
+ })
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rendering/icons"
4
+ require_relative "../base_processor"
5
+
6
+ module Docyard
7
+ module Components
8
+ module Processors
9
+ class IconProcessor < BaseProcessor
10
+ self.priority = 20
11
+
12
+ ICON_PATTERN = /:([a-z][a-z0-9-]*):(?:([a-z]+):)?/i
13
+
14
+ def postprocess(html)
15
+ segments = split_preserving_code_blocks(html)
16
+
17
+ segments.map do |segment|
18
+ segment[:type] == :code ? segment[:content] : process_segment(segment[:content])
19
+ end.join
20
+ end
21
+
22
+ private
23
+
24
+ def split_preserving_code_blocks(html)
25
+ segments = []
26
+ current_pos = 0
27
+
28
+ html.scan(%r{<(code|pre)[^>]*>.*?</\1>}m) do
29
+ match_start = Regexp.last_match.begin(0)
30
+ match_end = Regexp.last_match.end(0)
31
+
32
+ segments << { type: :text, content: html[current_pos...match_start] } if match_start > current_pos
33
+ segments << { type: :code, content: html[match_start...match_end] }
34
+
35
+ current_pos = match_end
36
+ end
37
+
38
+ segments << { type: :text, content: html[current_pos..] } if current_pos < html.length
39
+
40
+ segments.empty? ? [{ type: :text, content: html }] : segments
41
+ end
42
+
43
+ def process_segment(content)
44
+ content.gsub(ICON_PATTERN) do
45
+ icon_name = Regexp.last_match(1)
46
+ weight = Regexp.last_match(2) || "regular"
47
+ Icons.render(icon_name, weight) || Regexp.last_match(0)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Processors
8
+ class TableOfContentsProcessor < BaseProcessor
9
+ self.priority = 35
10
+
11
+ def postprocess(html)
12
+ headings = extract_headings(html)
13
+ context[:toc] = headings
14
+ html
15
+ end
16
+
17
+ private
18
+
19
+ def extract_headings(html)
20
+ headings = []
21
+
22
+ html.scan(%r{<(h[2-4])\s+id="([^"]+)">(.*?)</\1>}m) do
23
+ level = Regexp.last_match(1)[1].to_i
24
+ id = Regexp.last_match(2)
25
+ text = strip_html(Regexp.last_match(3))
26
+
27
+ headings << {
28
+ level: level,
29
+ id: id,
30
+ text: text
31
+ }
32
+ end
33
+
34
+ build_hierarchy(headings)
35
+ end
36
+
37
+ def build_hierarchy(headings)
38
+ return [] if headings.empty?
39
+
40
+ root = []
41
+ stack = []
42
+
43
+ headings.each do |heading|
44
+ heading[:children] = []
45
+
46
+ stack.pop while stack.any? && stack.last[:level] >= heading[:level]
47
+
48
+ if stack.empty?
49
+ root << heading
50
+ else
51
+ stack.last[:children] << heading
52
+ end
53
+
54
+ stack << heading
55
+ end
56
+
57
+ root
58
+ end
59
+
60
+ def strip_html(text)
61
+ text.gsub(%r{<a[^>]*class="heading-anchor"[^>]*>.*?</a>}, "")
62
+ .gsub(/<[^>]+>/, "")
63
+ .strip
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Processors
8
+ class TableWrapperProcessor < BaseProcessor
9
+ self.priority = 100
10
+
11
+ def postprocess(html)
12
+ wrapped = html.gsub(/<table([^>]*)>/) do
13
+ attributes = Regexp.last_match(1)
14
+ "<div class=\"table-wrapper\"><table#{attributes}>"
15
+ end
16
+
17
+ wrapped.gsub("</table>", "</table></div>")
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rendering/renderer"
4
+ require_relative "../base_processor"
5
+ require_relative "../support/tabs/parser"
6
+ require "securerandom"
7
+
8
+ module Docyard
9
+ module Components
10
+ module Processors
11
+ class TabsProcessor < BaseProcessor
12
+ self.priority = 15
13
+
14
+ TabsParser = Support::Tabs::Parser
15
+
16
+ def preprocess(content)
17
+ return content unless content.include?(":::tabs")
18
+
19
+ content.gsub(/^:::[ \t]*tabs[ \t]*\n(.*?)^:::[ \t]*$/m) do
20
+ process_tabs_block(Regexp.last_match(1))
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def process_tabs_block(tabs_content)
27
+ tabs = TabsParser.parse(tabs_content)
28
+ return "" if tabs.empty?
29
+
30
+ wrap_in_nomarkdown(render_tabs(tabs))
31
+ end
32
+
33
+ def render_tabs(tabs)
34
+ Renderer.new.render_partial(
35
+ "_tabs", {
36
+ tabs: tabs,
37
+ group_id: SecureRandom.hex(4)
38
+ }
39
+ )
40
+ end
41
+
42
+ def wrap_in_nomarkdown(html)
43
+ "{::nomarkdown}\n#{html}\n{:/nomarkdown}"
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "patterns"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Support
8
+ module CodeBlock
9
+ module FeatureExtractor
10
+ include Patterns
11
+
12
+ CODE_FENCE_REGEX = /^```(\w+)(?:\s*\[([^\]]+)\])?(:\S+)?(?:\s*\{([^}\n]+)\})?[ \t]*\n(.*?)^```/m
13
+
14
+ module_function
15
+
16
+ def process_markdown(markdown)
17
+ blocks = []
18
+ cleaned = markdown.gsub(CODE_FENCE_REGEX) do
19
+ block_data = extract_block_data(Regexp.last_match)
20
+ blocks << block_data
21
+ "```#{block_data[:lang]}\n#{block_data[:cleaned_content]}```"
22
+ end
23
+ { cleaned_markdown: cleaned, blocks: blocks }
24
+ end
25
+
26
+ def extract_block_data(match)
27
+ code_content = match[5]
28
+ diff_info = extract_diff_lines(code_content)
29
+ focus_info = extract_focus_lines(diff_info[:cleaned_content])
30
+ error_info = extract_error_lines(focus_info[:cleaned_content])
31
+ warning_info = extract_warning_lines(error_info[:cleaned_content])
32
+
33
+ build_block_result(match, diff_info, focus_info, error_info, warning_info)
34
+ end
35
+
36
+ def build_block_result(match, diff_info, focus_info, error_info, warning_info)
37
+ {
38
+ lang: match[1],
39
+ title: match[2],
40
+ option: match[3],
41
+ highlights: parse_highlights(match[4]),
42
+ diff_lines: diff_info[:lines],
43
+ focus_lines: focus_info[:lines],
44
+ error_lines: error_info[:lines],
45
+ warning_lines: warning_info[:lines],
46
+ cleaned_content: warning_info[:cleaned_content]
47
+ }
48
+ end
49
+
50
+ def parse_highlights(highlights_str)
51
+ return [] if highlights_str.nil? || highlights_str.strip.empty?
52
+
53
+ highlights_str.split(",").flat_map { |part| parse_highlight_part(part.strip) }.uniq.sort
54
+ end
55
+
56
+ def parse_highlight_part(part)
57
+ return (part.split("-")[0].to_i..part.split("-")[1].to_i).to_a if part.include?("-")
58
+
59
+ [part.to_i]
60
+ end
61
+
62
+ def extract_diff_lines(code_content)
63
+ lines = code_content.lines
64
+ diff_lines = {}
65
+ cleaned_lines = []
66
+
67
+ lines.each_with_index do |line, index|
68
+ line_num = index + 1
69
+
70
+ if (match = line.match(DIFF_MARKER_PATTERN))
71
+ diff_type = match.captures.compact.first
72
+ diff_lines[line_num] = diff_type == "++" ? :addition : :deletion
73
+ cleaned_line = line.gsub(DIFF_MARKER_PATTERN, "")
74
+ cleaned_lines << cleaned_line
75
+ else
76
+ cleaned_lines << line
77
+ end
78
+ end
79
+
80
+ { lines: diff_lines, cleaned_content: cleaned_lines.join }
81
+ end
82
+
83
+ def extract_focus_lines(code_content)
84
+ extract_marker_lines(code_content, FOCUS_MARKER_PATTERN)
85
+ end
86
+
87
+ def extract_error_lines(code_content)
88
+ extract_marker_lines(code_content, ERROR_MARKER_PATTERN)
89
+ end
90
+
91
+ def extract_warning_lines(code_content)
92
+ extract_marker_lines(code_content, WARNING_MARKER_PATTERN)
93
+ end
94
+
95
+ def extract_marker_lines(code_content, pattern)
96
+ lines = code_content.lines
97
+ marker_lines = {}
98
+ cleaned_lines = []
99
+
100
+ lines.each_with_index do |line, index|
101
+ line_num = index + 1
102
+
103
+ if line.match?(pattern)
104
+ marker_lines[line_num] = true
105
+ cleaned_lines << line.gsub(pattern, "")
106
+ else
107
+ cleaned_lines << line
108
+ end
109
+ end
110
+
111
+ { lines: marker_lines, cleaned_content: cleaned_lines.join }
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../../rendering/language_mapping"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Support
8
+ module CodeBlock
9
+ module IconDetector
10
+ MANUAL_ICON_PATTERN = /^:([a-z0-9-]+):\s*(.+)$/i
11
+
12
+ module_function
13
+
14
+ def detect(title, language)
15
+ return { title: nil, icon: nil, icon_source: nil } if title.nil?
16
+
17
+ if (match = title.match(MANUAL_ICON_PATTERN))
18
+ return {
19
+ title: match[2].strip,
20
+ icon: match[1],
21
+ icon_source: "phosphor"
22
+ }
23
+ end
24
+
25
+ icon, icon_source = auto_detect_icon(language)
26
+ { title: title, icon: icon, icon_source: icon_source }
27
+ end
28
+
29
+ def auto_detect_icon(language)
30
+ return [nil, nil] if language.nil?
31
+
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
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ module Support
6
+ module CodeBlock
7
+ class LineParser
8
+ def initialize(code_content)
9
+ @code_content = code_content
10
+ @lines = []
11
+ @current_line = ""
12
+ @open_tags = []
13
+ @in_tag = false
14
+ @tag_buffer = ""
15
+ end
16
+
17
+ def parse
18
+ @code_content.each_char { |char| process_char(char) }
19
+ finalize
20
+ end
21
+
22
+ private
23
+
24
+ def process_char(char)
25
+ case char
26
+ when "<" then start_tag(char)
27
+ when ">" then end_tag_if_applicable(char)
28
+ when "\n" then handle_newline
29
+ else handle_regular_char(char)
30
+ end
31
+ end
32
+
33
+ def start_tag(char)
34
+ @in_tag = true
35
+ @tag_buffer = char
36
+ end
37
+
38
+ def end_tag_if_applicable(char)
39
+ if @in_tag
40
+ @in_tag = false
41
+ @tag_buffer += char
42
+
43
+ if @tag_buffer.start_with?("</")
44
+ @open_tags.pop
45
+ elsif !@tag_buffer.end_with?("/>")
46
+ @open_tags << @tag_buffer
47
+ end
48
+
49
+ @current_line += @tag_buffer
50
+ @tag_buffer = ""
51
+ else
52
+ @current_line += char
53
+ end
54
+ end
55
+
56
+ def handle_newline
57
+ closing_tags = @open_tags.reverse.map { |tag| closing_tag_for(tag) }.join
58
+ @lines << "#{@current_line}#{closing_tags}\n"
59
+ @current_line = @open_tags.join
60
+ end
61
+
62
+ def handle_regular_char(char)
63
+ if @in_tag
64
+ @tag_buffer += char
65
+ else
66
+ @current_line += char
67
+ end
68
+ end
69
+
70
+ def finalize
71
+ @lines << @current_line unless @current_line.empty?
72
+ @lines << "" if @lines.empty?
73
+ @lines
74
+ end
75
+
76
+ def closing_tag_for(open_tag)
77
+ tag_name = open_tag.match(/<(\w+)/)[1]
78
+ "</#{tag_name}>"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "line_parser"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Support
8
+ module CodeBlock
9
+ module LineWrapper
10
+ module_function
11
+
12
+ def wrap_code_block(html, block_data)
13
+ html.gsub(%r{<pre[^>]*><code[^>]*>(.*?)</code></pre>}m) do
14
+ pre_match = Regexp.last_match(0)
15
+ code_content = Regexp.last_match(1)
16
+ lines = LineParser.new(code_content).parse
17
+ wrapped_lines = wrap_lines_with_classes(lines, block_data)
18
+ pre_match.sub(code_content, wrapped_lines.join)
19
+ end
20
+ end
21
+
22
+ def wrap_lines_with_classes(lines, block_data)
23
+ lines.each_with_index.map do |line, index|
24
+ source_line = index + 1
25
+ display_line = block_data[:start_line] + index
26
+ classes = build_line_classes(source_line, display_line, block_data)
27
+ %(<span class="#{classes}">#{line}</span>)
28
+ end
29
+ end
30
+
31
+ DIFF_CLASSES = { addition: "docyard-code-line--diff-add", deletion: "docyard-code-line--diff-remove" }.freeze
32
+
33
+ def build_line_classes(source_line, display_line, block_data)
34
+ (["docyard-code-line"] + feature_classes(source_line, display_line, block_data)).join(" ")
35
+ end
36
+
37
+ def feature_classes(source_line, display_line, block_data)
38
+ [
39
+ ("docyard-code-line--highlighted" if block_data[:highlights].include?(display_line)),
40
+ DIFF_CLASSES[block_data[:diff_lines][source_line]],
41
+ ("docyard-code-line--focus" if block_data[:focus_lines][source_line]),
42
+ ("docyard-code-line--error" if block_data[:error_lines][source_line]),
43
+ ("docyard-code-line--warning" if block_data[:warning_lines][source_line])
44
+ ].compact
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ module Support
6
+ module CodeBlock
7
+ module Patterns
8
+ DIFF_MARKER_PATTERN = %r{
9
+ (?:
10
+ //\s*\[!code\s*([+-]{2})\] |
11
+ \#\s*\[!code\s*([+-]{2})\] |
12
+ /\*\s*\[!code\s*([+-]{2})\]\s*\*/ |
13
+ --\s*\[!code\s*([+-]{2})\] |
14
+ <!--\s*\[!code\s*([+-]{2})\]\s*--> |
15
+ ;\s*\[!code\s*([+-]{2})\]
16
+ )[^\S\n]*
17
+ }x
18
+
19
+ FOCUS_MARKER_PATTERN = %r{
20
+ (?:
21
+ //\s*\[!code\s+focus\] |
22
+ \#\s*\[!code\s+focus\] |
23
+ /\*\s*\[!code\s+focus\]\s*\*/ |
24
+ --\s*\[!code\s+focus\] |
25
+ <!--\s*\[!code\s+focus\]\s*--> |
26
+ ;\s*\[!code\s+focus\]
27
+ )[^\S\n]*
28
+ }x
29
+
30
+ ERROR_MARKER_PATTERN = %r{
31
+ (?:
32
+ //\s*\[!code\s+error\] |
33
+ \#\s*\[!code\s+error\] |
34
+ /\*\s*\[!code\s+error\]\s*\*/ |
35
+ --\s*\[!code\s+error\] |
36
+ <!--\s*\[!code\s+error\]\s*--> |
37
+ ;\s*\[!code\s+error\]
38
+ )[^\S\n]*
39
+ }x
40
+
41
+ WARNING_MARKER_PATTERN = %r{
42
+ (?:
43
+ //\s*\[!code\s+warning\] |
44
+ \#\s*\[!code\s+warning\] |
45
+ /\*\s*\[!code\s+warning\]\s*\*/ |
46
+ --\s*\[!code\s+warning\] |
47
+ <!--\s*\[!code\s+warning\]\s*--> |
48
+ ;\s*\[!code\s+warning\]
49
+ )[^\S\n]*
50
+ }x
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rendering/language_mapping"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Support
8
+ class CodeDetector
9
+ def self.detect(content)
10
+ new(content).detect
11
+ end
12
+
13
+ def initialize(content)
14
+ @content = content
15
+ end
16
+
17
+ def detect
18
+ return nil unless code_only?
19
+
20
+ language = extract_language
21
+ return nil unless language
22
+
23
+ icon_for_language(language)
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :content
29
+
30
+ def code_only?
31
+ stripped = content.strip
32
+ return false unless stripped.start_with?("```") && stripped.end_with?("```")
33
+
34
+ parts = stripped.split("```")
35
+ parts.length == 2 && parts[0].empty?
36
+ end
37
+
38
+ def extract_language
39
+ parts = content.strip.split("```")
40
+ return nil unless parts[1]
41
+
42
+ lines = parts[1].split("\n", 2)
43
+ lang_line = lines[0].strip
44
+ return nil if lang_line.empty? || lang_line.include?(" ")
45
+
46
+ lang_line.downcase
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
+ end
59
+ end
60
+ end
61
+ end