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,150 @@
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 FileTreeProcessor < BaseProcessor
10
+ include Support::MarkdownCodeBlockHelper
11
+
12
+ FILETREE_PATTERN = /```filetree\n(.*?)```/m
13
+
14
+ self.priority = 8
15
+
16
+ def preprocess(content)
17
+ @code_block_ranges = find_code_block_ranges(content, exclude_language: "filetree")
18
+
19
+ content.gsub(FILETREE_PATTERN) do
20
+ match = Regexp.last_match
21
+ next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
22
+
23
+ build_file_tree(match[1])
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def build_file_tree(content)
30
+ lines = content.lines.map(&:chomp).reject(&:empty?)
31
+
32
+ items = parse_tree_structure(lines)
33
+ html = render_tree(items)
34
+
35
+ "\n\n<div class=\"docyard-filetree\" markdown=\"0\">\n#{html}</div>\n\n"
36
+ end
37
+
38
+ def parse_tree_structure(lines)
39
+ root_items = []
40
+ stack = [{ indent: -1, children: root_items }]
41
+
42
+ lines.each { |line| process_line(line, stack) }
43
+
44
+ root_items
45
+ end
46
+
47
+ def process_line(line, stack)
48
+ indent = line[/\A */].length
49
+ name = line.strip
50
+ return if name.empty?
51
+
52
+ item = build_item(name, indent)
53
+ unwind_stack(stack, indent)
54
+ stack.last[:children] << item
55
+
56
+ add_folder_to_stack(item, indent, stack) if item[:type] == :folder
57
+ end
58
+
59
+ def build_item(name, indent)
60
+ item = parse_item(name)
61
+ item[:indent] = indent
62
+ item
63
+ end
64
+
65
+ def unwind_stack(stack, indent)
66
+ stack.pop while stack.length > 1 && stack.last[:indent] >= indent
67
+ end
68
+
69
+ def add_folder_to_stack(item, indent, stack)
70
+ item[:children] = []
71
+ stack.push({ indent: indent, children: item[:children] })
72
+ end
73
+
74
+ def parse_item(name)
75
+ highlighted = name.end_with?(" *")
76
+ name = name.chomp(" *") if highlighted
77
+
78
+ name, comment = extract_comment(name)
79
+ type = name.end_with?("/") ? :folder : :file
80
+ name = name.chomp("/") if type == :folder
81
+
82
+ { name: name, type: type, highlighted: highlighted, comment: comment }
83
+ end
84
+
85
+ def extract_comment(name)
86
+ return [name, nil] unless name.include?(" # ")
87
+
88
+ name.split(" # ", 2)
89
+ end
90
+
91
+ def render_tree(items, depth = 0)
92
+ return "" if items.empty?
93
+
94
+ html = "<ul class=\"docyard-filetree__list\">\n"
95
+ items.each { |item| html += render_item(item, depth) }
96
+ html += "</ul>\n"
97
+ html
98
+ end
99
+
100
+ def render_item(item, depth)
101
+ classes = item_classes(item)
102
+
103
+ html = "<li class=\"#{classes}\">\n"
104
+ html += render_entry(item)
105
+ html += render_tree(item[:children], depth + 1) if render_children?(item)
106
+ html += "</li>\n"
107
+ html
108
+ end
109
+
110
+ def item_classes(item)
111
+ classes = ["docyard-filetree__item", "docyard-filetree__item--#{item[:type]}"]
112
+ classes << "docyard-filetree__item--highlighted" if item[:highlighted]
113
+ classes.join(" ")
114
+ end
115
+
116
+ def render_entry(item)
117
+ html = "<span class=\"docyard-filetree__entry\">"
118
+ html += icon_for(item[:type])
119
+ html += "<span class=\"docyard-filetree__name\">#{escape_html(item[:name])}</span>"
120
+ html += render_comment(item[:comment])
121
+ html += "</span>"
122
+ html
123
+ end
124
+
125
+ def render_comment(comment)
126
+ return "" unless comment
127
+
128
+ "<span class=\"docyard-filetree__comment\">#{escape_html(comment)}</span>"
129
+ end
130
+
131
+ def render_children?(item)
132
+ item[:type] == :folder && item[:children] && !item[:children].empty?
133
+ end
134
+
135
+ def icon_for(type)
136
+ icon_name = type == :folder ? "folder-open" : "file-text"
137
+ %(<i class="ph ph-#{icon_name}" aria-hidden="true"></i>)
138
+ end
139
+
140
+ def escape_html(text)
141
+ text.to_s
142
+ .gsub("&", "&amp;")
143
+ .gsub("<", "&lt;")
144
+ .gsub(">", "&gt;")
145
+ .gsub('"', "&quot;")
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../rendering/icons"
4
3
  require_relative "../base_processor"
5
4
 
6
5
  module Docyard
@@ -10,6 +9,7 @@ module Docyard
10
9
  self.priority = 20
11
10
 
12
11
  ICON_PATTERN = /:([a-z][a-z0-9-]*):(?:([a-z]+):)?/i
12
+ VALID_WEIGHTS = %w[regular bold fill light thin duotone].freeze
13
13
 
14
14
  def postprocess(html)
15
15
  segments = split_preserving_code_blocks(html)
@@ -44,9 +44,15 @@ module Docyard
44
44
  content.gsub(ICON_PATTERN) do
45
45
  icon_name = Regexp.last_match(1)
46
46
  weight = Regexp.last_match(2) || "regular"
47
- Icons.render(icon_name, weight) || Regexp.last_match(0)
47
+ render_icon(icon_name, weight)
48
48
  end
49
49
  end
50
+
51
+ def render_icon(name, weight)
52
+ weight = "regular" unless VALID_WEIGHTS.include?(weight)
53
+ weight_class = weight == "regular" ? "ph" : "ph-#{weight}"
54
+ %(<i class="#{weight_class} ph-#{name}" aria-hidden="true"></i>)
55
+ end
50
56
  end
51
57
  end
52
58
  end
@@ -0,0 +1,96 @@
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 ImageCaptionProcessor < BaseProcessor
10
+ include Support::MarkdownCodeBlockHelper
11
+
12
+ IMAGE_ATTRS_PATTERN = /!\[([^\]]*)\]\(([^)]+)\)\{([^}]+)\}/
13
+
14
+ self.priority = 5
15
+
16
+ def preprocess(content)
17
+ process_outside_code_blocks(content) do |segment|
18
+ process_images_with_attrs(segment)
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def process_images_with_attrs(content)
25
+ content.gsub(IMAGE_ATTRS_PATTERN) do
26
+ alt = Regexp.last_match(1)
27
+ src = Regexp.last_match(2)
28
+ attrs_string = Regexp.last_match(3)
29
+
30
+ attrs = parse_attributes(attrs_string)
31
+ build_image_html(alt, src, attrs)
32
+ end
33
+ end
34
+
35
+ def parse_attributes(attrs_string)
36
+ attrs = {}
37
+
38
+ attrs_string.scan(/(\w+)="([^"]*)"/) do |key, value|
39
+ attrs[key] = value
40
+ end
41
+
42
+ attrs[:nozoom] = true if attrs_string.include?("nozoom")
43
+
44
+ attrs
45
+ end
46
+
47
+ def build_image_html(alt, src, attrs)
48
+ if attrs["caption"]
49
+ build_figure(alt, src, attrs)
50
+ else
51
+ build_img(alt, src, attrs)
52
+ end
53
+ end
54
+
55
+ def build_figure(alt, src, attrs)
56
+ "\n\n" \
57
+ "<figure class=\"docyard-figure\" markdown=\"0\">\n" \
58
+ "#{build_img_tag(alt, src, attrs)}\n" \
59
+ "<figcaption>#{escape_html(attrs['caption'])}</figcaption>\n" \
60
+ "</figure>" \
61
+ "\n\n"
62
+ end
63
+
64
+ def build_img(alt, src, attrs)
65
+ "\n\n#{build_img_tag(alt, src, attrs)}\n\n"
66
+ end
67
+
68
+ def build_img_tag(alt, src, attrs)
69
+ parts = base_img_attrs(alt, src)
70
+ parts.concat(dimension_attrs(attrs))
71
+ parts << "data-no-zoom" if attrs[:nozoom]
72
+ "<img #{parts.join(' ')}>"
73
+ end
74
+
75
+ def base_img_attrs(alt, src)
76
+ ["src=\"#{escape_html(src)}\"", "alt=\"#{escape_html(alt)}\""]
77
+ end
78
+
79
+ def dimension_attrs(attrs)
80
+ result = []
81
+ result << "width=\"#{escape_html(attrs['width'])}\"" if attrs["width"]
82
+ result << "height=\"#{escape_html(attrs['height'])}\"" if attrs["height"]
83
+ result
84
+ end
85
+
86
+ def escape_html(text)
87
+ text.to_s
88
+ .gsub("&", "&amp;")
89
+ .gsub("<", "&lt;")
90
+ .gsub(">", "&gt;")
91
+ .gsub('"', "&quot;")
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,86 @@
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 IncludeProcessor < BaseProcessor
10
+ include Support::MarkdownCodeBlockHelper
11
+
12
+ INCLUDE_PATTERN = /<!--\s*@include:\s*([^\s]+)\s*-->/
13
+
14
+ self.priority = 0
15
+
16
+ def preprocess(content)
17
+ @current_file = context[:current_file]
18
+ @docs_root = context[:docs_root] || "docs"
19
+
20
+ process_outside_code_blocks(content) do |segment|
21
+ process_includes(segment, Set.new)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def process_includes(content, included_files)
28
+ content.gsub(INCLUDE_PATTERN) { |_| process_include(Regexp.last_match, included_files) }
29
+ end
30
+
31
+ def process_include(match, included_files)
32
+ filepath = match[1]
33
+ full_path = resolve_path(filepath)
34
+
35
+ error = validate_include(filepath, full_path, included_files)
36
+ return error if error
37
+
38
+ updated_included_files = included_files.dup.add(full_path)
39
+ file_content = File.read(full_path)
40
+
41
+ process_includes(file_content.strip, updated_included_files)
42
+ end
43
+
44
+ def validate_include(filepath, full_path, included_files)
45
+ return include_error(filepath, "File not found") unless full_path && File.exist?(full_path)
46
+ return include_error(filepath, "Circular include detected") if included_files.include?(full_path)
47
+ return include_error(filepath, "Use code snippets for non-markdown files") unless markdown_file?(filepath)
48
+
49
+ nil
50
+ end
51
+
52
+ def resolve_path(filepath)
53
+ if filepath.start_with?("./", "../")
54
+ resolve_relative_path(filepath)
55
+ else
56
+ resolve_docs_path(filepath)
57
+ end
58
+ end
59
+
60
+ def resolve_relative_path(filepath)
61
+ return nil unless @current_file
62
+
63
+ base_dir = File.dirname(@current_file)
64
+ full_path = File.expand_path(filepath, base_dir)
65
+
66
+ full_path if File.exist?(full_path)
67
+ end
68
+
69
+ def resolve_docs_path(filepath)
70
+ full_path = File.join(@docs_root, filepath)
71
+ full_path if File.exist?(full_path)
72
+ end
73
+
74
+ def markdown_file?(filepath)
75
+ ext = File.extname(filepath).downcase
76
+ %w[.md .markdown .mdx].include?(ext)
77
+ end
78
+
79
+ def include_error(filepath, message)
80
+ Docyard.logger.warn("Include failed: #{filepath} - #{message}")
81
+ "> [!WARNING]\n> Include error: #{filepath} - #{message}\n"
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rendering/renderer"
4
+ require_relative "../base_processor"
5
+ require_relative "../support/markdown_code_block_helper"
6
+ require "kramdown"
7
+ require "kramdown-parser-gfm"
8
+
9
+ module Docyard
10
+ module Components
11
+ module Processors
12
+ class StepsProcessor < BaseProcessor
13
+ include Support::MarkdownCodeBlockHelper
14
+
15
+ self.priority = 10
16
+
17
+ STEPS_PATTERN = /^:::steps\s*\n(.*?)^:::\s*$/m
18
+ STEP_HEADING_PATTERN = /^###\s+(.+)$/
19
+
20
+ def preprocess(markdown)
21
+ @code_block_ranges = find_code_block_ranges(markdown)
22
+
23
+ markdown.gsub(STEPS_PATTERN) do
24
+ match = Regexp.last_match
25
+ next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
26
+
27
+ content = match[1]
28
+ steps = parse_steps(content)
29
+
30
+ wrap_in_nomarkdown(render_steps_html(steps))
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def parse_steps(content)
37
+ steps = []
38
+ current_step = nil
39
+
40
+ content.lines.each do |line|
41
+ if line.match(STEP_HEADING_PATTERN)
42
+ steps << current_step if current_step
43
+ current_step = { title: Regexp.last_match(1).strip, content: "" }
44
+ elsif current_step
45
+ current_step[:content] += line
46
+ end
47
+ end
48
+
49
+ steps << current_step if current_step
50
+ steps
51
+ end
52
+
53
+ def render_steps_html(steps)
54
+ renderer = Renderer.new
55
+
56
+ steps_html = steps.map.with_index(1) do |step, index|
57
+ content_html = render_markdown_content(step[:content].strip)
58
+
59
+ renderer.render_partial(
60
+ "_step", {
61
+ number: index,
62
+ title: step[:title],
63
+ content_html: content_html,
64
+ is_last: index == steps.length
65
+ }
66
+ )
67
+ end.join("\n")
68
+
69
+ "<div class=\"docyard-steps\">\n#{steps_html}\n</div>"
70
+ end
71
+
72
+ def render_markdown_content(content_markdown)
73
+ return "" if content_markdown.empty?
74
+
75
+ Kramdown::Document.new(
76
+ content_markdown,
77
+ input: "GFM",
78
+ hard_wrap: false,
79
+ syntax_highlighter: "rouge"
80
+ ).to_html
81
+ end
82
+
83
+ def wrap_in_nomarkdown(html)
84
+ "{::nomarkdown}\n#{html}\n{:/nomarkdown}"
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../../rendering/renderer"
4
4
  require_relative "../base_processor"
5
+ require_relative "../support/markdown_code_block_helper"
5
6
  require_relative "../support/tabs/parser"
6
7
  require "securerandom"
7
8
 
@@ -9,6 +10,8 @@ module Docyard
9
10
  module Components
10
11
  module Processors
11
12
  class TabsProcessor < BaseProcessor
13
+ include Support::MarkdownCodeBlockHelper
14
+
12
15
  self.priority = 15
13
16
 
14
17
  TabsParser = Support::Tabs::Parser
@@ -16,8 +19,13 @@ module Docyard
16
19
  def preprocess(content)
17
20
  return content unless content.include?(":::tabs")
18
21
 
22
+ @code_block_ranges = find_code_block_ranges(content)
23
+
19
24
  content.gsub(/^:::[ \t]*tabs[ \t]*\n(.*?)^:::[ \t]*$/m) do
20
- process_tabs_block(Regexp.last_match(1))
25
+ match = Regexp.last_match
26
+ next match[0] if inside_code_block?(match.begin(0), @code_block_ranges)
27
+
28
+ process_tabs_block(match[1])
21
29
  end
22
30
  end
23
31
 
@@ -0,0 +1,57 @@
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 TooltipProcessor < BaseProcessor
10
+ include Support::MarkdownCodeBlockHelper
11
+
12
+ TOOLTIP_PATTERN = /:tooltip\[([^\]]+)\]\{([^}]+)\}/
13
+ self.priority = 6
14
+
15
+ def preprocess(content)
16
+ process_outside_code_blocks(content) do |segment|
17
+ segment.gsub(TOOLTIP_PATTERN) do |_match|
18
+ term = ::Regexp.last_match(1)
19
+ attributes = parse_attributes(::Regexp.last_match(2))
20
+ build_tooltip_tag(term, attributes)
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def parse_attributes(attr_string)
28
+ attributes = {}
29
+ attr_string.scan(/(\w+)="([^"]*)"/) do |key, value|
30
+ attributes[key.to_sym] = value
31
+ end
32
+ attributes
33
+ end
34
+
35
+ def build_tooltip_tag(term, attributes)
36
+ description = escape_html(attributes[:description] || "")
37
+ link = attributes[:link]
38
+ link_text = attributes[:link_text] || "Learn more"
39
+
40
+ data_attrs = %(data-description="#{description}")
41
+ data_attrs += %( data-link="#{escape_html(link)}") if link
42
+ data_attrs += %( data-link-text="#{escape_html(link_text)}") if link
43
+
44
+ %(<span class="docyard-tooltip" #{data_attrs}>#{term}</span>)
45
+ end
46
+
47
+ def escape_html(text)
48
+ text.to_s
49
+ .gsub("&", "&amp;")
50
+ .gsub("<", "&lt;")
51
+ .gsub(">", "&gt;")
52
+ .gsub('"', "&quot;")
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end