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,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
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Processors
8
+ # Restores placeholders after all code block processing.
9
+ # Runs after CodeBlockProcessor to ensure the HTML is fully rendered.
10
+ class CodeBlockExtendedFencePostprocessor < BaseProcessor
11
+ self.priority = 25
12
+
13
+ BACKTICK_PLACEHOLDER = "\u200B\u200B\u200B"
14
+ CODE_MARKER_PLACEHOLDER = "\u200B!\u200Bcode"
15
+
16
+ def postprocess(html)
17
+ html
18
+ .gsub(BACKTICK_PLACEHOLDER, "`")
19
+ .gsub(CODE_MARKER_PLACEHOLDER, "[!code")
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Processors
8
+ # Handles 4+ backtick code fences (extended fences) for showing raw syntax.
9
+ # Escapes special markers so they display as raw text instead of being processed.
10
+ # Example: ```` (4 backticks) wrapping ```js shows the raw markdown syntax.
11
+ class CodeBlockExtendedFencePreprocessor < BaseProcessor
12
+ self.priority = 1
13
+
14
+ # Match 4+ backticks, capture language, content, and closing backticks
15
+ EXTENDED_FENCE_REGEX = /^(`{4,})(\w*)[^\n]*\n(.*?)^\1/m
16
+
17
+ # Placeholders using zero-width spaces
18
+ BACKTICK_PLACEHOLDER = "\u200B\u200B\u200B"
19
+ CODE_MARKER_PLACEHOLDER = "\u200B!\u200Bcode"
20
+
21
+ def preprocess(content)
22
+ content.gsub(EXTENDED_FENCE_REGEX) { |_| process_extended_fence(Regexp.last_match) }
23
+ end
24
+
25
+ private
26
+
27
+ def process_extended_fence(match)
28
+ lang = match[2]
29
+ code = match[3].chomp
30
+
31
+ # Replace backticks and code markers with placeholders
32
+ # This prevents other preprocessors from matching/processing them
33
+ escaped_code = code
34
+ .gsub("`", BACKTICK_PLACEHOLDER)
35
+ .gsub("[!code", CODE_MARKER_PLACEHOLDER)
36
+
37
+ # Output as regular 3-backtick fence so it goes through normal processing
38
+ lang_spec = lang.empty? ? "text" : lang
39
+ "```#{lang_spec}\n#{escaped_code}\n```"
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../base_processor"
4
+ require_relative "../../rendering/icons"
4
5
 
5
6
  module Docyard
6
7
  module Components
@@ -10,10 +11,13 @@ module Docyard
10
11
 
11
12
  CODE_FENCE_REGEX = /^```(\w+)(?:\s*\[([^\]]+)\])?(:\S+)?(?:\s*\{([^}\n]+)\})?/
12
13
  TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
14
+ CODE_GROUP_BLOCK_REGEX = /^:::[ \t]*code-group[ \t]*\n.*?^:::[ \t]*$/m
15
+ EXCLUDED_LANGUAGES = %w[filetree].freeze
13
16
 
14
17
  def preprocess(content)
15
18
  context[:code_block_options] ||= []
16
19
  @tabs_ranges = find_tabs_ranges(content)
20
+ @code_group_ranges = find_code_group_ranges(content)
17
21
 
18
22
  process_code_fences(content)
19
23
  end
@@ -35,8 +39,27 @@ module Docyard
35
39
  end
36
40
 
37
41
  def process_fence_match(match)
38
- store_code_block_options(match) unless inside_tabs?(match.begin(0))
39
- "```#{match[1]}"
42
+ position = match.begin(0)
43
+ return match[0] if inside_special_block?(position)
44
+
45
+ original_lang = match[1]
46
+ return match[0] if excluded_language?(original_lang)
47
+
48
+ store_code_block_options(match)
49
+ highlight_lang = Icons.highlight_language(original_lang)
50
+ "```#{highlight_lang}"
51
+ end
52
+
53
+ def excluded_language?(lang)
54
+ EXCLUDED_LANGUAGES.include?(lang&.downcase)
55
+ end
56
+
57
+ def inside_special_block?(position)
58
+ inside_tabs?(position) || inside_code_group?(position)
59
+ end
60
+
61
+ def inside_code_group?(position)
62
+ @code_group_ranges.any? { |range| range.cover?(position) }
40
63
  end
41
64
 
42
65
  def store_code_block_options(match)
@@ -53,8 +76,16 @@ module Docyard
53
76
  end
54
77
 
55
78
  def find_tabs_ranges(content)
79
+ find_block_ranges(content, TABS_BLOCK_REGEX)
80
+ end
81
+
82
+ def find_code_group_ranges(content)
83
+ find_block_ranges(content, CODE_GROUP_BLOCK_REGEX)
84
+ end
85
+
86
+ def find_block_ranges(content, regex)
56
87
  ranges = []
57
- content.scan(TABS_BLOCK_REGEX) do
88
+ content.scan(regex) do
58
89
  match = Regexp.last_match
59
90
  ranges << (match.begin(0)...match.end(0))
60
91
  end
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../../rendering/icons"
4
- require_relative "../../rendering/language_mapping"
5
4
  require_relative "../../rendering/renderer"
6
5
  require_relative "../base_processor"
7
6
  require_relative "../support/code_block/icon_detector"
8
7
  require_relative "../support/code_block/line_wrapper"
8
+ require_relative "../support/code_block/line_number_resolver"
9
9
  require_relative "../support/tabs/range_finder"
10
10
 
11
11
  module Docyard
@@ -18,6 +18,7 @@ module Docyard
18
18
  self.priority = 20
19
19
 
20
20
  CodeBlockIconDetector = Support::CodeBlock::IconDetector
21
+ LineNumbers = Support::CodeBlock::LineNumberResolver
21
22
  TabsRangeFinder = Support::Tabs::RangeFinder
22
23
 
23
24
  def postprocess(html)
@@ -36,7 +37,6 @@ module Docyard
36
37
  @focus_lines = context[:code_block_focus_lines] || []
37
38
  @error_lines = context[:code_block_error_lines] || []
38
39
  @warning_lines = context[:code_block_warning_lines] || []
39
- @global_line_numbers = context.dig(:config, "markdown", "lineNumbers") || false
40
40
  @tabs_ranges = TabsRangeFinder.find_ranges(html)
41
41
  end
42
42
 
@@ -67,15 +67,21 @@ 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)
77
- start_line = extract_start_line(opts[:option])
78
- show_line_numbers = determine_line_numbers(opts[:option])
83
+ start_line = LineNumbers.start_line(opts[:option])
84
+ show_line_numbers = LineNumbers.enabled?(opts[:option])
79
85
  title_data = CodeBlockIconDetector.detect(opts[:title], opts[:lang])
80
86
 
81
87
  build_block_data(code_text, opts, show_line_numbers, start_line, title_data)
@@ -100,7 +106,7 @@ module Docyard
100
106
  error_lines: @error_lines[@block_index] || {},
101
107
  warning_lines: @warning_lines[@block_index] || {},
102
108
  show_line_numbers: show_line_numbers,
103
- line_numbers: show_line_numbers ? generate_line_numbers(code_text, start_line) : [],
109
+ line_numbers: show_line_numbers ? LineNumbers.generate_numbers(code_text, start_line) : [],
104
110
  start_line: start_line,
105
111
  title: title_data[:title],
106
112
  icon: title_data[:icon],
@@ -117,25 +123,6 @@ module Docyard
117
123
  wrap_code_block(original_html, block_data)
118
124
  end
119
125
 
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
126
  def inside_tabs?(position)
140
127
  @tabs_ranges.any? { |range| range.cover?(position) }
141
128
  end
@@ -0,0 +1,182 @@
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_block/line_number_resolver"
7
+ require_relative "../support/code_group/html_builder"
8
+ require_relative "../support/markdown_code_block_helper"
9
+ require_relative "../../rendering/icons"
10
+ require_relative "../../rendering/renderer"
11
+ require "securerandom"
12
+ require "kramdown"
13
+ require "kramdown-parser-gfm"
14
+ require "cgi"
15
+
16
+ module Docyard
17
+ module Components
18
+ module Processors
19
+ class CodeGroupProcessor < BaseProcessor
20
+ include Utils::HtmlHelpers
21
+ include Support::MarkdownCodeBlockHelper
22
+
23
+ self.priority = 12
24
+
25
+ CODE_GROUP_PATTERN = /^:::[ \t]*code-group[ \t]*\n(.*?)^:::[ \t]*$/m
26
+ CODE_BLOCK_PATTERN = /```(\w*)\s*\[([^\]]+)\]([^\n]*)\n(.*?)```/m
27
+
28
+ CodeBlockFeatureExtractor = Support::CodeBlock::FeatureExtractor
29
+ CodeBlockLineWrapper = Support::CodeBlock::LineWrapper
30
+ LineNumbers = Support::CodeBlock::LineNumberResolver
31
+ CodeGroupHtmlBuilder = Support::CodeGroup::HtmlBuilder
32
+
33
+ def preprocess(content)
34
+ return content unless content.include?(":::code-group")
35
+
36
+ @code_block_ranges = find_code_block_ranges(content)
37
+
38
+ content.gsub(CODE_GROUP_PATTERN) do
39
+ match = ::Regexp.last_match
40
+ next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
41
+
42
+ process_code_group(match[1])
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def process_code_group(inner_content)
49
+ blocks = extract_code_blocks(inner_content)
50
+ return "" if blocks.empty?
51
+
52
+ group_id = SecureRandom.hex(4)
53
+ html = CodeGroupHtmlBuilder.new(blocks, group_id).build
54
+ wrap_in_nomarkdown(html)
55
+ end
56
+
57
+ def extract_code_blocks(content)
58
+ blocks = []
59
+ content.scan(CODE_BLOCK_PATTERN) do
60
+ blocks << build_block_data(::Regexp.last_match)
61
+ end
62
+ blocks
63
+ end
64
+
65
+ def build_block_data(match)
66
+ lang = match[1] || ""
67
+ label = match[2]
68
+ options = match[3].strip
69
+ code = match[4]
70
+
71
+ code_with_newline = code.end_with?("\n") ? code : "#{code}\n"
72
+ markdown = "```#{lang}#{format_options(options)}\n#{code_with_newline}```"
73
+ extracted = CodeBlockFeatureExtractor.process_markdown(markdown)
74
+ rendered = render_and_enhance(extracted)
75
+
76
+ { label: label, lang: lang, content: rendered, code_text: code.strip }
77
+ end
78
+
79
+ def format_options(options)
80
+ return "" if options.empty?
81
+ return options if options.start_with?(":")
82
+
83
+ " #{options}"
84
+ end
85
+
86
+ def render_and_enhance(extracted)
87
+ html = render_markdown(extracted[:cleaned_markdown])
88
+ enhance_code_blocks(html, extracted[:blocks])
89
+ end
90
+
91
+ def render_markdown(markdown_content)
92
+ return "" if markdown_content.empty?
93
+
94
+ Kramdown::Document.new(
95
+ markdown_content,
96
+ input: "GFM",
97
+ hard_wrap: false,
98
+ syntax_highlighter: "rouge"
99
+ ).to_html
100
+ end
101
+
102
+ def enhance_code_blocks(html, blocks)
103
+ return html unless html.include?('<div class="highlight">')
104
+
105
+ block_index = 0
106
+ html.gsub(%r{<div class="highlight">(.*?)</div>}m) do
107
+ block_data = blocks[block_index] || {}
108
+ block_index += 1
109
+ render_enhanced_code_block(Regexp.last_match, block_data)
110
+ end
111
+ end
112
+
113
+ def render_enhanced_code_block(match, block_data)
114
+ original_html = match[0]
115
+ inner_html = match[1]
116
+ code_text = extract_code_text(inner_html)
117
+
118
+ processed_html = process_html_if_needed(original_html, block_data)
119
+ Renderer.new.render_partial("_code_block", build_locals(processed_html, code_text, block_data))
120
+ end
121
+
122
+ def process_html_if_needed(original_html, block_data)
123
+ return original_html unless needs_line_wrapping?(block_data)
124
+
125
+ CodeBlockLineWrapper.wrap_code_block(original_html, wrapper_data(block_data))
126
+ end
127
+
128
+ def needs_line_wrapping?(block_data)
129
+ %i[highlights diff_lines focus_lines error_lines warning_lines].any? do |key|
130
+ block_data[key]&.any?
131
+ end
132
+ end
133
+
134
+ def wrapper_data(block_data)
135
+ {
136
+ highlights: block_data[:highlights] || [],
137
+ diff_lines: block_data[:diff_lines] || {},
138
+ focus_lines: block_data[:focus_lines] || {},
139
+ error_lines: block_data[:error_lines] || {},
140
+ warning_lines: block_data[:warning_lines] || {},
141
+ start_line: LineNumbers.start_line(block_data[:option])
142
+ }
143
+ end
144
+
145
+ def build_locals(processed_html, code_text, block_data)
146
+ base_locals(processed_html, code_text, block_data).merge(feature_locals(block_data)).merge(title_locals)
147
+ end
148
+
149
+ def base_locals(processed_html, code_text, block_data)
150
+ show_ln = LineNumbers.enabled?(block_data[:option])
151
+ start = LineNumbers.start_line(block_data[:option])
152
+
153
+ {
154
+ code_block_html: processed_html, code_text: escape_html_attribute(code_text),
155
+ copy_icon: Icons.render("copy", "regular") || "", show_line_numbers: show_ln,
156
+ line_numbers: show_ln ? LineNumbers.generate_numbers(code_text, start) : [], start_line: start
157
+ }
158
+ end
159
+
160
+ def feature_locals(block_data)
161
+ {
162
+ highlights: block_data[:highlights] || [], diff_lines: block_data[:diff_lines] || {},
163
+ focus_lines: block_data[:focus_lines] || {}, error_lines: block_data[:error_lines] || {},
164
+ warning_lines: block_data[:warning_lines] || {}
165
+ }
166
+ end
167
+
168
+ def title_locals
169
+ { title: nil, icon: nil, icon_source: nil }
170
+ end
171
+
172
+ def extract_code_text(html)
173
+ CGI.unescapeHTML(html.gsub(/<[^>]+>/, "")).strip
174
+ end
175
+
176
+ def wrap_in_nomarkdown(html)
177
+ "{::nomarkdown}\n#{html}\n{:/nomarkdown}"
178
+ end
179
+ end
180
+ end
181
+ end
182
+ 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
@@ -119,6 +124,7 @@ module Docyard
119
124
  end
120
125
 
121
126
  def import_error(filepath, message)
127
+ Docyard.logger.warn("Code snippet import failed: #{filepath} - #{message}")
122
128
  "```\nError importing #{filepath}: #{message}\n```"
123
129
  end
124
130
  end
@@ -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