docyard 0.8.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -1
  3. data/lib/docyard/components/aliases.rb +12 -0
  4. data/lib/docyard/components/processors/abbreviation_processor.rb +72 -0
  5. data/lib/docyard/components/processors/accordion_processor.rb +81 -0
  6. data/lib/docyard/components/processors/badge_processor.rb +72 -0
  7. data/lib/docyard/components/processors/callout_processor.rb +8 -2
  8. data/lib/docyard/components/processors/cards_processor.rb +100 -0
  9. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +23 -2
  10. data/lib/docyard/components/processors/code_block_processor.rb +6 -0
  11. data/lib/docyard/components/processors/code_group_processor.rb +198 -0
  12. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +6 -1
  13. data/lib/docyard/components/processors/custom_anchor_processor.rb +42 -0
  14. data/lib/docyard/components/processors/file_tree_processor.rb +151 -0
  15. data/lib/docyard/components/processors/image_caption_processor.rb +96 -0
  16. data/lib/docyard/components/processors/include_processor.rb +86 -0
  17. data/lib/docyard/components/processors/steps_processor.rb +89 -0
  18. data/lib/docyard/components/processors/tabs_processor.rb +9 -1
  19. data/lib/docyard/components/processors/tooltip_processor.rb +57 -0
  20. data/lib/docyard/components/processors/video_embed_processor.rb +196 -0
  21. data/lib/docyard/components/support/code_group/html_builder.rb +122 -0
  22. data/lib/docyard/components/support/markdown_code_block_helper.rb +56 -0
  23. data/lib/docyard/config/branding_resolver.rb +30 -35
  24. data/lib/docyard/config/logo_detector.rb +39 -0
  25. data/lib/docyard/config.rb +6 -1
  26. data/lib/docyard/navigation/sidebar/file_resolver.rb +16 -4
  27. data/lib/docyard/navigation/sidebar/item.rb +6 -1
  28. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +4 -2
  29. data/lib/docyard/navigation/sidebar/metadata_reader.rb +8 -4
  30. data/lib/docyard/navigation/sidebar/renderer.rb +6 -2
  31. data/lib/docyard/navigation/sidebar/tree_builder.rb +2 -1
  32. data/lib/docyard/rendering/icons/phosphor.rb +3 -0
  33. data/lib/docyard/rendering/markdown.rb +24 -1
  34. data/lib/docyard/rendering/renderer.rb +2 -1
  35. data/lib/docyard/server/asset_handler.rb +1 -0
  36. data/lib/docyard/templates/assets/css/components/abbreviation.css +86 -0
  37. data/lib/docyard/templates/assets/css/components/accordion.css +138 -0
  38. data/lib/docyard/templates/assets/css/components/badges.css +47 -0
  39. data/lib/docyard/templates/assets/css/components/banner.css +202 -0
  40. data/lib/docyard/templates/assets/css/components/cards.css +100 -0
  41. data/lib/docyard/templates/assets/css/components/code-block.css +10 -0
  42. data/lib/docyard/templates/assets/css/components/code-group.css +281 -0
  43. data/lib/docyard/templates/assets/css/components/figure.css +22 -0
  44. data/lib/docyard/templates/assets/css/components/file-tree.css +124 -0
  45. data/lib/docyard/templates/assets/css/components/heading-anchor.css +21 -13
  46. data/lib/docyard/templates/assets/css/components/lightbox.css +65 -0
  47. data/lib/docyard/templates/assets/css/components/navigation.css +7 -0
  48. data/lib/docyard/templates/assets/css/components/prev-next.css +9 -18
  49. data/lib/docyard/templates/assets/css/components/steps.css +122 -0
  50. data/lib/docyard/templates/assets/css/components/tabs.css +1 -1
  51. data/lib/docyard/templates/assets/css/components/tooltip.css +113 -0
  52. data/lib/docyard/templates/assets/css/components/video.css +41 -0
  53. data/lib/docyard/templates/assets/css/markdown.css +5 -3
  54. data/lib/docyard/templates/assets/js/components/abbreviation.js +85 -0
  55. data/lib/docyard/templates/assets/js/components/banner.js +81 -0
  56. data/lib/docyard/templates/assets/js/components/code-group.js +283 -0
  57. data/lib/docyard/templates/assets/js/components/file-tree.js +39 -0
  58. data/lib/docyard/templates/assets/js/components/lightbox.js +72 -0
  59. data/lib/docyard/templates/assets/js/components/tooltip.js +118 -0
  60. data/lib/docyard/templates/layouts/default.html.erb +1 -0
  61. data/lib/docyard/templates/layouts/splash.html.erb +1 -0
  62. data/lib/docyard/templates/partials/_accordion.html.erb +9 -0
  63. data/lib/docyard/templates/partials/_banner.html.erb +27 -0
  64. data/lib/docyard/templates/partials/_card.html.erb +23 -0
  65. data/lib/docyard/templates/partials/_nav_group.html.erb +6 -0
  66. data/lib/docyard/templates/partials/_nav_leaf.html.erb +3 -0
  67. data/lib/docyard/templates/partials/_step.html.erb +14 -0
  68. data/lib/docyard/version.rb +1 -1
  69. metadata +38 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77d317cd25d544eef47b5bdcecb0515b8937da93b347d39cac6fd1f87bb43b19
4
- data.tar.gz: bf97d9449013ba8bf370682271990f64b2c2092bfdc9bf0deb99cf2b67056cc2
3
+ metadata.gz: 51b2adfdb7f77c4366592cc3eea5a160fc42f85f17e44593e9bf192c3b470b48
4
+ data.tar.gz: 439d82b762c7bc6f485ab627365522b7e202833fc841c6b8655fb28e1e9b3e55
5
5
  SHA512:
6
- metadata.gz: 2e30597716798800240f4436c423284d1972fe5fa0abf1de8007b3279dde0a87ef32210cb830c84769c15db0c82e6c372337ae06baaecdb71fa866342c9697a3
7
- data.tar.gz: f0ae1edf5937153c9ed3ccb1cc7c599c7f650e16470ad976ef0918c09bc030502ecf89dad6a8a7c79ee8055530b61a65d556bbfd147e08a86349a2475777ee54
6
+ metadata.gz: a66355efb4615ae4552aa8656e63078989cdec734f41670dc172936060954a065fefb1a2733e51494747ef0ab9a1dbee399b3c8f37daf2718e92ba03c315df9f
7
+ data.tar.gz: be3cea82f91b60cc7e7c35be142b6c2ccd9be4323cbf1c5190987b6ba6894d4c8da38f55c141abda43211a5ac2b70d38283e1b340d62071ee5c22c87c9c35a51
data/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.0] - 2026-01-15
11
+
12
+ ### Added
13
+ - **Accordions** - Collapsible content sections with `:::details{title="..."}` syntax (#62)
14
+ - **Steps** - Numbered step-by-step instructions with `:::steps` syntax and vertical connector lines (#63)
15
+ - **Cards** - Grid of linked content blocks with `:::cards` and `::card{title="" icon="" href=""}` syntax (#64)
16
+ - **Badges** - Inline status indicators with `:badge[text]{type="success|warning|danger"}` syntax (#65)
17
+ - **Sidebar Badges** - Navigation labels via frontmatter `sidebar.badge` and `sidebar.badge_type` (#66)
18
+ - **Announcement Banner** - Dismissible top banner with optional action button via config (#56)
19
+ - **Markdown Inclusion** - Include content from other files with `<!--@include: ./file.md-->` syntax (#57)
20
+ - **Custom Anchor IDs** - Override auto-generated heading IDs with `## Heading {#custom-id}` syntax (#58)
21
+ - **Image Captions** - Figure elements with captions using `![](image.png){caption="..."}` syntax (#59)
22
+ - **Video Embeds** - YouTube and Vimeo embedding with `::youtube[ID]` and `::vimeo[ID]` syntax (#60)
23
+ - **File Tree** - Display directory structures with icons using `filetree` code blocks (#67)
24
+ - **Tooltips** - Inline hover definitions with `:tooltip[term]{description="..."}` syntax (#68)
25
+ - **Abbreviations** - Auto-expanding terms with `*[TERM]: Definition` syntax (#68)
26
+ - **Code Groups** - Tabbed code blocks with `:::code-group` syntax, syncs selection across page (#70)
27
+
28
+ ### Fixed
29
+ - **Copy Button Overlap** - Repositioned copy button to prevent overlapping code content in non-titled blocks (#71)
30
+ - **Code Fence Protection** - Preprocessors now skip content inside fenced code blocks, allowing documentation to show raw syntax examples (#72)
31
+
10
32
  ## [0.8.0] - 2026-01-13
11
33
 
12
34
  ### Added
@@ -140,7 +162,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
140
162
  - Initial gem structure
141
163
  - Project scaffolding
142
164
 
143
- [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.8.0...HEAD
165
+ [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.9.0...HEAD
166
+ [0.9.0]: https://github.com/sanifhimani/docyard/compare/v0.8.0...v0.9.0
144
167
  [0.8.0]: https://github.com/sanifhimani/docyard/compare/v0.7.0...v0.8.0
145
168
  [0.7.0]: https://github.com/sanifhimani/docyard/compare/v0.6.0...v0.7.0
146
169
  [0.6.0]: https://github.com/sanifhimani/docyard/compare/v0.5.0...v0.6.0
@@ -2,17 +2,29 @@
2
2
 
3
3
  module Docyard
4
4
  module Components
5
+ AbbreviationProcessor = Processors::AbbreviationProcessor
6
+ AccordionProcessor = Processors::AccordionProcessor
7
+ BadgeProcessor = Processors::BadgeProcessor
8
+ StepsProcessor = Processors::StepsProcessor
9
+ CardsProcessor = Processors::CardsProcessor
5
10
  CalloutProcessor = Processors::CalloutProcessor
6
11
  CodeBlockProcessor = Processors::CodeBlockProcessor
12
+ CodeGroupProcessor = Processors::CodeGroupProcessor
7
13
  CodeBlockDiffPreprocessor = Processors::CodeBlockDiffPreprocessor
8
14
  CodeBlockFocusPreprocessor = Processors::CodeBlockFocusPreprocessor
9
15
  CodeBlockOptionsPreprocessor = Processors::CodeBlockOptionsPreprocessor
10
16
  CodeSnippetImportPreprocessor = Processors::CodeSnippetImportPreprocessor
17
+ CustomAnchorProcessor = Processors::CustomAnchorProcessor
18
+ ImageCaptionProcessor = Processors::ImageCaptionProcessor
19
+ IncludeProcessor = Processors::IncludeProcessor
20
+ VideoEmbedProcessor = Processors::VideoEmbedProcessor
21
+ FileTreeProcessor = Processors::FileTreeProcessor
11
22
  HeadingAnchorProcessor = Processors::HeadingAnchorProcessor
12
23
  IconProcessor = Processors::IconProcessor
13
24
  TableOfContentsProcessor = Processors::TableOfContentsProcessor
14
25
  TableWrapperProcessor = Processors::TableWrapperProcessor
15
26
  TabsProcessor = Processors::TabsProcessor
27
+ TooltipProcessor = Processors::TooltipProcessor
16
28
 
17
29
  CodeDetector = Support::CodeDetector
18
30
  IconDetector = Support::Tabs::IconDetector
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+ require_relative "../support/markdown_code_block_helper"
5
+
6
+ module Docyard
7
+ module Components
8
+ module Processors
9
+ class AbbreviationProcessor < BaseProcessor
10
+ include Support::MarkdownCodeBlockHelper
11
+
12
+ DEFINITION_PATTERN = /^\*\[([^\]]+)\]:\s*(.+)$/
13
+ self.priority = 5
14
+
15
+ def preprocess(content)
16
+ abbreviations = extract_abbreviations_outside_code_blocks(content)
17
+ return content if abbreviations.empty?
18
+
19
+ process_outside_code_blocks(content) do |segment|
20
+ segment = remove_definitions(segment)
21
+ apply_abbreviations(segment, abbreviations)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def extract_abbreviations_outside_code_blocks(content)
28
+ abbreviations = {}
29
+ process_outside_code_blocks(content) do |segment|
30
+ segment.scan(DEFINITION_PATTERN) do |term, definition|
31
+ abbreviations[term] = definition.strip
32
+ end
33
+ segment
34
+ end
35
+ abbreviations
36
+ end
37
+
38
+ def remove_definitions(content)
39
+ content.gsub(/^[ \t]*\*\[([^\]]+)\]:\s*.+$\n?/, "")
40
+ end
41
+
42
+ def apply_abbreviations(content, abbreviations)
43
+ abbreviations.each do |term, definition|
44
+ pattern = build_term_pattern(term)
45
+ content = content.gsub(pattern) do |match|
46
+ build_abbr_tag(match, definition)
47
+ end
48
+ end
49
+ content
50
+ end
51
+
52
+ def build_term_pattern(term)
53
+ escaped = Regexp.escape(term)
54
+ /(?<![<\w])#{escaped}(?![>\w])/
55
+ end
56
+
57
+ def build_abbr_tag(term, definition)
58
+ escaped_definition = escape_html(definition)
59
+ %(<abbr class="docyard-abbr" data-definition="#{escaped_definition}">#{term}</abbr>)
60
+ end
61
+
62
+ def escape_html(text)
63
+ text.to_s
64
+ .gsub("&", "&amp;")
65
+ .gsub("<", "&lt;")
66
+ .gsub(">", "&gt;")
67
+ .gsub('"', "&quot;")
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -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)