docyard 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/CHANGELOG.md +34 -1
  4. data/lib/docyard/build/asset_bundler.rb +22 -7
  5. data/lib/docyard/build/file_copier.rb +49 -27
  6. data/lib/docyard/build/sitemap_generator.rb +6 -6
  7. data/lib/docyard/build/static_generator.rb +82 -50
  8. data/lib/docyard/builder.rb +20 -10
  9. data/lib/docyard/cli.rb +6 -3
  10. data/lib/docyard/components/aliases.rb +29 -0
  11. data/lib/docyard/components/processors/callout_processor.rb +124 -0
  12. data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +106 -0
  13. data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +79 -0
  14. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +78 -0
  15. data/lib/docyard/components/processors/code_block_processor.rb +175 -0
  16. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +127 -0
  17. data/lib/docyard/components/processors/heading_anchor_processor.rb +39 -0
  18. data/lib/docyard/components/processors/icon_processor.rb +53 -0
  19. data/lib/docyard/components/processors/table_of_contents_processor.rb +68 -0
  20. data/lib/docyard/components/processors/table_wrapper_processor.rb +22 -0
  21. data/lib/docyard/components/processors/tabs_processor.rb +48 -0
  22. data/lib/docyard/components/support/code_block/feature_extractor.rb +117 -0
  23. data/lib/docyard/components/support/code_block/icon_detector.rb +44 -0
  24. data/lib/docyard/components/support/code_block/line_parser.rb +84 -0
  25. data/lib/docyard/components/support/code_block/line_wrapper.rb +50 -0
  26. data/lib/docyard/components/support/code_block/patterns.rb +55 -0
  27. data/lib/docyard/components/support/code_detector.rb +61 -0
  28. data/lib/docyard/components/support/tabs/icon_detector.rb +62 -0
  29. data/lib/docyard/components/support/tabs/parser.rb +195 -0
  30. data/lib/docyard/components/support/tabs/range_finder.rb +46 -0
  31. data/lib/docyard/config/branding_resolver.rb +183 -0
  32. data/lib/docyard/{constants.rb → config/constants.rb} +7 -4
  33. data/lib/docyard/config/validator.rb +122 -99
  34. data/lib/docyard/config.rb +38 -36
  35. data/lib/docyard/initializer.rb +15 -76
  36. data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
  37. data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +6 -3
  38. data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
  39. data/lib/docyard/navigation/sidebar/config_parser.rb +208 -0
  40. data/lib/docyard/navigation/sidebar/file_resolver.rb +78 -0
  41. data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +2 -1
  42. data/lib/docyard/navigation/sidebar/item.rb +96 -0
  43. data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
  44. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +69 -0
  45. data/lib/docyard/navigation/sidebar/metadata_reader.rb +47 -0
  46. data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
  47. data/lib/docyard/navigation/sidebar/renderer.rb +144 -0
  48. data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
  49. data/lib/docyard/navigation/sidebar/tree_builder.rb +139 -0
  50. data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
  51. data/lib/docyard/navigation/sidebar_builder.rb +159 -0
  52. data/lib/docyard/rendering/icon_helpers.rb +13 -0
  53. data/lib/docyard/{icons → rendering/icons}/phosphor.rb +26 -1
  54. data/lib/docyard/{markdown.rb → rendering/markdown.rb} +19 -13
  55. data/lib/docyard/rendering/renderer.rb +163 -0
  56. data/lib/docyard/rendering/template_resolver.rb +172 -0
  57. data/lib/docyard/routing/fallback_resolver.rb +92 -0
  58. data/lib/docyard/search/build_indexer.rb +74 -0
  59. data/lib/docyard/search/dev_indexer.rb +155 -0
  60. data/lib/docyard/search/pagefind_support.rb +33 -0
  61. data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +24 -19
  62. data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
  63. data/lib/docyard/server/pagefind_handler.rb +63 -0
  64. data/lib/docyard/{preview_server.rb → server/preview_server.rb} +2 -2
  65. data/lib/docyard/server/rack_application.rb +192 -0
  66. data/lib/docyard/server/resolution_result.rb +29 -0
  67. data/lib/docyard/{router.rb → server/router.rb} +4 -4
  68. data/lib/docyard/templates/assets/css/code.css +18 -51
  69. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +143 -0
  70. data/lib/docyard/templates/assets/css/components/callout.css +67 -67
  71. data/lib/docyard/templates/assets/css/components/code-block.css +180 -282
  72. data/lib/docyard/templates/assets/css/components/heading-anchor.css +28 -15
  73. data/lib/docyard/templates/assets/css/components/icon.css +0 -1
  74. data/lib/docyard/templates/assets/css/components/logo.css +0 -2
  75. data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
  76. data/lib/docyard/templates/assets/css/components/navigation.css +186 -167
  77. data/lib/docyard/templates/assets/css/components/prev-next.css +76 -47
  78. data/lib/docyard/templates/assets/css/components/search.css +561 -0
  79. data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
  80. data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
  81. data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
  82. data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
  83. data/lib/docyard/templates/assets/css/landing.css +815 -0
  84. data/lib/docyard/templates/assets/css/layout.css +503 -87
  85. data/lib/docyard/templates/assets/css/main.css +1 -3
  86. data/lib/docyard/templates/assets/css/markdown.css +111 -93
  87. data/lib/docyard/templates/assets/css/reset.css +0 -3
  88. data/lib/docyard/templates/assets/css/typography.css +43 -41
  89. data/lib/docyard/templates/assets/css/variables.css +268 -208
  90. data/lib/docyard/templates/assets/favicon.svg +7 -8
  91. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
  92. data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
  93. data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
  94. data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
  95. data/lib/docyard/templates/assets/js/components/search.js +610 -0
  96. data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
  97. data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
  98. data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
  99. data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
  100. data/lib/docyard/templates/assets/js/theme.js +0 -3
  101. data/lib/docyard/templates/assets/logo-dark.svg +8 -2
  102. data/lib/docyard/templates/assets/logo.svg +7 -4
  103. data/lib/docyard/templates/config/docyard.yml.erb +37 -34
  104. data/lib/docyard/templates/errors/404.html.erb +1 -1
  105. data/lib/docyard/templates/errors/500.html.erb +1 -1
  106. data/lib/docyard/templates/layouts/default.html.erb +19 -56
  107. data/lib/docyard/templates/layouts/splash.html.erb +176 -0
  108. data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
  109. data/lib/docyard/templates/partials/_code_block.html.erb +6 -4
  110. data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
  111. data/lib/docyard/templates/partials/_features.html.erb +15 -0
  112. data/lib/docyard/templates/partials/_footer.html.erb +42 -0
  113. data/lib/docyard/templates/partials/_head.html.erb +22 -0
  114. data/lib/docyard/templates/partials/_header.html.erb +49 -0
  115. data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
  116. data/lib/docyard/templates/partials/_hero.html.erb +27 -0
  117. data/lib/docyard/templates/partials/_nav_group.html.erb +25 -11
  118. data/lib/docyard/templates/partials/_nav_leaf.html.erb +1 -1
  119. data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
  120. data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
  121. data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
  122. data/lib/docyard/templates/partials/_prev_next.html.erb +9 -3
  123. data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
  124. data/lib/docyard/templates/partials/_search_modal.html.erb +41 -0
  125. data/lib/docyard/templates/partials/_search_trigger.html.erb +18 -0
  126. data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
  127. data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
  128. data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
  129. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
  130. data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
  131. data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
  132. data/lib/docyard/utils/html_helpers.rb +14 -0
  133. data/lib/docyard/utils/path_resolver.rb +2 -1
  134. data/lib/docyard/utils/url_helpers.rb +20 -0
  135. data/lib/docyard/version.rb +1 -1
  136. data/lib/docyard.rb +22 -15
  137. metadata +89 -50
  138. data/lib/docyard/components/callout_processor.rb +0 -121
  139. data/lib/docyard/components/code_block_diff_preprocessor.rb +0 -104
  140. data/lib/docyard/components/code_block_feature_extractor.rb +0 -113
  141. data/lib/docyard/components/code_block_focus_preprocessor.rb +0 -77
  142. data/lib/docyard/components/code_block_icon_detector.rb +0 -40
  143. data/lib/docyard/components/code_block_line_wrapper.rb +0 -46
  144. data/lib/docyard/components/code_block_options_preprocessor.rb +0 -76
  145. data/lib/docyard/components/code_block_patterns.rb +0 -51
  146. data/lib/docyard/components/code_block_processor.rb +0 -176
  147. data/lib/docyard/components/code_detector.rb +0 -59
  148. data/lib/docyard/components/code_line_parser.rb +0 -80
  149. data/lib/docyard/components/code_snippet_import_preprocessor.rb +0 -125
  150. data/lib/docyard/components/heading_anchor_processor.rb +0 -34
  151. data/lib/docyard/components/icon_detector.rb +0 -57
  152. data/lib/docyard/components/icon_processor.rb +0 -51
  153. data/lib/docyard/components/table_of_contents_processor.rb +0 -64
  154. data/lib/docyard/components/table_wrapper_processor.rb +0 -18
  155. data/lib/docyard/components/tabs_parser.rb +0 -191
  156. data/lib/docyard/components/tabs_processor.rb +0 -44
  157. data/lib/docyard/components/tabs_range_finder.rb +0 -42
  158. data/lib/docyard/rack_application.rb +0 -172
  159. data/lib/docyard/renderer.rb +0 -120
  160. data/lib/docyard/routing/resolution_result.rb +0 -31
  161. data/lib/docyard/sidebar/config_parser.rb +0 -180
  162. data/lib/docyard/sidebar/item.rb +0 -58
  163. data/lib/docyard/sidebar/renderer.rb +0 -137
  164. data/lib/docyard/sidebar/tree_builder.rb +0 -59
  165. data/lib/docyard/sidebar_builder.rb +0 -102
  166. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
  167. data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
  168. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
  169. data/lib/docyard/templates/markdown/index.md.erb +0 -82
  170. /data/lib/docyard/{sidebar → navigation/sidebar}/title_extractor.rb +0 -0
  171. /data/lib/docyard/{icons → rendering/icons}/LICENSE.phosphor +0 -0
  172. /data/lib/docyard/{icons → rendering/icons}/file_types.rb +0 -0
  173. /data/lib/docyard/{icons.rb → rendering/icons.rb} +0 -0
  174. /data/lib/docyard/{language_mapping.rb → rendering/language_mapping.rb} +0 -0
  175. /data/lib/docyard/{file_watcher.rb → server/file_watcher.rb} +0 -0
  176. /data/lib/docyard/{errors.rb → utils/errors.rb} +0 -0
  177. /data/lib/docyard/{logging.rb → utils/logging.rb} +0 -0
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../language_mapping"
4
-
5
- module Docyard
6
- module Components
7
- class CodeDetector
8
- def self.detect(content)
9
- new(content).detect
10
- end
11
-
12
- def initialize(content)
13
- @content = content
14
- end
15
-
16
- def detect
17
- return nil unless code_only?
18
-
19
- language = extract_language
20
- return nil unless language
21
-
22
- icon_for_language(language)
23
- end
24
-
25
- private
26
-
27
- attr_reader :content
28
-
29
- def code_only?
30
- stripped = content.strip
31
- return false unless stripped.start_with?("```") && stripped.end_with?("```")
32
-
33
- parts = stripped.split("```")
34
- parts.length == 2 && parts[0].empty?
35
- end
36
-
37
- def extract_language
38
- parts = content.strip.split("```")
39
- return nil unless parts[1]
40
-
41
- lines = parts[1].split("\n", 2)
42
- lang_line = lines[0].strip
43
- return nil if lang_line.empty? || lang_line.include?(" ")
44
-
45
- lang_line.downcase
46
- end
47
-
48
- def icon_for_language(language)
49
- if LanguageMapping.terminal_language?(language)
50
- { icon: "terminal-window", source: "phosphor" }
51
- elsif (extension = LanguageMapping.extension_for(language))
52
- { icon: extension, source: "file-extension" }
53
- else
54
- { icon: "file", source: "phosphor" }
55
- end
56
- end
57
- end
58
- end
59
- end
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Docyard
4
- module Components
5
- class CodeLineParser
6
- def initialize(code_content)
7
- @code_content = code_content
8
- @lines = []
9
- @current_line = ""
10
- @open_tags = []
11
- @in_tag = false
12
- @tag_buffer = ""
13
- end
14
-
15
- def parse
16
- @code_content.each_char { |char| process_char(char) }
17
- finalize
18
- end
19
-
20
- private
21
-
22
- def process_char(char)
23
- case char
24
- when "<" then start_tag(char)
25
- when ">" then end_tag_if_applicable(char)
26
- when "\n" then handle_newline
27
- else handle_regular_char(char)
28
- end
29
- end
30
-
31
- def start_tag(char)
32
- @in_tag = true
33
- @tag_buffer = char
34
- end
35
-
36
- def end_tag_if_applicable(char)
37
- if @in_tag
38
- @in_tag = false
39
- @tag_buffer += char
40
-
41
- if @tag_buffer.start_with?("</")
42
- @open_tags.pop
43
- elsif !@tag_buffer.end_with?("/>")
44
- @open_tags << @tag_buffer
45
- end
46
-
47
- @current_line += @tag_buffer
48
- @tag_buffer = ""
49
- else
50
- @current_line += char
51
- end
52
- end
53
-
54
- def handle_newline
55
- closing_tags = @open_tags.reverse.map { |tag| closing_tag_for(tag) }.join
56
- @lines << "#{@current_line}#{closing_tags}\n"
57
- @current_line = @open_tags.join
58
- end
59
-
60
- def handle_regular_char(char)
61
- if @in_tag
62
- @tag_buffer += char
63
- else
64
- @current_line += char
65
- end
66
- end
67
-
68
- def finalize
69
- @lines << @current_line unless @current_line.empty?
70
- @lines << "" if @lines.empty?
71
- @lines
72
- end
73
-
74
- def closing_tag_for(open_tag)
75
- tag_name = open_tag.match(/<(\w+)/)[1]
76
- "</#{tag_name}>"
77
- end
78
- end
79
- end
80
- end
@@ -1,125 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "base_processor"
4
-
5
- module Docyard
6
- module Components
7
- class CodeSnippetImportPreprocessor < BaseProcessor
8
- EXTENSION_MAP = {
9
- "rb" => "ruby",
10
- "js" => "javascript",
11
- "ts" => "typescript",
12
- "py" => "python",
13
- "yml" => "yaml",
14
- "md" => "markdown",
15
- "sh" => "bash",
16
- "zsh" => "bash",
17
- "jsx" => "jsx",
18
- "tsx" => "tsx"
19
- }.freeze
20
-
21
- IMPORT_PATTERN = %r{^<<<\s+@/([^\s{#]+)(?:#([\w-]+))?(?:\{([^}]+)\})?\s*$}
22
-
23
- self.priority = 1
24
-
25
- def preprocess(content)
26
- @docs_root = context[:docs_root] || "docs"
27
- content.gsub(IMPORT_PATTERN) { |_| process_import(Regexp.last_match) }
28
- end
29
-
30
- private
31
-
32
- def process_import(match)
33
- filepath = match[1]
34
- region = match[2]
35
- options = match[3]
36
-
37
- file_content = read_file(filepath)
38
- return import_error(filepath, "File not found") unless file_content
39
-
40
- file_content = extract_region(file_content, region) if region
41
- return import_error(filepath, "Region '#{region}' not found") unless file_content
42
-
43
- build_code_block(file_content, filepath, options)
44
- end
45
-
46
- def read_file(filepath)
47
- full_path = File.join(@docs_root, filepath)
48
- return nil unless File.exist?(full_path)
49
-
50
- File.read(full_path)
51
- end
52
-
53
- def extract_region(content, region_name)
54
- region_start = %r{^[ \t]*(?://|#|/\*)\s*#region\s+#{Regexp.escape(region_name)}\b.*$}
55
- region_end = %r{^[ \t]*(?://|#|/\*|\*/)\s*#endregion\s*#{Regexp.escape(region_name)}?\b.*$}
56
-
57
- lines = content.lines
58
- start_index = lines.find_index { |line| line.match?(region_start) }
59
- return nil unless start_index
60
-
61
- end_index = lines[(start_index + 1)..].find_index { |line| line.match?(region_end) }
62
- return nil unless end_index
63
-
64
- end_index += start_index + 1
65
- lines[(start_index + 1)...end_index].join
66
- end
67
-
68
- def build_code_block(content, filepath, options)
69
- lang = detect_language(filepath)
70
- highlights = nil
71
-
72
- if options
73
- parsed = parse_options(options)
74
- lang = parsed[:lang] if parsed[:lang]
75
- highlights = parsed[:highlights] if parsed[:highlights]
76
- end
77
-
78
- content = extract_line_range(content, highlights) if highlights&.include?("-") && !highlights.include?(",")
79
-
80
- meta = build_meta_string(highlights, filepath)
81
-
82
- "```#{lang}#{meta}\n#{content.chomp}\n```"
83
- end
84
-
85
- def parse_options(options)
86
- parts = options.strip.split(/\s+/)
87
- result = { highlights: nil, lang: nil }
88
-
89
- parts.each do |part|
90
- if part.match?(/^[\d,-]+$/)
91
- result[:highlights] = part
92
- else
93
- result[:lang] = part
94
- end
95
- end
96
-
97
- result
98
- end
99
-
100
- def extract_line_range(content, range_str)
101
- return content unless range_str&.match?(/^\d+-\d+$/)
102
-
103
- start_line, end_line = range_str.split("-").map(&:to_i)
104
- lines = content.lines
105
- lines[(start_line - 1)..(end_line - 1)]&.join || content
106
- end
107
-
108
- def build_meta_string(highlights, filepath)
109
- parts = []
110
- parts << " [#{File.basename(filepath)}]" if filepath
111
- parts << " {#{highlights}}" if highlights && !highlights.match?(/^\d+-\d+$/)
112
- parts.join
113
- end
114
-
115
- def detect_language(filepath)
116
- ext = File.extname(filepath).delete_prefix(".")
117
- EXTENSION_MAP[ext] || ext
118
- end
119
-
120
- def import_error(filepath, message)
121
- "```\nError importing #{filepath}: #{message}\n```"
122
- end
123
- end
124
- end
125
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Docyard
4
- module Components
5
- class HeadingAnchorProcessor < BaseProcessor
6
- self.priority = 30
7
-
8
- def postprocess(html)
9
- add_anchor_links(html)
10
- end
11
-
12
- private
13
-
14
- def add_anchor_links(html)
15
- html.gsub(%r{<(h[2-6])\s+id="([^"]+)">(.*?)</\1>}m) do |_match|
16
- tag = Regexp.last_match(1)
17
- id = Regexp.last_match(2)
18
- content = Regexp.last_match(3)
19
-
20
- anchor_html = render_anchor_link(id)
21
-
22
- "<#{tag} id=\"#{id}\">#{content}#{anchor_html}</#{tag}>"
23
- end
24
- end
25
-
26
- def render_anchor_link(id)
27
- renderer = Renderer.new
28
- renderer.render_partial("_heading_anchor", {
29
- id: id
30
- })
31
- end
32
- end
33
- end
34
- end
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "code_detector"
4
-
5
- module Docyard
6
- module Components
7
- class IconDetector
8
- MANUAL_ICON_PATTERN = /^:([a-z0-9-]+):\s*(.+)$/i
9
-
10
- def self.detect(tab_name, tab_content)
11
- new(tab_name, tab_content).detect
12
- end
13
-
14
- def initialize(tab_name, tab_content)
15
- @tab_name = tab_name
16
- @tab_content = tab_content
17
- end
18
-
19
- def detect
20
- manual_icon || auto_detected_icon || no_icon
21
- end
22
-
23
- private
24
-
25
- attr_reader :tab_name, :tab_content
26
-
27
- def manual_icon
28
- return nil unless tab_name.match(MANUAL_ICON_PATTERN)
29
-
30
- {
31
- name: Regexp.last_match(2).strip,
32
- icon: Regexp.last_match(1),
33
- icon_source: "phosphor"
34
- }
35
- end
36
-
37
- def auto_detected_icon
38
- detected = CodeDetector.detect(tab_content)
39
- return nil unless detected
40
-
41
- {
42
- name: tab_name,
43
- icon: detected[:icon],
44
- icon_source: detected[:source]
45
- }
46
- end
47
-
48
- def no_icon
49
- {
50
- name: tab_name,
51
- icon: nil,
52
- icon_source: nil
53
- }
54
- end
55
- end
56
- end
57
- end
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../icons"
4
- require_relative "base_processor"
5
-
6
- module Docyard
7
- module Components
8
- class IconProcessor < BaseProcessor
9
- self.priority = 20
10
-
11
- ICON_PATTERN = /:([a-z][a-z0-9-]*):(?:([a-z]+):)?/i
12
-
13
- def postprocess(html)
14
- segments = split_preserving_code_blocks(html)
15
-
16
- segments.map do |segment|
17
- segment[:type] == :code ? segment[:content] : process_segment(segment[:content])
18
- end.join
19
- end
20
-
21
- private
22
-
23
- def split_preserving_code_blocks(html)
24
- segments = []
25
- current_pos = 0
26
-
27
- html.scan(%r{<(code|pre)[^>]*>.*?</\1>}m) do
28
- match_start = Regexp.last_match.begin(0)
29
- match_end = Regexp.last_match.end(0)
30
-
31
- segments << { type: :text, content: html[current_pos...match_start] } if match_start > current_pos
32
- segments << { type: :code, content: html[match_start...match_end] }
33
-
34
- current_pos = match_end
35
- end
36
-
37
- segments << { type: :text, content: html[current_pos..] } if current_pos < html.length
38
-
39
- segments.empty? ? [{ type: :text, content: html }] : segments
40
- end
41
-
42
- def process_segment(content)
43
- content.gsub(ICON_PATTERN) do
44
- icon_name = Regexp.last_match(1)
45
- weight = Regexp.last_match(2) || "regular"
46
- Icons.render(icon_name, weight) || Regexp.last_match(0)
47
- end
48
- end
49
- end
50
- end
51
- end
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Docyard
4
- module Components
5
- class TableOfContentsProcessor < BaseProcessor
6
- self.priority = 35
7
-
8
- def postprocess(html)
9
- headings = extract_headings(html)
10
- context[:toc] = headings
11
- html
12
- end
13
-
14
- private
15
-
16
- def extract_headings(html)
17
- headings = []
18
-
19
- html.scan(%r{<(h[2-4])\s+id="([^"]+)">(.*?)</\1>}m) do
20
- level = Regexp.last_match(1)[1].to_i
21
- id = Regexp.last_match(2)
22
- text = strip_html(Regexp.last_match(3))
23
-
24
- headings << {
25
- level: level,
26
- id: id,
27
- text: text
28
- }
29
- end
30
-
31
- build_hierarchy(headings)
32
- end
33
-
34
- def build_hierarchy(headings)
35
- return [] if headings.empty?
36
-
37
- root = []
38
- stack = []
39
-
40
- headings.each do |heading|
41
- heading[:children] = []
42
-
43
- stack.pop while stack.any? && stack.last[:level] >= heading[:level]
44
-
45
- if stack.empty?
46
- root << heading
47
- else
48
- stack.last[:children] << heading
49
- end
50
-
51
- stack << heading
52
- end
53
-
54
- root
55
- end
56
-
57
- def strip_html(text)
58
- text.gsub(%r{<a[^>]*class="heading-anchor"[^>]*>.*?</a>}, "")
59
- .gsub(/<[^>]+>/, "")
60
- .strip
61
- end
62
- end
63
- end
64
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Docyard
4
- module Components
5
- class TableWrapperProcessor < BaseProcessor
6
- self.priority = 100
7
-
8
- def postprocess(html)
9
- wrapped = html.gsub(/<table([^>]*)>/) do
10
- attributes = Regexp.last_match(1)
11
- "<div class=\"table-wrapper\"><table#{attributes}>"
12
- end
13
-
14
- wrapped.gsub("</table>", "</table></div>")
15
- end
16
- end
17
- end
18
- end
@@ -1,191 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "code_block_feature_extractor"
4
- require_relative "code_block_icon_detector"
5
- require_relative "code_block_line_wrapper"
6
- require_relative "icon_detector"
7
- require_relative "../icons"
8
- require_relative "../renderer"
9
- require "kramdown"
10
- require "kramdown-parser-gfm"
11
- require "cgi"
12
-
13
- module Docyard
14
- module Components
15
- class TabsParser
16
- def self.parse(content)
17
- new(content).parse
18
- end
19
-
20
- def initialize(content)
21
- @content = content
22
- end
23
-
24
- def parse
25
- sections.filter_map { |section| parse_section(section) }
26
- end
27
-
28
- private
29
-
30
- attr_reader :content
31
-
32
- def sections
33
- content.split(/^==[ \t]+/)
34
- end
35
-
36
- def parse_section(section)
37
- return nil if section.strip.empty?
38
-
39
- tab_name, tab_content = extract_tab_parts(section)
40
- return nil if tab_name.nil? || tab_name.empty?
41
-
42
- build_tab_data(tab_name, tab_content)
43
- end
44
-
45
- def extract_tab_parts(section)
46
- parts = section.split("\n", 2)
47
- [parts[0]&.strip, parts[1]&.strip || ""]
48
- end
49
-
50
- def build_tab_data(tab_name, tab_content)
51
- icon_data = IconDetector.detect(tab_name, tab_content)
52
-
53
- extracted = CodeBlockFeatureExtractor.process_markdown(tab_content)
54
- rendered_content = render_markdown(extracted[:cleaned_markdown])
55
- enhanced_content = enhance_code_blocks(rendered_content, extracted[:blocks])
56
-
57
- {
58
- name: icon_data[:name],
59
- content: enhanced_content,
60
- icon: icon_data[:icon],
61
- icon_source: icon_data[:icon_source]
62
- }
63
- end
64
-
65
- def render_markdown(markdown_content)
66
- return "" if markdown_content.empty?
67
-
68
- Kramdown::Document.new(
69
- markdown_content,
70
- input: "GFM",
71
- hard_wrap: false,
72
- syntax_highlighter: "rouge"
73
- ).to_html
74
- end
75
-
76
- def enhance_code_blocks(html, blocks)
77
- return html unless html.include?('<div class="highlight">')
78
-
79
- block_index = 0
80
- html.gsub(%r{<div class="highlight">(.*?)</div>}m) do
81
- block_data = blocks[block_index] || {}
82
- block_index += 1
83
- render_enhanced_code_block(Regexp.last_match, block_data)
84
- end
85
- end
86
-
87
- def render_enhanced_code_block(match, block_data)
88
- original_html = match[0]
89
- inner_html = match[1]
90
- code_text = extract_code_text(inner_html)
91
-
92
- processed_html = if needs_line_wrapping?(block_data)
93
- wrap_code_block_lines(original_html, block_data)
94
- else
95
- original_html
96
- end
97
-
98
- Renderer.new.render_partial("_code_block", build_full_locals(processed_html, code_text, block_data))
99
- end
100
-
101
- def needs_line_wrapping?(block_data)
102
- %i[highlights diff_lines focus_lines error_lines warning_lines].any? do |key|
103
- block_data[key]&.any?
104
- end
105
- end
106
-
107
- def wrap_code_block_lines(html, block_data)
108
- wrapper_data = {
109
- highlights: block_data[:highlights] || [],
110
- diff_lines: block_data[:diff_lines] || {},
111
- focus_lines: block_data[:focus_lines] || {},
112
- error_lines: block_data[:error_lines] || {},
113
- warning_lines: block_data[:warning_lines] || {},
114
- start_line: extract_start_line(block_data[:option])
115
- }
116
- CodeBlockLineWrapper.wrap_code_block(html, wrapper_data)
117
- end
118
-
119
- def build_full_locals(processed_html, code_text, block_data)
120
- title_data = CodeBlockIconDetector.detect(block_data[:title], block_data[:lang])
121
- show_line_numbers = line_numbers_enabled?(block_data[:option])
122
- start_line = extract_start_line(block_data[:option])
123
-
124
- base_locals(processed_html, code_text, show_line_numbers, start_line)
125
- .merge(feature_locals(block_data))
126
- .merge(title_locals(title_data))
127
- end
128
-
129
- def base_locals(processed_html, code_text, show_line_numbers, start_line)
130
- {
131
- code_block_html: processed_html,
132
- code_text: escape_html_attribute(code_text),
133
- copy_icon: Icons.render("copy", "regular") || "",
134
- show_line_numbers: show_line_numbers,
135
- line_numbers: show_line_numbers ? generate_line_numbers(code_text, start_line) : [],
136
- start_line: start_line
137
- }
138
- end
139
-
140
- def feature_locals(block_data)
141
- {
142
- highlights: block_data[:highlights] || [],
143
- diff_lines: block_data[:diff_lines] || {},
144
- focus_lines: block_data[:focus_lines] || {},
145
- error_lines: block_data[:error_lines] || {},
146
- warning_lines: block_data[:warning_lines] || {}
147
- }
148
- end
149
-
150
- def title_locals(title_data)
151
- {
152
- title: title_data[:title],
153
- icon: title_data[:icon],
154
- icon_source: title_data[:icon_source]
155
- }
156
- end
157
-
158
- def line_numbers_enabled?(block_option)
159
- return false if block_option == ":no-line-numbers"
160
- return true if block_option&.start_with?(":line-numbers")
161
-
162
- false
163
- end
164
-
165
- def extract_start_line(block_option)
166
- return 1 unless block_option&.include?("=")
167
-
168
- block_option.split("=").last.to_i
169
- end
170
-
171
- def generate_line_numbers(code_text, start_line)
172
- line_count = code_text.lines.count
173
- line_count = 1 if line_count.zero?
174
- (start_line...(start_line + line_count)).to_a
175
- end
176
-
177
- def extract_code_text(html)
178
- text = html.gsub(/<[^>]+>/, "")
179
- text = CGI.unescapeHTML(text)
180
- text.strip
181
- end
182
-
183
- def escape_html_attribute(text)
184
- text.gsub('"', "&quot;")
185
- .gsub("'", "&#39;")
186
- .gsub("<", "&lt;")
187
- .gsub(">", "&gt;")
188
- end
189
- end
190
- end
191
- end