docyard 0.5.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +20 -1
- data/lib/docyard/build/static_generator.rb +1 -1
- data/lib/docyard/components/base_processor.rb +6 -0
- data/lib/docyard/components/code_block_diff_preprocessor.rb +104 -0
- data/lib/docyard/components/code_block_feature_extractor.rb +113 -0
- data/lib/docyard/components/code_block_focus_preprocessor.rb +77 -0
- data/lib/docyard/components/code_block_icon_detector.rb +40 -0
- data/lib/docyard/components/code_block_line_wrapper.rb +46 -0
- data/lib/docyard/components/code_block_options_preprocessor.rb +76 -0
- data/lib/docyard/components/code_block_patterns.rb +51 -0
- data/lib/docyard/components/code_block_processor.rb +135 -14
- data/lib/docyard/components/code_line_parser.rb +80 -0
- data/lib/docyard/components/code_snippet_import_preprocessor.rb +125 -0
- data/lib/docyard/components/registry.rb +4 -4
- data/lib/docyard/components/table_of_contents_processor.rb +1 -1
- data/lib/docyard/components/tabs_parser.rb +135 -4
- data/lib/docyard/components/tabs_range_finder.rb +42 -0
- data/lib/docyard/config/validator.rb +8 -0
- data/lib/docyard/config.rb +7 -0
- data/lib/docyard/icons/file_types.rb +0 -13
- data/lib/docyard/markdown.rb +13 -5
- data/lib/docyard/rack_application.rb +1 -1
- data/lib/docyard/renderer.rb +4 -3
- data/lib/docyard/templates/assets/css/code.css +12 -4
- data/lib/docyard/templates/assets/css/components/code-block.css +427 -24
- data/lib/docyard/templates/assets/css/components/navigation.css +12 -9
- data/lib/docyard/templates/assets/css/components/tabs.css +50 -44
- data/lib/docyard/templates/assets/css/variables.css +44 -0
- data/lib/docyard/templates/partials/_code_block.html.erb +50 -2
- data/lib/docyard/version.rb +1 -1
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34bf24256bf638697dbc86f9d146667a4f17594c365070313eb19e74f8ab5134
|
|
4
|
+
data.tar.gz: 357ea04d8822f47c98e2aee4e09ca9e8cc69b7e4d11cbdce1491874aaa459b5c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc5c55d5ad69b73ef286eec3b4e202c7312b3dfd7d67b79d491612357e1c4e6e03d255a78af97706bdd5fe19353b7dd96f2da046a4c17f2e363342fc265b5628
|
|
7
|
+
data.tar.gz: 855fdcdcf312f3778fd204ca1fd36825c183b1abf0a5c938edff599693930d519668b8fdb0770f65ae00f9db43d1b794e31ca922b29dbde73143caf6abd2f64c
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
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
|
+
|
|
10
28
|
## [0.5.0] - 2025-11-18
|
|
11
29
|
|
|
12
30
|
### Added
|
|
@@ -91,7 +109,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
91
109
|
- Initial gem structure
|
|
92
110
|
- Project scaffolding
|
|
93
111
|
|
|
94
|
-
[Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.
|
|
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
|
|
95
114
|
[0.5.0]: https://github.com/sanifhimani/docyard/compare/v0.4.0...v0.5.0
|
|
96
115
|
[0.4.0]: https://github.com/sanifhimani/docyard/compare/v0.3.0...v0.4.0
|
|
97
116
|
[0.3.0]: https://github.com/sanifhimani/docyard/compare/v0.2.0...v0.3.0
|
|
@@ -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
|
|
@@ -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
|