docyard 0.4.0 → 0.6.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +29 -3
  4. data/README.md +37 -12
  5. data/lib/docyard/build/static_generator.rb +1 -1
  6. data/lib/docyard/components/base_processor.rb +6 -0
  7. data/lib/docyard/components/code_block_diff_preprocessor.rb +104 -0
  8. data/lib/docyard/components/code_block_feature_extractor.rb +113 -0
  9. data/lib/docyard/components/code_block_focus_preprocessor.rb +77 -0
  10. data/lib/docyard/components/code_block_icon_detector.rb +40 -0
  11. data/lib/docyard/components/code_block_line_wrapper.rb +46 -0
  12. data/lib/docyard/components/code_block_options_preprocessor.rb +76 -0
  13. data/lib/docyard/components/code_block_patterns.rb +51 -0
  14. data/lib/docyard/components/code_block_processor.rb +135 -14
  15. data/lib/docyard/components/code_line_parser.rb +80 -0
  16. data/lib/docyard/components/code_snippet_import_preprocessor.rb +125 -0
  17. data/lib/docyard/components/heading_anchor_processor.rb +34 -0
  18. data/lib/docyard/components/registry.rb +4 -4
  19. data/lib/docyard/components/table_of_contents_processor.rb +64 -0
  20. data/lib/docyard/components/tabs_parser.rb +135 -4
  21. data/lib/docyard/components/tabs_range_finder.rb +42 -0
  22. data/lib/docyard/config/validator.rb +8 -0
  23. data/lib/docyard/config.rb +18 -0
  24. data/lib/docyard/icons/file_types.rb +0 -13
  25. data/lib/docyard/icons/phosphor.rb +2 -1
  26. data/lib/docyard/markdown.rb +18 -4
  27. data/lib/docyard/prev_next_builder.rb +159 -0
  28. data/lib/docyard/rack_application.rb +25 -3
  29. data/lib/docyard/renderer.rb +20 -8
  30. data/lib/docyard/templates/assets/css/code.css +12 -4
  31. data/lib/docyard/templates/assets/css/components/code-block.css +427 -24
  32. data/lib/docyard/templates/assets/css/components/heading-anchor.css +77 -0
  33. data/lib/docyard/templates/assets/css/components/navigation.css +12 -9
  34. data/lib/docyard/templates/assets/css/components/prev-next.css +114 -0
  35. data/lib/docyard/templates/assets/css/components/table-of-contents.css +269 -0
  36. data/lib/docyard/templates/assets/css/components/tabs.css +50 -44
  37. data/lib/docyard/templates/assets/css/layout.css +58 -1
  38. data/lib/docyard/templates/assets/css/variables.css +45 -0
  39. data/lib/docyard/templates/assets/js/components/heading-anchor.js +90 -0
  40. data/lib/docyard/templates/assets/js/components/navigation.js +6 -2
  41. data/lib/docyard/templates/assets/js/components/table-of-contents.js +301 -0
  42. data/lib/docyard/templates/layouts/default.html.erb +9 -1
  43. data/lib/docyard/templates/partials/_code_block.html.erb +50 -2
  44. data/lib/docyard/templates/partials/_heading_anchor.html.erb +1 -0
  45. data/lib/docyard/templates/partials/_prev_next.html.erb +23 -0
  46. data/lib/docyard/templates/partials/_table_of_contents.html.erb +45 -0
  47. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +8 -0
  48. data/lib/docyard/version.rb +1 -1
  49. metadata +23 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db5ddabfe4a3b7bf25c24876b915188bd4768ca543772ad0a8cfc971ee791fee
4
- data.tar.gz: d906b1c2ab33102c9ade6e2cb23f6d067fdbf240d6a4a8e2aa27a52a9ba62b42
3
+ metadata.gz: 34bf24256bf638697dbc86f9d146667a4f17594c365070313eb19e74f8ab5134
4
+ data.tar.gz: 357ea04d8822f47c98e2aee4e09ca9e8cc69b7e4d11cbdce1491874aaa459b5c
5
5
  SHA512:
6
- metadata.gz: 2abc1d991cade7059cfaa0b54c2b6f88b33625937e8073cee9d8f61948149567a209a378efd225e9fe85b45b7579ef4e1d81599be5a8cbee970e9cb6e5218a3c
7
- data.tar.gz: dcd2baa65fcdd4c00825c6f895c035ffd53603ec22503233b600925084b6512b651c9a009687682f1f5cab3a0c8d8e3d0a5c7586634d6da174e4e4c5a95f9a53
6
+ metadata.gz: bc5c55d5ad69b73ef286eec3b4e202c7312b3dfd7d67b79d491612357e1c4e6e03d255a78af97706bdd5fe19353b7dd96f2da046a4c17f2e363342fc265b5628
7
+ data.tar.gz: 855fdcdcf312f3778fd204ca1fd36825c183b1abf0a5c938edff599693930d519668b8fdb0770f65ae00f9db43d1b794e31ca922b29dbde73143caf6abd2f64c
data/.rubocop.yml CHANGED
@@ -16,7 +16,7 @@ Metrics/BlockLength:
16
16
  - '*.gemspec'
17
17
 
18
18
  RSpec/ExampleLength:
19
- Max: 10
19
+ Max: 15
20
20
 
21
21
  Metrics/ClassLength:
22
22
  Max: 150
data/CHANGELOG.md CHANGED
@@ -7,7 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [0.4.0] - 2025-01-16
10
+ ## [0.6.0] - 2025-12-25
11
+
12
+ ### Added
13
+ - **Line Numbers** - Display line numbers on code blocks with `:line-numbers` or `:line-numbers=N` syntax, plus global config option (#33)
14
+ - **Line Highlighting** - Highlight specific lines in code blocks with `{1,3,5-7}` syntax (#34)
15
+ - **Diff Markers** - Show additions/deletions with `// [!code ++]` and `// [!code --]` comments, supports all major comment styles (#35)
16
+ - **Code Block Titles** - Add filename titles to code blocks with `[filename.js]` syntax, auto-detects file icons (#36)
17
+ - **Focus Mode** - Dim surrounding code with `// [!code focus]` to highlight important lines (#37)
18
+ - **Error/Warning Markers** - Highlight problematic lines with `// [!code error]` and `// [!code warning]` (#38)
19
+ - **Code Snippet Imports** - Import code from external files with `<<< @/filepath` syntax (#39)
20
+ - **VS Code Regions** - Import specific code sections with `<<< @/filepath#region-name` (#39)
21
+ - **Line Range Extraction** - Extract specific lines from imports with `<<< @/filepath{2-10}` (#39)
22
+ - **Language Override** - Override auto-detected language in imports with `<<< @/filepath{js}` (#39)
23
+
24
+ ### Changed
25
+ - Code block processor refactored for better maintainability with shared patterns module
26
+ - Improved code block CSS with support for all new marker types
27
+
28
+ ## [0.5.0] - 2025-11-18
29
+
30
+ ### Added
31
+ - **Table of Contents** - Auto-generated TOC from h2-h4 headings with clickable anchor links and smooth scrolling (#30)
32
+ - **Previous/Next Navigation** - Auto-detection from sidebar order with frontmatter override support and configurable labels (#31)
33
+
34
+ ## [0.4.0] - 2025-11-16
11
35
 
12
36
  ### Added
13
37
  - **Static site generation** - Build system with `docyard build` command (#27)
@@ -29,7 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
29
53
  - Component CSS accessibility and performance improvements (#24)
30
54
  - Table responsive styling with proper wrapper element (#23)
31
55
 
32
- ## [0.3.0] - 2025-01-09
56
+ ## [0.3.0] - 2025-11-09
33
57
 
34
58
  ### Added
35
59
  - Configuration system with optional `docyard.yml` file (#20)
@@ -85,7 +109,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
85
109
  - Initial gem structure
86
110
  - Project scaffolding
87
111
 
88
- [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.4.0...HEAD
112
+ [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.6.0...HEAD
113
+ [0.6.0]: https://github.com/sanifhimani/docyard/compare/v0.5.0...v0.6.0
114
+ [0.5.0]: https://github.com/sanifhimani/docyard/compare/v0.4.0...v0.5.0
89
115
  [0.4.0]: https://github.com/sanifhimani/docyard/compare/v0.3.0...v0.4.0
90
116
  [0.3.0]: https://github.com/sanifhimani/docyard/compare/v0.2.0...v0.3.0
91
117
  [0.2.0]: https://github.com/sanifhimani/docyard/compare/v0.1.0...v0.2.0
data/README.md CHANGED
@@ -20,6 +20,8 @@ Build beautiful documentation sites with hot reload, dark mode, and powerful mar
20
20
  ### Navigation
21
21
  - **Sidebar navigation** - Automatic sidebar with nested folders and collapsible sections
22
22
  - **Sidebar customization** - Custom ordering, icons, and external links via config
23
+ - **Table of Contents** - Auto-generated TOC with heading anchors and smooth scrolling
24
+ - **Previous/Next navigation** - Auto-detection from sidebar with frontmatter override support
23
25
  - **Active page highlighting** - Always know where you are
24
26
 
25
27
  ### Markdown
@@ -139,6 +141,32 @@ description: Page description
139
141
 
140
142
  Currently supported:
141
143
  - `title` - Page title (shown in `<title>` tag)
144
+ - `prev` - Customize or disable previous link
145
+ - `next` - Customize or disable next link
146
+
147
+ ### Customizing Navigation
148
+
149
+ Control previous/next links per page via frontmatter:
150
+
151
+ ```yaml
152
+ ---
153
+ title: My Page
154
+ prev: false # Disable previous link
155
+ next:
156
+ text: Custom Next Page
157
+ link: /custom-path
158
+ ---
159
+ ```
160
+
161
+ Configure labels globally in `docyard.yml`:
162
+
163
+ ```yaml
164
+ navigation:
165
+ footer:
166
+ enabled: true
167
+ prev_text: "← Back"
168
+ next_text: "Forward →"
169
+ ```
142
170
 
143
171
  ### Linking Between Pages
144
172
 
@@ -227,18 +255,15 @@ bundle exec rubocop
227
255
 
228
256
  ## Roadmap
229
257
 
230
- **v0.4.0 - Just shipped:**
231
- - Static site generation with `docyard build`
232
- - Asset bundling with minification and cache busting
233
- - SEO files (sitemap.xml, robots.txt)
234
- - Preview server for testing builds
235
- - Sidebar customization via config
236
- - Improved init templates
237
-
238
- **Next up:**
239
- - Search functionality
240
- - Table of contents
241
- - More markdown components
258
+ **v0.5.0 - Just shipped:**
259
+ - Table of Contents with heading anchors
260
+ - Previous/Next page navigation with auto-detection
261
+
262
+ **Next up (v0.6.0+):**
263
+ - Code block enhancements (line numbers, highlighting, diffs)
264
+ - Search functionality (client-side with Cmd/K)
265
+ - Details/collapsible blocks
266
+ - More markdown extensions
242
267
 
243
268
  ## Contributing
244
269
 
@@ -10,7 +10,7 @@ module Docyard
10
10
  def initialize(config, verbose: false)
11
11
  @config = config
12
12
  @verbose = verbose
13
- @renderer = Renderer.new(base_url: config.build.base_url)
13
+ @renderer = Renderer.new(base_url: config.build.base_url, config: config)
14
14
  end
15
15
 
16
16
  def generate
@@ -12,6 +12,12 @@ module Docyard
12
12
  end
13
13
  end
14
14
 
15
+ attr_reader :context
16
+
17
+ def initialize(context = {})
18
+ @context = context
19
+ end
20
+
15
21
  def preprocess(content)
16
22
  content
17
23
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_processor"
4
+ require_relative "code_block_patterns"
5
+
6
+ module Docyard
7
+ module Components
8
+ class CodeBlockDiffPreprocessor < BaseProcessor
9
+ include CodeBlockPatterns
10
+
11
+ self.priority = 6
12
+
13
+ CODE_BLOCK_REGEX = /^```(\w*).*?\n(.*?)^```/m
14
+ TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
15
+
16
+ def preprocess(content)
17
+ context[:code_block_diff_lines] ||= []
18
+ context[:code_block_error_lines] ||= []
19
+ context[:code_block_warning_lines] ||= []
20
+ @block_index = 0
21
+ @tabs_ranges = find_tabs_ranges(content)
22
+
23
+ content.gsub(CODE_BLOCK_REGEX) { |_| process_code_block(Regexp.last_match) }
24
+ end
25
+
26
+ private
27
+
28
+ def process_code_block(match)
29
+ return match[0] if inside_tabs?(match.begin(0))
30
+
31
+ result = extract_all_markers(match[2])
32
+ store_extracted_markers(result)
33
+ @block_index += 1
34
+ match[0].sub(match[2], result[:cleaned_content])
35
+ end
36
+
37
+ def store_extracted_markers(result)
38
+ context[:code_block_diff_lines][@block_index] = result[:diff_lines]
39
+ context[:code_block_error_lines][@block_index] = result[:error_lines]
40
+ context[:code_block_warning_lines][@block_index] = result[:warning_lines]
41
+ end
42
+
43
+ def extract_all_markers(code_content)
44
+ diff_info = extract_diff_lines(code_content)
45
+ error_info = extract_error_lines(diff_info[:cleaned_content])
46
+ warning_info = extract_warning_lines(error_info[:cleaned_content])
47
+
48
+ {
49
+ diff_lines: diff_info[:lines],
50
+ error_lines: error_info[:lines],
51
+ warning_lines: warning_info[:lines],
52
+ cleaned_content: warning_info[:cleaned_content]
53
+ }
54
+ end
55
+
56
+ def extract_diff_lines(code_content)
57
+ extract_marker_lines(code_content, DIFF_MARKER_PATTERN) do |match|
58
+ diff_type = match.captures.compact.first
59
+ diff_type == "++" ? :addition : :deletion
60
+ end
61
+ end
62
+
63
+ def extract_error_lines(code_content)
64
+ extract_marker_lines(code_content, ERROR_MARKER_PATTERN) { true }
65
+ end
66
+
67
+ def extract_warning_lines(code_content)
68
+ extract_marker_lines(code_content, WARNING_MARKER_PATTERN) { true }
69
+ end
70
+
71
+ def extract_marker_lines(code_content, pattern)
72
+ lines = code_content.lines
73
+ marker_lines = {}
74
+ cleaned_lines = []
75
+
76
+ lines.each_with_index do |line, index|
77
+ line_num = index + 1
78
+
79
+ if (match = line.match(pattern))
80
+ marker_lines[line_num] = yield(match)
81
+ cleaned_lines << line.gsub(pattern, "")
82
+ else
83
+ cleaned_lines << line
84
+ end
85
+ end
86
+
87
+ { lines: marker_lines, cleaned_content: cleaned_lines.join }
88
+ end
89
+
90
+ def inside_tabs?(position)
91
+ @tabs_ranges.any? { |range| range.cover?(position) }
92
+ end
93
+
94
+ def find_tabs_ranges(content)
95
+ ranges = []
96
+ content.scan(TABS_BLOCK_REGEX) do
97
+ match = Regexp.last_match
98
+ ranges << (match.begin(0)...match.end(0))
99
+ end
100
+ ranges
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "code_block_patterns"
4
+
5
+ module Docyard
6
+ module Components
7
+ module CodeBlockFeatureExtractor
8
+ include CodeBlockPatterns
9
+
10
+ CODE_FENCE_REGEX = /^```(\w+)(?:\s*\[([^\]]+)\])?(:\S+)?(?:\s*\{([^}\n]+)\})?[ \t]*\n(.*?)^```/m
11
+
12
+ module_function
13
+
14
+ def process_markdown(markdown)
15
+ blocks = []
16
+ cleaned = markdown.gsub(CODE_FENCE_REGEX) do
17
+ block_data = extract_block_data(Regexp.last_match)
18
+ blocks << block_data
19
+ "```#{block_data[:lang]}\n#{block_data[:cleaned_content]}```"
20
+ end
21
+ { cleaned_markdown: cleaned, blocks: blocks }
22
+ end
23
+
24
+ def extract_block_data(match)
25
+ code_content = match[5]
26
+ diff_info = extract_diff_lines(code_content)
27
+ focus_info = extract_focus_lines(diff_info[:cleaned_content])
28
+ error_info = extract_error_lines(focus_info[:cleaned_content])
29
+ warning_info = extract_warning_lines(error_info[:cleaned_content])
30
+
31
+ build_block_result(match, diff_info, focus_info, error_info, warning_info)
32
+ end
33
+
34
+ def build_block_result(match, diff_info, focus_info, error_info, warning_info)
35
+ {
36
+ lang: match[1],
37
+ title: match[2],
38
+ option: match[3],
39
+ highlights: parse_highlights(match[4]),
40
+ diff_lines: diff_info[:lines],
41
+ focus_lines: focus_info[:lines],
42
+ error_lines: error_info[:lines],
43
+ warning_lines: warning_info[:lines],
44
+ cleaned_content: warning_info[:cleaned_content]
45
+ }
46
+ end
47
+
48
+ def parse_highlights(highlights_str)
49
+ return [] if highlights_str.nil? || highlights_str.strip.empty?
50
+
51
+ highlights_str.split(",").flat_map { |part| parse_highlight_part(part.strip) }.uniq.sort
52
+ end
53
+
54
+ def parse_highlight_part(part)
55
+ return (part.split("-")[0].to_i..part.split("-")[1].to_i).to_a if part.include?("-")
56
+
57
+ [part.to_i]
58
+ end
59
+
60
+ def extract_diff_lines(code_content)
61
+ lines = code_content.lines
62
+ diff_lines = {}
63
+ cleaned_lines = []
64
+
65
+ lines.each_with_index do |line, index|
66
+ line_num = index + 1
67
+
68
+ if (match = line.match(DIFF_MARKER_PATTERN))
69
+ diff_type = match.captures.compact.first
70
+ diff_lines[line_num] = diff_type == "++" ? :addition : :deletion
71
+ cleaned_line = line.gsub(DIFF_MARKER_PATTERN, "")
72
+ cleaned_lines << cleaned_line
73
+ else
74
+ cleaned_lines << line
75
+ end
76
+ end
77
+
78
+ { lines: diff_lines, cleaned_content: cleaned_lines.join }
79
+ end
80
+
81
+ def extract_focus_lines(code_content)
82
+ extract_marker_lines(code_content, FOCUS_MARKER_PATTERN)
83
+ end
84
+
85
+ def extract_error_lines(code_content)
86
+ extract_marker_lines(code_content, ERROR_MARKER_PATTERN)
87
+ end
88
+
89
+ def extract_warning_lines(code_content)
90
+ extract_marker_lines(code_content, WARNING_MARKER_PATTERN)
91
+ end
92
+
93
+ def extract_marker_lines(code_content, pattern)
94
+ lines = code_content.lines
95
+ marker_lines = {}
96
+ cleaned_lines = []
97
+
98
+ lines.each_with_index do |line, index|
99
+ line_num = index + 1
100
+
101
+ if line.match?(pattern)
102
+ marker_lines[line_num] = true
103
+ cleaned_lines << line.gsub(pattern, "")
104
+ else
105
+ cleaned_lines << line
106
+ end
107
+ end
108
+
109
+ { lines: marker_lines, cleaned_content: cleaned_lines.join }
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ class CodeBlockFocusPreprocessor < BaseProcessor
8
+ self.priority = 7
9
+
10
+ FOCUS_MARKER_PATTERN = %r{
11
+ (?:
12
+ //\s*\[!code\s+focus\] |
13
+ \#\s*\[!code\s+focus\] |
14
+ /\*\s*\[!code\s+focus\]\s*\*/ |
15
+ --\s*\[!code\s+focus\] |
16
+ <!--\s*\[!code\s+focus\]\s*--> |
17
+ ;\s*\[!code\s+focus\]
18
+ )[^\S\n]*
19
+ }x
20
+
21
+ CODE_BLOCK_REGEX = /^```(\w*).*?\n(.*?)^```/m
22
+ TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
23
+
24
+ def preprocess(content)
25
+ context[:code_block_focus_lines] ||= []
26
+ @block_index = 0
27
+ @tabs_ranges = find_tabs_ranges(content)
28
+
29
+ content.gsub(CODE_BLOCK_REGEX) { |_| process_code_block(Regexp.last_match) }
30
+ end
31
+
32
+ private
33
+
34
+ def process_code_block(match)
35
+ return match[0] if inside_tabs?(match.begin(0))
36
+
37
+ focus_info = extract_focus_lines(match[2])
38
+ context[:code_block_focus_lines][@block_index] = focus_info[:lines]
39
+ @block_index += 1
40
+ match[0].sub(match[2], focus_info[:cleaned_content])
41
+ end
42
+
43
+ def extract_focus_lines(code_content)
44
+ lines = code_content.lines
45
+ focus_lines = {}
46
+ cleaned_lines = []
47
+
48
+ lines.each_with_index do |line, index|
49
+ line_num = index + 1
50
+
51
+ if line.match?(FOCUS_MARKER_PATTERN)
52
+ focus_lines[line_num] = true
53
+ cleaned_line = line.gsub(FOCUS_MARKER_PATTERN, "")
54
+ cleaned_lines << cleaned_line
55
+ else
56
+ cleaned_lines << line
57
+ end
58
+ end
59
+
60
+ { lines: focus_lines, cleaned_content: cleaned_lines.join }
61
+ end
62
+
63
+ def inside_tabs?(position)
64
+ @tabs_ranges.any? { |range| range.cover?(position) }
65
+ end
66
+
67
+ def find_tabs_ranges(content)
68
+ ranges = []
69
+ content.scan(TABS_BLOCK_REGEX) do
70
+ match = Regexp.last_match
71
+ ranges << (match.begin(0)...match.end(0))
72
+ end
73
+ ranges
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../language_mapping"
4
+
5
+ module Docyard
6
+ module Components
7
+ module CodeBlockIconDetector
8
+ MANUAL_ICON_PATTERN = /^:([a-z0-9-]+):\s*(.+)$/i
9
+
10
+ module_function
11
+
12
+ def detect(title, language)
13
+ return { title: nil, icon: nil, icon_source: nil } if title.nil?
14
+
15
+ if (match = title.match(MANUAL_ICON_PATTERN))
16
+ return {
17
+ title: match[2].strip,
18
+ icon: match[1],
19
+ icon_source: "phosphor"
20
+ }
21
+ end
22
+
23
+ icon, icon_source = auto_detect_icon(language)
24
+ { title: title, icon: icon, icon_source: icon_source }
25
+ end
26
+
27
+ def auto_detect_icon(language)
28
+ return [nil, nil] if language.nil?
29
+
30
+ if LanguageMapping.terminal_language?(language)
31
+ %w[terminal-window phosphor]
32
+ elsif (ext = LanguageMapping.extension_for(language))
33
+ [ext, "file-extension"]
34
+ else
35
+ %w[file phosphor]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "code_line_parser"
4
+
5
+ module Docyard
6
+ module Components
7
+ module CodeBlockLineWrapper
8
+ module_function
9
+
10
+ def wrap_code_block(html, block_data)
11
+ html.gsub(%r{<pre[^>]*><code[^>]*>(.*?)</code></pre>}m) do
12
+ pre_match = Regexp.last_match(0)
13
+ code_content = Regexp.last_match(1)
14
+ lines = CodeLineParser.new(code_content).parse
15
+ wrapped_lines = wrap_lines_with_classes(lines, block_data)
16
+ pre_match.sub(code_content, wrapped_lines.join)
17
+ end
18
+ end
19
+
20
+ def wrap_lines_with_classes(lines, block_data)
21
+ lines.each_with_index.map do |line, index|
22
+ source_line = index + 1
23
+ display_line = block_data[:start_line] + index
24
+ classes = build_line_classes(source_line, display_line, block_data)
25
+ %(<span class="#{classes}">#{line}</span>)
26
+ end
27
+ end
28
+
29
+ DIFF_CLASSES = { addition: "docyard-code-line--diff-add", deletion: "docyard-code-line--diff-remove" }.freeze
30
+
31
+ def build_line_classes(source_line, display_line, block_data)
32
+ (["docyard-code-line"] + feature_classes(source_line, display_line, block_data)).join(" ")
33
+ end
34
+
35
+ def feature_classes(source_line, display_line, block_data)
36
+ [
37
+ ("docyard-code-line--highlighted" if block_data[:highlights].include?(display_line)),
38
+ DIFF_CLASSES[block_data[:diff_lines][source_line]],
39
+ ("docyard-code-line--focus" if block_data[:focus_lines][source_line]),
40
+ ("docyard-code-line--error" if block_data[:error_lines][source_line]),
41
+ ("docyard-code-line--warning" if block_data[:warning_lines][source_line])
42
+ ].compact
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ class CodeBlockOptionsPreprocessor < BaseProcessor
8
+ self.priority = 5
9
+
10
+ CODE_FENCE_REGEX = /^```(\w+)(?:\s*\[([^\]]+)\])?(:\S+)?(?:\s*\{([^}\n]+)\})?/
11
+ TABS_BLOCK_REGEX = /^:::[ \t]*tabs[ \t]*\n.*?^:::[ \t]*$/m
12
+
13
+ def preprocess(content)
14
+ context[:code_block_options] ||= []
15
+ @tabs_ranges = find_tabs_ranges(content)
16
+
17
+ process_code_fences(content)
18
+ end
19
+
20
+ private
21
+
22
+ def process_code_fences(content)
23
+ result = +""
24
+ last_end = 0
25
+
26
+ content.scan(CODE_FENCE_REGEX) do
27
+ match = Regexp.last_match
28
+ result << content[last_end...match.begin(0)]
29
+ result << process_fence_match(match)
30
+ last_end = match.end(0)
31
+ end
32
+
33
+ result << content[last_end..]
34
+ end
35
+
36
+ def process_fence_match(match)
37
+ store_code_block_options(match) unless inside_tabs?(match.begin(0))
38
+ "```#{match[1]}"
39
+ end
40
+
41
+ def store_code_block_options(match)
42
+ context[:code_block_options] << {
43
+ lang: match[1],
44
+ title: match[2],
45
+ option: match[3],
46
+ highlights: parse_highlights(match[4])
47
+ }
48
+ end
49
+
50
+ def inside_tabs?(position)
51
+ @tabs_ranges.any? { |range| range.cover?(position) }
52
+ end
53
+
54
+ def find_tabs_ranges(content)
55
+ ranges = []
56
+ content.scan(TABS_BLOCK_REGEX) do
57
+ match = Regexp.last_match
58
+ ranges << (match.begin(0)...match.end(0))
59
+ end
60
+ ranges
61
+ end
62
+
63
+ def parse_highlights(highlights_str)
64
+ return [] if highlights_str.nil? || highlights_str.strip.empty?
65
+
66
+ highlights_str.split(",").flat_map { |part| parse_highlight_part(part.strip) }.uniq.sort
67
+ end
68
+
69
+ def parse_highlight_part(part)
70
+ return (part.split("-")[0].to_i..part.split("-")[1].to_i).to_a if part.include?("-")
71
+
72
+ [part.to_i]
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ module CodeBlockPatterns
6
+ DIFF_MARKER_PATTERN = %r{
7
+ (?:
8
+ //\s*\[!code\s*([+-]{2})\] |
9
+ \#\s*\[!code\s*([+-]{2})\] |
10
+ /\*\s*\[!code\s*([+-]{2})\]\s*\*/ |
11
+ --\s*\[!code\s*([+-]{2})\] |
12
+ <!--\s*\[!code\s*([+-]{2})\]\s*--> |
13
+ ;\s*\[!code\s*([+-]{2})\]
14
+ )[^\S\n]*
15
+ }x
16
+
17
+ FOCUS_MARKER_PATTERN = %r{
18
+ (?:
19
+ //\s*\[!code\s+focus\] |
20
+ \#\s*\[!code\s+focus\] |
21
+ /\*\s*\[!code\s+focus\]\s*\*/ |
22
+ --\s*\[!code\s+focus\] |
23
+ <!--\s*\[!code\s+focus\]\s*--> |
24
+ ;\s*\[!code\s+focus\]
25
+ )[^\S\n]*
26
+ }x
27
+
28
+ ERROR_MARKER_PATTERN = %r{
29
+ (?:
30
+ //\s*\[!code\s+error\] |
31
+ \#\s*\[!code\s+error\] |
32
+ /\*\s*\[!code\s+error\]\s*\*/ |
33
+ --\s*\[!code\s+error\] |
34
+ <!--\s*\[!code\s+error\]\s*--> |
35
+ ;\s*\[!code\s+error\]
36
+ )[^\S\n]*
37
+ }x
38
+
39
+ WARNING_MARKER_PATTERN = %r{
40
+ (?:
41
+ //\s*\[!code\s+warning\] |
42
+ \#\s*\[!code\s+warning\] |
43
+ /\*\s*\[!code\s+warning\]\s*\*/ |
44
+ --\s*\[!code\s+warning\] |
45
+ <!--\s*\[!code\s+warning\]\s*--> |
46
+ ;\s*\[!code\s+warning\]
47
+ )[^\S\n]*
48
+ }x
49
+ end
50
+ end
51
+ end