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
|
@@ -1,28 +1,138 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../icons"
|
|
4
|
+
require_relative "../language_mapping"
|
|
4
5
|
require_relative "../renderer"
|
|
5
6
|
require_relative "base_processor"
|
|
7
|
+
require_relative "code_block_icon_detector"
|
|
8
|
+
require_relative "code_block_line_wrapper"
|
|
9
|
+
require_relative "tabs_range_finder"
|
|
6
10
|
|
|
7
11
|
module Docyard
|
|
8
12
|
module Components
|
|
9
13
|
class CodeBlockProcessor < BaseProcessor
|
|
14
|
+
include CodeBlockLineWrapper
|
|
15
|
+
|
|
10
16
|
self.priority = 20
|
|
11
17
|
|
|
12
18
|
def postprocess(html)
|
|
13
19
|
return html unless html.include?('<div class="highlight">')
|
|
14
20
|
|
|
15
|
-
html
|
|
16
|
-
|
|
17
|
-
end
|
|
21
|
+
initialize_postprocess_state(html)
|
|
22
|
+
process_all_highlight_blocks(html)
|
|
18
23
|
end
|
|
19
24
|
|
|
20
25
|
private
|
|
21
26
|
|
|
27
|
+
def initialize_postprocess_state(html)
|
|
28
|
+
@block_index = 0
|
|
29
|
+
@options = context[:code_block_options] || []
|
|
30
|
+
@diff_lines = context[:code_block_diff_lines] || []
|
|
31
|
+
@focus_lines = context[:code_block_focus_lines] || []
|
|
32
|
+
@error_lines = context[:code_block_error_lines] || []
|
|
33
|
+
@warning_lines = context[:code_block_warning_lines] || []
|
|
34
|
+
@global_line_numbers = context.dig(:config, "markdown", "lineNumbers") || false
|
|
35
|
+
@tabs_ranges = TabsRangeFinder.find_ranges(html)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def process_all_highlight_blocks(html)
|
|
39
|
+
result = +""
|
|
40
|
+
last_end = 0
|
|
41
|
+
|
|
42
|
+
html.scan(%r{<div class="highlight">(.*?)</div>}m) do
|
|
43
|
+
match = Regexp.last_match
|
|
44
|
+
result << html[last_end...match.begin(0)]
|
|
45
|
+
result << process_highlight_match(match)
|
|
46
|
+
last_end = match.end(0)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
result << html[last_end..]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def process_highlight_match(match)
|
|
53
|
+
if inside_tabs?(match.begin(0))
|
|
54
|
+
match[0]
|
|
55
|
+
else
|
|
56
|
+
processed = process_code_block(match[0], match[1])
|
|
57
|
+
@block_index += 1
|
|
58
|
+
processed
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
22
62
|
def process_code_block(original_html, inner_html)
|
|
63
|
+
block_data = extract_block_data(inner_html)
|
|
64
|
+
processed_html = process_html_for_highlighting(original_html, block_data)
|
|
65
|
+
|
|
66
|
+
render_code_block_with_copy(block_data.merge(html: processed_html))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def extract_block_data(inner_html)
|
|
70
|
+
opts = current_block_options
|
|
23
71
|
code_text = extract_code_text(inner_html)
|
|
72
|
+
start_line = extract_start_line(opts[:option])
|
|
73
|
+
show_line_numbers = determine_line_numbers(opts[:option])
|
|
74
|
+
title_data = CodeBlockIconDetector.detect(opts[:title], opts[:lang])
|
|
24
75
|
|
|
25
|
-
|
|
76
|
+
build_block_data(code_text, opts, show_line_numbers, start_line, title_data)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def current_block_options
|
|
80
|
+
block_opts = @options[@block_index] || {}
|
|
81
|
+
{
|
|
82
|
+
option: block_opts[:option],
|
|
83
|
+
title: block_opts[:title],
|
|
84
|
+
lang: block_opts[:lang],
|
|
85
|
+
highlights: block_opts[:highlights] || []
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def build_block_data(code_text, opts, show_line_numbers, start_line, title_data)
|
|
90
|
+
{
|
|
91
|
+
text: code_text,
|
|
92
|
+
highlights: opts[:highlights],
|
|
93
|
+
diff_lines: @diff_lines[@block_index] || {},
|
|
94
|
+
focus_lines: @focus_lines[@block_index] || {},
|
|
95
|
+
error_lines: @error_lines[@block_index] || {},
|
|
96
|
+
warning_lines: @warning_lines[@block_index] || {},
|
|
97
|
+
show_line_numbers: show_line_numbers,
|
|
98
|
+
line_numbers: show_line_numbers ? generate_line_numbers(code_text, start_line) : [],
|
|
99
|
+
start_line: start_line,
|
|
100
|
+
title: title_data[:title],
|
|
101
|
+
icon: title_data[:icon],
|
|
102
|
+
icon_source: title_data[:icon_source]
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def process_html_for_highlighting(original_html, block_data)
|
|
107
|
+
needs_wrapping = block_data[:highlights].any? || block_data[:diff_lines].any? ||
|
|
108
|
+
block_data[:focus_lines].any? || block_data[:error_lines].any? ||
|
|
109
|
+
block_data[:warning_lines].any?
|
|
110
|
+
return original_html unless needs_wrapping
|
|
111
|
+
|
|
112
|
+
wrap_code_block(original_html, block_data)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def determine_line_numbers(block_option)
|
|
116
|
+
return false if block_option == ":no-line-numbers"
|
|
117
|
+
return true if block_option&.start_with?(":line-numbers")
|
|
118
|
+
|
|
119
|
+
@global_line_numbers
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def extract_start_line(block_option)
|
|
123
|
+
return 1 unless block_option&.include?("=")
|
|
124
|
+
|
|
125
|
+
block_option.split("=").last.to_i
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def generate_line_numbers(code_text, start_line)
|
|
129
|
+
line_count = code_text.lines.count
|
|
130
|
+
line_count = 1 if line_count.zero?
|
|
131
|
+
(start_line...(start_line + line_count)).to_a
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def inside_tabs?(position)
|
|
135
|
+
@tabs_ranges.any? { |range| range.cover?(position) }
|
|
26
136
|
end
|
|
27
137
|
|
|
28
138
|
def extract_code_text(html)
|
|
@@ -31,17 +141,28 @@ module Docyard
|
|
|
31
141
|
text.strip
|
|
32
142
|
end
|
|
33
143
|
|
|
34
|
-
def render_code_block_with_copy(
|
|
35
|
-
|
|
36
|
-
|
|
144
|
+
def render_code_block_with_copy(block_data)
|
|
145
|
+
Renderer.new.render_partial("_code_block", template_locals(block_data))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def template_locals(block_data)
|
|
149
|
+
base_locals(block_data).merge(line_feature_locals(block_data)).merge(title_locals(block_data))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def base_locals(block_data)
|
|
153
|
+
{ code_block_html: block_data[:html], code_text: escape_html_attribute(block_data[:text]),
|
|
154
|
+
copy_icon: Icons.render("copy", "regular") || "", show_line_numbers: block_data[:show_line_numbers],
|
|
155
|
+
line_numbers: block_data[:line_numbers], start_line: block_data[:start_line] }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def line_feature_locals(block_data)
|
|
159
|
+
{ highlights: block_data[:highlights], diff_lines: block_data[:diff_lines],
|
|
160
|
+
focus_lines: block_data[:focus_lines], error_lines: block_data[:error_lines],
|
|
161
|
+
warning_lines: block_data[:warning_lines] }
|
|
162
|
+
end
|
|
37
163
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
code_block_html: code_block_html,
|
|
41
|
-
code_text: escape_html_attribute(code_text),
|
|
42
|
-
copy_icon: copy_icon
|
|
43
|
-
}
|
|
44
|
-
)
|
|
164
|
+
def title_locals(block_data)
|
|
165
|
+
{ title: block_data[:title], icon: block_data[:icon], icon_source: block_data[:icon_source] }
|
|
45
166
|
end
|
|
46
167
|
|
|
47
168
|
def escape_html_attribute(text)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Components
|
|
5
|
+
class CodeLineParser
|
|
6
|
+
def initialize(code_content)
|
|
7
|
+
@code_content = code_content
|
|
8
|
+
@lines = []
|
|
9
|
+
@current_line = ""
|
|
10
|
+
@open_tags = []
|
|
11
|
+
@in_tag = false
|
|
12
|
+
@tag_buffer = ""
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parse
|
|
16
|
+
@code_content.each_char { |char| process_char(char) }
|
|
17
|
+
finalize
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def process_char(char)
|
|
23
|
+
case char
|
|
24
|
+
when "<" then start_tag(char)
|
|
25
|
+
when ">" then end_tag_if_applicable(char)
|
|
26
|
+
when "\n" then handle_newline
|
|
27
|
+
else handle_regular_char(char)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def start_tag(char)
|
|
32
|
+
@in_tag = true
|
|
33
|
+
@tag_buffer = char
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def end_tag_if_applicable(char)
|
|
37
|
+
if @in_tag
|
|
38
|
+
@in_tag = false
|
|
39
|
+
@tag_buffer += char
|
|
40
|
+
|
|
41
|
+
if @tag_buffer.start_with?("</")
|
|
42
|
+
@open_tags.pop
|
|
43
|
+
elsif !@tag_buffer.end_with?("/>")
|
|
44
|
+
@open_tags << @tag_buffer
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@current_line += @tag_buffer
|
|
48
|
+
@tag_buffer = ""
|
|
49
|
+
else
|
|
50
|
+
@current_line += char
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def handle_newline
|
|
55
|
+
closing_tags = @open_tags.reverse.map { |tag| closing_tag_for(tag) }.join
|
|
56
|
+
@lines << "#{@current_line}#{closing_tags}\n"
|
|
57
|
+
@current_line = @open_tags.join
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def handle_regular_char(char)
|
|
61
|
+
if @in_tag
|
|
62
|
+
@tag_buffer += char
|
|
63
|
+
else
|
|
64
|
+
@current_line += char
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def finalize
|
|
69
|
+
@lines << @current_line unless @current_line.empty?
|
|
70
|
+
@lines << "" if @lines.empty?
|
|
71
|
+
@lines
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def closing_tag_for(open_tag)
|
|
75
|
+
tag_name = open_tag.match(/<(\w+)/)[1]
|
|
76
|
+
"</#{tag_name}>"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_processor"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Components
|
|
7
|
+
class CodeSnippetImportPreprocessor < BaseProcessor
|
|
8
|
+
EXTENSION_MAP = {
|
|
9
|
+
"rb" => "ruby",
|
|
10
|
+
"js" => "javascript",
|
|
11
|
+
"ts" => "typescript",
|
|
12
|
+
"py" => "python",
|
|
13
|
+
"yml" => "yaml",
|
|
14
|
+
"md" => "markdown",
|
|
15
|
+
"sh" => "bash",
|
|
16
|
+
"zsh" => "bash",
|
|
17
|
+
"jsx" => "jsx",
|
|
18
|
+
"tsx" => "tsx"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
IMPORT_PATTERN = %r{^<<<\s+@/([^\s{#]+)(?:#([\w-]+))?(?:\{([^}]+)\})?\s*$}
|
|
22
|
+
|
|
23
|
+
self.priority = 1
|
|
24
|
+
|
|
25
|
+
def preprocess(content)
|
|
26
|
+
@docs_root = context[:docs_root] || "docs"
|
|
27
|
+
content.gsub(IMPORT_PATTERN) { |_| process_import(Regexp.last_match) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def process_import(match)
|
|
33
|
+
filepath = match[1]
|
|
34
|
+
region = match[2]
|
|
35
|
+
options = match[3]
|
|
36
|
+
|
|
37
|
+
file_content = read_file(filepath)
|
|
38
|
+
return import_error(filepath, "File not found") unless file_content
|
|
39
|
+
|
|
40
|
+
file_content = extract_region(file_content, region) if region
|
|
41
|
+
return import_error(filepath, "Region '#{region}' not found") unless file_content
|
|
42
|
+
|
|
43
|
+
build_code_block(file_content, filepath, options)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def read_file(filepath)
|
|
47
|
+
full_path = File.join(@docs_root, filepath)
|
|
48
|
+
return nil unless File.exist?(full_path)
|
|
49
|
+
|
|
50
|
+
File.read(full_path)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def extract_region(content, region_name)
|
|
54
|
+
region_start = %r{^[ \t]*(?://|#|/\*)\s*#region\s+#{Regexp.escape(region_name)}\b.*$}
|
|
55
|
+
region_end = %r{^[ \t]*(?://|#|/\*|\*/)\s*#endregion\s*#{Regexp.escape(region_name)}?\b.*$}
|
|
56
|
+
|
|
57
|
+
lines = content.lines
|
|
58
|
+
start_index = lines.find_index { |line| line.match?(region_start) }
|
|
59
|
+
return nil unless start_index
|
|
60
|
+
|
|
61
|
+
end_index = lines[(start_index + 1)..].find_index { |line| line.match?(region_end) }
|
|
62
|
+
return nil unless end_index
|
|
63
|
+
|
|
64
|
+
end_index += start_index + 1
|
|
65
|
+
lines[(start_index + 1)...end_index].join
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_code_block(content, filepath, options)
|
|
69
|
+
lang = detect_language(filepath)
|
|
70
|
+
highlights = nil
|
|
71
|
+
|
|
72
|
+
if options
|
|
73
|
+
parsed = parse_options(options)
|
|
74
|
+
lang = parsed[:lang] if parsed[:lang]
|
|
75
|
+
highlights = parsed[:highlights] if parsed[:highlights]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
content = extract_line_range(content, highlights) if highlights&.include?("-") && !highlights.include?(",")
|
|
79
|
+
|
|
80
|
+
meta = build_meta_string(highlights, filepath)
|
|
81
|
+
|
|
82
|
+
"```#{lang}#{meta}\n#{content.chomp}\n```"
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def parse_options(options)
|
|
86
|
+
parts = options.strip.split(/\s+/)
|
|
87
|
+
result = { highlights: nil, lang: nil }
|
|
88
|
+
|
|
89
|
+
parts.each do |part|
|
|
90
|
+
if part.match?(/^[\d,-]+$/)
|
|
91
|
+
result[:highlights] = part
|
|
92
|
+
else
|
|
93
|
+
result[:lang] = part
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
result
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def extract_line_range(content, range_str)
|
|
101
|
+
return content unless range_str&.match?(/^\d+-\d+$/)
|
|
102
|
+
|
|
103
|
+
start_line, end_line = range_str.split("-").map(&:to_i)
|
|
104
|
+
lines = content.lines
|
|
105
|
+
lines[(start_line - 1)..(end_line - 1)]&.join || content
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_meta_string(highlights, filepath)
|
|
109
|
+
parts = []
|
|
110
|
+
parts << " [#{File.basename(filepath)}]" if filepath
|
|
111
|
+
parts << " {#{highlights}}" if highlights && !highlights.match?(/^\d+-\d+$/)
|
|
112
|
+
parts.join
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def detect_language(filepath)
|
|
116
|
+
ext = File.extname(filepath).delete_prefix(".")
|
|
117
|
+
EXTENSION_MAP[ext] || ext
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def import_error(filepath, message)
|
|
121
|
+
"```\nError importing #{filepath}: #{message}\n```"
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -11,15 +11,15 @@ module Docyard
|
|
|
11
11
|
@processors.sort_by! { |p| p.priority || 100 }
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def run_preprocessors(content)
|
|
14
|
+
def run_preprocessors(content, context = {})
|
|
15
15
|
@processors.reduce(content) do |processed_content, processor_class|
|
|
16
|
-
processor_class.new.preprocess(processed_content)
|
|
16
|
+
processor_class.new(context).preprocess(processed_content)
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
def run_postprocessors(html)
|
|
20
|
+
def run_postprocessors(html, context = {})
|
|
21
21
|
@processors.reduce(html) do |processed_html, processor_class|
|
|
22
|
-
processor_class.new.postprocess(processed_html)
|
|
22
|
+
processor_class.new(context).postprocess(processed_html)
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "code_block_feature_extractor"
|
|
4
|
+
require_relative "code_block_icon_detector"
|
|
5
|
+
require_relative "code_block_line_wrapper"
|
|
3
6
|
require_relative "icon_detector"
|
|
7
|
+
require_relative "../icons"
|
|
8
|
+
require_relative "../renderer"
|
|
4
9
|
require "kramdown"
|
|
5
10
|
require "kramdown-parser-gfm"
|
|
11
|
+
require "cgi"
|
|
6
12
|
|
|
7
13
|
module Docyard
|
|
8
14
|
module Components
|
|
@@ -30,16 +36,27 @@ module Docyard
|
|
|
30
36
|
def parse_section(section)
|
|
31
37
|
return nil if section.strip.empty?
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
tab_name = parts[0]&.strip
|
|
39
|
+
tab_name, tab_content = extract_tab_parts(section)
|
|
35
40
|
return nil if tab_name.nil? || tab_name.empty?
|
|
36
41
|
|
|
37
|
-
tab_content
|
|
42
|
+
build_tab_data(tab_name, tab_content)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def extract_tab_parts(section)
|
|
46
|
+
parts = section.split("\n", 2)
|
|
47
|
+
[parts[0]&.strip, parts[1]&.strip || ""]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def build_tab_data(tab_name, tab_content)
|
|
38
51
|
icon_data = IconDetector.detect(tab_name, tab_content)
|
|
39
52
|
|
|
53
|
+
extracted = CodeBlockFeatureExtractor.process_markdown(tab_content)
|
|
54
|
+
rendered_content = render_markdown(extracted[:cleaned_markdown])
|
|
55
|
+
enhanced_content = enhance_code_blocks(rendered_content, extracted[:blocks])
|
|
56
|
+
|
|
40
57
|
{
|
|
41
58
|
name: icon_data[:name],
|
|
42
|
-
content:
|
|
59
|
+
content: enhanced_content,
|
|
43
60
|
icon: icon_data[:icon],
|
|
44
61
|
icon_source: icon_data[:icon_source]
|
|
45
62
|
}
|
|
@@ -55,6 +72,120 @@ module Docyard
|
|
|
55
72
|
syntax_highlighter: "rouge"
|
|
56
73
|
).to_html
|
|
57
74
|
end
|
|
75
|
+
|
|
76
|
+
def enhance_code_blocks(html, blocks)
|
|
77
|
+
return html unless html.include?('<div class="highlight">')
|
|
78
|
+
|
|
79
|
+
block_index = 0
|
|
80
|
+
html.gsub(%r{<div class="highlight">(.*?)</div>}m) do
|
|
81
|
+
block_data = blocks[block_index] || {}
|
|
82
|
+
block_index += 1
|
|
83
|
+
render_enhanced_code_block(Regexp.last_match, block_data)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def render_enhanced_code_block(match, block_data)
|
|
88
|
+
original_html = match[0]
|
|
89
|
+
inner_html = match[1]
|
|
90
|
+
code_text = extract_code_text(inner_html)
|
|
91
|
+
|
|
92
|
+
processed_html = if needs_line_wrapping?(block_data)
|
|
93
|
+
wrap_code_block_lines(original_html, block_data)
|
|
94
|
+
else
|
|
95
|
+
original_html
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
Renderer.new.render_partial("_code_block", build_full_locals(processed_html, code_text, block_data))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def needs_line_wrapping?(block_data)
|
|
102
|
+
%i[highlights diff_lines focus_lines error_lines warning_lines].any? do |key|
|
|
103
|
+
block_data[key]&.any?
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def wrap_code_block_lines(html, block_data)
|
|
108
|
+
wrapper_data = {
|
|
109
|
+
highlights: block_data[:highlights] || [],
|
|
110
|
+
diff_lines: block_data[:diff_lines] || {},
|
|
111
|
+
focus_lines: block_data[:focus_lines] || {},
|
|
112
|
+
error_lines: block_data[:error_lines] || {},
|
|
113
|
+
warning_lines: block_data[:warning_lines] || {},
|
|
114
|
+
start_line: extract_start_line(block_data[:option])
|
|
115
|
+
}
|
|
116
|
+
CodeBlockLineWrapper.wrap_code_block(html, wrapper_data)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def build_full_locals(processed_html, code_text, block_data)
|
|
120
|
+
title_data = CodeBlockIconDetector.detect(block_data[:title], block_data[:lang])
|
|
121
|
+
show_line_numbers = line_numbers_enabled?(block_data[:option])
|
|
122
|
+
start_line = extract_start_line(block_data[:option])
|
|
123
|
+
|
|
124
|
+
base_locals(processed_html, code_text, show_line_numbers, start_line)
|
|
125
|
+
.merge(feature_locals(block_data))
|
|
126
|
+
.merge(title_locals(title_data))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def base_locals(processed_html, code_text, show_line_numbers, start_line)
|
|
130
|
+
{
|
|
131
|
+
code_block_html: processed_html,
|
|
132
|
+
code_text: escape_html_attribute(code_text),
|
|
133
|
+
copy_icon: Icons.render("copy", "regular") || "",
|
|
134
|
+
show_line_numbers: show_line_numbers,
|
|
135
|
+
line_numbers: show_line_numbers ? generate_line_numbers(code_text, start_line) : [],
|
|
136
|
+
start_line: start_line
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def feature_locals(block_data)
|
|
141
|
+
{
|
|
142
|
+
highlights: block_data[:highlights] || [],
|
|
143
|
+
diff_lines: block_data[:diff_lines] || {},
|
|
144
|
+
focus_lines: block_data[:focus_lines] || {},
|
|
145
|
+
error_lines: block_data[:error_lines] || {},
|
|
146
|
+
warning_lines: block_data[:warning_lines] || {}
|
|
147
|
+
}
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def title_locals(title_data)
|
|
151
|
+
{
|
|
152
|
+
title: title_data[:title],
|
|
153
|
+
icon: title_data[:icon],
|
|
154
|
+
icon_source: title_data[:icon_source]
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def line_numbers_enabled?(block_option)
|
|
159
|
+
return false if block_option == ":no-line-numbers"
|
|
160
|
+
return true if block_option&.start_with?(":line-numbers")
|
|
161
|
+
|
|
162
|
+
false
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def extract_start_line(block_option)
|
|
166
|
+
return 1 unless block_option&.include?("=")
|
|
167
|
+
|
|
168
|
+
block_option.split("=").last.to_i
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def generate_line_numbers(code_text, start_line)
|
|
172
|
+
line_count = code_text.lines.count
|
|
173
|
+
line_count = 1 if line_count.zero?
|
|
174
|
+
(start_line...(start_line + line_count)).to_a
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def extract_code_text(html)
|
|
178
|
+
text = html.gsub(/<[^>]+>/, "")
|
|
179
|
+
text = CGI.unescapeHTML(text)
|
|
180
|
+
text.strip
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def escape_html_attribute(text)
|
|
184
|
+
text.gsub('"', """)
|
|
185
|
+
.gsub("'", "'")
|
|
186
|
+
.gsub("<", "<")
|
|
187
|
+
.gsub(">", ">")
|
|
188
|
+
end
|
|
58
189
|
end
|
|
59
190
|
end
|
|
60
191
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Components
|
|
5
|
+
module TabsRangeFinder
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def find_ranges(html)
|
|
9
|
+
ranges = []
|
|
10
|
+
start_pattern = '<div class="docyard-tabs"'
|
|
11
|
+
|
|
12
|
+
pos = 0
|
|
13
|
+
while (start_pos = html.index(start_pattern, pos))
|
|
14
|
+
end_pos = find_matching_close_div(html, start_pos)
|
|
15
|
+
ranges << (start_pos...end_pos) if end_pos
|
|
16
|
+
pos = end_pos || (start_pos + 1)
|
|
17
|
+
end
|
|
18
|
+
ranges
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def find_matching_close_div(html, start_pos)
|
|
22
|
+
depth = 0
|
|
23
|
+
pos = start_pos
|
|
24
|
+
|
|
25
|
+
while pos < html.length
|
|
26
|
+
if html[pos, 4] == "<div"
|
|
27
|
+
depth += 1
|
|
28
|
+
pos += 4
|
|
29
|
+
elsif html[pos, 6] == "</div>"
|
|
30
|
+
depth -= 1
|
|
31
|
+
return pos + 6 if depth.zero?
|
|
32
|
+
|
|
33
|
+
pos += 6
|
|
34
|
+
else
|
|
35
|
+
pos += 1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -12,6 +12,7 @@ module Docyard
|
|
|
12
12
|
validate_site_section
|
|
13
13
|
validate_branding_section
|
|
14
14
|
validate_build_section
|
|
15
|
+
validate_markdown_section
|
|
15
16
|
|
|
16
17
|
raise ConfigError, format_errors if @errors.any?
|
|
17
18
|
end
|
|
@@ -48,6 +49,13 @@ module Docyard
|
|
|
48
49
|
validate_boolean(build["clean"], "build.clean")
|
|
49
50
|
end
|
|
50
51
|
|
|
52
|
+
def validate_markdown_section
|
|
53
|
+
markdown = @config["markdown"]
|
|
54
|
+
return unless markdown
|
|
55
|
+
|
|
56
|
+
validate_boolean(markdown["lineNumbers"], "markdown.lineNumbers") if markdown.key?("lineNumbers")
|
|
57
|
+
end
|
|
58
|
+
|
|
51
59
|
def validate_string(value, field_name)
|
|
52
60
|
return if value.nil?
|
|
53
61
|
return if value.is_a?(String)
|
data/lib/docyard/config.rb
CHANGED
|
@@ -34,6 +34,9 @@ module Docyard
|
|
|
34
34
|
"prev_text" => "Previous",
|
|
35
35
|
"next_text" => "Next"
|
|
36
36
|
}
|
|
37
|
+
},
|
|
38
|
+
"markdown" => {
|
|
39
|
+
"lineNumbers" => false
|
|
37
40
|
}
|
|
38
41
|
}.freeze
|
|
39
42
|
|
|
@@ -74,6 +77,10 @@ module Docyard
|
|
|
74
77
|
@navigation ||= ConfigSection.new(data["navigation"])
|
|
75
78
|
end
|
|
76
79
|
|
|
80
|
+
def markdown
|
|
81
|
+
@markdown ||= ConfigSection.new(data["markdown"])
|
|
82
|
+
end
|
|
83
|
+
|
|
77
84
|
private
|
|
78
85
|
|
|
79
86
|
def load_config_data
|