docyard 0.6.0 → 0.7.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -1
  3. data/lib/docyard/build/static_generator.rb +2 -43
  4. data/lib/docyard/builder.rb +14 -4
  5. data/lib/docyard/cli.rb +6 -3
  6. data/lib/docyard/components/aliases.rb +29 -0
  7. data/lib/docyard/components/processors/callout_processor.rb +124 -0
  8. data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +106 -0
  9. data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +79 -0
  10. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +78 -0
  11. data/lib/docyard/components/processors/code_block_processor.rb +175 -0
  12. data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +127 -0
  13. data/lib/docyard/components/processors/heading_anchor_processor.rb +39 -0
  14. data/lib/docyard/components/processors/icon_processor.rb +53 -0
  15. data/lib/docyard/components/processors/table_of_contents_processor.rb +68 -0
  16. data/lib/docyard/components/processors/table_wrapper_processor.rb +22 -0
  17. data/lib/docyard/components/processors/tabs_processor.rb +48 -0
  18. data/lib/docyard/components/support/code_block/feature_extractor.rb +117 -0
  19. data/lib/docyard/components/support/code_block/icon_detector.rb +44 -0
  20. data/lib/docyard/components/support/code_block/line_parser.rb +84 -0
  21. data/lib/docyard/components/support/code_block/line_wrapper.rb +50 -0
  22. data/lib/docyard/components/support/code_block/patterns.rb +55 -0
  23. data/lib/docyard/components/support/code_detector.rb +61 -0
  24. data/lib/docyard/components/support/tabs/icon_detector.rb +62 -0
  25. data/lib/docyard/components/support/tabs/parser.rb +195 -0
  26. data/lib/docyard/components/support/tabs/range_finder.rb +46 -0
  27. data/lib/docyard/config/branding_resolver.rb +74 -0
  28. data/lib/docyard/{constants.rb → config/constants.rb} +1 -0
  29. data/lib/docyard/config.rb +10 -1
  30. data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +2 -2
  31. data/lib/docyard/{sidebar → navigation/sidebar}/renderer.rb +3 -14
  32. data/lib/docyard/{sidebar → navigation/sidebar}/tree_builder.rb +9 -2
  33. data/lib/docyard/{sidebar_builder.rb → navigation/sidebar_builder.rb} +3 -15
  34. data/lib/docyard/{icons → rendering/icons}/phosphor.rb +4 -1
  35. data/lib/docyard/{markdown.rb → rendering/markdown.rb} +14 -13
  36. data/lib/docyard/{renderer.rb → rendering/renderer.rb} +20 -17
  37. data/lib/docyard/search/build_indexer.rb +74 -0
  38. data/lib/docyard/search/dev_indexer.rb +110 -0
  39. data/lib/docyard/search/pagefind_support.rb +31 -0
  40. data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +1 -1
  41. data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
  42. data/lib/docyard/{preview_server.rb → server/preview_server.rb} +1 -1
  43. data/lib/docyard/{rack_application.rb → server/rack_application.rb} +52 -49
  44. data/lib/docyard/server/resolution_result.rb +29 -0
  45. data/lib/docyard/{router.rb → server/router.rb} +4 -4
  46. data/lib/docyard/templates/assets/css/components/search.css +549 -0
  47. data/lib/docyard/templates/assets/css/layout.css +15 -1
  48. data/lib/docyard/templates/assets/js/components/search.js +685 -0
  49. data/lib/docyard/templates/layouts/default.html.erb +14 -2
  50. data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
  51. data/lib/docyard/templates/partials/_heading_anchor.html.erb +1 -1
  52. data/lib/docyard/templates/partials/_prev_next.html.erb +1 -1
  53. data/lib/docyard/templates/partials/_search_modal.html.erb +45 -0
  54. data/lib/docyard/templates/partials/_search_trigger.html.erb +22 -0
  55. data/lib/docyard/utils/html_helpers.rb +14 -0
  56. data/lib/docyard/utils/path_resolver.rb +2 -1
  57. data/lib/docyard/utils/url_helpers.rb +20 -0
  58. data/lib/docyard/version.rb +1 -1
  59. data/lib/docyard.rb +22 -15
  60. metadata +57 -46
  61. data/lib/docyard/components/callout_processor.rb +0 -121
  62. data/lib/docyard/components/code_block_diff_preprocessor.rb +0 -104
  63. data/lib/docyard/components/code_block_feature_extractor.rb +0 -113
  64. data/lib/docyard/components/code_block_focus_preprocessor.rb +0 -77
  65. data/lib/docyard/components/code_block_icon_detector.rb +0 -40
  66. data/lib/docyard/components/code_block_line_wrapper.rb +0 -46
  67. data/lib/docyard/components/code_block_options_preprocessor.rb +0 -76
  68. data/lib/docyard/components/code_block_patterns.rb +0 -51
  69. data/lib/docyard/components/code_block_processor.rb +0 -176
  70. data/lib/docyard/components/code_detector.rb +0 -59
  71. data/lib/docyard/components/code_line_parser.rb +0 -80
  72. data/lib/docyard/components/code_snippet_import_preprocessor.rb +0 -125
  73. data/lib/docyard/components/heading_anchor_processor.rb +0 -34
  74. data/lib/docyard/components/icon_detector.rb +0 -57
  75. data/lib/docyard/components/icon_processor.rb +0 -51
  76. data/lib/docyard/components/table_of_contents_processor.rb +0 -64
  77. data/lib/docyard/components/table_wrapper_processor.rb +0 -18
  78. data/lib/docyard/components/tabs_parser.rb +0 -191
  79. data/lib/docyard/components/tabs_processor.rb +0 -44
  80. data/lib/docyard/components/tabs_range_finder.rb +0 -42
  81. data/lib/docyard/routing/resolution_result.rb +0 -31
  82. /data/lib/docyard/{sidebar → navigation/sidebar}/config_parser.rb +0 -0
  83. /data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +0 -0
  84. /data/lib/docyard/{sidebar → navigation/sidebar}/item.rb +0 -0
  85. /data/lib/docyard/{sidebar → navigation/sidebar}/title_extractor.rb +0 -0
  86. /data/lib/docyard/{icons → rendering/icons}/LICENSE.phosphor +0 -0
  87. /data/lib/docyard/{icons → rendering/icons}/file_types.rb +0 -0
  88. /data/lib/docyard/{icons.rb → rendering/icons.rb} +0 -0
  89. /data/lib/docyard/{language_mapping.rb → rendering/language_mapping.rb} +0 -0
  90. /data/lib/docyard/{file_watcher.rb → server/file_watcher.rb} +0 -0
  91. /data/lib/docyard/{errors.rb → utils/errors.rb} +0 -0
  92. /data/lib/docyard/{logging.rb → utils/logging.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 34bf24256bf638697dbc86f9d146667a4f17594c365070313eb19e74f8ab5134
4
- data.tar.gz: 357ea04d8822f47c98e2aee4e09ca9e8cc69b7e4d11cbdce1491874aaa459b5c
3
+ metadata.gz: e127990af44c6db555b00cfd2c7a89be408e6e4bfddf2bed9a753387f708ee87
4
+ data.tar.gz: ed54f92691fe29fc8dd83b8b797504162283bba12840c49c120a4341ac22a66c
5
5
  SHA512:
6
- metadata.gz: bc5c55d5ad69b73ef286eec3b4e202c7312b3dfd7d67b79d491612357e1c4e6e03d255a78af97706bdd5fe19353b7dd96f2da046a4c17f2e363342fc265b5628
7
- data.tar.gz: 855fdcdcf312f3778fd204ca1fd36825c183b1abf0a5c938edff599693930d519668b8fdb0770f65ae00f9db43d1b794e31ca922b29dbde73143caf6abd2f64c
6
+ metadata.gz: 7ae09bfea3ce92e5b5b8cee3e7317d6d57b253e17314adcfbf9f468cc0635a54d3cbd55a90f28807514875359e4d93b4e1c68fee99b7ec65a9ab12d34a21c6c2
7
+ data.tar.gz: 3c2148376ab27c804a148666795420afe24fe23cb890fa76fa2602af5434d9e60d589fcbb460454210d2dfe1cc38f1f8584db41b1209db14b8b21ec3d7abeb68
data/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-01-01
11
+
12
+ ### Added
13
+ - **Full-text Search** - Pagefind-powered search with Cmd/Ctrl+K modal, keyboard navigation, and highlighting (#41)
14
+ - **Search Configuration** - Customize placeholder text, enable/disable search, and exclude paths via `docyard.yml` (#41)
15
+ - **Dev Server Search** - Opt-in search indexing during development with `--search` flag (#41)
16
+
17
+ ### Changed
18
+ - Major codebase reorganization for improved maintainability (#42)
19
+ - Components reorganized into `processors/` and `support/` subdirectories (#42)
20
+ - Consolidated `server/`, `rendering/`, `navigation/`, `config/`, and `search/` modules (#42)
21
+ - Extracted shared utilities into `utils/` module (#42)
22
+
10
23
  ## [0.6.0] - 2025-12-25
11
24
 
12
25
  ### Added
@@ -109,7 +122,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
109
122
  - Initial gem structure
110
123
  - Project scaffolding
111
124
 
112
- [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.6.0...HEAD
125
+ [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.7.0...HEAD
126
+ [0.7.0]: https://github.com/sanifhimani/docyard/compare/v0.6.0...v0.7.0
113
127
  [0.6.0]: https://github.com/sanifhimani/docyard/compare/v0.5.0...v0.6.0
114
128
  [0.5.0]: https://github.com/sanifhimani/docyard/compare/v0.4.0...v0.5.0
115
129
  [0.4.0]: https://github.com/sanifhimani/docyard/compare/v0.3.0...v0.4.0
@@ -18,7 +18,7 @@ module Docyard
18
18
  puts "\n[✓] Found #{markdown_files.size} markdown files"
19
19
 
20
20
  progress = TTY::ProgressBar.new(
21
- "Generating pages [:bar] :current/:total (:percent%)",
21
+ "Generating pages [:bar] :current/:total (:percent)",
22
22
  total: markdown_files.size,
23
23
  width: 50
24
24
  )
@@ -89,48 +89,7 @@ module Docyard
89
89
  end
90
90
 
91
91
  def branding_options
92
- default_branding.merge(config_branding_options)
93
- end
94
-
95
- def default_branding
96
- {
97
- site_title: Constants::DEFAULT_SITE_TITLE,
98
- site_description: "",
99
- logo: Constants::DEFAULT_LOGO_PATH,
100
- logo_dark: Constants::DEFAULT_LOGO_DARK_PATH,
101
- favicon: nil,
102
- display_logo: true,
103
- display_title: true
104
- }
105
- end
106
-
107
- def config_branding_options
108
- site = config.site
109
- branding = config.branding
110
-
111
- {
112
- site_title: site.title || Constants::DEFAULT_SITE_TITLE,
113
- site_description: site.description || "",
114
- logo: resolve_logo(branding.logo, branding.logo_dark),
115
- logo_dark: resolve_logo_dark(branding.logo, branding.logo_dark),
116
- favicon: branding.favicon
117
- }.merge(appearance_options(branding.appearance))
118
- end
119
-
120
- def appearance_options(appearance)
121
- appearance ||= {}
122
- {
123
- display_logo: appearance["logo"] != false,
124
- display_title: appearance["title"] != false
125
- }
126
- end
127
-
128
- def resolve_logo(logo, logo_dark)
129
- logo || logo_dark || Constants::DEFAULT_LOGO_PATH
130
- end
131
-
132
- def resolve_logo_dark(logo, logo_dark)
133
- logo_dark || logo || Constants::DEFAULT_LOGO_DARK_PATH
92
+ BrandingResolver.new(config).resolve
134
93
  end
135
94
 
136
95
  def log(message)
@@ -22,8 +22,9 @@ module Docyard
22
22
  bundles_created = bundle_assets
23
23
  assets_copied = copy_static_files
24
24
  generate_seo_files
25
+ pages_indexed = generate_search_index
25
26
 
26
- display_summary(pages_built, bundles_created, assets_copied)
27
+ display_summary(pages_built, bundles_created, assets_copied, pages_indexed)
27
28
  true
28
29
  rescue StandardError => e
29
30
  error "Build failed: #{e.message}"
@@ -68,7 +69,12 @@ module Docyard
68
69
  sitemap_gen.generate
69
70
 
70
71
  File.write(File.join(config.build.output_dir, "robots.txt"), robots_txt_content)
71
- log "[] Generated robots.txt"
72
+ log "[+] Generated robots.txt"
73
+ end
74
+
75
+ def generate_search_index
76
+ indexer = Search::BuildIndexer.new(config, verbose: verbose)
77
+ indexer.index
72
78
  end
73
79
 
74
80
  def robots_txt_content
@@ -83,13 +89,17 @@ module Docyard
83
89
  ROBOTS
84
90
  end
85
91
 
86
- def display_summary(pages, bundles, assets)
92
+ def display_summary(pages, bundles, assets, indexed = 0)
87
93
  elapsed = Time.now - start_time
88
94
 
89
95
  puts "\n#{'=' * 50}"
90
96
  puts "Build complete in #{format('%.2f', elapsed)}s"
91
97
  puts "Output: #{config.build.output_dir}/"
92
- puts "#{pages} pages, #{bundles} bundles, #{assets} static files"
98
+
99
+ summary = "#{pages} pages, #{bundles} bundles, #{assets} static files"
100
+ summary += ", #{indexed} pages indexed" if indexed.positive?
101
+ puts summary
102
+
93
103
  puts "=" * 50
94
104
  end
95
105
 
data/lib/docyard/cli.rb CHANGED
@@ -34,18 +34,21 @@ module Docyard
34
34
  desc "preview", "Preview the built site locally"
35
35
  method_option :port, type: :numeric, default: 4000, aliases: "-p", desc: "Port to run preview server on"
36
36
  def preview
37
- require_relative "preview_server"
37
+ require_relative "server/preview_server"
38
38
  Docyard::PreviewServer.new(port: options[:port]).start
39
39
  end
40
40
 
41
41
  desc "serve", "Start the development server"
42
42
  method_option :port, type: :numeric, default: 4200, aliases: "-p", desc: "Port to run the server on"
43
43
  method_option :host, type: :string, default: "localhost", aliases: "-h", desc: "Host to bind the server to"
44
+ method_option :search, type: :boolean, default: false, aliases: "-s",
45
+ desc: "Enable search indexing (slower startup)"
44
46
  def serve
45
- require_relative "server"
47
+ require_relative "server/dev_server"
46
48
  server = Docyard::Server.new(
47
49
  port: options[:port],
48
- host: options[:host]
50
+ host: options[:host],
51
+ search: options[:search]
49
52
  )
50
53
  server.start
51
54
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ CalloutProcessor = Processors::CalloutProcessor
6
+ CodeBlockProcessor = Processors::CodeBlockProcessor
7
+ CodeBlockDiffPreprocessor = Processors::CodeBlockDiffPreprocessor
8
+ CodeBlockFocusPreprocessor = Processors::CodeBlockFocusPreprocessor
9
+ CodeBlockOptionsPreprocessor = Processors::CodeBlockOptionsPreprocessor
10
+ CodeSnippetImportPreprocessor = Processors::CodeSnippetImportPreprocessor
11
+ HeadingAnchorProcessor = Processors::HeadingAnchorProcessor
12
+ IconProcessor = Processors::IconProcessor
13
+ TableOfContentsProcessor = Processors::TableOfContentsProcessor
14
+ TableWrapperProcessor = Processors::TableWrapperProcessor
15
+ TabsProcessor = Processors::TabsProcessor
16
+
17
+ CodeDetector = Support::CodeDetector
18
+ IconDetector = Support::Tabs::IconDetector
19
+
20
+ CodeBlockFeatureExtractor = Support::CodeBlock::FeatureExtractor
21
+ CodeBlockIconDetector = Support::CodeBlock::IconDetector
22
+ CodeBlockLineWrapper = Support::CodeBlock::LineWrapper
23
+ CodeBlockPatterns = Support::CodeBlock::Patterns
24
+ CodeLineParser = Support::CodeBlock::LineParser
25
+
26
+ TabsParser = Support::Tabs::Parser
27
+ TabsRangeFinder = Support::Tabs::RangeFinder
28
+ end
29
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../rendering/icons"
4
+ require_relative "../../rendering/renderer"
5
+ require_relative "../base_processor"
6
+ require "kramdown"
7
+ require "kramdown-parser-gfm"
8
+
9
+ module Docyard
10
+ module Components
11
+ module Processors
12
+ class CalloutProcessor < BaseProcessor
13
+ self.priority = 10
14
+
15
+ CALLOUT_TYPES = {
16
+ "note" => { title: "Note", icon: "info", color: "note" },
17
+ "tip" => { title: "Tip", icon: "lightbulb", color: "tip" },
18
+ "important" => { title: "Important", icon: "warning-circle", color: "important" },
19
+ "warning" => { title: "Warning", icon: "warning", color: "warning" },
20
+ "danger" => { title: "Danger", icon: "siren", color: "danger" }
21
+ }.freeze
22
+
23
+ GITHUB_ALERT_TYPES = {
24
+ "NOTE" => "note",
25
+ "TIP" => "tip",
26
+ "IMPORTANT" => "important",
27
+ "WARNING" => "warning",
28
+ "CAUTION" => "danger"
29
+ }.freeze
30
+
31
+ def preprocess(markdown)
32
+ process_container_syntax(markdown)
33
+ end
34
+
35
+ def postprocess(html)
36
+ process_github_alerts(html)
37
+ end
38
+
39
+ private
40
+
41
+ def process_container_syntax(markdown)
42
+ 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))
45
+ end
46
+ end
47
+
48
+ def process_callout_match(original_match, type_raw, custom_title, content_markdown)
49
+ type = type_raw.downcase
50
+ return original_match unless CALLOUT_TYPES.key?(type)
51
+
52
+ config = CALLOUT_TYPES[type]
53
+ title = determine_title(custom_title, config[:title])
54
+ content_html = render_markdown_content(content_markdown.strip)
55
+
56
+ wrap_in_nomarkdown(render_callout_html(type, title, content_html, config[:icon]))
57
+ end
58
+
59
+ def determine_title(custom_title, default_title)
60
+ title = custom_title&.strip
61
+ title.nil? || title.empty? ? default_title : title
62
+ end
63
+
64
+ def render_markdown_content(content_markdown)
65
+ return "" if content_markdown.empty?
66
+
67
+ Kramdown::Document.new(
68
+ content_markdown,
69
+ input: "GFM",
70
+ hard_wrap: false,
71
+ syntax_highlighter: "rouge"
72
+ ).to_html
73
+ end
74
+
75
+ def wrap_in_nomarkdown(html)
76
+ "{::nomarkdown}\n#{html}\n{:/nomarkdown}"
77
+ end
78
+
79
+ def process_github_alerts(html)
80
+ github_alert_regex = %r{
81
+ <blockquote>\s*
82
+ <p>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*
83
+ (?:<br\s*/>)?\s*
84
+ (.*?)</p>
85
+ (.*?)
86
+ </blockquote>
87
+ }mx
88
+
89
+ html.gsub(github_alert_regex) do
90
+ process_github_alert_match(Regexp.last_match(1), Regexp.last_match(2), Regexp.last_match(3))
91
+ end
92
+ end
93
+
94
+ def process_github_alert_match(alert_type, first_para, rest_content)
95
+ type = GITHUB_ALERT_TYPES[alert_type]
96
+ config = CALLOUT_TYPES[type]
97
+ content_html = combine_alert_content(first_para.strip, rest_content.strip)
98
+
99
+ render_callout_html(type, config[:title], content_html, config[:icon])
100
+ end
101
+
102
+ def combine_alert_content(first_para, rest_content)
103
+ return "<p>#{first_para}</p>" if rest_content.empty?
104
+
105
+ "<p>#{first_para}</p>#{rest_content}"
106
+ end
107
+
108
+ def render_callout_html(type, title, content_html, icon_name)
109
+ icon_svg = Icons.render(icon_name, "duotone") || ""
110
+ renderer = Renderer.new
111
+
112
+ renderer.render_partial(
113
+ "_callout", {
114
+ type: type,
115
+ title: title,
116
+ content_html: content_html,
117
+ icon_svg: icon_svg
118
+ }
119
+ )
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+ require_relative "../support/code_block/patterns"
5
+
6
+ module Docyard
7
+ module Components
8
+ module Processors
9
+ class CodeBlockDiffPreprocessor < BaseProcessor
10
+ include Support::CodeBlock::Patterns
11
+
12
+ self.priority = 6
13
+
14
+ CODE_BLOCK_REGEX = /^```(\w*).*?\n(.*?)^```/m
15
+ TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
16
+
17
+ def preprocess(content)
18
+ context[:code_block_diff_lines] ||= []
19
+ context[:code_block_error_lines] ||= []
20
+ context[:code_block_warning_lines] ||= []
21
+ @block_index = 0
22
+ @tabs_ranges = find_tabs_ranges(content)
23
+
24
+ content.gsub(CODE_BLOCK_REGEX) { |_| process_code_block(Regexp.last_match) }
25
+ end
26
+
27
+ private
28
+
29
+ def process_code_block(match)
30
+ return match[0] if inside_tabs?(match.begin(0))
31
+
32
+ result = extract_all_markers(match[2])
33
+ store_extracted_markers(result)
34
+ @block_index += 1
35
+ match[0].sub(match[2], result[:cleaned_content])
36
+ end
37
+
38
+ def store_extracted_markers(result)
39
+ context[:code_block_diff_lines][@block_index] = result[:diff_lines]
40
+ context[:code_block_error_lines][@block_index] = result[:error_lines]
41
+ context[:code_block_warning_lines][@block_index] = result[:warning_lines]
42
+ end
43
+
44
+ def extract_all_markers(code_content)
45
+ diff_info = extract_diff_lines(code_content)
46
+ error_info = extract_error_lines(diff_info[:cleaned_content])
47
+ warning_info = extract_warning_lines(error_info[:cleaned_content])
48
+
49
+ {
50
+ diff_lines: diff_info[:lines],
51
+ error_lines: error_info[:lines],
52
+ warning_lines: warning_info[:lines],
53
+ cleaned_content: warning_info[:cleaned_content]
54
+ }
55
+ end
56
+
57
+ def extract_diff_lines(code_content)
58
+ extract_marker_lines(code_content, DIFF_MARKER_PATTERN) do |match|
59
+ diff_type = match.captures.compact.first
60
+ diff_type == "++" ? :addition : :deletion
61
+ end
62
+ end
63
+
64
+ def extract_error_lines(code_content)
65
+ extract_marker_lines(code_content, ERROR_MARKER_PATTERN) { true }
66
+ end
67
+
68
+ def extract_warning_lines(code_content)
69
+ extract_marker_lines(code_content, WARNING_MARKER_PATTERN) { true }
70
+ end
71
+
72
+ def extract_marker_lines(code_content, pattern)
73
+ lines = code_content.lines
74
+ marker_lines = {}
75
+ cleaned_lines = []
76
+
77
+ lines.each_with_index do |line, index|
78
+ line_num = index + 1
79
+
80
+ if (match = line.match(pattern))
81
+ marker_lines[line_num] = yield(match)
82
+ cleaned_lines << line.gsub(pattern, "")
83
+ else
84
+ cleaned_lines << line
85
+ end
86
+ end
87
+
88
+ { lines: marker_lines, cleaned_content: cleaned_lines.join }
89
+ end
90
+
91
+ def inside_tabs?(position)
92
+ @tabs_ranges.any? { |range| range.cover?(position) }
93
+ end
94
+
95
+ def find_tabs_ranges(content)
96
+ ranges = []
97
+ content.scan(TABS_BLOCK_REGEX) do
98
+ match = Regexp.last_match
99
+ ranges << (match.begin(0)...match.end(0))
100
+ end
101
+ ranges
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Processors
8
+ class CodeBlockFocusPreprocessor < BaseProcessor
9
+ self.priority = 7
10
+
11
+ FOCUS_MARKER_PATTERN = %r{
12
+ (?:
13
+ //\s*\[!code\s+focus\] |
14
+ \#\s*\[!code\s+focus\] |
15
+ /\*\s*\[!code\s+focus\]\s*\*/ |
16
+ --\s*\[!code\s+focus\] |
17
+ <!--\s*\[!code\s+focus\]\s*--> |
18
+ ;\s*\[!code\s+focus\]
19
+ )[^\S\n]*
20
+ }x
21
+
22
+ CODE_BLOCK_REGEX = /^```(\w*).*?\n(.*?)^```/m
23
+ TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
24
+
25
+ def preprocess(content)
26
+ context[:code_block_focus_lines] ||= []
27
+ @block_index = 0
28
+ @tabs_ranges = find_tabs_ranges(content)
29
+
30
+ content.gsub(CODE_BLOCK_REGEX) { |_| process_code_block(Regexp.last_match) }
31
+ end
32
+
33
+ private
34
+
35
+ def process_code_block(match)
36
+ return match[0] if inside_tabs?(match.begin(0))
37
+
38
+ focus_info = extract_focus_lines(match[2])
39
+ context[:code_block_focus_lines][@block_index] = focus_info[:lines]
40
+ @block_index += 1
41
+ match[0].sub(match[2], focus_info[:cleaned_content])
42
+ end
43
+
44
+ def extract_focus_lines(code_content)
45
+ lines = code_content.lines
46
+ focus_lines = {}
47
+ cleaned_lines = []
48
+
49
+ lines.each_with_index do |line, index|
50
+ line_num = index + 1
51
+
52
+ if line.match?(FOCUS_MARKER_PATTERN)
53
+ focus_lines[line_num] = true
54
+ cleaned_line = line.gsub(FOCUS_MARKER_PATTERN, "")
55
+ cleaned_lines << cleaned_line
56
+ else
57
+ cleaned_lines << line
58
+ end
59
+ end
60
+
61
+ { lines: focus_lines, cleaned_content: cleaned_lines.join }
62
+ end
63
+
64
+ def inside_tabs?(position)
65
+ @tabs_ranges.any? { |range| range.cover?(position) }
66
+ end
67
+
68
+ def find_tabs_ranges(content)
69
+ ranges = []
70
+ content.scan(TABS_BLOCK_REGEX) do
71
+ match = Regexp.last_match
72
+ ranges << (match.begin(0)...match.end(0))
73
+ end
74
+ ranges
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ module Processors
8
+ class CodeBlockOptionsPreprocessor < BaseProcessor
9
+ self.priority = 5
10
+
11
+ CODE_FENCE_REGEX = /^```(\w+)(?:\s*\[([^\]]+)\])?(:\S+)?(?:\s*\{([^}\n]+)\})?/
12
+ TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
13
+
14
+ def preprocess(content)
15
+ context[:code_block_options] ||= []
16
+ @tabs_ranges = find_tabs_ranges(content)
17
+
18
+ process_code_fences(content)
19
+ end
20
+
21
+ private
22
+
23
+ def process_code_fences(content)
24
+ result = +""
25
+ last_end = 0
26
+
27
+ content.scan(CODE_FENCE_REGEX) do
28
+ match = Regexp.last_match
29
+ result << content[last_end...match.begin(0)]
30
+ result << process_fence_match(match)
31
+ last_end = match.end(0)
32
+ end
33
+
34
+ result << content[last_end..]
35
+ end
36
+
37
+ def process_fence_match(match)
38
+ store_code_block_options(match) unless inside_tabs?(match.begin(0))
39
+ "```#{match[1]}"
40
+ end
41
+
42
+ def store_code_block_options(match)
43
+ context[:code_block_options] << {
44
+ lang: match[1],
45
+ title: match[2],
46
+ option: match[3],
47
+ highlights: parse_highlights(match[4])
48
+ }
49
+ end
50
+
51
+ def inside_tabs?(position)
52
+ @tabs_ranges.any? { |range| range.cover?(position) }
53
+ end
54
+
55
+ def find_tabs_ranges(content)
56
+ ranges = []
57
+ content.scan(TABS_BLOCK_REGEX) do
58
+ match = Regexp.last_match
59
+ ranges << (match.begin(0)...match.end(0))
60
+ end
61
+ ranges
62
+ end
63
+
64
+ def parse_highlights(highlights_str)
65
+ return [] if highlights_str.nil? || highlights_str.strip.empty?
66
+
67
+ highlights_str.split(",").flat_map { |part| parse_highlight_part(part.strip) }.uniq.sort
68
+ end
69
+
70
+ def parse_highlight_part(part)
71
+ return (part.split("-")[0].to_i..part.split("-")[1].to_i).to_a if part.include?("-")
72
+
73
+ [part.to_i]
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end