docyard 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -1
- data/lib/docyard/build/static_generator.rb +2 -43
- data/lib/docyard/builder.rb +14 -4
- data/lib/docyard/cli.rb +6 -3
- data/lib/docyard/components/aliases.rb +29 -0
- data/lib/docyard/components/processors/callout_processor.rb +124 -0
- data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +106 -0
- data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +79 -0
- data/lib/docyard/components/processors/code_block_options_preprocessor.rb +78 -0
- data/lib/docyard/components/processors/code_block_processor.rb +175 -0
- data/lib/docyard/components/processors/code_snippet_import_preprocessor.rb +127 -0
- data/lib/docyard/components/processors/heading_anchor_processor.rb +39 -0
- data/lib/docyard/components/processors/icon_processor.rb +53 -0
- data/lib/docyard/components/processors/table_of_contents_processor.rb +68 -0
- data/lib/docyard/components/processors/table_wrapper_processor.rb +22 -0
- data/lib/docyard/components/processors/tabs_processor.rb +48 -0
- data/lib/docyard/components/support/code_block/feature_extractor.rb +117 -0
- data/lib/docyard/components/support/code_block/icon_detector.rb +44 -0
- data/lib/docyard/components/support/code_block/line_parser.rb +84 -0
- data/lib/docyard/components/support/code_block/line_wrapper.rb +50 -0
- data/lib/docyard/components/support/code_block/patterns.rb +55 -0
- data/lib/docyard/components/support/code_detector.rb +61 -0
- data/lib/docyard/components/support/tabs/icon_detector.rb +62 -0
- data/lib/docyard/components/support/tabs/parser.rb +195 -0
- data/lib/docyard/components/support/tabs/range_finder.rb +46 -0
- data/lib/docyard/config/branding_resolver.rb +74 -0
- data/lib/docyard/{constants.rb → config/constants.rb} +1 -0
- data/lib/docyard/config.rb +10 -1
- data/lib/docyard/{prev_next_builder.rb → navigation/prev_next_builder.rb} +2 -2
- data/lib/docyard/{sidebar → navigation/sidebar}/renderer.rb +3 -14
- data/lib/docyard/{sidebar → navigation/sidebar}/tree_builder.rb +9 -2
- data/lib/docyard/{sidebar_builder.rb → navigation/sidebar_builder.rb} +3 -15
- data/lib/docyard/{icons → rendering/icons}/phosphor.rb +4 -1
- data/lib/docyard/{markdown.rb → rendering/markdown.rb} +14 -13
- data/lib/docyard/{renderer.rb → rendering/renderer.rb} +20 -17
- data/lib/docyard/search/build_indexer.rb +74 -0
- data/lib/docyard/search/dev_indexer.rb +110 -0
- data/lib/docyard/search/pagefind_support.rb +31 -0
- data/lib/docyard/{asset_handler.rb → server/asset_handler.rb} +1 -1
- data/lib/docyard/{server.rb → server/dev_server.rb} +32 -9
- data/lib/docyard/{preview_server.rb → server/preview_server.rb} +1 -1
- data/lib/docyard/{rack_application.rb → server/rack_application.rb} +52 -49
- data/lib/docyard/server/resolution_result.rb +29 -0
- data/lib/docyard/{router.rb → server/router.rb} +4 -4
- data/lib/docyard/templates/assets/css/components/search.css +549 -0
- data/lib/docyard/templates/assets/css/layout.css +15 -1
- data/lib/docyard/templates/assets/js/components/search.js +685 -0
- data/lib/docyard/templates/layouts/default.html.erb +14 -2
- data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
- data/lib/docyard/templates/partials/_heading_anchor.html.erb +1 -1
- data/lib/docyard/templates/partials/_prev_next.html.erb +1 -1
- data/lib/docyard/templates/partials/_search_modal.html.erb +45 -0
- data/lib/docyard/templates/partials/_search_trigger.html.erb +22 -0
- data/lib/docyard/utils/html_helpers.rb +14 -0
- data/lib/docyard/utils/path_resolver.rb +2 -1
- data/lib/docyard/utils/url_helpers.rb +20 -0
- data/lib/docyard/version.rb +1 -1
- data/lib/docyard.rb +22 -15
- metadata +57 -46
- data/lib/docyard/components/callout_processor.rb +0 -121
- data/lib/docyard/components/code_block_diff_preprocessor.rb +0 -104
- data/lib/docyard/components/code_block_feature_extractor.rb +0 -113
- data/lib/docyard/components/code_block_focus_preprocessor.rb +0 -77
- data/lib/docyard/components/code_block_icon_detector.rb +0 -40
- data/lib/docyard/components/code_block_line_wrapper.rb +0 -46
- data/lib/docyard/components/code_block_options_preprocessor.rb +0 -76
- data/lib/docyard/components/code_block_patterns.rb +0 -51
- data/lib/docyard/components/code_block_processor.rb +0 -176
- data/lib/docyard/components/code_detector.rb +0 -59
- data/lib/docyard/components/code_line_parser.rb +0 -80
- data/lib/docyard/components/code_snippet_import_preprocessor.rb +0 -125
- data/lib/docyard/components/heading_anchor_processor.rb +0 -34
- data/lib/docyard/components/icon_detector.rb +0 -57
- data/lib/docyard/components/icon_processor.rb +0 -51
- data/lib/docyard/components/table_of_contents_processor.rb +0 -64
- data/lib/docyard/components/table_wrapper_processor.rb +0 -18
- data/lib/docyard/components/tabs_parser.rb +0 -191
- data/lib/docyard/components/tabs_processor.rb +0 -44
- data/lib/docyard/components/tabs_range_finder.rb +0 -42
- data/lib/docyard/routing/resolution_result.rb +0 -31
- /data/lib/docyard/{sidebar → navigation/sidebar}/config_parser.rb +0 -0
- /data/lib/docyard/{sidebar → navigation/sidebar}/file_system_scanner.rb +0 -0
- /data/lib/docyard/{sidebar → navigation/sidebar}/item.rb +0 -0
- /data/lib/docyard/{sidebar → navigation/sidebar}/title_extractor.rb +0 -0
- /data/lib/docyard/{icons → rendering/icons}/LICENSE.phosphor +0 -0
- /data/lib/docyard/{icons → rendering/icons}/file_types.rb +0 -0
- /data/lib/docyard/{icons.rb → rendering/icons.rb} +0 -0
- /data/lib/docyard/{language_mapping.rb → rendering/language_mapping.rb} +0 -0
- /data/lib/docyard/{file_watcher.rb → server/file_watcher.rb} +0 -0
- /data/lib/docyard/{errors.rb → utils/errors.rb} +0 -0
- /data/lib/docyard/{logging.rb → utils/logging.rb} +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Components
|
|
5
|
+
module Support
|
|
6
|
+
module CodeBlock
|
|
7
|
+
class LineParser
|
|
8
|
+
def initialize(code_content)
|
|
9
|
+
@code_content = code_content
|
|
10
|
+
@lines = []
|
|
11
|
+
@current_line = ""
|
|
12
|
+
@open_tags = []
|
|
13
|
+
@in_tag = false
|
|
14
|
+
@tag_buffer = ""
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parse
|
|
18
|
+
@code_content.each_char { |char| process_char(char) }
|
|
19
|
+
finalize
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def process_char(char)
|
|
25
|
+
case char
|
|
26
|
+
when "<" then start_tag(char)
|
|
27
|
+
when ">" then end_tag_if_applicable(char)
|
|
28
|
+
when "\n" then handle_newline
|
|
29
|
+
else handle_regular_char(char)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def start_tag(char)
|
|
34
|
+
@in_tag = true
|
|
35
|
+
@tag_buffer = char
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def end_tag_if_applicable(char)
|
|
39
|
+
if @in_tag
|
|
40
|
+
@in_tag = false
|
|
41
|
+
@tag_buffer += char
|
|
42
|
+
|
|
43
|
+
if @tag_buffer.start_with?("</")
|
|
44
|
+
@open_tags.pop
|
|
45
|
+
elsif !@tag_buffer.end_with?("/>")
|
|
46
|
+
@open_tags << @tag_buffer
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@current_line += @tag_buffer
|
|
50
|
+
@tag_buffer = ""
|
|
51
|
+
else
|
|
52
|
+
@current_line += char
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def handle_newline
|
|
57
|
+
closing_tags = @open_tags.reverse.map { |tag| closing_tag_for(tag) }.join
|
|
58
|
+
@lines << "#{@current_line}#{closing_tags}\n"
|
|
59
|
+
@current_line = @open_tags.join
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def handle_regular_char(char)
|
|
63
|
+
if @in_tag
|
|
64
|
+
@tag_buffer += char
|
|
65
|
+
else
|
|
66
|
+
@current_line += char
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def finalize
|
|
71
|
+
@lines << @current_line unless @current_line.empty?
|
|
72
|
+
@lines << "" if @lines.empty?
|
|
73
|
+
@lines
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def closing_tag_for(open_tag)
|
|
77
|
+
tag_name = open_tag.match(/<(\w+)/)[1]
|
|
78
|
+
"</#{tag_name}>"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "line_parser"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Components
|
|
7
|
+
module Support
|
|
8
|
+
module CodeBlock
|
|
9
|
+
module LineWrapper
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def wrap_code_block(html, block_data)
|
|
13
|
+
html.gsub(%r{<pre[^>]*><code[^>]*>(.*?)</code></pre>}m) do
|
|
14
|
+
pre_match = Regexp.last_match(0)
|
|
15
|
+
code_content = Regexp.last_match(1)
|
|
16
|
+
lines = LineParser.new(code_content).parse
|
|
17
|
+
wrapped_lines = wrap_lines_with_classes(lines, block_data)
|
|
18
|
+
pre_match.sub(code_content, wrapped_lines.join)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def wrap_lines_with_classes(lines, block_data)
|
|
23
|
+
lines.each_with_index.map do |line, index|
|
|
24
|
+
source_line = index + 1
|
|
25
|
+
display_line = block_data[:start_line] + index
|
|
26
|
+
classes = build_line_classes(source_line, display_line, block_data)
|
|
27
|
+
%(<span class="#{classes}">#{line}</span>)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
DIFF_CLASSES = { addition: "docyard-code-line--diff-add", deletion: "docyard-code-line--diff-remove" }.freeze
|
|
32
|
+
|
|
33
|
+
def build_line_classes(source_line, display_line, block_data)
|
|
34
|
+
(["docyard-code-line"] + feature_classes(source_line, display_line, block_data)).join(" ")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def feature_classes(source_line, display_line, block_data)
|
|
38
|
+
[
|
|
39
|
+
("docyard-code-line--highlighted" if block_data[:highlights].include?(display_line)),
|
|
40
|
+
DIFF_CLASSES[block_data[:diff_lines][source_line]],
|
|
41
|
+
("docyard-code-line--focus" if block_data[:focus_lines][source_line]),
|
|
42
|
+
("docyard-code-line--error" if block_data[:error_lines][source_line]),
|
|
43
|
+
("docyard-code-line--warning" if block_data[:warning_lines][source_line])
|
|
44
|
+
].compact
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Components
|
|
5
|
+
module Support
|
|
6
|
+
module CodeBlock
|
|
7
|
+
module Patterns
|
|
8
|
+
DIFF_MARKER_PATTERN = %r{
|
|
9
|
+
(?:
|
|
10
|
+
//\s*\[!code\s*([+-]{2})\] |
|
|
11
|
+
\#\s*\[!code\s*([+-]{2})\] |
|
|
12
|
+
/\*\s*\[!code\s*([+-]{2})\]\s*\*/ |
|
|
13
|
+
--\s*\[!code\s*([+-]{2})\] |
|
|
14
|
+
<!--\s*\[!code\s*([+-]{2})\]\s*--> |
|
|
15
|
+
;\s*\[!code\s*([+-]{2})\]
|
|
16
|
+
)[^\S\n]*
|
|
17
|
+
}x
|
|
18
|
+
|
|
19
|
+
FOCUS_MARKER_PATTERN = %r{
|
|
20
|
+
(?:
|
|
21
|
+
//\s*\[!code\s+focus\] |
|
|
22
|
+
\#\s*\[!code\s+focus\] |
|
|
23
|
+
/\*\s*\[!code\s+focus\]\s*\*/ |
|
|
24
|
+
--\s*\[!code\s+focus\] |
|
|
25
|
+
<!--\s*\[!code\s+focus\]\s*--> |
|
|
26
|
+
;\s*\[!code\s+focus\]
|
|
27
|
+
)[^\S\n]*
|
|
28
|
+
}x
|
|
29
|
+
|
|
30
|
+
ERROR_MARKER_PATTERN = %r{
|
|
31
|
+
(?:
|
|
32
|
+
//\s*\[!code\s+error\] |
|
|
33
|
+
\#\s*\[!code\s+error\] |
|
|
34
|
+
/\*\s*\[!code\s+error\]\s*\*/ |
|
|
35
|
+
--\s*\[!code\s+error\] |
|
|
36
|
+
<!--\s*\[!code\s+error\]\s*--> |
|
|
37
|
+
;\s*\[!code\s+error\]
|
|
38
|
+
)[^\S\n]*
|
|
39
|
+
}x
|
|
40
|
+
|
|
41
|
+
WARNING_MARKER_PATTERN = %r{
|
|
42
|
+
(?:
|
|
43
|
+
//\s*\[!code\s+warning\] |
|
|
44
|
+
\#\s*\[!code\s+warning\] |
|
|
45
|
+
/\*\s*\[!code\s+warning\]\s*\*/ |
|
|
46
|
+
--\s*\[!code\s+warning\] |
|
|
47
|
+
<!--\s*\[!code\s+warning\]\s*--> |
|
|
48
|
+
;\s*\[!code\s+warning\]
|
|
49
|
+
)[^\S\n]*
|
|
50
|
+
}x
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../rendering/language_mapping"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Components
|
|
7
|
+
module Support
|
|
8
|
+
class CodeDetector
|
|
9
|
+
def self.detect(content)
|
|
10
|
+
new(content).detect
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(content)
|
|
14
|
+
@content = content
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def detect
|
|
18
|
+
return nil unless code_only?
|
|
19
|
+
|
|
20
|
+
language = extract_language
|
|
21
|
+
return nil unless language
|
|
22
|
+
|
|
23
|
+
icon_for_language(language)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :content
|
|
29
|
+
|
|
30
|
+
def code_only?
|
|
31
|
+
stripped = content.strip
|
|
32
|
+
return false unless stripped.start_with?("```") && stripped.end_with?("```")
|
|
33
|
+
|
|
34
|
+
parts = stripped.split("```")
|
|
35
|
+
parts.length == 2 && parts[0].empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def extract_language
|
|
39
|
+
parts = content.strip.split("```")
|
|
40
|
+
return nil unless parts[1]
|
|
41
|
+
|
|
42
|
+
lines = parts[1].split("\n", 2)
|
|
43
|
+
lang_line = lines[0].strip
|
|
44
|
+
return nil if lang_line.empty? || lang_line.include?(" ")
|
|
45
|
+
|
|
46
|
+
lang_line.downcase
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def icon_for_language(language)
|
|
50
|
+
if LanguageMapping.terminal_language?(language)
|
|
51
|
+
{ icon: "terminal-window", source: "phosphor" }
|
|
52
|
+
elsif (extension = LanguageMapping.extension_for(language))
|
|
53
|
+
{ icon: extension, source: "file-extension" }
|
|
54
|
+
else
|
|
55
|
+
{ icon: "file", source: "phosphor" }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../code_detector"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Components
|
|
7
|
+
module Support
|
|
8
|
+
module Tabs
|
|
9
|
+
class IconDetector
|
|
10
|
+
MANUAL_ICON_PATTERN = /^:([a-z0-9-]+):\s*(.+)$/i
|
|
11
|
+
CodeDetector = Support::CodeDetector
|
|
12
|
+
|
|
13
|
+
def self.detect(tab_name, tab_content)
|
|
14
|
+
new(tab_name, tab_content).detect
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(tab_name, tab_content)
|
|
18
|
+
@tab_name = tab_name
|
|
19
|
+
@tab_content = tab_content
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def detect
|
|
23
|
+
manual_icon || auto_detected_icon || no_icon
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :tab_name, :tab_content
|
|
29
|
+
|
|
30
|
+
def manual_icon
|
|
31
|
+
return nil unless tab_name.match(MANUAL_ICON_PATTERN)
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
name: Regexp.last_match(2).strip,
|
|
35
|
+
icon: Regexp.last_match(1),
|
|
36
|
+
icon_source: "phosphor"
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def auto_detected_icon
|
|
41
|
+
detected = CodeDetector.detect(tab_content)
|
|
42
|
+
return nil unless detected
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
name: tab_name,
|
|
46
|
+
icon: detected[:icon],
|
|
47
|
+
icon_source: detected[:source]
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def no_icon
|
|
52
|
+
{
|
|
53
|
+
name: tab_name,
|
|
54
|
+
icon: nil,
|
|
55
|
+
icon_source: nil
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../code_block/feature_extractor"
|
|
4
|
+
require_relative "../code_block/icon_detector"
|
|
5
|
+
require_relative "../code_block/line_wrapper"
|
|
6
|
+
require_relative "icon_detector"
|
|
7
|
+
require_relative "../../../rendering/icons"
|
|
8
|
+
require_relative "../../../rendering/renderer"
|
|
9
|
+
require "kramdown"
|
|
10
|
+
require "kramdown-parser-gfm"
|
|
11
|
+
require "cgi"
|
|
12
|
+
|
|
13
|
+
module Docyard
|
|
14
|
+
module Components
|
|
15
|
+
module Support
|
|
16
|
+
module Tabs
|
|
17
|
+
class Parser
|
|
18
|
+
include Utils::HtmlHelpers
|
|
19
|
+
|
|
20
|
+
IconDetector = Tabs::IconDetector
|
|
21
|
+
CodeBlockFeatureExtractor = CodeBlock::FeatureExtractor
|
|
22
|
+
CodeBlockIconDetector = CodeBlock::IconDetector
|
|
23
|
+
CodeBlockLineWrapper = CodeBlock::LineWrapper
|
|
24
|
+
|
|
25
|
+
def self.parse(content)
|
|
26
|
+
new(content).parse
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(content)
|
|
30
|
+
@content = content
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def parse
|
|
34
|
+
sections.filter_map { |section| parse_section(section) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader :content
|
|
40
|
+
|
|
41
|
+
def sections
|
|
42
|
+
content.split(/^==[ \t]+/)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def parse_section(section)
|
|
46
|
+
return nil if section.strip.empty?
|
|
47
|
+
|
|
48
|
+
tab_name, tab_content = extract_tab_parts(section)
|
|
49
|
+
return nil if tab_name.nil? || tab_name.empty?
|
|
50
|
+
|
|
51
|
+
build_tab_data(tab_name, tab_content)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def extract_tab_parts(section)
|
|
55
|
+
parts = section.split("\n", 2)
|
|
56
|
+
[parts[0]&.strip, parts[1]&.strip || ""]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def build_tab_data(tab_name, tab_content)
|
|
60
|
+
icon_data = IconDetector.detect(tab_name, tab_content)
|
|
61
|
+
|
|
62
|
+
extracted = CodeBlockFeatureExtractor.process_markdown(tab_content)
|
|
63
|
+
rendered_content = render_markdown(extracted[:cleaned_markdown])
|
|
64
|
+
enhanced_content = enhance_code_blocks(rendered_content, extracted[:blocks])
|
|
65
|
+
|
|
66
|
+
{
|
|
67
|
+
name: icon_data[:name],
|
|
68
|
+
content: enhanced_content,
|
|
69
|
+
icon: icon_data[:icon],
|
|
70
|
+
icon_source: icon_data[:icon_source]
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_markdown(markdown_content)
|
|
75
|
+
return "" if markdown_content.empty?
|
|
76
|
+
|
|
77
|
+
Kramdown::Document.new(
|
|
78
|
+
markdown_content,
|
|
79
|
+
input: "GFM",
|
|
80
|
+
hard_wrap: false,
|
|
81
|
+
syntax_highlighter: "rouge"
|
|
82
|
+
).to_html
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def enhance_code_blocks(html, blocks)
|
|
86
|
+
return html unless html.include?('<div class="highlight">')
|
|
87
|
+
|
|
88
|
+
block_index = 0
|
|
89
|
+
html.gsub(%r{<div class="highlight">(.*?)</div>}m) do
|
|
90
|
+
block_data = blocks[block_index] || {}
|
|
91
|
+
block_index += 1
|
|
92
|
+
render_enhanced_code_block(Regexp.last_match, block_data)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def render_enhanced_code_block(match, block_data)
|
|
97
|
+
original_html = match[0]
|
|
98
|
+
inner_html = match[1]
|
|
99
|
+
code_text = extract_code_text(inner_html)
|
|
100
|
+
|
|
101
|
+
processed_html = if needs_line_wrapping?(block_data)
|
|
102
|
+
wrap_code_block_lines(original_html, block_data)
|
|
103
|
+
else
|
|
104
|
+
original_html
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
Renderer.new.render_partial("_code_block", build_full_locals(processed_html, code_text, block_data))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def needs_line_wrapping?(block_data)
|
|
111
|
+
%i[highlights diff_lines focus_lines error_lines warning_lines].any? do |key|
|
|
112
|
+
block_data[key]&.any?
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def wrap_code_block_lines(html, block_data)
|
|
117
|
+
wrapper_data = {
|
|
118
|
+
highlights: block_data[:highlights] || [],
|
|
119
|
+
diff_lines: block_data[:diff_lines] || {},
|
|
120
|
+
focus_lines: block_data[:focus_lines] || {},
|
|
121
|
+
error_lines: block_data[:error_lines] || {},
|
|
122
|
+
warning_lines: block_data[:warning_lines] || {},
|
|
123
|
+
start_line: extract_start_line(block_data[:option])
|
|
124
|
+
}
|
|
125
|
+
CodeBlockLineWrapper.wrap_code_block(html, wrapper_data)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def build_full_locals(processed_html, code_text, block_data)
|
|
129
|
+
title_data = CodeBlockIconDetector.detect(block_data[:title], block_data[:lang])
|
|
130
|
+
show_line_numbers = line_numbers_enabled?(block_data[:option])
|
|
131
|
+
start_line = extract_start_line(block_data[:option])
|
|
132
|
+
|
|
133
|
+
base_locals(processed_html, code_text, show_line_numbers, start_line)
|
|
134
|
+
.merge(feature_locals(block_data))
|
|
135
|
+
.merge(title_locals(title_data))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def base_locals(processed_html, code_text, show_line_numbers, start_line)
|
|
139
|
+
{
|
|
140
|
+
code_block_html: processed_html,
|
|
141
|
+
code_text: escape_html_attribute(code_text),
|
|
142
|
+
copy_icon: Icons.render("copy", "regular") || "",
|
|
143
|
+
show_line_numbers: show_line_numbers,
|
|
144
|
+
line_numbers: show_line_numbers ? generate_line_numbers(code_text, start_line) : [],
|
|
145
|
+
start_line: start_line
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def feature_locals(block_data)
|
|
150
|
+
{
|
|
151
|
+
highlights: block_data[:highlights] || [],
|
|
152
|
+
diff_lines: block_data[:diff_lines] || {},
|
|
153
|
+
focus_lines: block_data[:focus_lines] || {},
|
|
154
|
+
error_lines: block_data[:error_lines] || {},
|
|
155
|
+
warning_lines: block_data[:warning_lines] || {}
|
|
156
|
+
}
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def title_locals(title_data)
|
|
160
|
+
{
|
|
161
|
+
title: title_data[:title],
|
|
162
|
+
icon: title_data[:icon],
|
|
163
|
+
icon_source: title_data[:icon_source]
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def line_numbers_enabled?(block_option)
|
|
168
|
+
return false if block_option == ":no-line-numbers"
|
|
169
|
+
return true if block_option&.start_with?(":line-numbers")
|
|
170
|
+
|
|
171
|
+
false
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def extract_start_line(block_option)
|
|
175
|
+
return 1 unless block_option&.include?("=")
|
|
176
|
+
|
|
177
|
+
block_option.split("=").last.to_i
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def generate_line_numbers(code_text, start_line)
|
|
181
|
+
line_count = code_text.lines.count
|
|
182
|
+
line_count = 1 if line_count.zero?
|
|
183
|
+
(start_line...(start_line + line_count)).to_a
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def extract_code_text(html)
|
|
187
|
+
text = html.gsub(/<[^>]+>/, "")
|
|
188
|
+
text = CGI.unescapeHTML(text)
|
|
189
|
+
text.strip
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
module Components
|
|
5
|
+
module Support
|
|
6
|
+
module Tabs
|
|
7
|
+
module RangeFinder
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def find_ranges(html)
|
|
11
|
+
ranges = []
|
|
12
|
+
start_pattern = '<div class="docyard-tabs"'
|
|
13
|
+
|
|
14
|
+
pos = 0
|
|
15
|
+
while (start_pos = html.index(start_pattern, pos))
|
|
16
|
+
end_pos = find_matching_close_div(html, start_pos)
|
|
17
|
+
ranges << (start_pos...end_pos) if end_pos
|
|
18
|
+
pos = end_pos || (start_pos + 1)
|
|
19
|
+
end
|
|
20
|
+
ranges
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def find_matching_close_div(html, start_pos)
|
|
24
|
+
depth = 0
|
|
25
|
+
pos = start_pos
|
|
26
|
+
|
|
27
|
+
while pos < html.length
|
|
28
|
+
if html[pos, 4] == "<div"
|
|
29
|
+
depth += 1
|
|
30
|
+
pos += 4
|
|
31
|
+
elsif html[pos, 6] == "</div>"
|
|
32
|
+
depth -= 1
|
|
33
|
+
return pos + 6 if depth.zero?
|
|
34
|
+
|
|
35
|
+
pos += 6
|
|
36
|
+
else
|
|
37
|
+
pos += 1
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Docyard
|
|
4
|
+
class BrandingResolver
|
|
5
|
+
def initialize(config)
|
|
6
|
+
@config = config
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def resolve
|
|
10
|
+
return default_branding unless config
|
|
11
|
+
|
|
12
|
+
default_branding.merge(config_branding_options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_reader :config
|
|
18
|
+
|
|
19
|
+
def default_branding
|
|
20
|
+
{
|
|
21
|
+
site_title: Constants::DEFAULT_SITE_TITLE,
|
|
22
|
+
site_description: "",
|
|
23
|
+
logo: Constants::DEFAULT_LOGO_PATH,
|
|
24
|
+
logo_dark: Constants::DEFAULT_LOGO_DARK_PATH,
|
|
25
|
+
favicon: nil,
|
|
26
|
+
display_logo: true,
|
|
27
|
+
display_title: true
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def config_branding_options
|
|
32
|
+
site_options.merge(logo_options).merge(search_options).merge(appearance_options)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def site_options
|
|
36
|
+
{
|
|
37
|
+
site_title: config.site.title || Constants::DEFAULT_SITE_TITLE,
|
|
38
|
+
site_description: config.site.description || "",
|
|
39
|
+
favicon: config.branding.favicon
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def logo_options
|
|
44
|
+
branding = config.branding
|
|
45
|
+
{
|
|
46
|
+
logo: resolve_logo(branding.logo, branding.logo_dark),
|
|
47
|
+
logo_dark: resolve_logo_dark(branding.logo, branding.logo_dark)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def search_options
|
|
52
|
+
{
|
|
53
|
+
search_enabled: config.search.enabled != false,
|
|
54
|
+
search_placeholder: config.search.placeholder || "Search documentation..."
|
|
55
|
+
}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def appearance_options
|
|
59
|
+
appearance = config.branding.appearance || {}
|
|
60
|
+
{
|
|
61
|
+
display_logo: appearance["logo"] != false,
|
|
62
|
+
display_title: appearance["title"] != false
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def resolve_logo(logo, logo_dark)
|
|
67
|
+
logo || logo_dark || Constants::DEFAULT_LOGO_PATH
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def resolve_logo_dark(logo, logo_dark)
|
|
71
|
+
logo_dark || logo || Constants::DEFAULT_LOGO_DARK_PATH
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/docyard/config.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
require_relative "config/validator"
|
|
5
|
-
require_relative "constants"
|
|
5
|
+
require_relative "config/constants"
|
|
6
6
|
|
|
7
7
|
module Docyard
|
|
8
8
|
class Config
|
|
@@ -37,6 +37,11 @@ module Docyard
|
|
|
37
37
|
},
|
|
38
38
|
"markdown" => {
|
|
39
39
|
"lineNumbers" => false
|
|
40
|
+
},
|
|
41
|
+
"search" => {
|
|
42
|
+
"enabled" => true,
|
|
43
|
+
"placeholder" => "Search documentation...",
|
|
44
|
+
"exclude" => []
|
|
40
45
|
}
|
|
41
46
|
}.freeze
|
|
42
47
|
|
|
@@ -81,6 +86,10 @@ module Docyard
|
|
|
81
86
|
@markdown ||= ConfigSection.new(data["markdown"])
|
|
82
87
|
end
|
|
83
88
|
|
|
89
|
+
def search
|
|
90
|
+
@search ||= ConfigSection.new(data["search"])
|
|
91
|
+
end
|
|
92
|
+
|
|
84
93
|
private
|
|
85
94
|
|
|
86
95
|
def load_config_data
|