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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +29 -3
- data/README.md +37 -12
- 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/heading_anchor_processor.rb +34 -0
- data/lib/docyard/components/registry.rb +4 -4
- data/lib/docyard/components/table_of_contents_processor.rb +64 -0
- 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 +18 -0
- data/lib/docyard/icons/file_types.rb +0 -13
- data/lib/docyard/icons/phosphor.rb +2 -1
- data/lib/docyard/markdown.rb +18 -4
- data/lib/docyard/prev_next_builder.rb +159 -0
- data/lib/docyard/rack_application.rb +25 -3
- data/lib/docyard/renderer.rb +20 -8
- 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/heading-anchor.css +77 -0
- data/lib/docyard/templates/assets/css/components/navigation.css +12 -9
- data/lib/docyard/templates/assets/css/components/prev-next.css +114 -0
- data/lib/docyard/templates/assets/css/components/table-of-contents.css +269 -0
- data/lib/docyard/templates/assets/css/components/tabs.css +50 -44
- data/lib/docyard/templates/assets/css/layout.css +58 -1
- data/lib/docyard/templates/assets/css/variables.css +45 -0
- data/lib/docyard/templates/assets/js/components/heading-anchor.js +90 -0
- data/lib/docyard/templates/assets/js/components/navigation.js +6 -2
- data/lib/docyard/templates/assets/js/components/table-of-contents.js +301 -0
- data/lib/docyard/templates/layouts/default.html.erb +9 -1
- data/lib/docyard/templates/partials/_code_block.html.erb +50 -2
- data/lib/docyard/templates/partials/_heading_anchor.html.erb +1 -0
- data/lib/docyard/templates/partials/_prev_next.html.erb +23 -0
- data/lib/docyard/templates/partials/_table_of_contents.html.erb +45 -0
- data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +8 -0
- data/lib/docyard/version.rb +1 -1
- metadata +23 -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,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.
|
|
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-
|
|
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.
|
|
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.
|
|
231
|
-
-
|
|
232
|
-
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
-
|
|
236
|
-
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
@@ -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
|