docyard 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/CHANGELOG.md +43 -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/components/aliases.rb +12 -0
  10. data/lib/docyard/components/processors/abbreviation_processor.rb +72 -0
  11. data/lib/docyard/components/processors/accordion_processor.rb +81 -0
  12. data/lib/docyard/components/processors/badge_processor.rb +72 -0
  13. data/lib/docyard/components/processors/callout_processor.rb +8 -2
  14. data/lib/docyard/components/processors/cards_processor.rb +100 -0
  15. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +23 -2
  16. data/lib/docyard/components/processors/code_block_processor.rb +6 -0
  17. data/lib/docyard/components/processors/code_group_processor.rb +198 -0
  18. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +6 -1
  19. data/lib/docyard/components/processors/custom_anchor_processor.rb +42 -0
  20. data/lib/docyard/components/processors/file_tree_processor.rb +151 -0
  21. data/lib/docyard/components/processors/image_caption_processor.rb +96 -0
  22. data/lib/docyard/components/processors/include_processor.rb +86 -0
  23. data/lib/docyard/components/processors/steps_processor.rb +89 -0
  24. data/lib/docyard/components/processors/tabs_processor.rb +9 -1
  25. data/lib/docyard/components/processors/tooltip_processor.rb +57 -0
  26. data/lib/docyard/components/processors/video_embed_processor.rb +196 -0
  27. data/lib/docyard/components/support/code_group/html_builder.rb +122 -0
  28. data/lib/docyard/components/support/markdown_code_block_helper.rb +56 -0
  29. data/lib/docyard/config/branding_resolver.rb +121 -17
  30. data/lib/docyard/config/constants.rb +6 -4
  31. data/lib/docyard/config/logo_detector.rb +39 -0
  32. data/lib/docyard/config/validator.rb +122 -99
  33. data/lib/docyard/config.rb +40 -42
  34. data/lib/docyard/initializer.rb +15 -76
  35. data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
  36. data/lib/docyard/navigation/prev_next_builder.rb +4 -1
  37. data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
  38. data/lib/docyard/navigation/sidebar/config_parser.rb +136 -108
  39. data/lib/docyard/navigation/sidebar/file_resolver.rb +90 -0
  40. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +2 -1
  41. data/lib/docyard/navigation/sidebar/item.rb +50 -7
  42. data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
  43. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +71 -0
  44. data/lib/docyard/navigation/sidebar/metadata_reader.rb +51 -0
  45. data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
  46. data/lib/docyard/navigation/sidebar/renderer.rb +60 -38
  47. data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
  48. data/lib/docyard/navigation/sidebar/tree_builder.rb +100 -26
  49. data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
  50. data/lib/docyard/navigation/sidebar_builder.rb +105 -36
  51. data/lib/docyard/rendering/icon_helpers.rb +13 -0
  52. data/lib/docyard/rendering/icons/phosphor.rb +26 -1
  53. data/lib/docyard/rendering/markdown.rb +29 -1
  54. data/lib/docyard/rendering/renderer.rb +75 -34
  55. data/lib/docyard/rendering/template_resolver.rb +172 -0
  56. data/lib/docyard/routing/fallback_resolver.rb +92 -0
  57. data/lib/docyard/search/build_indexer.rb +1 -1
  58. data/lib/docyard/search/dev_indexer.rb +51 -6
  59. data/lib/docyard/search/pagefind_support.rb +2 -0
  60. data/lib/docyard/server/asset_handler.rb +25 -19
  61. data/lib/docyard/server/pagefind_handler.rb +63 -0
  62. data/lib/docyard/server/preview_server.rb +1 -1
  63. data/lib/docyard/server/rack_application.rb +81 -64
  64. data/lib/docyard/templates/assets/css/code.css +18 -51
  65. data/lib/docyard/templates/assets/css/components/abbreviation.css +86 -0
  66. data/lib/docyard/templates/assets/css/components/accordion.css +138 -0
  67. data/lib/docyard/templates/assets/css/components/badges.css +47 -0
  68. data/lib/docyard/templates/assets/css/components/banner.css +202 -0
  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/cards.css +100 -0
  72. data/lib/docyard/templates/assets/css/components/code-block.css +190 -282
  73. data/lib/docyard/templates/assets/css/components/code-group.css +281 -0
  74. data/lib/docyard/templates/assets/css/components/figure.css +22 -0
  75. data/lib/docyard/templates/assets/css/components/file-tree.css +124 -0
  76. data/lib/docyard/templates/assets/css/components/heading-anchor.css +36 -15
  77. data/lib/docyard/templates/assets/css/components/icon.css +0 -1
  78. data/lib/docyard/templates/assets/css/components/lightbox.css +65 -0
  79. data/lib/docyard/templates/assets/css/components/logo.css +0 -2
  80. data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
  81. data/lib/docyard/templates/assets/css/components/navigation.css +193 -167
  82. data/lib/docyard/templates/assets/css/components/prev-next.css +68 -48
  83. data/lib/docyard/templates/assets/css/components/search.css +186 -174
  84. data/lib/docyard/templates/assets/css/components/steps.css +122 -0
  85. data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
  86. data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
  87. data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
  88. data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
  89. data/lib/docyard/templates/assets/css/components/tooltip.css +113 -0
  90. data/lib/docyard/templates/assets/css/components/video.css +41 -0
  91. data/lib/docyard/templates/assets/css/landing.css +815 -0
  92. data/lib/docyard/templates/assets/css/layout.css +489 -87
  93. data/lib/docyard/templates/assets/css/main.css +1 -3
  94. data/lib/docyard/templates/assets/css/markdown.css +113 -93
  95. data/lib/docyard/templates/assets/css/reset.css +0 -3
  96. data/lib/docyard/templates/assets/css/typography.css +43 -41
  97. data/lib/docyard/templates/assets/css/variables.css +268 -208
  98. data/lib/docyard/templates/assets/favicon.svg +7 -8
  99. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
  100. data/lib/docyard/templates/assets/js/components/abbreviation.js +85 -0
  101. data/lib/docyard/templates/assets/js/components/banner.js +81 -0
  102. data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
  103. data/lib/docyard/templates/assets/js/components/code-group.js +283 -0
  104. data/lib/docyard/templates/assets/js/components/file-tree.js +39 -0
  105. data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
  106. data/lib/docyard/templates/assets/js/components/lightbox.js +72 -0
  107. data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
  108. data/lib/docyard/templates/assets/js/components/search.js +0 -75
  109. data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
  110. data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
  111. data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
  112. data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
  113. data/lib/docyard/templates/assets/js/components/tooltip.js +118 -0
  114. data/lib/docyard/templates/assets/js/theme.js +0 -3
  115. data/lib/docyard/templates/assets/logo-dark.svg +8 -2
  116. data/lib/docyard/templates/assets/logo.svg +7 -4
  117. data/lib/docyard/templates/config/docyard.yml.erb +37 -34
  118. data/lib/docyard/templates/errors/404.html.erb +1 -1
  119. data/lib/docyard/templates/errors/500.html.erb +1 -1
  120. data/lib/docyard/templates/layouts/default.html.erb +19 -67
  121. data/lib/docyard/templates/layouts/splash.html.erb +177 -0
  122. data/lib/docyard/templates/partials/_accordion.html.erb +9 -0
  123. data/lib/docyard/templates/partials/_banner.html.erb +27 -0
  124. data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
  125. data/lib/docyard/templates/partials/_card.html.erb +23 -0
  126. data/lib/docyard/templates/partials/_code_block.html.erb +5 -3
  127. data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
  128. data/lib/docyard/templates/partials/_features.html.erb +15 -0
  129. data/lib/docyard/templates/partials/_footer.html.erb +42 -0
  130. data/lib/docyard/templates/partials/_head.html.erb +22 -0
  131. data/lib/docyard/templates/partials/_header.html.erb +49 -0
  132. data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
  133. data/lib/docyard/templates/partials/_hero.html.erb +27 -0
  134. data/lib/docyard/templates/partials/_nav_group.html.erb +31 -11
  135. data/lib/docyard/templates/partials/_nav_leaf.html.erb +4 -1
  136. data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
  137. data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
  138. data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
  139. data/lib/docyard/templates/partials/_prev_next.html.erb +8 -2
  140. data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
  141. data/lib/docyard/templates/partials/_search_modal.html.erb +2 -6
  142. data/lib/docyard/templates/partials/_search_trigger.html.erb +2 -6
  143. data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
  144. data/lib/docyard/templates/partials/_step.html.erb +14 -0
  145. data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
  146. data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
  147. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
  148. data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
  149. data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
  150. data/lib/docyard/version.rb +1 -1
  151. metadata +70 -5
  152. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
  153. data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
  154. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
  155. data/lib/docyard/templates/markdown/index.md.erb +0 -82
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rendering/icons"
4
+ require_relative "../../rendering/renderer"
5
+ require_relative "../base_processor"
6
+ require_relative "../support/markdown_code_block_helper"
7
+ require "kramdown"
8
+ require "kramdown-parser-gfm"
9
+
10
+ module Docyard
11
+ module Components
12
+ module Processors
13
+ class AccordionProcessor < BaseProcessor
14
+ include Support::MarkdownCodeBlockHelper
15
+
16
+ self.priority = 10
17
+
18
+ DETAILS_PATTERN = /^:::details(?:\{([^}]*)\})?\s*\n(.*?)^:::\s*$/m
19
+
20
+ def preprocess(markdown)
21
+ @code_block_ranges = find_code_block_ranges(markdown)
22
+
23
+ markdown.gsub(DETAILS_PATTERN) do
24
+ match = Regexp.last_match
25
+ next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
26
+
27
+ attributes = parse_attributes(match[1])
28
+ content_markdown = match[2]
29
+
30
+ title = attributes["title"] || "Details"
31
+ open = attributes.key?("open")
32
+ content_html = render_markdown_content(content_markdown.strip)
33
+
34
+ wrap_in_nomarkdown(render_accordion_html(title, content_html, open))
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def parse_attributes(attr_string)
41
+ return {} if attr_string.nil? || attr_string.empty?
42
+
43
+ attrs = {}
44
+ attr_string.scan(/(\w+)(?:="([^"]*)")?/) do |key, value|
45
+ attrs[key] = value || true
46
+ end
47
+ attrs
48
+ end
49
+
50
+ def render_markdown_content(content_markdown)
51
+ return "" if content_markdown.empty?
52
+
53
+ Kramdown::Document.new(
54
+ content_markdown,
55
+ input: "GFM",
56
+ hard_wrap: false,
57
+ syntax_highlighter: "rouge"
58
+ ).to_html
59
+ end
60
+
61
+ def wrap_in_nomarkdown(html)
62
+ "{::nomarkdown}\n#{html}\n{:/nomarkdown}"
63
+ end
64
+
65
+ def render_accordion_html(title, content_html, open)
66
+ icon_svg = Icons.render("caret-right") || ""
67
+ renderer = Renderer.new
68
+
69
+ renderer.render_partial(
70
+ "_accordion", {
71
+ title: title,
72
+ content_html: content_html,
73
+ icon_svg: icon_svg,
74
+ open: open
75
+ }
76
+ )
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Processors
8
+ class BadgeProcessor < BaseProcessor
9
+ self.priority = 15
10
+
11
+ BADGE_PATTERN = /:badge\[([^\]]*)\](?:\{([^}]*)\})?/
12
+
13
+ VALID_TYPES = %w[default success warning danger].freeze
14
+
15
+ def postprocess(html)
16
+ segments = split_preserving_code_blocks(html)
17
+
18
+ segments.map do |segment|
19
+ segment[:type] == :code ? segment[:content] : process_segment(segment[:content])
20
+ end.join
21
+ end
22
+
23
+ private
24
+
25
+ def split_preserving_code_blocks(html)
26
+ segments = []
27
+ current_pos = 0
28
+
29
+ html.scan(%r{<(code|pre)[^>]*>.*?</\1>}m) do
30
+ match_start = Regexp.last_match.begin(0)
31
+ match_end = Regexp.last_match.end(0)
32
+
33
+ segments << { type: :text, content: html[current_pos...match_start] } if match_start > current_pos
34
+ segments << { type: :code, content: html[match_start...match_end] }
35
+
36
+ current_pos = match_end
37
+ end
38
+
39
+ segments << { type: :text, content: html[current_pos..] } if current_pos < html.length
40
+
41
+ segments.empty? ? [{ type: :text, content: html }] : segments
42
+ end
43
+
44
+ def process_segment(content)
45
+ content.gsub(BADGE_PATTERN) do
46
+ text = Regexp.last_match(1)
47
+ attrs = Regexp.last_match(2)
48
+
49
+ render_badge(text, parse_attributes(attrs))
50
+ end
51
+ end
52
+
53
+ def parse_attributes(attrs_string)
54
+ return {} if attrs_string.nil? || attrs_string.empty?
55
+
56
+ attrs = {}
57
+ attrs_string.scan(/(\w+)=["'\u201C\u201D]([^"'\u201C\u201D]*)["'\u201C\u201D]/) do |key, value|
58
+ attrs[key] = value
59
+ end
60
+ attrs
61
+ end
62
+
63
+ def render_badge(text, attrs)
64
+ type = attrs["type"] || "default"
65
+ type = "default" unless VALID_TYPES.include?(type)
66
+
67
+ %(<span class="docyard-badge docyard-badge--#{type}">#{text}</span>)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -3,6 +3,7 @@
3
3
  require_relative "../../rendering/icons"
4
4
  require_relative "../../rendering/renderer"
5
5
  require_relative "../base_processor"
6
+ require_relative "../support/markdown_code_block_helper"
6
7
  require "kramdown"
7
8
  require "kramdown-parser-gfm"
8
9
 
@@ -10,6 +11,8 @@ module Docyard
10
11
  module Components
11
12
  module Processors
12
13
  class CalloutProcessor < BaseProcessor
14
+ include Support::MarkdownCodeBlockHelper
15
+
13
16
  self.priority = 10
14
17
 
15
18
  CALLOUT_TYPES = {
@@ -29,6 +32,7 @@ module Docyard
29
32
  }.freeze
30
33
 
31
34
  def preprocess(markdown)
35
+ @code_block_ranges = find_code_block_ranges(markdown)
32
36
  process_container_syntax(markdown)
33
37
  end
34
38
 
@@ -40,8 +44,10 @@ module Docyard
40
44
 
41
45
  def process_container_syntax(markdown)
42
46
  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))
47
+ match = Regexp.last_match
48
+ next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
49
+
50
+ process_callout_match(match[0], match[1], match[2], match[3])
45
51
  end
46
52
  end
47
53
 
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rendering/icons"
4
+ require_relative "../../rendering/renderer"
5
+ require_relative "../base_processor"
6
+ require_relative "../support/markdown_code_block_helper"
7
+ require "kramdown"
8
+ require "kramdown-parser-gfm"
9
+
10
+ module Docyard
11
+ module Components
12
+ module Processors
13
+ class CardsProcessor < BaseProcessor
14
+ include Support::MarkdownCodeBlockHelper
15
+
16
+ self.priority = 10
17
+
18
+ CARDS_PATTERN = /^:::cards\s*\n(.*?)^:::\s*$/m
19
+ CARD_PATTERN = /^::card\{([^}]*)\}\s*\n(.*?)^::\s*$/m
20
+
21
+ def preprocess(markdown)
22
+ @code_block_ranges = find_code_block_ranges(markdown)
23
+
24
+ markdown.gsub(CARDS_PATTERN) do
25
+ match = Regexp.last_match
26
+ next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
27
+
28
+ content = match[1]
29
+ cards = parse_cards(content)
30
+
31
+ wrap_in_nomarkdown(render_cards_html(cards))
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def parse_cards(content)
38
+ cards = []
39
+
40
+ content.scan(CARD_PATTERN) do |attrs_string, card_content|
41
+ attrs = parse_attributes(attrs_string)
42
+ cards << {
43
+ title: attrs["title"] || "Card",
44
+ icon: attrs["icon"],
45
+ href: attrs["href"],
46
+ content: card_content.strip
47
+ }
48
+ end
49
+
50
+ cards
51
+ end
52
+
53
+ def parse_attributes(attr_string)
54
+ return {} if attr_string.nil? || attr_string.empty?
55
+
56
+ attrs = {}
57
+ attr_string.scan(/(\w+)="([^"]*)"/) do |key, value|
58
+ attrs[key] = value
59
+ end
60
+ attrs
61
+ end
62
+
63
+ def render_cards_html(cards)
64
+ renderer = Renderer.new
65
+
66
+ cards_html = cards.map do |card|
67
+ icon_svg = card[:icon] ? Icons.render(card[:icon]) : nil
68
+ content_html = render_markdown_content(card[:content])
69
+
70
+ renderer.render_partial(
71
+ "_card", {
72
+ title: card[:title],
73
+ icon_svg: icon_svg,
74
+ href: card[:href],
75
+ content_html: content_html
76
+ }
77
+ )
78
+ end.join("\n")
79
+
80
+ "<div class=\"docyard-cards\">\n#{cards_html}\n</div>"
81
+ end
82
+
83
+ def render_markdown_content(content_markdown)
84
+ return "" if content_markdown.empty?
85
+
86
+ Kramdown::Document.new(
87
+ content_markdown,
88
+ input: "GFM",
89
+ hard_wrap: false,
90
+ syntax_highlighter: "rouge"
91
+ ).to_html
92
+ end
93
+
94
+ def wrap_in_nomarkdown(html)
95
+ "{::nomarkdown}\n#{html}\n{:/nomarkdown}"
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -10,10 +10,12 @@ module Docyard
10
10
 
11
11
  CODE_FENCE_REGEX = /^```(\w+)(?:\s*\[([^\]]+)\])?(:\S+)?(?:\s*\{([^}\n]+)\})?/
12
12
  TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
13
+ CODE_GROUP_BLOCK_REGEX = /^:::[ \t]*code-group[ \t]*\n.*?^:::[ \t]*$/m
13
14
 
14
15
  def preprocess(content)
15
16
  context[:code_block_options] ||= []
16
17
  @tabs_ranges = find_tabs_ranges(content)
18
+ @code_group_ranges = find_code_group_ranges(content)
17
19
 
18
20
  process_code_fences(content)
19
21
  end
@@ -35,10 +37,21 @@ module Docyard
35
37
  end
36
38
 
37
39
  def process_fence_match(match)
38
- store_code_block_options(match) unless inside_tabs?(match.begin(0))
40
+ position = match.begin(0)
41
+ return match[0] if inside_special_block?(position)
42
+
43
+ store_code_block_options(match)
39
44
  "```#{match[1]}"
40
45
  end
41
46
 
47
+ def inside_special_block?(position)
48
+ inside_tabs?(position) || inside_code_group?(position)
49
+ end
50
+
51
+ def inside_code_group?(position)
52
+ @code_group_ranges.any? { |range| range.cover?(position) }
53
+ end
54
+
42
55
  def store_code_block_options(match)
43
56
  context[:code_block_options] << {
44
57
  lang: match[1],
@@ -53,8 +66,16 @@ module Docyard
53
66
  end
54
67
 
55
68
  def find_tabs_ranges(content)
69
+ find_block_ranges(content, TABS_BLOCK_REGEX)
70
+ end
71
+
72
+ def find_code_group_ranges(content)
73
+ find_block_ranges(content, CODE_GROUP_BLOCK_REGEX)
74
+ end
75
+
76
+ def find_block_ranges(content, regex)
56
77
  ranges = []
57
- content.scan(TABS_BLOCK_REGEX) do
78
+ content.scan(regex) do
58
79
  match = Regexp.last_match
59
80
  ranges << (match.begin(0)...match.end(0))
60
81
  end
@@ -67,10 +67,16 @@ module Docyard
67
67
  def process_code_block(original_html, inner_html)
68
68
  block_data = extract_block_data(inner_html)
69
69
  processed_html = process_html_for_highlighting(original_html, block_data)
70
+ processed_html = inject_scroll_spacer(processed_html) unless block_data[:title]
70
71
 
71
72
  render_code_block_with_copy(block_data.merge(html: processed_html))
72
73
  end
73
74
 
75
+ def inject_scroll_spacer(html)
76
+ spacer = '<span class="docyard-code-block__scroll-spacer" aria-hidden="true"></span>'
77
+ html.sub("\n", "#{spacer}\n")
78
+ end
79
+
74
80
  def extract_block_data(inner_html)
75
81
  opts = current_block_options
76
82
  code_text = extract_code_text(inner_html)
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+ require_relative "../support/code_block/feature_extractor"
5
+ require_relative "../support/code_block/line_wrapper"
6
+ require_relative "../support/code_group/html_builder"
7
+ require_relative "../support/markdown_code_block_helper"
8
+ require_relative "../../rendering/icons"
9
+ require_relative "../../rendering/renderer"
10
+ require "securerandom"
11
+ require "kramdown"
12
+ require "kramdown-parser-gfm"
13
+ require "cgi"
14
+
15
+ module Docyard
16
+ module Components
17
+ module Processors
18
+ class CodeGroupProcessor < BaseProcessor
19
+ include Utils::HtmlHelpers
20
+ include Support::MarkdownCodeBlockHelper
21
+
22
+ self.priority = 12
23
+
24
+ CODE_GROUP_PATTERN = /^:::[ \t]*code-group[ \t]*\n(.*?)^:::[ \t]*$/m
25
+ CODE_BLOCK_PATTERN = /```(\w*)\s*\[([^\]]+)\]([^\n]*)\n(.*?)```/m
26
+
27
+ CodeBlockFeatureExtractor = Support::CodeBlock::FeatureExtractor
28
+ CodeBlockLineWrapper = Support::CodeBlock::LineWrapper
29
+ CodeGroupHtmlBuilder = Support::CodeGroup::HtmlBuilder
30
+
31
+ def preprocess(content)
32
+ return content unless content.include?(":::code-group")
33
+
34
+ @code_block_ranges = find_code_block_ranges(content)
35
+
36
+ content.gsub(CODE_GROUP_PATTERN) do
37
+ match = ::Regexp.last_match
38
+ next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
39
+
40
+ process_code_group(match[1])
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def process_code_group(inner_content)
47
+ blocks = extract_code_blocks(inner_content)
48
+ return "" if blocks.empty?
49
+
50
+ group_id = SecureRandom.hex(4)
51
+ html = CodeGroupHtmlBuilder.new(blocks, group_id).build
52
+ wrap_in_nomarkdown(html)
53
+ end
54
+
55
+ def extract_code_blocks(content)
56
+ blocks = []
57
+ content.scan(CODE_BLOCK_PATTERN) do
58
+ blocks << build_block_data(::Regexp.last_match)
59
+ end
60
+ blocks
61
+ end
62
+
63
+ def build_block_data(match)
64
+ lang = match[1] || ""
65
+ label = match[2]
66
+ options = match[3].strip
67
+ code = match[4]
68
+
69
+ code_with_newline = code.end_with?("\n") ? code : "#{code}\n"
70
+ markdown = "```#{lang}#{format_options(options)}\n#{code_with_newline}```"
71
+ extracted = CodeBlockFeatureExtractor.process_markdown(markdown)
72
+ rendered = render_and_enhance(extracted)
73
+
74
+ { label: label, lang: lang, content: rendered, code_text: code.strip }
75
+ end
76
+
77
+ def format_options(options)
78
+ return "" if options.empty?
79
+ return options if options.start_with?(":")
80
+
81
+ " #{options}"
82
+ end
83
+
84
+ def render_and_enhance(extracted)
85
+ html = render_markdown(extracted[:cleaned_markdown])
86
+ enhance_code_blocks(html, extracted[:blocks])
87
+ end
88
+
89
+ def render_markdown(markdown_content)
90
+ return "" if markdown_content.empty?
91
+
92
+ Kramdown::Document.new(
93
+ markdown_content,
94
+ input: "GFM",
95
+ hard_wrap: false,
96
+ syntax_highlighter: "rouge"
97
+ ).to_html
98
+ end
99
+
100
+ def enhance_code_blocks(html, blocks)
101
+ return html unless html.include?('<div class="highlight">')
102
+
103
+ block_index = 0
104
+ html.gsub(%r{<div class="highlight">(.*?)</div>}m) do
105
+ block_data = blocks[block_index] || {}
106
+ block_index += 1
107
+ render_enhanced_code_block(Regexp.last_match, block_data)
108
+ end
109
+ end
110
+
111
+ def render_enhanced_code_block(match, block_data)
112
+ original_html = match[0]
113
+ inner_html = match[1]
114
+ code_text = extract_code_text(inner_html)
115
+
116
+ processed_html = process_html_if_needed(original_html, block_data)
117
+ Renderer.new.render_partial("_code_block", build_locals(processed_html, code_text, block_data))
118
+ end
119
+
120
+ def process_html_if_needed(original_html, block_data)
121
+ return original_html unless needs_line_wrapping?(block_data)
122
+
123
+ CodeBlockLineWrapper.wrap_code_block(original_html, wrapper_data(block_data))
124
+ end
125
+
126
+ def needs_line_wrapping?(block_data)
127
+ %i[highlights diff_lines focus_lines error_lines warning_lines].any? do |key|
128
+ block_data[key]&.any?
129
+ end
130
+ end
131
+
132
+ def wrapper_data(block_data)
133
+ {
134
+ highlights: block_data[:highlights] || [],
135
+ diff_lines: block_data[:diff_lines] || {},
136
+ focus_lines: block_data[:focus_lines] || {},
137
+ error_lines: block_data[:error_lines] || {},
138
+ warning_lines: block_data[:warning_lines] || {},
139
+ start_line: extract_start_line(block_data[:option])
140
+ }
141
+ end
142
+
143
+ def build_locals(processed_html, code_text, block_data)
144
+ base_locals(processed_html, code_text, block_data).merge(feature_locals(block_data)).merge(title_locals)
145
+ end
146
+
147
+ def base_locals(processed_html, code_text, block_data)
148
+ show_ln = line_numbers_enabled?(block_data[:option])
149
+ start = extract_start_line(block_data[:option])
150
+
151
+ {
152
+ code_block_html: processed_html, code_text: escape_html_attribute(code_text),
153
+ copy_icon: Icons.render("copy", "regular") || "", show_line_numbers: show_ln,
154
+ line_numbers: show_ln ? generate_line_numbers(code_text, start) : [], start_line: start
155
+ }
156
+ end
157
+
158
+ def feature_locals(block_data)
159
+ {
160
+ highlights: block_data[:highlights] || [], diff_lines: block_data[:diff_lines] || {},
161
+ focus_lines: block_data[:focus_lines] || {}, error_lines: block_data[:error_lines] || {},
162
+ warning_lines: block_data[:warning_lines] || {}
163
+ }
164
+ end
165
+
166
+ def title_locals
167
+ { title: nil, icon: nil, icon_source: nil }
168
+ end
169
+
170
+ def line_numbers_enabled?(option)
171
+ return false if option == ":no-line-numbers"
172
+ return true if option&.start_with?(":line-numbers")
173
+
174
+ false
175
+ end
176
+
177
+ def extract_start_line(option)
178
+ return 1 unless option&.include?("=")
179
+
180
+ option.split("=").last.to_i
181
+ end
182
+
183
+ def generate_line_numbers(code_text, start_line)
184
+ count = [code_text.lines.count, 1].max
185
+ (start_line...(start_line + count)).to_a
186
+ end
187
+
188
+ def extract_code_text(html)
189
+ CGI.unescapeHTML(html.gsub(/<[^>]+>/, "")).strip
190
+ end
191
+
192
+ def wrap_in_nomarkdown(html)
193
+ "{::nomarkdown}\n#{html}\n{:/nomarkdown}"
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../base_processor"
4
+ require_relative "../support/markdown_code_block_helper"
4
5
 
5
6
  module Docyard
6
7
  module Components
7
8
  module Processors
8
9
  class CodeSnippetImportPreprocessor < BaseProcessor
10
+ include Support::MarkdownCodeBlockHelper
11
+
9
12
  EXTENSION_MAP = {
10
13
  "rb" => "ruby",
11
14
  "js" => "javascript",
@@ -25,7 +28,9 @@ module Docyard
25
28
 
26
29
  def preprocess(content)
27
30
  @docs_root = context[:docs_root] || "docs"
28
- content.gsub(IMPORT_PATTERN) { |_| process_import(Regexp.last_match) }
31
+ process_outside_code_blocks(content) do |segment|
32
+ segment.gsub(IMPORT_PATTERN) { |_| process_import(Regexp.last_match) }
33
+ end
29
34
  end
30
35
 
31
36
  private
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Processors
8
+ class CustomAnchorProcessor < BaseProcessor
9
+ CUSTOM_ID_PATTERN = /\s*\{#([\w-]+)\}\s*$/
10
+
11
+ self.priority = 25
12
+
13
+ def postprocess(html)
14
+ process_custom_anchors(html)
15
+ end
16
+
17
+ private
18
+
19
+ def process_custom_anchors(html)
20
+ html.gsub(%r{<(h[1-6])(\s+id="[^"]*")?>(.+?)</\1>}m) do
21
+ tag = Regexp.last_match(1)
22
+ existing_attr = Regexp.last_match(2) || ""
23
+ content = Regexp.last_match(3)
24
+
25
+ if content.match?(CUSTOM_ID_PATTERN)
26
+ process_heading_with_custom_id(tag, content)
27
+ else
28
+ "<#{tag}#{existing_attr}>#{content}</#{tag}>"
29
+ end
30
+ end
31
+ end
32
+
33
+ def process_heading_with_custom_id(tag, content)
34
+ custom_id = content.match(CUSTOM_ID_PATTERN)[1]
35
+ clean_content = content.sub(CUSTOM_ID_PATTERN, "")
36
+
37
+ "<#{tag} id=\"#{custom_id}\">#{clean_content}</#{tag}>"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end